diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html index 1c643fcc3e4..56332cc424b 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.html @@ -26,14 +26,7 @@ -

- {{ "openExtensionManuallyPart1" | i18n }} - - {{ "openExtensionManuallyPart2" | i18n }} -

+
diff --git a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts index 624275a8297..177311cbfde 100644 --- a/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts +++ b/apps/web/src/app/vault/components/browser-extension-prompt/browser-extension-prompt.component.ts @@ -3,17 +3,17 @@ import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { ButtonComponent, IconModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { VaultIcons } from "@bitwarden/vault"; import { BrowserExtensionPromptService, BrowserPromptState, } from "../../services/browser-extension-prompt.service"; +import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component"; @Component({ selector: "vault-browser-extension-prompt", templateUrl: "./browser-extension-prompt.component.html", - imports: [CommonModule, I18nPipe, ButtonComponent, IconModule], + imports: [CommonModule, I18nPipe, ButtonComponent, IconModule, ManuallyOpenExtensionComponent], }) export class BrowserExtensionPromptComponent implements OnInit, OnDestroy { /** Current state of the prompt page */ @@ -22,8 +22,6 @@ export class BrowserExtensionPromptComponent implements OnInit, OnDestroy { /** All available page states */ protected BrowserPromptState = BrowserPromptState; - protected BitwardenIcon = VaultIcons.BitwardenIcon; - /** Content of the meta[name="viewport"] element */ private viewportContent: string | null = null; diff --git a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html new file mode 100644 index 00000000000..22c36e51177 --- /dev/null +++ b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.html @@ -0,0 +1,8 @@ +

+ {{ "openExtensionManuallyPart1" | i18n }} + + {{ "openExtensionManuallyPart2" | i18n }} +

diff --git a/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts new file mode 100644 index 00000000000..22041b61198 --- /dev/null +++ b/apps/web/src/app/vault/components/manually-open-extension/manually-open-extension.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +import { IconModule } from "@bitwarden/components"; +import { I18nPipe } from "@bitwarden/ui-common"; +import { VaultIcons } from "@bitwarden/vault"; + +@Component({ + selector: "vault-manually-open-extension", + templateUrl: "./manually-open-extension.component.html", + imports: [I18nPipe, IconModule], +}) +export class ManuallyOpenExtensionComponent { + protected BitwardenIcon = VaultIcons.BitwardenIcon; +} diff --git a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts index d053e05c36b..6bde812065b 100644 --- a/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/add-extension-videos.component.ts @@ -34,8 +34,8 @@ export class AddExtensionVideosComponent { /** CSS classes for the video container, pulled into the class only for readability. */ protected videoContainerClass = [ "tw-absolute tw-left-0 tw-top-0 tw-w-[15rem] tw-opacity-0 md:tw-opacity-100 md:tw-relative lg:tw-w-[17rem] tw-max-w-full tw-aspect-[0.807]", - `[${this.cssOverlayVariable}:0.7] after:tw-absolute after:tw-top-0 after:tw-left-0 after:tw-size-full after:tw-bg-primary-100 after:tw-content-[''] after:tw-rounded-lg after:tw-opacity-[--overlay-opacity]`, - `[${this.cssBorderVariable}:0] before:tw-absolute before:tw-top-0 before:tw-left-0 before:tw-w-full before:tw-h-2 before:tw-bg-primary-600 before:tw-content-[''] before:tw-rounded-t-lg before:tw-opacity-[--border-opacity]`, + `[--overlay-opacity:0.7] after:tw-absolute after:tw-top-0 after:tw-left-0 after:tw-size-full after:tw-bg-primary-100 after:tw-content-[''] after:tw-rounded-lg after:tw-opacity-[--overlay-opacity]`, + `[--border-opacity:0] before:tw-absolute before:tw-top-0 before:tw-left-0 before:tw-w-full before:tw-h-2 before:tw-bg-primary-600 before:tw-content-[''] before:tw-rounded-t-lg before:tw-opacity-[--border-opacity]`, "after:tw-transition-opacity after:tw-duration-400 after:tw-ease-linear", "before:tw-transition-opacity before:tw-duration-400 before:tw-ease-linear", ].join(" "); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html index c23fa0aac35..ac24383a4d3 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.html @@ -54,3 +54,7 @@

+ +
+ +
diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts index e824cd92f37..8bb80e6fb44 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { Router, RouterModule } from "@angular/router"; import { BehaviorSubject } from "rxjs"; @@ -11,10 +11,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 { AnonLayoutWrapperDataService } from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service"; -import { SetupExtensionComponent } from "./setup-extension.component"; +import { SetupExtensionComponent, SetupExtensionState } from "./setup-extension.component"; describe("SetupExtensionComponent", () => { let fixture: ComponentFixture; @@ -24,12 +26,14 @@ describe("SetupExtensionComponent", () => { const navigate = jest.fn().mockResolvedValue(true); const openExtension = jest.fn().mockResolvedValue(true); const update = jest.fn().mockResolvedValue(true); + const setAnonLayoutWrapperData = jest.fn(); const extensionInstalled$ = new BehaviorSubject(null); beforeEach(async () => { navigate.mockClear(); openExtension.mockClear(); update.mockClear(); + setAnonLayoutWrapperData.mockClear(); getFeatureFlag.mockClear().mockResolvedValue(true); window.matchMedia = jest.fn().mockReturnValue(false); @@ -40,6 +44,7 @@ describe("SetupExtensionComponent", () => { { provide: ConfigService, useValue: { getFeatureFlag } }, { provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } }, { provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } }, + { provide: AnonLayoutWrapperDataService, useValue: { setAnonLayoutWrapperData } }, { provide: AccountService, useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) }, @@ -136,6 +141,27 @@ describe("SetupExtensionComponent", () => { it("dismisses the extension page", () => { expect(update).toHaveBeenCalledTimes(1); }); + + it("shows error state when extension fails to open", fakeAsync(() => { + openExtension.mockRejectedValueOnce(new Error("Failed to open extension")); + + const openExtensionButton = fixture.debugElement.query(By.css("button")); + + openExtensionButton.triggerEventHandler("click"); + + tick(); + + expect(component["state"]).toBe(SetupExtensionState.ManualOpen); + expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({ + pageTitle: { + key: "somethingWentWrong", + }, + pageIcon: VaultIcons.BrowserExtensionIcon, + hideIcon: false, + hideCardWrapper: false, + maxWidth: "md", + }); + })); }); }); }); diff --git a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts index 14770ca5d6c..67d13ef1e4f 100644 --- a/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts +++ b/apps/web/src/app/vault/components/setup-extension/setup-extension.component.ts @@ -15,6 +15,7 @@ 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, @@ -25,6 +26,7 @@ import { VaultIcons } from "@bitwarden/vault"; 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, @@ -32,10 +34,11 @@ import { } from "./add-extension-later-dialog.component"; import { AddExtensionVideosComponent } from "./add-extension-videos.component"; -const SetupExtensionState = { +export const SetupExtensionState = { Loading: "loading", NeedsExtension: "needs-extension", Success: "success", + ManualOpen: "manual-open", } as const; type SetupExtensionState = UnionOfValues; @@ -51,6 +54,7 @@ type SetupExtensionState = UnionOfValues; IconModule, RouterModule, AddExtensionVideosComponent, + ManuallyOpenExtensionComponent, ], }) export class SetupExtensionComponent implements OnInit, OnDestroy { @@ -63,6 +67,7 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { private stateProvider = inject(StateProvider); private accountService = inject(AccountService); private document = inject(DOCUMENT); + private anonLayoutWrapperDataService = inject(AnonLayoutWrapperDataService); protected SetupExtensionState = SetupExtensionState; protected PartyIcon = VaultIcons.Party; @@ -153,8 +158,21 @@ export class SetupExtensionComponent implements OnInit, OnDestroy { } /** Opens the browser extension */ - openExtension() { - void this.webBrowserExtensionInteractionService.openExtension(); + 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: VaultIcons.BrowserExtensionIcon, + hideIcon: false, + hideCardWrapper: false, + maxWidth: "md", + }); + }); } /** Update local state to never show this page again. */ diff --git a/apps/web/src/app/vault/services/web-browser-interaction.service.ts b/apps/web/src/app/vault/services/web-browser-interaction.service.ts index 1f91942591b..ed5e2ef9948 100644 --- a/apps/web/src/app/vault/services/web-browser-interaction.service.ts +++ b/apps/web/src/app/vault/services/web-browser-interaction.service.ts @@ -25,7 +25,7 @@ import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum * used to allow for the extension to open and then emit to the message. * NOTE: This value isn't computed by any means, it is just a reasonable timeout for the extension to respond. */ -const OPEN_RESPONSE_TIMEOUT_MS = 1500; +const OPEN_RESPONSE_TIMEOUT_MS = 2000; /** * Timeout for checking if the extension is installed. diff --git a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts index 34fdc5b60fc..4b570df9814 100644 --- a/libs/components/src/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/components/src/anon-layout/anon-layout-wrapper.component.ts @@ -157,6 +157,14 @@ export class AnonLayoutWrapperComponent implements OnInit { this.hideCardWrapper = data.hideCardWrapper; } + if (data.hideIcon !== undefined) { + this.hideIcon = data.hideIcon; + } + + if (data.maxWidth !== undefined) { + this.maxWidth = data.maxWidth; + } + // Manually fire change detection to avoid ExpressionChangedAfterItHasBeenCheckedError // when setting the page data from a service this.changeDetectorRef.detectChanges();