From 81212deaad9b43f408040771b68b7600d87ed567 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Tue, 30 Jul 2024 11:45:26 -0400 Subject: [PATCH] [PM-7306] Onboarding users to new UI (#10267) * Added translation keys * created simple dialog (cherry picked from commit c12257cf51ca5e0d773a160afb6860a8f5df66b7) * added announcement svg (cherry picked from commit 635103120b500103b93dc1a8cbefc34dd396445b) * removed announcement svg, moved svg to component, refactored component (cherry picked from commit 50db6aa40fd90d92afeeb60e919f98f268bd69b5) * renamed state definition (cherry picked from commit 4c3618c46ee5ffab7050fbc9f6d779ee59c0c26b) * created vault ui onboarding service (cherry picked from commit 19ba3c42656d4f891ba3635da06f3ff7656b6028) * added vault ui dialog to vault component (cherry picked from commit 56527c8e5eda7df2f222ceebdeba168e2f1ae278) * moved updating the state to vault component * updated the link and fixed minor issues * moved onboarding logic from component to service and fixed review comments --- apps/browser/src/_locales/en/messages.json | 6 ++ .../vault-ui-onboarding.component.ts | 73 +++++++++++++++ .../components/vault/vault-v2.component.ts | 8 +- .../services/vault-ui-onboarding.service.ts | 88 +++++++++++++++++++ .../src/platform/state/state-definitions.ts | 1 + 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts create mode 100644 apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5ce5831e2e..8b9ce12afd 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -4014,5 +4014,11 @@ }, "itemLocation": { "message": "Item Location" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts new file mode 100644 index 0000000000..6324e4139d --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts @@ -0,0 +1,73 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + ButtonModule, + DialogModule, + DialogService, + IconModule, + svgIcon, +} from "@bitwarden/components"; + +const announcementIcon = svgIcon` + + + + + + + + + + + + + + + + +`; + +@Component({ + standalone: true, + selector: "app-vault-ui-onboarding", + template: ` + +
+ +
+ + {{ "bitwardenNewLook" | i18n }} + + + {{ "bitwardenNewLookDesc" | i18n }} + + + + + {{ "learnMore" | i18n }} + + + + +
+ `, + imports: [CommonModule, DialogModule, ButtonModule, JslibModule, IconModule], +}) +export class VaultUiOnboardingComponent { + icon = announcementIcon; + + static open(dialogService: DialogService) { + return dialogService.open(VaultUiOnboardingComponent); + } + + navigateToLink = async () => { + window.open( + "https://bitwarden.com/blog/bringing-intuitive-workflows-and-visual-updates-to-the-bitwarden-browser/", + "_blank", + ); + }; +} diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts index 0e25647661..11331277a9 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts @@ -15,6 +15,7 @@ import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-he import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; +import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; import { NewItemDropdownV2Component, @@ -49,9 +50,11 @@ enum VaultState { VaultV2SearchComponent, NewItemDropdownV2Component, ], + providers: [VaultUiOnboardingService], }) export class VaultV2Component implements OnInit, OnDestroy { cipherType = CipherType; + protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; protected loading$ = this.vaultPopupItemsService.loading$; @@ -79,6 +82,7 @@ export class VaultV2Component implements OnInit, OnDestroy { constructor( private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupListFiltersService: VaultPopupListFiltersService, + private vaultUiOnboardingService: VaultUiOnboardingService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, @@ -104,7 +108,9 @@ export class VaultV2Component implements OnInit, OnDestroy { }); } - ngOnInit(): void {} + async ngOnInit() { + await this.vaultUiOnboardingService.showOnboardingDialog(); + } ngOnDestroy(): void {} } diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts new file mode 100644 index 0000000000..151f8517d5 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + GlobalState, + KeyDefinition, + StateProvider, + VAULT_BROWSER_UI_ONBOARDING, +} from "@bitwarden/common/platform/state"; +import { DialogService } from "@bitwarden/components"; + +import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component"; + +// Key definition for the Vault UI onboarding state. +// This key is used to store the state of the new UI information dialog. +export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition( + VAULT_BROWSER_UI_ONBOARDING, + "dialogState", + { + deserializer: (obj) => obj, + }, +); + +@Injectable() +export class VaultUiOnboardingService { + // TODO: Update this date to the release date of the new Browser UI + private onboardingUiReleaseDate = new Date("2024-07-25"); + + private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal( + GLOBAL_VAULT_UI_ONBOARDING, + ); + + private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe( + map((x) => x ?? false), + ); + + constructor( + private stateProvider: StateProvider, + private dialogService: DialogService, + private apiService: ApiService, + ) {} + + /** + * Checks whether the onboarding dialog should be shown and opens it if necessary. + * The dialog is shown if the user has not previously viewed it and is not a new account. + */ + async showOnboardingDialog(): Promise { + const hasViewedDialog = await this.getVaultUiOnboardingState(); + + if (!hasViewedDialog && !(await this.isNewAccount())) { + await this.openVaultUiOnboardingDialog(); + } + } + + private async openVaultUiOnboardingDialog(): Promise { + const dialogRef = VaultUiOnboardingComponent.open(this.dialogService); + + const result = firstValueFrom(dialogRef.closed); + + // Update the onboarding state when the dialog is closed + await this.setVaultUiOnboardingState(true); + + return result; + } + + private async isNewAccount(): Promise { + const userProfile = await this.apiService.getProfile(); + const profileCreationDate = new Date(userProfile.creationDate); + return profileCreationDate > this.onboardingUiReleaseDate; + } + + /** + * Updates and saves the state indicating whether the user has viewed + * the new UI onboarding information dialog. + */ + private async setVaultUiOnboardingState(value: boolean): Promise { + await this.vaultUiOnboardingState.update(() => value); + } + + /** + * Retrieves the current state indicating whether the user has viewed + * the new UI onboarding information dialog.s + */ + private async getVaultUiOnboardingState(): Promise { + return await firstValueFrom(this.vaultUiOnboardingState$); + } +} diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 0b55e7be77..fb00942f3e 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -165,3 +165,4 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro web: "disk-local", }); export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk"); +export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk");