1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[PM-24119] Manually open extension message (#15827)

* refactor manually open extension error message to a separate component

* allow icons and max width to be updated via setAnonLayoutWrapperData

* set error state when the extension fails to open

* bump timeout to 2000ms. I was seeing false error states when attempting to open the extension

* fix initialization of css variables
This commit is contained in:
Nick Krantz
2025-08-05 08:42:05 -05:00
committed by GitHub
parent 920145b393
commit 7145092889
10 changed files with 89 additions and 20 deletions

View File

@@ -26,14 +26,7 @@
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.ManualOpen">
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
{{ "openExtensionManuallyPart1" | i18n }}
<bit-icon
[icon]="BitwardenIcon"
class="[&>svg]:tw-align-baseline [&>svg]:-tw-mb-[2px]"
></bit-icon>
{{ "openExtensionManuallyPart2" | i18n }}
</p>
<vault-manually-open-extension></vault-manually-open-extension>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.MobileBrowser">

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
{{ "openExtensionManuallyPart1" | i18n }}
<bit-icon
[icon]="BitwardenIcon"
class="[&>svg]:tw-align-baseline [&>svg]:-tw-mb-[2px]"
></bit-icon>
{{ "openExtensionManuallyPart2" | i18n }}
</p>

View File

@@ -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;
}

View File

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

View File

@@ -54,3 +54,7 @@
</a>
</p>
</section>
<section *ngIf="state === SetupExtensionState.ManualOpen" aria-live="polite" class="tw-text-center">
<vault-manually-open-extension></vault-manually-open-extension>
</section>

View File

@@ -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<SetupExtensionComponent>;
@@ -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<boolean | null>(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",
});
}));
});
});
});

View File

@@ -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<typeof SetupExtensionState>;
@@ -51,6 +54,7 @@ type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
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. */

View File

@@ -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.

View File

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