diff --git a/apps/browser/src/autofill/content/abstractions/content-message-handler.ts b/apps/browser/src/autofill/content/abstractions/content-message-handler.ts
index 6e1fda71a55..8231bd688c9 100644
--- a/apps/browser/src/autofill/content/abstractions/content-message-handler.ts
+++ b/apps/browser/src/autofill/content/abstractions/content-message-handler.ts
@@ -16,6 +16,7 @@ type ContentMessageWindowEventHandlers = {
authResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
webAuthnResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
duoResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
+ checkIfBWExtensionInstalled: () => void;
};
export {
diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts
index 03055961d7d..07532e606da 100644
--- a/apps/browser/src/autofill/content/content-message-handler.spec.ts
+++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts
@@ -25,6 +25,17 @@ describe("ContentMessageHandler", () => {
jest.clearAllMocks();
});
+ describe("handled web vault extension response", () => {
+ it("sends a message 'hasBWInstalled'", () => {
+ const mockPostMessage = jest.fn();
+ window.postMessage = mockPostMessage;
+
+ postWindowMessage({ command: "checkIfBWExtensionInstalled" });
+
+ expect(mockPostMessage).toHaveBeenCalled();
+ });
+ });
+
describe("handled window messages", () => {
it("ignores messages from other sources", () => {
postWindowMessage({ command: "authResult" }, "https://localhost/", null);
diff --git a/apps/browser/src/autofill/content/content-message-handler.ts b/apps/browser/src/autofill/content/content-message-handler.ts
index 0cb9f45b43e..8df9b90e11c 100644
--- a/apps/browser/src/autofill/content/content-message-handler.ts
+++ b/apps/browser/src/autofill/content/content-message-handler.ts
@@ -24,10 +24,18 @@ const windowMessageHandlers: ContentMessageWindowEventHandlers = {
handleAuthResultMessage(data, referrer),
webAuthnResult: ({ data, referrer }: { data: any; referrer: string }) =>
handleWebAuthnResultMessage(data, referrer),
+ checkIfBWExtensionInstalled: () => handleExtensionInstallCheck(),
duoResult: ({ data, referrer }: { data: any; referrer: string }) =>
handleDuoResultMessage(data, referrer),
};
+/**
+ * Handles the post to the web vault showing the extension has been installed
+ */
+function handleExtensionInstallCheck() {
+ window.postMessage({ command: "hasBWInstalled" });
+}
+
/**
* Handles the auth result message from the window.
*
diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html
index a6a71c3df8a..ae6ff2c36b2 100644
--- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.html
@@ -27,7 +27,12 @@
{{ "onboardingImportDataDetailsLink" | i18n }}
- {{ "onboardingImportDataDetailsPartTwo" | i18n }}
+
+ {{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }}
+
+ 0">
+ {{ "onboardingImportDataDetailsPartTwoWithOrgs" | i18n }}
+
@@ -35,6 +40,8 @@
[title]="'installBrowserExtension' | i18n"
icon="bwi-cli"
(click)="navigateToExtension()"
+ route="[]"
+ [completed]="onboardingTasks.installExtension"
>
{{ "installBrowserExtensionDetails" | i18n }}
diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts
index 6bf9609898a..7e7660ddbf8 100644
--- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.spec.ts
@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing";
import { mock, MockProxy } from "jest-mock-extended";
-import { of } from "rxjs";
+import { Subject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -38,11 +38,9 @@ describe("VaultOnboardingComponent", () => {
mockStateProvider = {
getActive: jest.fn().mockReturnValue(
of({
- vaultTasks: {
- createAccount: true,
- importData: false,
- installExtension: false,
- },
+ createAccount: true,
+ importData: false,
+ installExtension: false,
}),
),
};
@@ -61,9 +59,6 @@ describe("VaultOnboardingComponent", () => {
{ provide: StateProvider, useValue: mockStateProvider },
],
}).compileComponents();
- });
-
- beforeEach(() => {
fixture = TestBed.createComponent(VaultOnboardingComponent);
component = fixture.componentInstance;
setInstallExtLinkSpy = jest.spyOn(component, "setInstallExtLink");
@@ -71,6 +66,7 @@ describe("VaultOnboardingComponent", () => {
.spyOn(component, "individualVaultPolicyCheck")
.mockReturnValue(undefined);
jest.spyOn(component, "checkCreationDate").mockReturnValue(null);
+ jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
(component as any).vaultOnboardingService.vaultOnboardingState$ = of({
createAccount: true,
importData: false,
@@ -143,4 +139,43 @@ describe("VaultOnboardingComponent", () => {
expect(spy).toHaveBeenCalled();
});
});
+
+ describe("checkBrowserExtension", () => {
+ it("should call getMessages when showOnboarding is true", () => {
+ const messageEventSubject = new Subject();
+ const messageEvent = new MessageEvent("message", { data: "hasBWInstalled" });
+ const getMessagesSpy = jest.spyOn(component, "getMessages");
+
+ (component as any).showOnboarding = true;
+ component.checkForBrowserExtension();
+ messageEventSubject.next(messageEvent);
+
+ void fixture.whenStable().then(() => {
+ expect(window.postMessage).toHaveBeenCalledWith({ command: "checkIfBWExtensionInstalled" });
+ expect(getMessagesSpy).toHaveBeenCalled();
+ });
+ });
+
+ it("should set installExtension to true when hasBWInstalled command is passed", async () => {
+ const saveCompletedTasksSpy = jest.spyOn(
+ (component as any).vaultOnboardingService,
+ "setVaultOnboardingTasks",
+ );
+
+ (component as any).vaultOnboardingService.vaultOnboardingState$ = of({
+ createAccount: true,
+ importData: false,
+ installExtension: false,
+ });
+
+ const eventData = { data: { command: "hasBWInstalled" } };
+
+ (component as any).showOnboarding = true;
+
+ await component.ngOnInit();
+ await component.getMessages(eventData);
+
+ expect(saveCompletedTasksSpy).toHaveBeenCalled();
+ });
+ });
});
diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts
index 1243c5b0838..ac7584a88e0 100644
--- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts
@@ -9,13 +9,13 @@ import {
SimpleChanges,
OnChanges,
} from "@angular/core";
-import { Router } from "@angular/router";
-import { Subject, takeUntil, Observable, firstValueFrom } from "rxjs";
+import { Subject, takeUntil, Observable, firstValueFrom, fromEvent } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -35,6 +35,7 @@ import { VaultOnboardingTasks } from "./services/vault-onboarding.service";
})
export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
@Input() ciphers: CipherView[];
+ @Input() orgs: Organization[];
@Output() onAddCipher = new EventEmitter();
extensionUrl: string;
@@ -52,7 +53,6 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
constructor(
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
- protected router: Router,
private apiService: ApiService,
private configService: ConfigServiceAbstraction,
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
@@ -67,15 +67,18 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
await this.setOnboardingTasks();
this.setInstallExtLink();
this.individualVaultPolicyCheck();
+ this.checkForBrowserExtension();
}
async ngOnChanges(changes: SimpleChanges) {
if (this.showOnboarding && changes?.ciphers) {
- await this.saveCompletedTasks({
+ const currentTasks = await firstValueFrom(this.onboardingTasks$);
+ const updatedTasks = {
createAccount: true,
importData: this.ciphers.length > 0,
- installExtension: false,
- });
+ installExtension: currentTasks.installExtension,
+ };
+ await this.vaultOnboardingService.setVaultOnboardingTasks(updatedTasks);
}
}
@@ -84,6 +87,30 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
this.destroy$.complete();
}
+ checkForBrowserExtension() {
+ if (this.showOnboarding) {
+ fromEvent(window, "message")
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((event) => {
+ void this.getMessages(event);
+ });
+
+ window.postMessage({ command: "checkIfBWExtensionInstalled" });
+ }
+ }
+
+ async getMessages(event: any) {
+ if (event.data.command === "hasBWInstalled" && this.showOnboarding) {
+ const currentTasks = await firstValueFrom(this.onboardingTasks$);
+ const updatedTasks = {
+ createAccount: currentTasks.createAccount,
+ importData: currentTasks.importData,
+ installExtension: true,
+ };
+ await this.vaultOnboardingService.setVaultOnboardingTasks(updatedTasks);
+ }
+ }
+
async checkCreationDate() {
const userProfile = await this.apiService.getProfile();
const profileCreationDate = new Date(userProfile.creationDate);
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html
index b0592c58360..b59e554f5ac 100644
--- a/apps/web/src/app/vault/individual-vault/vault.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault.component.html
@@ -11,7 +11,8 @@
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
>
-
+
+
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 17b98a65341..df97651d935 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -1349,13 +1349,17 @@
},
"onboardingImportDataDetailsPartOne": {
"message": "If you don't have any data to import, you can create a ",
- "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership."
+ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)"
},
"onboardingImportDataDetailsLink": {
"message": "new item",
- "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership."
+ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)"
},
- "onboardingImportDataDetailsPartTwo": {
+ "onboardingImportDataDetailsPartTwoNoOrgs": {
+ "message": " instead.",
+ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead."
+ },
+ "onboardingImportDataDetailsPartTwoWithOrgs": {
"message": " instead. You may need to wait until your administrator confirms your organization membership.",
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership."
},