PM-3231 Vault Onboarding Part 1 (#6905)
* Onboarding Component moved to web for sharing. Vault Onboarding Component created for new users. Still behind feature flag.
This commit is contained in:
parent
373a865a76
commit
fd8c26601a
|
@ -12,7 +12,7 @@
|
||||||
<a bitLink *ngIf="route" [routerLink]="route">
|
<a bitLink *ngIf="route" [routerLink]="route">
|
||||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" bitLink *ngIf="!route">
|
<button type="button" bitLink *ngIf="!route" [disabled]="isDisabled">
|
||||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-onboarding-task",
|
selector: "app-onboarding-task",
|
||||||
templateUrl: "./onboarding-task.component.html",
|
templateUrl: "./onboarding-task.component.html",
|
||||||
host: {
|
host: {
|
||||||
class: "tw-max-w-max",
|
class: "tw-max-w-max",
|
||||||
|
@ -20,6 +20,9 @@ export class OnboardingTaskComponent {
|
||||||
@Input()
|
@Input()
|
||||||
route: string | any[];
|
route: string | any[];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
isDisabled: boolean = false;
|
||||||
|
|
||||||
handleClick(ev: MouseEvent) {
|
handleClick(ev: MouseEvent) {
|
||||||
/**
|
/**
|
||||||
* If the main `ng-content` is clicked, we don't want to trigger the task's click handler.
|
* If the main `ng-content` is clicked, we don't want to trigger the task's click handler.
|
|
@ -3,7 +3,7 @@ import { Component, ContentChildren, EventEmitter, Input, Output, QueryList } fr
|
||||||
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "sm-onboarding",
|
selector: "app-onboarding",
|
||||||
templateUrl: "./onboarding.component.html",
|
templateUrl: "./onboarding.component.html",
|
||||||
})
|
})
|
||||||
export class OnboardingComponent {
|
export class OnboardingComponent {
|
|
@ -1,7 +1,8 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { ProgressModule } from "@bitwarden/components";
|
import { ProgressModule } from "@bitwarden/components";
|
||||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
|
||||||
|
import { SharedModule } from "../../shared.module";
|
||||||
|
|
||||||
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
||||||
import { OnboardingComponent } from "./onboarding.component";
|
import { OnboardingComponent } from "./onboarding.component";
|
|
@ -5,7 +5,8 @@ import { delay, of, startWith } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { LinkModule, IconModule, ProgressModule } from "@bitwarden/components";
|
import { LinkModule, IconModule, ProgressModule } from "@bitwarden/components";
|
||||||
import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/app/core/tests";
|
|
||||||
|
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||||
|
|
||||||
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
||||||
import { OnboardingComponent } from "./onboarding.component";
|
import { OnboardingComponent } from "./onboarding.component";
|
||||||
|
@ -36,8 +37,8 @@ const Template: Story = (args) => ({
|
||||||
...args,
|
...args,
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<sm-onboarding title="Get started">
|
<app-onboarding title="Get started">
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
[title]="'createServiceAccount' | i18n"
|
[title]="'createServiceAccount' | i18n"
|
||||||
icon="bwi-cli"
|
icon="bwi-cli"
|
||||||
[completed]="createServiceAccount"
|
[completed]="createServiceAccount"
|
||||||
|
@ -45,23 +46,23 @@ const Template: Story = (args) => ({
|
||||||
<span>
|
<span>
|
||||||
{{ "downloadThe" | i18n }} <a bitLink routerLink="">{{ "smCLI" | i18n }}</a>
|
{{ "downloadThe" | i18n }} <a bitLink routerLink="">{{ "smCLI" | i18n }}</a>
|
||||||
</span>
|
</span>
|
||||||
</sm-onboarding-task>
|
</app-onboarding-task>
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
[title]="'createProject' | i18n"
|
[title]="'createProject' | i18n"
|
||||||
icon="bwi-collection"
|
icon="bwi-collection"
|
||||||
[completed]="createProject"
|
[completed]="createProject"
|
||||||
></sm-onboarding-task>
|
></app-onboarding-task>
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
[title]="'importSecrets' | i18n"
|
[title]="'importSecrets' | i18n"
|
||||||
icon="bwi-download"
|
icon="bwi-download"
|
||||||
[completed]="importSecrets$ | async"
|
[completed]="importSecrets$ | async"
|
||||||
></sm-onboarding-task>
|
></app-onboarding-task>
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
[title]="'createSecret' | i18n"
|
[title]="'createSecret' | i18n"
|
||||||
icon="bwi-key"
|
icon="bwi-key"
|
||||||
[completed]="createSecret"
|
[completed]="createSecret"
|
||||||
></sm-onboarding-task>
|
></app-onboarding-task>
|
||||||
</sm-onboarding>
|
</app-onboarding>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { VaultOnboardingTasks } from "../vault-onboarding.service";
|
||||||
|
|
||||||
|
export abstract class VaultOnboardingService {
|
||||||
|
vaultOnboardingState$: Observable<VaultOnboardingTasks>;
|
||||||
|
abstract setVaultOnboardingTasks(newState: VaultOnboardingTasks): Promise<void>;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActiveUserState,
|
||||||
|
KeyDefinition,
|
||||||
|
StateProvider,
|
||||||
|
VAULT_ONBOARDING,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./abstraction/vault-onboarding.service";
|
||||||
|
|
||||||
|
export type VaultOnboardingTasks = {
|
||||||
|
createAccount: boolean;
|
||||||
|
importData: boolean;
|
||||||
|
installExtension: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VAULT_ONBOARDING_KEY = new KeyDefinition<VaultOnboardingTasks>(VAULT_ONBOARDING, "tasks", {
|
||||||
|
deserializer: (jsonData) => jsonData,
|
||||||
|
});
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VaultOnboardingService implements VaultOnboardingServiceAbstraction {
|
||||||
|
private vaultOnboardingState: ActiveUserState<VaultOnboardingTasks>;
|
||||||
|
vaultOnboardingState$: Observable<VaultOnboardingTasks>;
|
||||||
|
|
||||||
|
constructor(private stateProvider: StateProvider) {
|
||||||
|
this.vaultOnboardingState = this.stateProvider.getActive(VAULT_ONBOARDING_KEY);
|
||||||
|
this.vaultOnboardingState$ = this.vaultOnboardingState.state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVaultOnboardingTasks(newState: VaultOnboardingTasks): Promise<void> {
|
||||||
|
await this.vaultOnboardingState.update(() => {
|
||||||
|
return { ...newState };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
<div
|
||||||
|
*ngIf="
|
||||||
|
isNewAccount && showOnboarding && (showOnboardingAccess$ | async) && onboardingTasks$
|
||||||
|
| async as onboardingTasks
|
||||||
|
"
|
||||||
|
class="tw-mb-6"
|
||||||
|
>
|
||||||
|
<app-onboarding
|
||||||
|
*ngIf="onboardingTasks"
|
||||||
|
[title]="'getStarted' | i18n"
|
||||||
|
(dismiss)="hideOnboarding()"
|
||||||
|
>
|
||||||
|
<app-onboarding-task
|
||||||
|
[title]="'createAnAccount' | i18n"
|
||||||
|
[completed]="onboardingTasks.createAccount"
|
||||||
|
[isDisabled]="true"
|
||||||
|
></app-onboarding-task>
|
||||||
|
|
||||||
|
<app-onboarding-task
|
||||||
|
[title]="'importData' | i18n"
|
||||||
|
icon="bwi-save"
|
||||||
|
[route]="['/tools/import']"
|
||||||
|
[completed]="onboardingTasks.importData"
|
||||||
|
>
|
||||||
|
<p class="tw-pl-1">
|
||||||
|
{{ "onboardingImportDataDetailsPartOne" | i18n }}
|
||||||
|
<button type="button" bitLink (click)="emitToAddCipher()">
|
||||||
|
{{ "onboardingImportDataDetailsLink" | i18n }}
|
||||||
|
</button>
|
||||||
|
{{ "onboardingImportDataDetailsPartTwo" | i18n }}
|
||||||
|
</p>
|
||||||
|
</app-onboarding-task>
|
||||||
|
|
||||||
|
<app-onboarding-task
|
||||||
|
[title]="'installBrowserExtension' | i18n"
|
||||||
|
icon="bwi-cli"
|
||||||
|
(click)="navigateToExtension()"
|
||||||
|
>
|
||||||
|
<span class="tw-pl-1">
|
||||||
|
{{ "installBrowserExtensionDetails" | i18n }}
|
||||||
|
</span>
|
||||||
|
</app-onboarding-task>
|
||||||
|
</app-onboarding>
|
||||||
|
</div>
|
|
@ -0,0 +1,146 @@
|
||||||
|
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 { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
|
||||||
|
import { VaultOnboardingComponent } from "./vault-onboarding.component";
|
||||||
|
|
||||||
|
describe("VaultOnboardingComponent", () => {
|
||||||
|
let component: VaultOnboardingComponent;
|
||||||
|
let fixture: ComponentFixture<VaultOnboardingComponent>;
|
||||||
|
let mockPlatformUtilsService: Partial<PlatformUtilsService>;
|
||||||
|
let mockApiService: Partial<ApiService>;
|
||||||
|
let mockPolicyService: MockProxy<PolicyService>;
|
||||||
|
let mockI18nService: MockProxy<I18nService>;
|
||||||
|
let mockConfigService: MockProxy<ConfigServiceAbstraction>;
|
||||||
|
let mockVaultOnboardingService: MockProxy<VaultOnboardingServiceAbstraction>;
|
||||||
|
let mockStateProvider: Partial<StateProvider>;
|
||||||
|
let setInstallExtLinkSpy: any;
|
||||||
|
let individualVaultPolicyCheckSpy: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPolicyService = mock<PolicyService>();
|
||||||
|
mockI18nService = mock<I18nService>();
|
||||||
|
mockPlatformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
mockApiService = {
|
||||||
|
getProfile: jest.fn(),
|
||||||
|
};
|
||||||
|
mockConfigService = mock<ConfigServiceAbstraction>();
|
||||||
|
mockVaultOnboardingService = mock<VaultOnboardingServiceAbstraction>();
|
||||||
|
mockStateProvider = {
|
||||||
|
getActive: jest.fn().mockReturnValue(
|
||||||
|
of({
|
||||||
|
vaultTasks: {
|
||||||
|
createAccount: true,
|
||||||
|
importData: false,
|
||||||
|
installExtension: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [RouterTestingModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: PlatformUtilsService, useValue: mockPlatformUtilsService },
|
||||||
|
{ provide: PolicyService, useValue: mockPolicyService },
|
||||||
|
{ provide: VaultOnboardingServiceAbstraction, useValue: mockVaultOnboardingService },
|
||||||
|
{ provide: I18nService, useValue: mockI18nService },
|
||||||
|
{ provide: ApiService, useValue: mockApiService },
|
||||||
|
{ provide: ConfigServiceAbstraction, useValue: mockConfigService },
|
||||||
|
{ provide: StateProvider, useValue: mockStateProvider },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(VaultOnboardingComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
setInstallExtLinkSpy = jest.spyOn(component, "setInstallExtLink");
|
||||||
|
individualVaultPolicyCheckSpy = jest
|
||||||
|
.spyOn(component, "individualVaultPolicyCheck")
|
||||||
|
.mockReturnValue(undefined);
|
||||||
|
jest.spyOn(component, "checkCreationDate").mockReturnValue(null);
|
||||||
|
(component as any).vaultOnboardingService.vaultOnboardingState$ = of({
|
||||||
|
createAccount: true,
|
||||||
|
importData: false,
|
||||||
|
installExtension: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ngOnInit", () => {
|
||||||
|
it("should call setInstallExtLink", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect(setInstallExtLinkSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call individualVaultPolicyCheck", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect(individualVaultPolicyCheckSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("show and hide onboarding component", () => {
|
||||||
|
it("should set showOnboarding to true", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect((component as any).showOnboarding).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set showOnboarding to false if dismiss is clicked", async () => {
|
||||||
|
await component.ngOnInit();
|
||||||
|
(component as any).hideOnboarding();
|
||||||
|
expect((component as any).showOnboarding).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setInstallExtLink", () => {
|
||||||
|
it("should set extensionUrl to Chrome Web Store when isChrome is true", async () => {
|
||||||
|
jest.spyOn((component as any).platformUtilsService, "isChrome").mockReturnValue(true);
|
||||||
|
const expected =
|
||||||
|
"https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb";
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect(component.extensionUrl).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set extensionUrl to Firefox Store when isFirefox is true", async () => {
|
||||||
|
jest.spyOn((component as any).platformUtilsService, "isFirefox").mockReturnValue(true);
|
||||||
|
const expected = "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/";
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect(component.extensionUrl).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set extensionUrl when isSafari is true", async () => {
|
||||||
|
jest.spyOn((component as any).platformUtilsService, "isSafari").mockReturnValue(true);
|
||||||
|
const expected = "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12";
|
||||||
|
await component.ngOnInit();
|
||||||
|
expect(component.extensionUrl).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("individualVaultPolicyCheck", () => {
|
||||||
|
it("should set isIndividualPolicyVault to true", async () => {
|
||||||
|
individualVaultPolicyCheckSpy.mockRestore();
|
||||||
|
const spy = jest
|
||||||
|
.spyOn((component as any).policyService, "policyAppliesToActiveUser$")
|
||||||
|
.mockReturnValue(of(true));
|
||||||
|
|
||||||
|
await component.individualVaultPolicyCheck();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,165 @@
|
||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
OnDestroy,
|
||||||
|
SimpleChanges,
|
||||||
|
OnChanges,
|
||||||
|
} from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { Subject, takeUntil, Observable, firstValueFrom } 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 { 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";
|
||||||
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
import { LinkModule } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { OnboardingModule } from "../../../shared/components/onboarding/onboarding.module";
|
||||||
|
|
||||||
|
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
|
||||||
|
import { VaultOnboardingTasks } from "./services/vault-onboarding.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
imports: [OnboardingModule, CommonModule, JslibModule, LinkModule],
|
||||||
|
selector: "app-vault-onboarding",
|
||||||
|
templateUrl: "vault-onboarding.component.html",
|
||||||
|
})
|
||||||
|
export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
@Input() ciphers: CipherView[];
|
||||||
|
@Output() onAddCipher = new EventEmitter<void>();
|
||||||
|
|
||||||
|
extensionUrl: string;
|
||||||
|
isIndividualPolicyVault: boolean;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
isNewAccount: boolean;
|
||||||
|
private readonly onboardingReleaseDate = new Date("2024-01-01");
|
||||||
|
showOnboardingAccess$: Observable<boolean>;
|
||||||
|
|
||||||
|
protected currentTasks: VaultOnboardingTasks;
|
||||||
|
|
||||||
|
protected onboardingTasks$: Observable<VaultOnboardingTasks>;
|
||||||
|
protected showOnboarding = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected policyService: PolicyService,
|
||||||
|
protected router: Router,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private configService: ConfigServiceAbstraction,
|
||||||
|
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.showOnboardingAccess$ = await this.configService.getFeatureFlag$<boolean>(
|
||||||
|
FeatureFlag.VaultOnboarding,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$;
|
||||||
|
await this.setOnboardingTasks();
|
||||||
|
this.setInstallExtLink();
|
||||||
|
this.individualVaultPolicyCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnChanges(changes: SimpleChanges) {
|
||||||
|
if (this.showOnboarding && changes?.ciphers) {
|
||||||
|
await this.saveCompletedTasks({
|
||||||
|
createAccount: true,
|
||||||
|
importData: this.ciphers.length > 0,
|
||||||
|
installExtension: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkCreationDate() {
|
||||||
|
const userProfile = await this.apiService.getProfile();
|
||||||
|
const profileCreationDate = new Date(userProfile.creationDate);
|
||||||
|
|
||||||
|
this.isNewAccount = this.onboardingReleaseDate < profileCreationDate ? true : false;
|
||||||
|
|
||||||
|
if (!this.isNewAccount) {
|
||||||
|
await this.hideOnboarding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async hideOnboarding() {
|
||||||
|
await this.saveCompletedTasks({
|
||||||
|
createAccount: true,
|
||||||
|
importData: true,
|
||||||
|
installExtension: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setOnboardingTasks() {
|
||||||
|
const currentTasks = await firstValueFrom(this.onboardingTasks$);
|
||||||
|
if (currentTasks == null) {
|
||||||
|
const freshStart = {
|
||||||
|
createAccount: true,
|
||||||
|
importData: this.ciphers?.length > 0,
|
||||||
|
installExtension: false,
|
||||||
|
};
|
||||||
|
await this.saveCompletedTasks(freshStart);
|
||||||
|
} else if (currentTasks) {
|
||||||
|
this.showOnboarding = Object.values(currentTasks).includes(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.showOnboarding) {
|
||||||
|
await this.checkCreationDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveCompletedTasks(vaultTasks: VaultOnboardingTasks) {
|
||||||
|
this.showOnboarding = Object.values(vaultTasks).includes(false);
|
||||||
|
await this.vaultOnboardingService.setVaultOnboardingTasks(vaultTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
individualVaultPolicyCheck() {
|
||||||
|
this.policyService
|
||||||
|
.policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((data) => {
|
||||||
|
this.isIndividualPolicyVault = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
emitToAddCipher() {
|
||||||
|
this.onAddCipher.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
setInstallExtLink() {
|
||||||
|
if (this.platformUtilsService.isChrome()) {
|
||||||
|
this.extensionUrl =
|
||||||
|
"https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb";
|
||||||
|
} else if (this.platformUtilsService.isFirefox()) {
|
||||||
|
this.extensionUrl =
|
||||||
|
"https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/";
|
||||||
|
} else if (this.platformUtilsService.isSafari()) {
|
||||||
|
this.extensionUrl = "https://apps.apple.com/us/app/bitwarden/id1352778147?mt=12";
|
||||||
|
} else if (this.platformUtilsService.isOpera()) {
|
||||||
|
this.extensionUrl =
|
||||||
|
"https://addons.opera.com/extensions/details/bitwarden-free-password-manager/";
|
||||||
|
} else if (this.platformUtilsService.isEdge()) {
|
||||||
|
this.extensionUrl =
|
||||||
|
"https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh";
|
||||||
|
} else {
|
||||||
|
this.extensionUrl = "https://bitwarden.com/download/#downloads-web-browser";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToExtension() {
|
||||||
|
window.open(this.extensionUrl, "_blank");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
|
<app-vault-onboarding [ciphers]="ciphers" (onAddCipher)="addCipher()"> </app-vault-onboarding>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
<div class="groupings">
|
<div class="groupings">
|
||||||
|
|
|
@ -13,6 +13,9 @@ import { OrganizationBadgeModule } from "./organization-badge/organization-badge
|
||||||
import { PipesModule } from "./pipes/pipes.module";
|
import { PipesModule } from "./pipes/pipes.module";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||||
|
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./vault-onboarding/services/abstraction/vault-onboarding.service";
|
||||||
|
import { VaultOnboardingService } from "./vault-onboarding/services/vault-onboarding.service";
|
||||||
|
import { VaultOnboardingComponent } from "./vault-onboarding/vault-onboarding.component";
|
||||||
import { VaultRoutingModule } from "./vault-routing.module";
|
import { VaultRoutingModule } from "./vault-routing.module";
|
||||||
import { VaultComponent } from "./vault.component";
|
import { VaultComponent } from "./vault.component";
|
||||||
|
|
||||||
|
@ -30,8 +33,15 @@ import { VaultComponent } from "./vault.component";
|
||||||
BreadcrumbsModule,
|
BreadcrumbsModule,
|
||||||
VaultItemsModule,
|
VaultItemsModule,
|
||||||
CollectionDialogModule,
|
CollectionDialogModule,
|
||||||
|
VaultOnboardingComponent,
|
||||||
],
|
],
|
||||||
declarations: [VaultComponent, VaultHeaderComponent],
|
declarations: [VaultComponent, VaultHeaderComponent],
|
||||||
exports: [VaultComponent],
|
exports: [VaultComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: VaultOnboardingServiceAbstraction,
|
||||||
|
useClass: VaultOnboardingService,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class VaultModule {}
|
export class VaultModule {}
|
||||||
|
|
|
@ -1347,6 +1347,18 @@
|
||||||
"importData": {
|
"importData": {
|
||||||
"message": "Import data"
|
"message": "Import data"
|
||||||
},
|
},
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"onboardingImportDataDetailsPartTwo": {
|
||||||
|
"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."
|
||||||
|
},
|
||||||
"importError": {
|
"importError": {
|
||||||
"message": "Import error"
|
"message": "Import error"
|
||||||
},
|
},
|
||||||
|
@ -6912,6 +6924,9 @@
|
||||||
"message": "SDK",
|
"message": "SDK",
|
||||||
"description": "Software Development Kit"
|
"description": "Software Development Kit"
|
||||||
},
|
},
|
||||||
|
"createAnAccount": {
|
||||||
|
"message": "Create an account"
|
||||||
|
},
|
||||||
"createSecret": {
|
"createSecret": {
|
||||||
"message": "Create a secret"
|
"message": "Create a secret"
|
||||||
},
|
},
|
||||||
|
@ -7456,6 +7471,12 @@
|
||||||
"message": "See detailed instructions on our help site at",
|
"message": "See detailed instructions on our help site at",
|
||||||
"description": "This is followed a by a hyperlink to the help website."
|
"description": "This is followed a by a hyperlink to the help website."
|
||||||
},
|
},
|
||||||
|
"installBrowserExtension": {
|
||||||
|
"message": "Install browser extension"
|
||||||
|
},
|
||||||
|
"installBrowserExtensionDetails": {
|
||||||
|
"message": "Use the extension to quickly save logins and auto-fill forms without opening the web app."
|
||||||
|
},
|
||||||
"projectAccessUpdated": {
|
"projectAccessUpdated": {
|
||||||
"message": "Project access updated"
|
"message": "Project access updated"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
</app-header>
|
</app-header>
|
||||||
|
|
||||||
<div *ngIf="!loading && view$ | async as view; else spinner">
|
<div *ngIf="!loading && view$ | async as view; else spinner">
|
||||||
<sm-onboarding [title]="'getStarted' | i18n" *ngIf="showOnboarding" (dismiss)="hideOnboarding()">
|
<app-onboarding [title]="'getStarted' | i18n" *ngIf="showOnboarding" (dismiss)="hideOnboarding()">
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
[title]="'createServiceAccount' | i18n"
|
[title]="'createServiceAccount' | i18n"
|
||||||
(click)="openServiceAccountDialog()"
|
(click)="openServiceAccountDialog()"
|
||||||
icon="bwi-cli"
|
icon="bwi-cli"
|
||||||
|
@ -16,29 +16,29 @@
|
||||||
"smCLI" | i18n
|
"smCLI" | i18n
|
||||||
}}</a>
|
}}</a>
|
||||||
</span>
|
</span>
|
||||||
</sm-onboarding-task>
|
</app-onboarding-task>
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
*ngIf="userIsAdmin"
|
*ngIf="userIsAdmin"
|
||||||
[title]="'createProject' | i18n"
|
[title]="'createProject' | i18n"
|
||||||
(click)="openNewProjectDialog()"
|
(click)="openNewProjectDialog()"
|
||||||
icon="bwi-collection"
|
icon="bwi-collection"
|
||||||
[completed]="view.tasks.createProject"
|
[completed]="view.tasks.createProject"
|
||||||
></sm-onboarding-task>
|
></app-onboarding-task>
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
*ngIf="userIsAdmin"
|
*ngIf="userIsAdmin"
|
||||||
[title]="'importSecrets' | i18n"
|
[title]="'importSecrets' | i18n"
|
||||||
[route]="['settings', 'import']"
|
[route]="['settings', 'import']"
|
||||||
icon="bwi-download"
|
icon="bwi-download"
|
||||||
[completed]="view.tasks.importSecrets"
|
[completed]="view.tasks.importSecrets"
|
||||||
></sm-onboarding-task>
|
></app-onboarding-task>
|
||||||
<sm-onboarding-task
|
<app-onboarding-task
|
||||||
*ngIf="view.tasks.createProject"
|
*ngIf="view.tasks.createProject"
|
||||||
[title]="'createSecret' | i18n"
|
[title]="'createSecret' | i18n"
|
||||||
(click)="openSecretDialog()"
|
(click)="openSecretDialog()"
|
||||||
icon="bwi-key"
|
icon="bwi-key"
|
||||||
[completed]="view.tasks.createSecret"
|
[completed]="view.tasks.createSecret"
|
||||||
></sm-onboarding-task>
|
></app-onboarding-task>
|
||||||
</sm-onboarding>
|
</app-onboarding>
|
||||||
|
|
||||||
<div class="tw-mt-6 tw-flex tw-flex-col tw-gap-6">
|
<div class="tw-mt-6 tw-flex tw-flex-col tw-gap-6">
|
||||||
<sm-section>
|
<sm-section>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { OnboardingModule } from "../../../../../../apps/web/src/app/shared/components/onboarding/onboarding.module";
|
||||||
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
import { SecretsManagerSharedModule } from "../shared/sm-shared.module";
|
||||||
|
|
||||||
import { OnboardingModule } from "./onboarding.module";
|
|
||||||
import { OverviewRoutingModule } from "./overview-routing.module";
|
import { OverviewRoutingModule } from "./overview-routing.module";
|
||||||
import { OverviewComponent } from "./overview.component";
|
import { OverviewComponent } from "./overview.component";
|
||||||
import { SectionComponent } from "./section.component";
|
import { SectionComponent } from "./section.component";
|
||||||
|
|
|
@ -4,6 +4,7 @@ export enum FeatureFlag {
|
||||||
ItemShare = "item-share",
|
ItemShare = "item-share",
|
||||||
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
|
FlexibleCollectionsV1 = "flexible-collections-v-1", // v-1 is intentional
|
||||||
BulkCollectionAccess = "bulk-collection-access",
|
BulkCollectionAccess = "bulk-collection-access",
|
||||||
|
VaultOnboarding = "vault-onboarding",
|
||||||
GeneratorToolsModernization = "generator-tools-modernization",
|
GeneratorToolsModernization = "generator-tools-modernization",
|
||||||
KeyRotationImprovements = "key-rotation-improvements",
|
KeyRotationImprovements = "key-rotation-improvements",
|
||||||
FlexibleCollectionsMigration = "flexible-collections-migration",
|
FlexibleCollectionsMigration = "flexible-collections-migration",
|
||||||
|
|
|
@ -16,6 +16,7 @@ export class ProfileResponse extends BaseResponse {
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
key: string;
|
key: string;
|
||||||
avatarColor: string;
|
avatarColor: string;
|
||||||
|
creationDate: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
securityStamp: string;
|
securityStamp: string;
|
||||||
forcePasswordReset: boolean;
|
forcePasswordReset: boolean;
|
||||||
|
@ -37,6 +38,7 @@ export class ProfileResponse extends BaseResponse {
|
||||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||||
this.key = this.getResponseProperty("Key");
|
this.key = this.getResponseProperty("Key");
|
||||||
this.avatarColor = this.getResponseProperty("AvatarColor");
|
this.avatarColor = this.getResponseProperty("AvatarColor");
|
||||||
|
this.creationDate = this.getResponseProperty("CreationDate");
|
||||||
this.privateKey = this.getResponseProperty("PrivateKey");
|
this.privateKey = this.getResponseProperty("PrivateKey");
|
||||||
this.securityStamp = this.getResponseProperty("SecurityStamp");
|
this.securityStamp = this.getResponseProperty("SecurityStamp");
|
||||||
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
|
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
|
||||||
|
|
|
@ -27,6 +27,10 @@ export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||||
|
|
||||||
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk");
|
||||||
|
|
||||||
|
export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", {
|
||||||
|
web: "disk-local",
|
||||||
|
});
|
||||||
|
|
||||||
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
export const GENERATOR_DISK = new StateDefinition("generator", "disk");
|
||||||
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
export const GENERATOR_MEMORY = new StateDefinition("generator", "memory");
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue