mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +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>
|
||||||
|
|
||||||
<ng-container *ngIf="pageState === BrowserPromptState.ManualOpen">
|
<ng-container *ngIf="pageState === BrowserPromptState.ManualOpen">
|
||||||
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
|
<vault-manually-open-extension></vault-manually-open-extension>
|
||||||
{{ "openExtensionManuallyPart1" | i18n }}
|
|
||||||
<bit-icon
|
|
||||||
[icon]="BitwardenIcon"
|
|
||||||
class="[&>svg]:tw-align-baseline [&>svg]:-tw-mb-[2px]"
|
|
||||||
></bit-icon>
|
|
||||||
{{ "openExtensionManuallyPart2" | i18n }}
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="pageState === BrowserPromptState.MobileBrowser">
|
<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 { ButtonComponent, IconModule } from "@bitwarden/components";
|
||||||
import { I18nPipe } from "@bitwarden/ui-common";
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
import { VaultIcons } from "@bitwarden/vault";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BrowserExtensionPromptService,
|
BrowserExtensionPromptService,
|
||||||
BrowserPromptState,
|
BrowserPromptState,
|
||||||
} from "../../services/browser-extension-prompt.service";
|
} from "../../services/browser-extension-prompt.service";
|
||||||
|
import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "vault-browser-extension-prompt",
|
selector: "vault-browser-extension-prompt",
|
||||||
templateUrl: "./browser-extension-prompt.component.html",
|
templateUrl: "./browser-extension-prompt.component.html",
|
||||||
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule],
|
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule, ManuallyOpenExtensionComponent],
|
||||||
})
|
})
|
||||||
export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
||||||
/** Current state of the prompt page */
|
/** Current state of the prompt page */
|
||||||
@@ -22,8 +22,6 @@ export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
|
|||||||
/** All available page states */
|
/** All available page states */
|
||||||
protected BrowserPromptState = BrowserPromptState;
|
protected BrowserPromptState = BrowserPromptState;
|
||||||
|
|
||||||
protected BitwardenIcon = VaultIcons.BitwardenIcon;
|
|
||||||
|
|
||||||
/** Content of the meta[name="viewport"] element */
|
/** Content of the meta[name="viewport"] element */
|
||||||
private viewportContent: string | null = null;
|
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. */
|
/** CSS classes for the video container, pulled into the class only for readability. */
|
||||||
protected videoContainerClass = [
|
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]",
|
"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]`,
|
`[--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]`,
|
||||||
`[${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]`,
|
`[--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",
|
"after:tw-transition-opacity after:tw-duration-400 after:tw-ease-linear",
|
||||||
"before:tw-transition-opacity before:tw-duration-400 before:tw-ease-linear",
|
"before:tw-transition-opacity before:tw-duration-400 before:tw-ease-linear",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
|
|||||||
@@ -54,3 +54,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</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 { By } from "@angular/platform-browser";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
import { Router, RouterModule } from "@angular/router";
|
||||||
import { BehaviorSubject } from "rxjs";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
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 { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
||||||
|
|
||||||
import { SetupExtensionComponent } from "./setup-extension.component";
|
import { SetupExtensionComponent, SetupExtensionState } from "./setup-extension.component";
|
||||||
|
|
||||||
describe("SetupExtensionComponent", () => {
|
describe("SetupExtensionComponent", () => {
|
||||||
let fixture: ComponentFixture<SetupExtensionComponent>;
|
let fixture: ComponentFixture<SetupExtensionComponent>;
|
||||||
@@ -24,12 +26,14 @@ describe("SetupExtensionComponent", () => {
|
|||||||
const navigate = jest.fn().mockResolvedValue(true);
|
const navigate = jest.fn().mockResolvedValue(true);
|
||||||
const openExtension = jest.fn().mockResolvedValue(true);
|
const openExtension = jest.fn().mockResolvedValue(true);
|
||||||
const update = jest.fn().mockResolvedValue(true);
|
const update = jest.fn().mockResolvedValue(true);
|
||||||
|
const setAnonLayoutWrapperData = jest.fn();
|
||||||
const extensionInstalled$ = new BehaviorSubject<boolean | null>(null);
|
const extensionInstalled$ = new BehaviorSubject<boolean | null>(null);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
navigate.mockClear();
|
navigate.mockClear();
|
||||||
openExtension.mockClear();
|
openExtension.mockClear();
|
||||||
update.mockClear();
|
update.mockClear();
|
||||||
|
setAnonLayoutWrapperData.mockClear();
|
||||||
getFeatureFlag.mockClear().mockResolvedValue(true);
|
getFeatureFlag.mockClear().mockResolvedValue(true);
|
||||||
window.matchMedia = jest.fn().mockReturnValue(false);
|
window.matchMedia = jest.fn().mockReturnValue(false);
|
||||||
|
|
||||||
@@ -40,6 +44,7 @@ describe("SetupExtensionComponent", () => {
|
|||||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||||
{ provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } },
|
{ provide: WebBrowserInteractionService, useValue: { extensionInstalled$, openExtension } },
|
||||||
{ provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } },
|
{ provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } },
|
||||||
|
{ provide: AnonLayoutWrapperDataService, useValue: { setAnonLayoutWrapperData } },
|
||||||
{
|
{
|
||||||
provide: AccountService,
|
provide: AccountService,
|
||||||
useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) },
|
useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) },
|
||||||
@@ -136,6 +141,27 @@ describe("SetupExtensionComponent", () => {
|
|||||||
it("dismisses the extension page", () => {
|
it("dismisses the extension page", () => {
|
||||||
expect(update).toHaveBeenCalledTimes(1);
|
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 { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
|
||||||
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
|
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
|
||||||
import {
|
import {
|
||||||
|
AnonLayoutWrapperDataService,
|
||||||
ButtonComponent,
|
ButtonComponent,
|
||||||
DialogRef,
|
DialogRef,
|
||||||
DialogService,
|
DialogService,
|
||||||
@@ -25,6 +26,7 @@ import { VaultIcons } from "@bitwarden/vault";
|
|||||||
|
|
||||||
import { SETUP_EXTENSION_DISMISSED } from "../../guards/setup-extension-redirect.guard";
|
import { SETUP_EXTENSION_DISMISSED } from "../../guards/setup-extension-redirect.guard";
|
||||||
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
|
||||||
|
import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AddExtensionLaterDialogComponent,
|
AddExtensionLaterDialogComponent,
|
||||||
@@ -32,10 +34,11 @@ import {
|
|||||||
} from "./add-extension-later-dialog.component";
|
} from "./add-extension-later-dialog.component";
|
||||||
import { AddExtensionVideosComponent } from "./add-extension-videos.component";
|
import { AddExtensionVideosComponent } from "./add-extension-videos.component";
|
||||||
|
|
||||||
const SetupExtensionState = {
|
export const SetupExtensionState = {
|
||||||
Loading: "loading",
|
Loading: "loading",
|
||||||
NeedsExtension: "needs-extension",
|
NeedsExtension: "needs-extension",
|
||||||
Success: "success",
|
Success: "success",
|
||||||
|
ManualOpen: "manual-open",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
|
type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
|
||||||
@@ -51,6 +54,7 @@ type SetupExtensionState = UnionOfValues<typeof SetupExtensionState>;
|
|||||||
IconModule,
|
IconModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
AddExtensionVideosComponent,
|
AddExtensionVideosComponent,
|
||||||
|
ManuallyOpenExtensionComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class SetupExtensionComponent implements OnInit, OnDestroy {
|
export class SetupExtensionComponent implements OnInit, OnDestroy {
|
||||||
@@ -63,6 +67,7 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
|
|||||||
private stateProvider = inject(StateProvider);
|
private stateProvider = inject(StateProvider);
|
||||||
private accountService = inject(AccountService);
|
private accountService = inject(AccountService);
|
||||||
private document = inject(DOCUMENT);
|
private document = inject(DOCUMENT);
|
||||||
|
private anonLayoutWrapperDataService = inject(AnonLayoutWrapperDataService);
|
||||||
|
|
||||||
protected SetupExtensionState = SetupExtensionState;
|
protected SetupExtensionState = SetupExtensionState;
|
||||||
protected PartyIcon = VaultIcons.Party;
|
protected PartyIcon = VaultIcons.Party;
|
||||||
@@ -153,8 +158,21 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Opens the browser extension */
|
/** Opens the browser extension */
|
||||||
openExtension() {
|
async openExtension() {
|
||||||
void this.webBrowserExtensionInteractionService.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. */
|
/** 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.
|
* 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.
|
* 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.
|
* Timeout for checking if the extension is installed.
|
||||||
|
|||||||
@@ -157,6 +157,14 @@ export class AnonLayoutWrapperComponent implements OnInit {
|
|||||||
this.hideCardWrapper = data.hideCardWrapper;
|
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
|
// Manually fire change detection to avoid ExpressionChangedAfterItHasBeenCheckedError
|
||||||
// when setting the page data from a service
|
// when setting the page data from a service
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
|
|||||||
Reference in New Issue
Block a user