mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
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:
@@ -0,0 +1,25 @@
|
||||
<ng-template #content>
|
||||
<i class="bwi bwi-fw !tw-mr-4" [ngClass]="completed ? 'bwi-check tw-text-success' : icon"></i
|
||||
><span
|
||||
[ngClass]="{
|
||||
'tw-text-primary-700 tw-line-through tw-decoration-primary-700 tw-opacity-50': completed
|
||||
}"
|
||||
>{{ title }}<i class="bwi bwi-angle-right tw-ml-1"></i
|
||||
></span>
|
||||
</ng-template>
|
||||
|
||||
<li class="tw-list-none">
|
||||
<a bitLink *ngIf="route" [routerLink]="route">
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</a>
|
||||
<button type="button" bitLink *ngIf="!route" [disabled]="isDisabled">
|
||||
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||
</button>
|
||||
<div
|
||||
class="tw-ml-8 tw-mt-1 tw-text-sm"
|
||||
[ngClass]="{ 'tw-opacity-50': completed }"
|
||||
(click)="handleClick($event)"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</li>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-onboarding-task",
|
||||
templateUrl: "./onboarding-task.component.html",
|
||||
host: {
|
||||
class: "tw-max-w-max",
|
||||
},
|
||||
})
|
||||
export class OnboardingTaskComponent {
|
||||
@Input()
|
||||
completed = false;
|
||||
|
||||
@Input()
|
||||
icon = "bwi-info-circle";
|
||||
|
||||
@Input()
|
||||
title: string;
|
||||
|
||||
@Input()
|
||||
route: string | any[];
|
||||
|
||||
@Input()
|
||||
isDisabled: boolean = false;
|
||||
|
||||
handleClick(ev: MouseEvent) {
|
||||
/**
|
||||
* If the main `ng-content` is clicked, we don't want to trigger the task's click handler.
|
||||
*/
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<details #details class="tw-rounded-sm tw-bg-background-alt tw-text-main" (toggle)="toggle()" open>
|
||||
<summary class="tw-list-none tw-p-2 tw-px-4">
|
||||
<div class="tw-flex tw-select-none tw-items-center tw-gap-4">
|
||||
<i class="bwi bwi-dashboard tw-text-3xl tw-text-primary-500" aria-hidden="true"></i>
|
||||
<div class="tw-text-lg">{{ title }}</div>
|
||||
<bit-progress class="tw-flex-1" [showText]="false" [barWidth]="barWidth"></bit-progress>
|
||||
<span *ngIf="tasks.length > 0; else spinner">
|
||||
{{ "complete" | i18n: amountCompleted : tasks.length }}
|
||||
</span>
|
||||
<i
|
||||
class="bwi tw-my-auto"
|
||||
[ngClass]="open ? 'bwi-angle-down' : 'bwi-angle-up'"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
</summary>
|
||||
<ul class="tw-mb-0 tw-ml-6 tw-flex tw-flex-col tw-gap-4">
|
||||
<ng-content></ng-content>
|
||||
</ul>
|
||||
<div class="tw-p-4 tw-pt-0">
|
||||
<button bitLink type="button" class="tw-ml-auto tw-block" (click)="dismiss.emit()">
|
||||
{{ "dismiss" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<ng-template #spinner>
|
||||
<i class="bwi bwi-spinner bwi-spin"></i>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component, ContentChildren, EventEmitter, Input, Output, QueryList } from "@angular/core";
|
||||
|
||||
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-onboarding",
|
||||
templateUrl: "./onboarding.component.html",
|
||||
})
|
||||
export class OnboardingComponent {
|
||||
@ContentChildren(OnboardingTaskComponent) tasks: QueryList<OnboardingTaskComponent>;
|
||||
@Input() title: string;
|
||||
|
||||
@Output() dismiss = new EventEmitter<void>();
|
||||
|
||||
protected open = true;
|
||||
protected visible = false;
|
||||
|
||||
protected get amountCompleted(): number {
|
||||
return this.tasks.filter((task) => task.completed).length;
|
||||
}
|
||||
|
||||
protected get barWidth(): number {
|
||||
return this.tasks.length === 0 ? 0 : (this.amountCompleted / this.tasks.length) * 100;
|
||||
}
|
||||
|
||||
protected toggle() {
|
||||
this.open = !this.open;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ProgressModule } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../shared.module";
|
||||
|
||||
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
||||
import { OnboardingComponent } from "./onboarding.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, ProgressModule],
|
||||
exports: [OnboardingComponent, OnboardingTaskComponent],
|
||||
declarations: [OnboardingComponent, OnboardingTaskComponent],
|
||||
})
|
||||
export class OnboardingModule {}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { Meta, Story, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { delay, of, startWith } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { LinkModule, IconModule, ProgressModule } from "@bitwarden/components";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../core/tests";
|
||||
|
||||
import { OnboardingTaskComponent } from "./onboarding-task.component";
|
||||
import { OnboardingComponent } from "./onboarding.component";
|
||||
|
||||
export default {
|
||||
title: "Web/Onboarding",
|
||||
component: OnboardingComponent,
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, RouterModule, LinkModule, IconModule, ProgressModule],
|
||||
declarations: [OnboardingTaskComponent],
|
||||
}),
|
||||
applicationConfig({
|
||||
providers: [
|
||||
importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => ({
|
||||
props: {
|
||||
createServiceAccount: false,
|
||||
importSecrets$: of(false),
|
||||
createSecret: false,
|
||||
createProject: false,
|
||||
...args,
|
||||
},
|
||||
template: `
|
||||
<app-onboarding title="Get started">
|
||||
<app-onboarding-task
|
||||
[title]="'createServiceAccount' | i18n"
|
||||
icon="bwi-cli"
|
||||
[completed]="createServiceAccount"
|
||||
>
|
||||
<span>
|
||||
{{ "downloadThe" | i18n }} <a bitLink routerLink="">{{ "smCLI" | i18n }}</a>
|
||||
</span>
|
||||
</app-onboarding-task>
|
||||
<app-onboarding-task
|
||||
[title]="'createProject' | i18n"
|
||||
icon="bwi-collection"
|
||||
[completed]="createProject"
|
||||
></app-onboarding-task>
|
||||
<app-onboarding-task
|
||||
[title]="'importSecrets' | i18n"
|
||||
icon="bwi-download"
|
||||
[completed]="importSecrets$ | async"
|
||||
></app-onboarding-task>
|
||||
<app-onboarding-task
|
||||
[title]="'createSecret' | i18n"
|
||||
icon="bwi-key"
|
||||
[completed]="createSecret"
|
||||
></app-onboarding-task>
|
||||
</app-onboarding>
|
||||
`,
|
||||
});
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
|
||||
export const Partial = Template.bind({});
|
||||
Partial.args = {
|
||||
...Template.args,
|
||||
createServiceAccount: true,
|
||||
createProject: true,
|
||||
};
|
||||
|
||||
export const Full = Template.bind({});
|
||||
Full.args = {
|
||||
...Template.args,
|
||||
createServiceAccount: true,
|
||||
createProject: true,
|
||||
createSecret: true,
|
||||
importSecrets$: of(true).pipe(delay(0), startWith(false)),
|
||||
};
|
||||
@@ -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">
|
||||
<app-vault-onboarding [ciphers]="ciphers" (onAddCipher)="addCipher()"> </app-vault-onboarding>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<div class="groupings">
|
||||
|
||||
@@ -13,6 +13,9 @@ import { OrganizationBadgeModule } from "./organization-badge/organization-badge
|
||||
import { PipesModule } from "./pipes/pipes.module";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
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 { VaultComponent } from "./vault.component";
|
||||
|
||||
@@ -30,8 +33,15 @@ import { VaultComponent } from "./vault.component";
|
||||
BreadcrumbsModule,
|
||||
VaultItemsModule,
|
||||
CollectionDialogModule,
|
||||
VaultOnboardingComponent,
|
||||
],
|
||||
declarations: [VaultComponent, VaultHeaderComponent],
|
||||
exports: [VaultComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultOnboardingServiceAbstraction,
|
||||
useClass: VaultOnboardingService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultModule {}
|
||||
|
||||
@@ -1347,6 +1347,18 @@
|
||||
"importData": {
|
||||
"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": {
|
||||
"message": "Import error"
|
||||
},
|
||||
@@ -6912,6 +6924,9 @@
|
||||
"message": "SDK",
|
||||
"description": "Software Development Kit"
|
||||
},
|
||||
"createAnAccount": {
|
||||
"message": "Create an account"
|
||||
},
|
||||
"createSecret": {
|
||||
"message": "Create a secret"
|
||||
},
|
||||
@@ -7456,6 +7471,12 @@
|
||||
"message": "See detailed instructions on our help site at",
|
||||
"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": {
|
||||
"message": "Project access updated"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user