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();