1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

new customization options callout

This commit is contained in:
jaasen-livefront
2025-02-18 17:29:02 -08:00
parent 80bf2db0d9
commit f3054c9b27
6 changed files with 94 additions and 6 deletions

View File

@@ -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"

View File

@@ -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<boolean>(
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<boolean> {
return this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.state$.pipe(map((dismissed) => !!dismissed));
}
async dismissCallout(userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.update(() => true);
}
}

View File

@@ -85,4 +85,21 @@
></app-vault-list-items-container>
</div>
</ng-container>
<button
type="button"
style="position: absolute; bottom: 15px; right: 40px"
[bitPopoverTriggerFor]="newCustomizationOptionsCallout"
[position]="'above-end'"
[popoverOpen]="showNewCustomizationSettingsCallout"
#triggerRef="popoverTrigger"
></button>
<bit-popover
[title]="'newCustomizationOptionsCalloutTitle' | i18n"
#newCustomizationOptionsCallout
>
{{ "newCustomizationOptionsCalloutContent" | i18n }}
<a routerLink="/tabs/settings">
{{ "newCustomizationOptionsCalloutLink" | i18n }}
</a>
</bit-popover>
</popup-page>

View File

@@ -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;