mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-29 16:26:11 +01:00
PM-4109 Vault Onboarding M2 (#7920)
Onboarding component now detects if extension is installed
This commit is contained in:
parent
d20caf479b
commit
4733f45eaf
@ -16,6 +16,7 @@ type ContentMessageWindowEventHandlers = {
|
||||
authResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
|
||||
webAuthnResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
|
||||
duoResult: ({ data, referrer }: ContentMessageWindowEventParams) => void;
|
||||
checkIfBWExtensionInstalled: () => void;
|
||||
};
|
||||
|
||||
export {
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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 }}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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."
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user