import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; import { routerTransition } from "./app-routing.animations"; import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component"; @Component({ selector: "app-root", styles: [], animations: [routerTransition], template: `
`, }) export class AppComponent implements OnInit, OnDestroy { private lastActivity: Date; private activeUserId: UserId; private recordActivitySubject = new Subject(); private destroy$ = new Subject(); constructor( private authService: AuthService, private i18nService: I18nService, private router: Router, private stateService: BrowserStateService, private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, private messageListener: MessageListener, private toastService: ToastService, private accountService: AccountService, ) {} async ngOnInit() { // Component states must not persist between closing and reopening the popup, otherwise they become dead objects // Clear them aggressively to make sure this doesn't occur await this.clearComponentStates(); this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { this.activeUserId = account?.id; }); this.authService.activeAccountStatus$ .pipe( filter((status) => status === AuthenticationStatus.Unlocked), concatMap(async () => { await this.recordActivity(); }), takeUntil(this.destroy$), ) .subscribe(); this.ngZone.runOutsideAngular(() => { window.onmousedown = () => this.recordActivity(); window.ontouchstart = () => this.recordActivity(); window.onclick = () => this.recordActivity(); window.onscroll = () => this.recordActivity(); window.onkeypress = () => this.recordActivity(); }); this.messageListener.allMessages$ .pipe( tap((msg: any) => { if (msg.command === "doneLoggingOut") { this.authService.logOut(async () => { if (msg.expired) { this.toastService.showToast({ variant: "warning", title: this.i18nService.t("loggedOut"), message: this.i18nService.t("loginExpired"), }); } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["home"]); }); this.changeDetectorRef.detectChanges(); } else if (msg.command === "authBlocked") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["home"]); } else if ( msg.command === "locked" && (msg.userId == null || msg.userId == this.activeUserId) ) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["lock"]); } else if (msg.command === "showDialog") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showDialog(msg); } else if (msg.command === "showNativeMessagingFinterprintDialog") { // TODO: Should be refactored to live in another service. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.showNativeMessagingFingerprintDialog(msg); } else if (msg.command === "showToast") { this.toastService._showToast(msg); } else if (msg.command === "reloadProcess") { const forceWindowReload = this.platformUtilsService.isSafari() || this.platformUtilsService.isFirefox() || this.platformUtilsService.isOpera(); // Wait to make sure background has reloaded first. window.setTimeout( () => BrowserApi.reloadExtension(forceWindowReload ? window : null), 2000, ); } else if (msg.command === "reloadPopup") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/"]); } else if (msg.command === "convertAccountToKeyConnector") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/remove-password"]); } else if (msg.command === "switchAccountFinish") { // TODO: unset loading? // this.loading = false; } else if (msg.command == "update-temp-password") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/update-temp-password"]); } }), takeUntil(this.destroy$), ) .subscribe(); // eslint-disable-next-line rxjs/no-async-subscribe this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => { if (event instanceof NavigationEnd) { const url = event.urlAfterRedirects || event.url || ""; if ( url.startsWith("/tabs/") && (window as any).previousPopupUrl != null && (window as any).previousPopupUrl.startsWith("/tabs/") ) { await this.clearComponentStates(); } if (url.startsWith("/tabs/")) { await this.cipherService.setAddEditCipherInfo(null); } (window as any).previousPopupUrl = url; // Clear route direction after animation (400ms) if ((window as any).routeDirection != null) { window.setTimeout(() => { (window as any).routeDirection = null; }, 400); } } }); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } getState(outlet: RouterOutlet) { if (outlet.activatedRouteData.state === "ciphers") { const routeDirection = (window as any).routeDirection != null ? (window as any).routeDirection : ""; return ( "ciphers_direction=" + routeDirection + "_" + (outlet.activatedRoute.queryParams as any).value.folderId + "_" + (outlet.activatedRoute.queryParams as any).value.collectionId ); } else { return outlet.activatedRouteData.state; } } private async recordActivity() { if (this.activeUserId == null) { return; } const now = new Date(); if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; await this.accountService.setAccountActivity(this.activeUserId, now); } private showToast(msg: any) { this.platformUtilsService.showToast(msg.type, msg.title, msg.text, msg.options); } private async showDialog(msg: SimpleDialogOptions) { await this.dialogService.openSimpleDialog(msg); } private async showNativeMessagingFingerprintDialog(msg: any) { const dialogRef = DesktopSyncVerificationDialogComponent.open(this.dialogService, { fingerprint: msg.fingerprint, }); return firstValueFrom(dialogRef.closed); } private async clearComponentStates() { if (!(await this.stateService.getIsAuthenticated())) { return; } await Promise.all([ this.vaultBrowserStateService.setBrowserGroupingsComponentState(null), this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null), this.browserSendStateService.setBrowserSendComponentState(null), this.browserSendStateService.setBrowserSendTypeComponentState(null), ]); } }