1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 13:53:34 +00:00

PM-4109 Vault Onboarding M2 (#7920)

Onboarding component now detects if extension is installed
This commit is contained in:
Jason Ng
2024-02-27 10:18:04 -05:00
committed by GitHub
parent d20caf479b
commit 4733f45eaf
8 changed files with 114 additions and 20 deletions

View File

@@ -16,6 +16,7 @@ type ContentMessageWindowEventHandlers = {
authResult: ({ data, referrer }: ContentMessageWindowEventParams) => void; authResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
webAuthnResult: ({ data, referrer }: ContentMessageWindowEventParams) => void; webAuthnResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
duoResult: ({ data, referrer }: ContentMessageWindowEventParams) => void; duoResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
checkIfBWExtensionInstalled: () => void;
}; };
export { export {

View File

@@ -25,6 +25,17 @@ describe("ContentMessageHandler", () => {
jest.clearAllMocks(); 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", () => { describe("handled window messages", () => {
it("ignores messages from other sources", () => { it("ignores messages from other sources", () => {
postWindowMessage({ command: "authResult" }, "https://localhost/", null); postWindowMessage({ command: "authResult" }, "https://localhost/", null);

View File

@@ -24,10 +24,18 @@ const windowMessageHandlers: ContentMessageWindowEventHandlers = {
handleAuthResultMessage(data, referrer), handleAuthResultMessage(data, referrer),
webAuthnResult: ({ data, referrer }: { data: any; referrer: string }) => webAuthnResult: ({ data, referrer }: { data: any; referrer: string }) =>
handleWebAuthnResultMessage(data, referrer), handleWebAuthnResultMessage(data, referrer),
checkIfBWExtensionInstalled: () => handleExtensionInstallCheck(),
duoResult: ({ data, referrer }: { data: any; referrer: string }) => duoResult: ({ data, referrer }: { data: any; referrer: string }) =>
handleDuoResultMessage(data, referrer), 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. * Handles the auth result message from the window.
* *

View File

@@ -27,7 +27,12 @@
<button type="button" bitLink (click)="emitToAddCipher()"> <button type="button" bitLink (click)="emitToAddCipher()">
{{ "onboardingImportDataDetailsLink" | i18n }} {{ "onboardingImportDataDetailsLink" | i18n }}
</button> </button>
{{ "onboardingImportDataDetailsPartTwo" | i18n }} <span *ngIf="orgs == null || orgs.length === 0">
{{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }}
</span>
<span *ngIf="orgs.length > 0">
{{ "onboardingImportDataDetailsPartTwoWithOrgs" | i18n }}
</span>
</p> </p>
</app-onboarding-task> </app-onboarding-task>
@@ -35,6 +40,8 @@
[title]="'installBrowserExtension' | i18n" [title]="'installBrowserExtension' | i18n"
icon="bwi-cli" icon="bwi-cli"
(click)="navigateToExtension()" (click)="navigateToExtension()"
route="[]"
[completed]="onboardingTasks.installExtension"
> >
<span class="tw-pl-1"> <span class="tw-pl-1">
{{ "installBrowserExtensionDetails" | i18n }} {{ "installBrowserExtensionDetails" | i18n }}

View File

@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { RouterTestingModule } from "@angular/router/testing"; import { RouterTestingModule } from "@angular/router/testing";
import { mock, MockProxy } from "jest-mock-extended"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@@ -38,11 +38,9 @@ describe("VaultOnboardingComponent", () => {
mockStateProvider = { mockStateProvider = {
getActive: jest.fn().mockReturnValue( getActive: jest.fn().mockReturnValue(
of({ of({
vaultTasks: { createAccount: true,
createAccount: true, importData: false,
importData: false, installExtension: false,
installExtension: false,
},
}), }),
), ),
}; };
@@ -61,9 +59,6 @@ describe("VaultOnboardingComponent", () => {
{ provide: StateProvider, useValue: mockStateProvider }, { provide: StateProvider, useValue: mockStateProvider },
], ],
}).compileComponents(); }).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(VaultOnboardingComponent); fixture = TestBed.createComponent(VaultOnboardingComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
setInstallExtLinkSpy = jest.spyOn(component, "setInstallExtLink"); setInstallExtLinkSpy = jest.spyOn(component, "setInstallExtLink");
@@ -71,6 +66,7 @@ describe("VaultOnboardingComponent", () => {
.spyOn(component, "individualVaultPolicyCheck") .spyOn(component, "individualVaultPolicyCheck")
.mockReturnValue(undefined); .mockReturnValue(undefined);
jest.spyOn(component, "checkCreationDate").mockReturnValue(null); jest.spyOn(component, "checkCreationDate").mockReturnValue(null);
jest.spyOn(window, "postMessage").mockImplementation(jest.fn());
(component as any).vaultOnboardingService.vaultOnboardingState$ = of({ (component as any).vaultOnboardingService.vaultOnboardingState$ = of({
createAccount: true, createAccount: true,
importData: false, importData: false,
@@ -143,4 +139,43 @@ describe("VaultOnboardingComponent", () => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
}); });
describe("checkBrowserExtension", () => {
it("should call getMessages when showOnboarding is true", () => {
const messageEventSubject = new Subject<MessageEvent>();
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();
});
});
}); });

View File

@@ -9,13 +9,13 @@ import {
SimpleChanges, SimpleChanges,
OnChanges, OnChanges,
} from "@angular/core"; } from "@angular/core";
import { Router } from "@angular/router"; import { Subject, takeUntil, Observable, firstValueFrom, fromEvent } from "rxjs";
import { Subject, takeUntil, Observable, firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; 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 { export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
@Input() ciphers: CipherView[]; @Input() ciphers: CipherView[];
@Input() orgs: Organization[];
@Output() onAddCipher = new EventEmitter<void>(); @Output() onAddCipher = new EventEmitter<void>();
extensionUrl: string; extensionUrl: string;
@@ -52,7 +53,6 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
constructor( constructor(
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService, protected policyService: PolicyService,
protected router: Router,
private apiService: ApiService, private apiService: ApiService,
private configService: ConfigServiceAbstraction, private configService: ConfigServiceAbstraction,
private vaultOnboardingService: VaultOnboardingServiceAbstraction, private vaultOnboardingService: VaultOnboardingServiceAbstraction,
@@ -67,15 +67,18 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
await this.setOnboardingTasks(); await this.setOnboardingTasks();
this.setInstallExtLink(); this.setInstallExtLink();
this.individualVaultPolicyCheck(); this.individualVaultPolicyCheck();
this.checkForBrowserExtension();
} }
async ngOnChanges(changes: SimpleChanges) { async ngOnChanges(changes: SimpleChanges) {
if (this.showOnboarding && changes?.ciphers) { if (this.showOnboarding && changes?.ciphers) {
await this.saveCompletedTasks({ const currentTasks = await firstValueFrom(this.onboardingTasks$);
const updatedTasks = {
createAccount: true, createAccount: true,
importData: this.ciphers.length > 0, 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(); this.destroy$.complete();
} }
checkForBrowserExtension() {
if (this.showOnboarding) {
fromEvent<MessageEvent>(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() { async checkCreationDate() {
const userProfile = await this.apiService.getProfile(); const userProfile = await this.apiService.getProfile();
const profileCreationDate = new Date(userProfile.creationDate); const profileCreationDate = new Date(userProfile.creationDate);

View File

@@ -11,7 +11,8 @@
(onDeleteCollection)="deleteCollection(selectedCollection.node)" (onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-vault-header> ></app-vault-header>
<app-vault-onboarding [ciphers]="ciphers" (onAddCipher)="addCipher()"> </app-vault-onboarding> <app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
</app-vault-onboarding>
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">

View File

@@ -1349,13 +1349,17 @@
}, },
"onboardingImportDataDetailsPartOne": { "onboardingImportDataDetailsPartOne": {
"message": "If you don't have any data to import, you can create a ", "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": { "onboardingImportDataDetailsLink": {
"message": "new item", "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.", "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." "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."
}, },