mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +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:
@@ -1,6 +1,6 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
|
||||
import { postWindowMessage, sendMockExtensionMessage } from "../spec/testing-utils";
|
||||
|
||||
@@ -34,10 +34,10 @@ describe("ContentMessageHandler", () => {
|
||||
const mockPostMessage = jest.fn();
|
||||
window.postMessage = mockPostMessage;
|
||||
|
||||
postWindowMessage({ command: VaultOnboardingMessages.checkBwInstalled });
|
||||
postWindowMessage({ command: VaultMessages.checkBwInstalled });
|
||||
|
||||
expect(mockPostMessage).toHaveBeenCalledWith({
|
||||
command: VaultOnboardingMessages.HasBwInstalled,
|
||||
command: VaultMessages.HasBwInstalled,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
|
||||
import {
|
||||
ContentMessageWindowData,
|
||||
@@ -26,16 +26,17 @@ const windowMessageHandlers: ContentMessageWindowEventHandlers = {
|
||||
handleAuthResultMessage(data, referrer),
|
||||
webAuthnResult: ({ data, referrer }: { data: any; referrer: string }) =>
|
||||
handleWebAuthnResultMessage(data, referrer),
|
||||
checkIfBWExtensionInstalled: () => handleExtensionInstallCheck(),
|
||||
[VaultMessages.checkBwInstalled]: () => handleExtensionInstallCheck(),
|
||||
duoResult: ({ data, referrer }: { data: any; referrer: string }) =>
|
||||
handleDuoResultMessage(data, referrer),
|
||||
[VaultMessages.OpenPopup]: () => handleOpenPopupMessage(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the post to the web vault showing the extension has been installed
|
||||
*/
|
||||
function handleExtensionInstallCheck() {
|
||||
window.postMessage({ command: VaultOnboardingMessages.HasBwInstalled });
|
||||
window.postMessage({ command: VaultMessages.HasBwInstalled });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +72,10 @@ function handleWebAuthnResultMessage(data: ContentMessageWindowData, referrer: s
|
||||
sendExtensionRuntimeMessage({ command, data: data.data, remember, referrer });
|
||||
}
|
||||
|
||||
function handleOpenPopupMessage() {
|
||||
sendExtensionRuntimeMessage({ command: VaultMessages.OpenPopup });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the window message event.
|
||||
*
|
||||
|
||||
@@ -1592,13 +1592,16 @@ export default class MainBackground {
|
||||
}
|
||||
|
||||
async openPopup() {
|
||||
// Chrome APIs cannot open popup
|
||||
const browserAction = BrowserApi.getBrowserAction();
|
||||
|
||||
// TODO: Do we need to open this popup?
|
||||
if (!this.isSafari) {
|
||||
if ("openPopup" in browserAction && typeof browserAction.openPopup === "function") {
|
||||
await browserAction.openPopup();
|
||||
return;
|
||||
}
|
||||
await SafariApp.sendMessageToApp("showPopover", null, true);
|
||||
|
||||
if (this.isSafari) {
|
||||
await SafariApp.sendMessageToApp("showPopover", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
async reseedStorage() {
|
||||
|
||||
@@ -289,7 +289,7 @@ export default class RuntimeBackground {
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
await this.main.openPopup();
|
||||
await this.openPopup();
|
||||
break;
|
||||
case "bgUpdateContextMenu":
|
||||
case "editedCipher":
|
||||
@@ -405,13 +405,40 @@ export default class RuntimeBackground {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/** Returns the browser tabs that have the web vault open */
|
||||
private async getBwTabs() {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
const urlObj = new URL(vaultUrl);
|
||||
|
||||
return await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });
|
||||
}
|
||||
|
||||
private async openPopup() {
|
||||
await this.main.openPopup();
|
||||
|
||||
const announcePopupOpen = async () => {
|
||||
const isOpen = await this.platformUtilsService.isViewOpen();
|
||||
const tabs = await this.getBwTabs();
|
||||
|
||||
if (isOpen && tabs.length > 0) {
|
||||
// Send message to all vault tabs that the extension has opened
|
||||
for (const tab of tabs) {
|
||||
await BrowserApi.executeScriptInTab(tab.id, {
|
||||
file: "content/send-popup-open-message.js",
|
||||
runAt: "document_end",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Give the popup a buffer to open
|
||||
setTimeout(announcePopupOpen, 100);
|
||||
}
|
||||
|
||||
async sendBwInstalledMessageToVault() {
|
||||
try {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const vaultUrl = env.getWebVaultUrl();
|
||||
const urlObj = new URL(vaultUrl);
|
||||
|
||||
const tabs = await BrowserApi.tabsQuery({ url: `${urlObj.href}*` });
|
||||
const tabs = await this.getBwTabs();
|
||||
|
||||
if (!tabs?.length) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
|
||||
(function (globalContext) {
|
||||
globalContext.postMessage({ command: VaultOnboardingMessages.HasBwInstalled });
|
||||
globalContext.postMessage({ command: VaultMessages.HasBwInstalled });
|
||||
})(window);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { VaultMessages } from "@bitwarden/common/vault/enums/vault-messages.enum";
|
||||
|
||||
(function (globalContext) {
|
||||
// Send a message to the window that the popup opened
|
||||
globalContext.postMessage({ command: VaultMessages.PopupOpened });
|
||||
})(window);
|
||||
@@ -207,6 +207,7 @@ const mainConfig = {
|
||||
"./src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts",
|
||||
"encrypt-worker": "../../libs/common/src/key-management/crypto/services/encrypt.worker.ts",
|
||||
"content/send-on-installed-message": "./src/vault/content/send-on-installed-message.ts",
|
||||
"content/send-popup-open-message": "./src/vault/content/send-popup-open-message.ts",
|
||||
},
|
||||
optimization: {
|
||||
minimize: ENV !== "development",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
8
libs/common/src/vault/enums/vault-messages.enum.ts
Normal file
8
libs/common/src/vault/enums/vault-messages.enum.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
const VaultMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
OpenPopup: "openPopup",
|
||||
PopupOpened: "popupOpened",
|
||||
} as const;
|
||||
|
||||
export { VaultMessages };
|
||||
@@ -1,6 +0,0 @@
|
||||
const VaultOnboardingMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
} as const;
|
||||
|
||||
export { VaultOnboardingMessages };
|
||||
20
libs/vault/src/icons/bitwarden-icon.ts
Normal file
20
libs/vault/src/icons/bitwarden-icon.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const BitwardenIcon = svgIcon`
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_11934_25684)">
|
||||
<path d="M17.3333 0H2.66667C1.19391 0 0 1.19391 0 2.66667V17.3333C0 18.8061 1.19391 20 2.66667 20H17.3333C18.8061 20 20 18.8061 20 17.3333V2.66667C20 1.19391 18.8061 0 17.3333 0Z" fill="#175DDC"/>
|
||||
<mask id="mask0_11934_25684" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="3" width="14" height="14">
|
||||
<path d="M17 3H3V17H17V3Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_11934_25684)">
|
||||
<path d="M15.6599 3.17501C15.607 3.11944 15.5435 3.07526 15.4731 3.04507C15.4027 3.01489 15.327 2.99958 15.2504 3.00001H4.75052C4.67396 2.99958 4.59784 3.01489 4.5274 3.04507C4.45696 3.07526 4.39352 3.11944 4.34102 3.17501C4.28546 3.22751 4.24127 3.29138 4.21109 3.36182C4.1809 3.43226 4.16559 3.50794 4.16602 3.58451V10.5844C4.16821 11.1173 4.27146 11.6449 4.47052 12.1393C4.65996 12.6271 4.91458 13.0874 5.22739 13.5069C5.54895 13.9274 5.90901 14.3163 6.30276 14.6698C6.66807 15.0049 7.05306 15.3186 7.4551 15.6086C7.8051 15.8571 8.1726 16.0925 8.5576 16.3148C8.9426 16.537 9.21431 16.6871 9.3731 16.7654C9.53322 16.8441 9.66272 16.9063 9.75897 16.9474C9.83422 16.9837 9.91694 17.0016 10.0005 16.9999C10.0827 17.0012 10.1641 16.982 10.2376 16.9448C10.3356 16.9019 10.4633 16.8415 10.6252 16.7628C10.7871 16.684 11.0627 16.5335 11.4407 16.3121C11.8187 16.0908 12.1906 15.8545 12.5432 15.606C12.9457 15.3155 13.3311 15.0023 13.6973 14.6671C14.0915 14.3141 14.4515 13.9247 14.7727 13.5043C15.085 13.0843 15.3397 12.6245 15.5295 12.1367C15.729 11.6423 15.8323 11.1147 15.834 10.5818V3.58188C15.8345 3.50576 15.8192 3.43051 15.789 3.36051C15.7588 3.29051 15.715 3.22751 15.6599 3.17501ZM14.3063 10.6483C14.3063 13.1858 10.0005 15.3654 10.0005 15.3654V4.49975H14.3063V10.6483Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_11934_25684">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
17
libs/vault/src/icons/browser-extension.ts
Normal file
17
libs/vault/src/icons/browser-extension.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { svgIcon } from "@bitwarden/components";
|
||||
|
||||
export const BrowserExtensionIcon = svgIcon`
|
||||
<svg width="115" height="114" viewBox="0 0 115 114" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect class="tw-stroke-art-primary" width="107.16" height="70.68" rx="10" transform="matrix(-1 0 0 1 111.08 21.66)" stroke-width="4"/>
|
||||
<rect class="tw-stroke-art-accent" x="77.88" y="36.91" width="24.79" height="33.34" rx="5" stroke-width="2"/>
|
||||
<rect class="tw-fill-art-primary" x="97.97" y="25.65" width="5.7" height="5.7" rx="2" />
|
||||
<path class="tw-fill-art-accent" d="M96.9272 52.2068L96.9822 50.6928L95.7022 51.5048L95.3718 50.9267L96.7207 50.2248L95.3718 49.5228L95.7022 48.9447L96.9822 49.7568L96.9272 48.2427H97.5879L97.5328 49.7568L98.8129 48.9447L99.1432 49.5228L97.7943 50.2248L99.1432 50.9267L98.8129 51.5048L97.5328 50.6928L97.5879 52.2068H96.9272Z" />
|
||||
<path class="tw-fill-art-accent" d="M92.0822 52.2068L92.1373 50.6928L90.8572 51.5048L90.5269 50.9267L91.8757 50.2248L90.5269 49.5228L90.8572 48.9447L92.1373 49.7568L92.0822 48.2427H92.7429L92.6878 49.7568L93.9679 48.9447L94.2982 49.5228L92.9494 50.2248L94.2982 50.9267L93.9679 51.5048L92.6878 50.6928L92.7429 52.2068H92.0822Z" />
|
||||
<path class="tw-fill-art-accent" d="M87.2371 52.2068L87.2922 50.6928L86.0121 51.5048L85.6818 50.9267L87.0307 50.2248L85.6818 49.5228L86.0121 48.9447L87.2922 49.7568L87.2371 48.2427H87.8978L87.8427 49.7568L89.1228 48.9447L89.4532 49.5228L88.1043 50.2248L89.4532 50.9267L89.1228 51.5048L87.8427 50.6928L87.8978 52.2068H87.2371Z" />
|
||||
<path class="tw-fill-art-accent" d="M82.3921 52.2068L82.4472 50.6928L81.1671 51.5048L80.8368 50.9267L82.1857 50.2248L80.8368 49.5228L81.1671 48.9447L82.4472 49.7568L82.3921 48.2427H83.0528L82.9978 49.7568L84.2778 48.9447L84.6082 49.5228L83.2593 50.2248L84.6082 50.9267L84.2778 51.5048L82.9978 50.6928L83.0528 52.2068H82.3921Z" />
|
||||
<path class="tw-stroke-art-primary" d="M25.58 29.07V28.5" stroke-width="4" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-art-primary" d="M18.74 29.07V28.5" stroke-width="4" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-art-primary" d="M11.9 29.07V28.5" stroke-width="4" stroke-linecap="round"/>
|
||||
<path class="tw-stroke-art-primary" d="M110.51 33.6054H4.49" stroke-width="2"/>
|
||||
</svg>
|
||||
`;
|
||||
@@ -4,3 +4,5 @@ export * from "./vault";
|
||||
export * from "./empty-trash";
|
||||
export * from "./exclamation-triangle";
|
||||
export * from "./user-lock";
|
||||
export * from "./browser-extension";
|
||||
export * from "./bitwarden-icon";
|
||||
|
||||
Reference in New Issue
Block a user