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:
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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."
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user