1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-21 11:54:02 +00:00
Files
browser/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts

179 lines
6.5 KiB
TypeScript

import { DOCUMENT, NgIf } from "@angular/common";
import { Component, DestroyRef, inject, OnDestroy, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { Router, RouterModule } from "@angular/router";
import { firstValueFrom, pairwise, startWith } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BrowserExtensionIcon, Party } from "@bitwarden/assets/svg";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
import {
AnonLayoutWrapperDataService,
ButtonComponent,
DialogRef,
DialogService,
IconModule,
LinkModule,
} from "@bitwarden/components";
import { SETUP_EXTENSION_DISMISSED } from "../../guards/setup-extension-redirect.guard";
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component";
import {
AddExtensionLaterDialogComponent,
AddExtensionLaterDialogData,
} from "./add-extension-later-dialog.component";
import { AddExtensionVideosComponent } from "./add-extension-videos.component";
export const SetupExtensionState = {
Loading: "loading",
NeedsExtension: "needs-extension",
Success: "success",
AlreadyInstalled: "already-installed",
ManualOpen: "manual-open",
} as const;
type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
@Component({
selector: "vault-setup-extension",
templateUrl: "./setup-extension.component.html",
imports: [
NgIf,
JslibModule,
ButtonComponent,
LinkModule,
IconModule,
RouterModule,
AddExtensionVideosComponent,
ManuallyOpenExtensionComponent,
],
})
export class SetupExtensionComponent implements OnInit, OnDestroy {
private webBrowserExtensionInteractionService = inject(WebBrowserInteractionService);
private router = inject(Router);
private destroyRef = inject(DestroyRef);
private platformUtilsService = inject(PlatformUtilsService);
private dialogService = inject(DialogService);
private stateProvider = inject(StateProvider);
private accountService = inject(AccountService);
private document = inject(DOCUMENT);
private anonLayoutWrapperDataService = inject(AnonLayoutWrapperDataService);
protected SetupExtensionState = SetupExtensionState;
protected PartyIcon = Party;
/** The current state of the setup extension component. */
protected state: SetupExtensionState = SetupExtensionState.Loading;
/** Download Url for the extension based on the browser */
protected webStoreUrl: string = "";
/** Reference to the add it later dialog */
protected dialogRef: DialogRef | null = null;
private viewportContent: string | null = null;
async ngOnInit() {
// It is not be uncommon for users to hit this page from smaller viewports.
// There are global styles that set a min-width for the page which cause it to render poorly.
// Remove them here.
// https://github.com/bitwarden/clients/blob/main/apps/web/src/scss/base.scss#L6
this.document.body.style.minWidth = "auto";
const viewportMeta = this.document.querySelector('meta[name="viewport"]');
// Save the current viewport content to reset it when the component is destroyed
this.viewportContent = viewportMeta?.getAttribute("content") ?? null;
viewportMeta?.setAttribute("content", "width=device-width, initial-scale=1.0");
await this.conditionallyRedirectUser();
this.webStoreUrl = getWebStoreUrl(this.platformUtilsService.getDevice());
this.webBrowserExtensionInteractionService.extensionInstalled$
.pipe(takeUntilDestroyed(this.destroyRef), startWith(null), pairwise())
.subscribe(([previousState, currentState]) => {
// User landed on the page and the extension is already installed, show already installed state
if (previousState === null && currentState) {
void this.dismissExtensionPage();
this.state = SetupExtensionState.AlreadyInstalled;
}
// Extension was not installed and now it is, show success state
if (previousState === false && currentState) {
this.dialogRef?.close();
void this.dismissExtensionPage();
this.state = SetupExtensionState.Success;
}
// Extension is not installed
if (currentState === false) {
this.state = SetupExtensionState.NeedsExtension;
}
});
}
ngOnDestroy(): void {
// Reset the body min-width when the component is destroyed
this.document.body.style.minWidth = "";
if (this.viewportContent !== null) {
this.document
.querySelector('meta[name="viewport"]')
?.setAttribute("content", this.viewportContent);
}
}
/** Conditionally redirects the user to the vault upon landing on the page. */
async conditionallyRedirectUser() {
const isMobile = Utils.isMobileBrowser;
if (isMobile) {
await this.dismissExtensionPage();
await this.router.navigate(["/vault"]);
}
}
/** Opens the add extension later dialog */
addItLater() {
this.dialogRef = this.dialogService.open<unknown, AddExtensionLaterDialogData>(
AddExtensionLaterDialogComponent,
{
data: {
onDismiss: this.dismissExtensionPage.bind(this),
},
},
);
}
/** Opens the browser extension */
async openExtension() {
await this.webBrowserExtensionInteractionService.openExtension().catch(() => {
this.state = SetupExtensionState.ManualOpen;
// Update the anon layout data to show the proper error design
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "somethingWentWrong",
},
pageIcon: BrowserExtensionIcon,
hideCardWrapper: false,
maxWidth: "md",
});
});
}
/** Update local state to never show this page again. */
private async dismissExtensionPage() {
const accountId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
void this.stateProvider.getUser(accountId, SETUP_EXTENSION_DISMISSED).update(() => true);
}
}