diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 9ce3e048cde..eb6080a5213 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2093,6 +2093,15 @@ "vaultTimeoutAction1": { "message": "Timeout action" }, + "newCustomizationOptionsCalloutTitle": { + "message": "New customization options" + }, + "newCustomizationOptionsCalloutContent": { + "message": "Customize your vault experience with quick copy actions, compact mode, and more!" + }, + "newCustomizationOptionsCalloutLink": { + "message": "View all Appearance settings" + }, "lock": { "message": "Lock", "description": "Verb form: to make secure or inaccessible by" diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts new file mode 100644 index 00000000000..a7c52ed4c51 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts @@ -0,0 +1,35 @@ +import { inject, Injectable } from "@angular/core"; +import { map, Observable } from "rxjs"; + +import { + BANNERS_DISMISSED_DISK, + StateProvider, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; + +export const NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY = new UserKeyDefinition( + BANNERS_DISMISSED_DISK, + "newCustomizationOptionsCalloutDismissed", + { + deserializer: (calloutDismissed) => calloutDismissed, + clearOn: [], // Do not clear dismissed callouts + }, +); + +@Injectable() +export class VaultPageService { + private stateProvider = inject(StateProvider); + + isCalloutDismissed(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY) + .state$.pipe(map((dismissed) => !!dismissed)); + } + + async dismissCallout(userId: UserId): Promise { + await this.stateProvider + .getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY) + .update(() => true); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html index 8cb538a429a..ec13ce03acd 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html @@ -85,4 +85,21 @@ > + + + {{ "newCustomizationOptionsCalloutContent" | i18n }} + + {{ "newCustomizationOptionsCalloutLink" | i18n }} + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index be5c33aab70..d00f2f96ba0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -2,7 +2,7 @@ import { CdkVirtualScrollableElement, ScrollingModule } from "@angular/cdk/scrol import { CommonModule } from "@angular/common"; import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { RouterLink } from "@angular/router"; +import { Router, RouterLink } from "@angular/router"; import { combineLatest, filter, @@ -15,18 +15,22 @@ import { } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { VaultProfileService } from "@bitwarden/angular/vault/services/vault-profile.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { BannerComponent, ButtonModule, DialogService, + IconButtonModule, Icons, NoItemsModule, + PopoverModule, + SharedModule, } from "@bitwarden/components"; import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault"; @@ -45,6 +49,7 @@ import { NewItemInitialValues, } from "./new-item-dropdown/new-item-dropdown-v2.component"; import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; +import { VaultPageService } from "./vault-page.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; @@ -60,6 +65,7 @@ enum VaultState { standalone: true, imports: [ BlockedInjectionBanner, + PopoverModule, PopupPageComponent, PopupHeaderComponent, PopOutComponent, @@ -72,12 +78,15 @@ enum VaultState { ButtonModule, RouterLink, NewItemDropdownV2Component, + IconButtonModule, ScrollingModule, VaultHeaderV2Component, DecryptionFailureDialogComponent, BannerComponent, AtRiskPasswordCalloutComponent, + SharedModule, ], + providers: [VaultPageService], }) export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; @@ -110,6 +119,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { protected noResultsIcon = Icons.NoResults; protected VaultStateEnum = VaultState; + protected showNewCustomizationSettingsCallout = true; + protected activeUserId: UserId | null = null; private allFilters$ = this.vaultPopupListFiltersService.allFilters$; @@ -121,6 +132,9 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { private destroyRef: DestroyRef, private cipherService: CipherService, private dialogService: DialogService, + private vaultProfileService: VaultProfileService, + private vaultPageService: VaultPageService, + private router: Router, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, @@ -158,10 +172,10 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { } async ngOnInit() { - const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.cipherService - .failedToDecryptCiphers$(activeUserId) + .failedToDecryptCiphers$(this.activeUserId) .pipe( map((ciphers) => ciphers.filter((c) => !c.isDeleted)), filter((ciphers) => ciphers.length > 0), @@ -173,10 +187,22 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { cipherIds: ciphers.map((c) => c.id as CipherId), }); }); + + // const profileCreatedDate = await this.vaultProfileService.getProfileCreationDate( + // this.activeUserId, + // ); + // const hasCalloutBeenDismissed = this.vaultPageService.isCalloutDismissed(this.activeUserId); + // Show the new customization settings callout if the user has not dismissed it and the account was created before December 25, 2024 + this.showNewCustomizationSettingsCallout = true; + // this.showNewCustomizationSettingsCallout = + // !hasCalloutBeenDismissed && profileCreatedDate < new Date("2024-12-25"); } - ngOnDestroy(): void { + async ngOnDestroy() { this.vaultScrollPositionService.stop(); + if (this.activeUserId) { + await this.vaultPageService.dismissCallout(this.activeUserId); + } } protected readonly FeatureFlag = FeatureFlag; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 7788f4986bf..9f9cc3278e6 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -35,6 +35,7 @@ export * from "./search"; export * from "./section"; export * from "./select"; export * from "./shared/compact-mode.service"; +export * from "./shared"; export * from "./table"; export * from "./tabs"; export * from "./toast"; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 0a5a66337ac..4663f0a9332 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -194,11 +194,11 @@ --tw-ring-offset-color: #002b36; } -@import "./popover/popover.component.css"; @import "./search/search.component.css"; @import "./toast/toast.tokens.css"; @import "./toast/toastr.css"; +@import "./popover/popover.component.css"; /** * tw-break-words does not work with table cells: