diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index fa300b4253b..df35facff3c 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2208,15 +2208,6 @@ "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/new-settings-callout/new-settings-callout.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html deleted file mode 100644 index 6cc60eed6d5..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - -
- {{ "newCustomizationOptionsCalloutContent" | i18n }} - - {{ "newCustomizationOptionsCalloutLink" | i18n }} - -
-
-
diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts deleted file mode 100644 index 713dc21c424..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/new-settings-callout/new-settings-callout.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -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 { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { ButtonModule, PopoverModule } from "@bitwarden/components"; - -import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service"; -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, - private copyButtonService: VaultPopupCopyButtonsService, - private vaultSettingsService: VaultSettingsService, - ) {} - - async ngOnInit() { - this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - - const showQuickCopyActions = await firstValueFrom(this.copyButtonService.showQuickCopyActions$); - const clickItemsToAutofillVaultView = await firstValueFrom( - this.vaultSettingsService.clickItemsToAutofillVaultView$, - ); - - 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 = - !showQuickCopyActions && - !clickItemsToAutofillVaultView && - !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(); - } -} 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 deleted file mode 100644 index a7c52ed4c51..00000000000 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-page.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -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 b46002d645e..43a96fc616e 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 @@ -107,5 +107,4 @@ > - 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 58a6ba0000b..ec4f3939204 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 @@ -56,9 +56,7 @@ import { NewItemDropdownV2Component, NewItemInitialValues, } 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 { VaultPageService } from "./vault-page.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "."; @@ -90,12 +88,10 @@ enum VaultState { ScrollingModule, VaultHeaderV2Component, AtRiskPasswordCalloutComponent, - NewSettingsCalloutComponent, SpotlightComponent, RouterModule, TypographyModule, ], - providers: [VaultPageService], }) export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { @ViewChild(CdkVirtualScrollableElement) virtualScrollElement?: CdkVirtualScrollableElement; @@ -152,7 +148,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { protected noResultsIcon = Icons.NoResults; protected VaultStateEnum = VaultState; - protected showNewCustomizationSettingsCallout = false; constructor( private vaultPopupItemsService: VaultPopupItemsService, diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index b409f52d936..bea79963b0b 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -69,12 +69,13 @@ import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date"; import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed"; +import { RemoveNewCustomizationOptionsCalloutDismissed } from "./migrations/71-remove-new-customization-options-callout-dismissed"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 70; +export const CURRENT_VERSION = 71; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -146,7 +147,8 @@ export function createMigrationBuilder() { .with(RemoveUnassignedItemsBannerDismissed, 66, 67) .with(MoveLastSyncDate, 67, 68) .with(MigrateIncorrectFolderKey, 68, 69) - .with(RemoveAcBannersDismissed, 69, CURRENT_VERSION); + .with(RemoveAcBannersDismissed, 69, 70) + .with(RemoveNewCustomizationOptionsCalloutDismissed, 70, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts new file mode 100644 index 00000000000..f2c83346a62 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.spec.ts @@ -0,0 +1,50 @@ +import { runMigrator } from "../migration-helper.spec"; +import { IRREVERSIBLE } from "../migrator"; + +import { RemoveNewCustomizationOptionsCalloutDismissed } from "./71-remove-new-customization-options-callout-dismissed"; + +describe("RemoveNewCustomizationOptionsCalloutDismissed", () => { + const sut = new RemoveNewCustomizationOptionsCalloutDismissed(70, 71); + + describe("migrate", () => { + it("deletes new customization options callout dismissed from all users", async () => { + const output = await runMigrator(sut, { + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + user_user1_bannersDismissed_newCustomizationOptionsCalloutDismissed: true, + user_user2_bannersDismissed_newCustomizationOptionsCalloutDismissed: true, + }); + + expect(output).toEqual({ + global_account_accounts: { + user1: { + email: "user1@email.com", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "user2@email.com", + name: "User 2", + emailVerified: true, + }, + }, + }); + }); + }); + + describe("rollback", () => { + it("is irreversible", async () => { + await expect(runMigrator(sut, {}, "rollback")).rejects.toThrow(IRREVERSIBLE); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts new file mode 100644 index 00000000000..7260048daf6 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/71-remove-new-customization-options-callout-dismissed.ts @@ -0,0 +1,23 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { IRREVERSIBLE, Migrator } from "../migrator"; + +export const SHOW_CALLOUT_KEY: KeyDefinitionLike = { + key: "newCustomizationOptionsCalloutDismissed", + stateDefinition: { name: "bannersDismissed" }, +}; + +export class RemoveNewCustomizationOptionsCalloutDismissed extends Migrator<70, 71> { + async migrate(helper: MigrationHelper): Promise { + await Promise.all( + (await helper.getAccounts()).map(async ({ userId }) => { + if (helper.getFromUser(userId, SHOW_CALLOUT_KEY) != null) { + await helper.removeFromUser(userId, SHOW_CALLOUT_KEY); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + throw IRREVERSIBLE; + } +}