1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-17564] Prompt Browser Extension (#13349)

* add browser extension prompt page with initial loading state

* add browser extension icon

* move browser extension prompt to state

* add installation link for error state

* automatically open extension when possible for browser-reprompt-page

* refactor browser tabs query into a standalone method

* add success message state for auto-opening browsers

* Refactor `VaultOnboardingMessages` to `VaultMessages` to be more generic

* add auto-open extension messages to `VaultMessages` enum

* add bitwarden icon

* Add manual error state for firefox users

* add extension prompt routing

* fix incorrect imports

* add mobile screen for browser prompt

* remove comment

* fix typo in code comment

* update key for `checkBwInstalled` method

* add check for safari before attempting to send a message

* break translation for manual opening into two parts
This commit is contained in:
Nick Krantz
2025-02-19 13:00:07 -06:00
committed by GitHub
parent 661ee03698
commit dae4f7b3cc
24 changed files with 869 additions and 32 deletions

View File

@@ -92,6 +92,8 @@ import { CredentialGeneratorComponent } from "./tools/credential-generator/crede
import { ReportsModule } from "./tools/reports";
import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send-access";
import { SendComponent } from "./tools/send/send.component";
import { BrowserExtensionPromptInstallComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt-install.component";
import { BrowserExtensionPromptComponent } from "./vault/components/browser-extension-prompt/browser-extension-prompt.component";
import { VaultModule } from "./vault/individual-vault/vault.module";
const routes: Routes = [
@@ -695,6 +697,23 @@ const routes: Routes = [
maxWidth: "3xl",
} satisfies AnonLayoutWrapperData,
},
{
path: "browser-extension-prompt",
data: {
pageIcon: VaultIcons.BrowserExtensionIcon,
} satisfies AnonLayoutWrapperData,
children: [
{
path: "",
component: BrowserExtensionPromptComponent,
},
{
path: "",
component: BrowserExtensionPromptInstallComponent,
outlet: "secondary",
},
],
},
],
},
{

View File

@@ -0,0 +1,4 @@
<div class="tw-text-center" *ngIf="shouldShow$ | async">
<p class="tw-mb-0">{{ "doNotHaveExtension" | i18n }}</p>
<a bitLink [href]="webStoreUrl">{{ "installExtension" | i18n }}</a>
</div>

View File

@@ -0,0 +1,145 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { BehaviorSubject } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
import { BrowserExtensionPromptInstallComponent } from "./browser-extension-prompt-install.component";
describe("BrowserExtensionInstallComponent", () => {
let fixture: ComponentFixture<BrowserExtensionPromptInstallComponent>;
let component: BrowserExtensionPromptInstallComponent;
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
const getDevice = jest.fn();
beforeEach(async () => {
getDevice.mockClear();
await TestBed.configureTestingModule({
providers: [
{
provide: BrowserExtensionPromptService,
useValue: { pageState$ },
},
{
provide: I18nService,
useValue: { t: (key: string) => key },
},
{
provide: PlatformUtilsService,
useValue: { getDevice },
},
],
}).compileComponents();
fixture = TestBed.createComponent(BrowserExtensionPromptInstallComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("only shows during error state", () => {
expect(fixture.nativeElement.textContent).toBe("");
pageState$.next(BrowserPromptState.Success);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe("");
pageState$.next(BrowserPromptState.Error);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).not.toBe("");
pageState$.next(BrowserPromptState.ManualOpen);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).not.toBe("");
});
describe("error state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Error);
fixture.detectChanges();
});
it("shows error text", () => {
const errorText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(errorText.textContent).toBe("doNotHaveExtension");
});
it("links to bitwarden installation page by default", () => {
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://bitwarden.com/download/#downloads-web-browser",
);
});
it("links to bitwarden installation page for Chrome", () => {
getDevice.mockReturnValue(DeviceType.ChromeBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://chrome.google.com/webstore/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb",
);
});
it("links to bitwarden installation page for Firefox", () => {
getDevice.mockReturnValue(DeviceType.FirefoxBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/",
);
});
it("links to bitwarden installation page for Safari", () => {
getDevice.mockReturnValue(DeviceType.SafariBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12",
);
});
it("links to bitwarden installation page for Opera", () => {
getDevice.mockReturnValue(DeviceType.OperaBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://addons.opera.com/extensions/details/bitwarden-free-password-manager/",
);
});
it("links to bitwarden installation page for Edge", () => {
getDevice.mockReturnValue(DeviceType.EdgeBrowser);
component.ngOnInit();
fixture.detectChanges();
const link = fixture.debugElement.query(By.css("a")).nativeElement;
expect(link.getAttribute("href")).toBe(
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
);
});
});
});

View File

@@ -0,0 +1,66 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { map } from "rxjs";
import { DeviceType } from "@bitwarden/common/enums";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { LinkModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
/** Device specific Urls for the extension */
const WebStoreUrls: Partial<Record<DeviceType, string>> = {
[DeviceType.ChromeBrowser]:
"https://chrome.google.com/webstore/detail/bitwarden-password-manage/nngceckbapebfimnlniiiahkandclblb",
[DeviceType.FirefoxBrowser]:
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/",
[DeviceType.SafariBrowser]: "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12",
[DeviceType.OperaBrowser]:
"https://addons.opera.com/extensions/details/bitwarden-free-password-manager/",
[DeviceType.EdgeBrowser]:
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh",
};
@Component({
selector: "vault-browser-extension-prompt-install",
templateUrl: "./browser-extension-prompt-install.component.html",
standalone: true,
imports: [CommonModule, I18nPipe, LinkModule],
})
export class BrowserExtensionPromptInstallComponent implements OnInit {
/** The install link should only show for the error states */
protected shouldShow$ = this.browserExtensionPromptService.pageState$.pipe(
map((state) => state === BrowserPromptState.Error || state === BrowserPromptState.ManualOpen),
);
/** All available page states */
protected BrowserPromptState = BrowserPromptState;
/**
* Installation link for the extension
*/
protected webStoreUrl: string = "https://bitwarden.com/download/#downloads-web-browser";
constructor(
private browserExtensionPromptService: BrowserExtensionPromptService,
private platformService: PlatformUtilsService,
) {}
ngOnInit(): void {
this.setBrowserStoreLink();
}
/** If available, set web store specific URL for the extension */
private setBrowserStoreLink(): void {
const deviceType = this.platformService.getDevice();
const platformSpecificUrl = WebStoreUrls[deviceType];
if (platformSpecificUrl) {
this.webStoreUrl = platformSpecificUrl;
}
}
}

View File

@@ -0,0 +1,44 @@
<div class="tw-text-center" *ngIf="pageState$ | async as pageState">
<ng-container *ngIf="pageState === BrowserPromptState.Loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600" aria-hidden="true"></i>
<p bitTypography="body1" class="tw-mb-0 tw-mt-2">{{ "openingExtension" | i18n }}</p>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.Error">
<p bitTypography="body1" class="tw-mb-4 tw-text-xl">{{ "openingExtensionError" | i18n }}</p>
<button
bitButton
buttonType="primary"
type="button"
(click)="openExtension()"
id="bw-extension-prompt-button"
>
{{ "openExtension" | i18n }}
<i class="bwi bwi-external-link tw-ml-2" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.Success">
<i class="bwi tw-text-2xl bwi-check-circle tw-text-success-700" aria-hidden="true"></i>
<p bitTypography="body1" class="tw-mb-4 tw-text-xl">
{{ "openedExtensionViewAtRiskPasswords" | i18n }}
</p>
</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>
</ng-container>
<ng-container *ngIf="pageState === BrowserPromptState.MobileBrowser">
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
{{ "reopenLinkOnDesktop" | i18n }}
</p>
</ng-container>
</div>

View File

@@ -0,0 +1,104 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { BehaviorSubject } from "rxjs";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "../../services/browser-extension-prompt.service";
import { BrowserExtensionPromptComponent } from "./browser-extension-prompt.component";
describe("BrowserExtensionPromptComponent", () => {
let fixture: ComponentFixture<BrowserExtensionPromptComponent>;
const start = jest.fn();
const pageState$ = new BehaviorSubject(BrowserPromptState.Loading);
beforeEach(async () => {
start.mockClear();
await TestBed.configureTestingModule({
providers: [
{
provide: BrowserExtensionPromptService,
useValue: { start, pageState$ },
},
{
provide: I18nService,
useValue: { t: (key: string) => key },
},
],
}).compileComponents();
fixture = TestBed.createComponent(BrowserExtensionPromptComponent);
fixture.detectChanges();
});
it("calls start on initialization", () => {
expect(start).toHaveBeenCalledTimes(1);
});
describe("loading state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Loading);
fixture.detectChanges();
});
it("shows loading text", () => {
const element = fixture.nativeElement;
expect(element.textContent.trim()).toBe("openingExtension");
});
});
describe("error state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Error);
fixture.detectChanges();
});
it("shows error text", () => {
const errorText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(errorText.textContent.trim()).toBe("openingExtensionError");
});
});
describe("success state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.Success);
fixture.detectChanges();
});
it("shows success message", () => {
const successText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(successText.textContent.trim()).toBe("openedExtensionViewAtRiskPasswords");
});
});
describe("mobile state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.MobileBrowser);
fixture.detectChanges();
});
it("shows mobile message", () => {
const mobileText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(mobileText.textContent.trim()).toBe("reopenLinkOnDesktop");
});
});
describe("manual error state", () => {
beforeEach(() => {
pageState$.next(BrowserPromptState.ManualOpen);
fixture.detectChanges();
});
it("shows manual open error message", () => {
const manualText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart1");
expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart2");
});
});
});

View File

@@ -0,0 +1,37 @@
import { CommonModule } from "@angular/common";
import { Component, 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";
@Component({
selector: "vault-browser-extension-prompt",
templateUrl: "./browser-extension-prompt.component.html",
standalone: true,
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule],
})
export class BrowserExtensionPromptComponent implements OnInit {
/** Current state of the prompt page */
protected pageState$ = this.browserExtensionPromptService.pageState$;
/** All available page states */
protected BrowserPromptState = BrowserPromptState;
protected BitwardenIcon = VaultIcons.BitwardenIcon;
constructor(private browserExtensionPromptService: BrowserExtensionPromptService) {}
ngOnInit(): void {
this.browserExtensionPromptService.start();
}
openExtension(): void {
this.browserExtensionPromptService.openExtension();
}
}

View File

@@ -16,7 +16,7 @@ import { StateProvider } from "@bitwarden/common/platform/state";
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
import { VaultOnboardingComponent } from "./vault-onboarding.component";
@@ -158,7 +158,7 @@ describe("VaultOnboardingComponent", () => {
it("should call getMessages when showOnboarding is true", () => {
const messageEventSubject = new Subject<MessageEvent>();
const messageEvent = new MessageEvent("message", {
data: VaultOnboardingMessages.HasBwInstalled,
data: VaultMessages.HasBwInstalled,
});
const getMessagesSpy = jest.spyOn(component, "getMessages");
@@ -168,7 +168,7 @@ describe("VaultOnboardingComponent", () => {
void fixture.whenStable().then(() => {
expect(window.postMessage).toHaveBeenCalledWith({
command: VaultOnboardingMessages.checkBwInstalled,
command: VaultMessages.checkBwInstalled,
});
expect(getMessagesSpy).toHaveBeenCalled();
});
@@ -188,7 +188,7 @@ describe("VaultOnboardingComponent", () => {
installExtension: false,
});
});
const eventData = { data: { command: VaultOnboardingMessages.HasBwInstalled } };
const eventData = { data: { command: VaultMessages.HasBwInstalled } };
(component as any).showOnboarding = true;

View File

@@ -24,7 +24,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LinkModule } from "@bitwarden/components";
@@ -106,12 +106,12 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
void this.getMessages(event);
});
window.postMessage({ command: VaultOnboardingMessages.checkBwInstalled });
window.postMessage({ command: VaultMessages.checkBwInstalled });
}
}
async getMessages(event: any) {
if (event.data.command === VaultOnboardingMessages.HasBwInstalled && this.showOnboarding) {
if (event.data.command === VaultMessages.HasBwInstalled && this.showOnboarding) {
const currentTasks = await firstValueFrom(this.onboardingTasks$);
const updatedTasks = {
createAccount: currentTasks.createAccount,

View File

@@ -0,0 +1,173 @@
import { TestBed } from "@angular/core/testing";
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
import {
BrowserExtensionPromptService,
BrowserPromptState,
} from "./browser-extension-prompt.service";
describe("BrowserExtensionPromptService", () => {
let service: BrowserExtensionPromptService;
const setAnonLayoutWrapperData = jest.fn();
const isFirefox = jest.fn().mockReturnValue(false);
const postMessage = jest.fn();
window.postMessage = postMessage;
beforeEach(() => {
setAnonLayoutWrapperData.mockClear();
postMessage.mockClear();
isFirefox.mockClear();
TestBed.configureTestingModule({
providers: [
BrowserExtensionPromptService,
{ provide: AnonLayoutWrapperDataService, useValue: { setAnonLayoutWrapperData } },
{ provide: PlatformUtilsService, useValue: { isFirefox } },
],
});
jest.useFakeTimers();
service = TestBed.inject(BrowserExtensionPromptService);
});
afterEach(() => {
jest.clearAllTimers();
jest.useRealTimers();
});
it("defaults page state to loading", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.Loading);
done();
});
});
describe("start", () => {
it("posts message to check for extension", () => {
service.start();
expect(window.postMessage).toHaveBeenCalledWith({
command: VaultMessages.checkBwInstalled,
});
});
it("sets timeout for error state", () => {
service.start();
expect(service["extensionCheckTimeout"]).not.toBeNull();
});
it("attempts to open the extension when installed", () => {
service.start();
window.dispatchEvent(
new MessageEvent("message", { data: { command: VaultMessages.HasBwInstalled } }),
);
expect(window.postMessage).toHaveBeenCalledTimes(2);
expect(window.postMessage).toHaveBeenCalledWith({ command: VaultMessages.OpenPopup });
});
});
describe("success state", () => {
beforeEach(() => {
service.start();
window.dispatchEvent(
new MessageEvent("message", { data: { command: VaultMessages.PopupOpened } }),
);
});
it("sets layout title", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "openedExtension" },
});
});
it("sets success page state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.Success);
done();
});
});
it("clears the error timeout", () => {
expect(service["extensionCheckTimeout"]).toBeUndefined();
});
});
describe("firefox", () => {
beforeEach(() => {
isFirefox.mockReturnValue(true);
service.start();
});
afterEach(() => {
isFirefox.mockReturnValue(false);
});
it("sets manual open state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.ManualOpen);
done();
});
});
it("sets error state after timeout", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "somethingWentWrong" },
});
});
});
describe("mobile state", () => {
beforeEach(() => {
Utils.isMobileBrowser = true;
service.start();
});
afterEach(() => {
Utils.isMobileBrowser = false;
});
it("sets mobile state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.MobileBrowser);
done();
});
});
it("sets desktop required title", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "desktopRequired" },
});
});
it("clears the error timeout", () => {
expect(service["extensionCheckTimeout"]).toBeUndefined();
});
});
describe("error state", () => {
beforeEach(() => {
service.start();
jest.advanceTimersByTime(1000);
});
it("sets error state", (done) => {
service.pageState$.subscribe((state) => {
expect(state).toBe(BrowserPromptState.Error);
done();
});
});
it("sets error state after timeout", () => {
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: { key: "somethingWentWrong" },
});
});
});
});

View File

@@ -0,0 +1,125 @@
import { DestroyRef, Injectable } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { BehaviorSubject, fromEvent } from "rxjs";
import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
export enum BrowserPromptState {
Loading = "loading",
Error = "error",
Success = "success",
ManualOpen = "manualOpen",
MobileBrowser = "mobileBrowser",
}
type PromptErrorStates = BrowserPromptState.Error | BrowserPromptState.ManualOpen;
@Injectable({
providedIn: "root",
})
export class BrowserExtensionPromptService {
private _pageState$ = new BehaviorSubject<BrowserPromptState>(BrowserPromptState.Loading);
/** Current state of the prompt page */
pageState$ = this._pageState$.asObservable();
/** Timeout identifier for extension check */
private extensionCheckTimeout: number | undefined;
constructor(
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private destroyRef: DestroyRef,
private platformUtilsService: PlatformUtilsService,
) {}
start(): void {
if (Utils.isMobileBrowser) {
this.setMobileState();
return;
}
// Firefox does not support automatically opening the extension,
// it currently requires a user gesture within the context of the extension to open.
// Show message to direct the user to manually open the extension.
// Mozilla Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1799344
if (this.platformUtilsService.isFirefox()) {
this.setErrorState(BrowserPromptState.ManualOpen);
return;
}
this.checkForBrowserExtension();
}
/** Post a message to the extension to open */
openExtension() {
window.postMessage({ command: VaultMessages.OpenPopup });
}
/** Send message checking for the browser extension */
private checkForBrowserExtension() {
fromEvent<MessageEvent>(window, "message")
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
void this.getMessages(event);
});
window.postMessage({ command: VaultMessages.checkBwInstalled });
// Wait a second for the extension to respond and open, else show the error state
this.extensionCheckTimeout = window.setTimeout(() => {
this.setErrorState();
}, 1000);
}
/** Handle window message events */
private getMessages(event: any) {
if (event.data.command === VaultMessages.HasBwInstalled) {
this.openExtension();
}
if (event.data.command === VaultMessages.PopupOpened) {
this.setSuccessState();
}
}
/** Show message that this page should be opened on a desktop browser */
private setMobileState() {
this.clearExtensionCheckTimeout();
this._pageState$.next(BrowserPromptState.MobileBrowser);
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "desktopRequired",
},
});
}
/** Show the open extension success state */
private setSuccessState() {
this.clearExtensionCheckTimeout();
this._pageState$.next(BrowserPromptState.Success);
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "openedExtension",
},
});
}
/** Show open extension error state */
private setErrorState(errorState?: PromptErrorStates) {
this.clearExtensionCheckTimeout();
this._pageState$.next(errorState ?? BrowserPromptState.Error);
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "somethingWentWrong",
},
});
}
private clearExtensionCheckTimeout() {
window.clearTimeout(this.extensionCheckTimeout);
this.extensionCheckTimeout = undefined;
}
}

View File

@@ -9275,7 +9275,12 @@
},
"deviceManagementDesc":{
"message": "Configure device management for Bitwarden using the implementation guide for your platform."
},
"desktopRequired": {
"message": "Desktop required"
},
"reopenLinkOnDesktop": {
"message": "Reopen this link from your email on a desktop."
},
"integrationCardTooltip":{
"message": "Launch $INTEGRATION$ implementation guide.",
@@ -10270,6 +10275,38 @@
"organizationNameMaxLength": {
"message": "Organization name cannot exceed 50 characters."
},
"openingExtension": {
"message": "Opening the Bitwarden browser extension"
},
"somethingWentWrong":{
"message": "Something went wrong..."
},
"openingExtensionError": {
"message": "We had trouble opening the Bitwarden browser extension. Click the button to open it now."
},
"openExtension": {
"message": "Open extension"
},
"doNotHaveExtension": {
"message": "Don't have the Bitwarden browser extension?"
},
"installExtension": {
"message": "Install extension"
},
"openedExtension": {
"message": "Opened the browser extension"
},
"openedExtensionViewAtRiskPasswords": {
"message": "Successfully opened the Bitwarden browser extension. You can now review your at-risk passwords."
},
"openExtensionManuallyPart1": {
"message": "We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon",
"description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'"
},
"openExtensionManuallyPart2": {
"message": "from the toolbar.",
"description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'"
},
"resellerRenewalWarningMsg": {
"message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {