1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[PM-18131] - Add popover prompt for new settings (#13518)

* Update the Appearance settings page to include click to fill setting

* fix tests

* fix tests

* add tests

* new customization options callout

* use classes instead of inline styling

* revert changes to index and tw-theme

* remove shared module

* Revert "remove shared module"

This reverts commit 0b68aaae23.

* Revert "revert changes to index and tw-theme"

This reverts commit 4a05f0ca20.

* Revert "use classes instead of inline styling"

This reverts commit 0e441c4284.

* Revert "new customization options callout"

This reverts commit f3054c9b27.

* new customization options callout

* use classes instead of inline styling

* revert changes to index and tw-theme

* remove shared module

* finalize new settings callout

* revert changes to vault-v2.component

* remove unused modules. add error handling. adjust html
This commit is contained in:
Jordan Aasen
2025-02-24 14:51:55 -08:00
committed by GitHub
parent e06a482d6e
commit fb20aa556b
6 changed files with 151 additions and 1 deletions

View File

@@ -2143,6 +2143,15 @@
"vaultTimeoutAction1": { "vaultTimeoutAction1": {
"message": "Timeout action" "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": { "lock": {
"message": "Lock", "message": "Lock",
"description": "Verb form: to make secure or inaccessible by" "description": "Verb form: to make secure or inaccessible by"

View File

@@ -0,0 +1,22 @@
<ng-container *ngIf="showNewCustomizationSettingsCallout">
<button
type="button"
class="tw-absolute tw-bottom-[12px] tw-right-[47px]"
[bitPopoverTriggerFor]="newCustomizationOptionsCallout"
[position]="'above-end'"
[popoverOpen]="true"
#triggerRef="popoverTrigger"
></button>
<bit-popover
[title]="'newCustomizationOptionsCalloutTitle' | i18n"
#newCustomizationOptionsCallout
(closed)="dismissCallout()"
>
<div bitTypography="body2" (click)="goToAppearance()">
{{ "newCustomizationOptionsCalloutContent" | i18n }}
<a routerLink="/appearance">
{{ "newCustomizationOptionsCalloutLink" | i18n }}
</a>
</div>
</bit-popover>
</ng-container>

View File

@@ -0,0 +1,69 @@
import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom } 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { UserId } from "@bitwarden/common/types/guid";
import { ButtonModule, PopoverModule } from "@bitwarden/components";
import { VaultPageService } from "../vault-page.service";
@Component({
selector: "new-settings-callout",
templateUrl: "new-settings-callout.component.html",
standalone: true,
imports: [PopoverModule, JslibModule, CommonModule, ButtonModule],
providers: [VaultPageService],
})
export class NewSettingsCalloutComponent implements OnInit, OnDestroy {
protected showNewCustomizationSettingsCallout = false;
protected activeUserId: UserId | null = null;
constructor(
private accountService: AccountService,
private vaultProfileService: VaultProfileService,
private vaultPageService: VaultPageService,
private router: Router,
private logService: LogService,
) {}
async ngOnInit() {
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
let profileCreatedDate: Date;
try {
profileCreatedDate = await this.vaultProfileService.getProfileCreationDate(this.activeUserId);
} catch (e) {
this.logService.error("Error getting profile creation date", e);
// Default to before the cutoff date to ensure the callout is shown
profileCreatedDate = new Date("2024-12-24");
}
const hasCalloutBeenDismissed = await firstValueFrom(
this.vaultPageService.isCalloutDismissed(this.activeUserId),
);
this.showNewCustomizationSettingsCallout =
!hasCalloutBeenDismissed && profileCreatedDate < new Date("2024-12-25");
}
async goToAppearance() {
await this.router.navigate(["/appearance"]);
}
async dismissCallout() {
if (this.activeUserId) {
await this.vaultPageService.dismissCallout(this.activeUserId);
}
}
async ngOnDestroy() {
await this.dismissCallout();
}
}

View File

@@ -0,0 +1,41 @@
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);
async unDismissCallout(userId: UserId): Promise<void> {
await this.stateProvider
.getUser(userId, NEW_CUSTOMIZATION_OPTIONS_CALLOUT_DISMISSED_KEY)
.update(() => false);
}
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,5 @@
></app-vault-list-items-container> ></app-vault-list-items-container>
</div> </div>
</ng-container> </ng-container>
<new-settings-callout></new-settings-callout>
</popup-page> </popup-page>

View File

@@ -15,6 +15,7 @@ import {
} from "rxjs"; } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -44,7 +45,9 @@ import {
NewItemDropdownV2Component, NewItemDropdownV2Component,
NewItemInitialValues, NewItemInitialValues,
} from "./new-item-dropdown/new-item-dropdown-v2.component"; } from "./new-item-dropdown/new-item-dropdown-v2.component";
import { NewSettingsCalloutComponent } from "./new-settings-callout/new-settings-callout.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component"; import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { VaultPageService } from "./vault-page.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
@@ -77,7 +80,9 @@ enum VaultState {
DecryptionFailureDialogComponent, DecryptionFailureDialogComponent,
BannerComponent, BannerComponent,
AtRiskPasswordCalloutComponent, AtRiskPasswordCalloutComponent,
NewSettingsCalloutComponent,
], ],
providers: [VaultPageService],
}) })
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement;
@@ -115,6 +120,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
protected noResultsIcon = Icons.NoResults; protected noResultsIcon = Icons.NoResults;
protected VaultStateEnum = VaultState; protected VaultStateEnum = VaultState;
protected showNewCustomizationSettingsCallout = false;
constructor( constructor(
private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupItemsService: VaultPopupItemsService,
@@ -124,6 +130,8 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private cipherService: CipherService, private cipherService: CipherService,
private dialogService: DialogService, private dialogService: DialogService,
private vaultProfileService: VaultProfileService,
private vaultPageService: VaultPageService,
) { ) {
combineLatest([ combineLatest([
this.vaultPopupItemsService.emptyVault$, this.vaultPopupItemsService.emptyVault$,
@@ -178,7 +186,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
}); });
} }
ngOnDestroy(): void { ngOnDestroy() {
this.vaultScrollPositionService.stop(); this.vaultScrollPositionService.stop();
} }