import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { ToastrService } from "ngx-toastr"; import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.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: number = null; private activeUserId: string; private destroy$ = new Subject(); constructor( private toastrService: ToastrService, private broadcasterService: BroadcasterService, 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: ForegroundPlatformUtilsService, private dialogService: DialogService, private browserMessagingApi: ZonedMessageListenerService, ) {} 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.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => { this.activeUserId = userId; }); this.authService.activeAccountStatus$ .pipe( map((status) => status === AuthenticationStatus.Unlocked), filter((unlocked) => 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(); }); const bitwardenPopupMainMessageListener = (msg: any, sender: any) => { if (msg.command === "doneLoggingOut") { this.authService.logOut(async () => { if (msg.expired) { this.showToast({ type: "warning", title: this.i18nService.t("loggedOut"), text: 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.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"]); } else { msg.webExtSender = sender; this.broadcasterService.send(msg); } }; (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); // 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().getTime(); if (this.lastActivity != null && now - this.lastActivity < 250) { return; } this.lastActivity = now; await this.stateService.setLastActive(now, { userId: this.activeUserId }); } 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), ]); } }