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:
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(" ");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user