1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02: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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 114 additions and 20 deletions

View File

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

View File

@ -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);

View File

@ -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.
*

View File

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

View File

@ -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<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,
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<void>();
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<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() {
const userProfile = await this.apiService.getProfile();
const profileCreationDate = new Date(userProfile.creationDate);

View File

@ -11,7 +11,8 @@
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></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="col-3">

View File

@ -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."
},