From 441783627b9af11a0ca339dcc7aa789787b3c468 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:28:34 -0600 Subject: [PATCH] [PM-26359] Archive Upgrade - Browser (#16904) * add archive upgrade flow to more options menu * add reprompt for archiving a cipher * add premium badge for archive in settings * update showArchive to only look at the feature flag * add premium badge for browser settings * add event to prompt for premium * formatting * update test --- apps/browser/src/_locales/en/messages.json | 3 ++ .../item-more-options.component.html | 24 +++++++++-- .../item-more-options.component.spec.ts | 5 ++- .../item-more-options.component.ts | 40 ++++++++++++------- .../settings/vault-settings-v2.component.html | 28 +++++++++---- .../settings/vault-settings-v2.component.ts | 21 +++++++--- 6 files changed, 90 insertions(+), 31 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 14915175da..6009bcba1b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -585,6 +585,9 @@ "archiveItemConfirmDesc": { "message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?" }, + "upgradeToUseArchive": { + "message": "A premium membership is required to use Archive." + }, "edit": { "message": "Edit" }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index b6a7002139..5c5171ac81 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -51,10 +51,26 @@ {{ "assignToCollections" | i18n }} - @if (canArchive$ | async) { - - {{ "archiveVerb" | i18n }} - + @if (showArchive$ | async) { + @if (canArchive$ | async) { + + {{ "archiveVerb" | i18n }} + + } @else { + + + {{ "archiveVerb" | i18n }} + + + + + + } } @if (canDelete$ | async) { diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts index 7b71c2b470..577b7d9677 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.spec.ts @@ -106,7 +106,10 @@ describe("ItemMoreOptionsComponent", () => { }, { provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } }, { provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } }, - { provide: CipherArchiveService, useValue: { userCanArchive$: () => of(true) } }, + { + provide: CipherArchiveService, + useValue: { userCanArchive$: () => of(true), hasArchiveFlagEnabled$: () => of(true) }, + }, { provide: ToastService, useValue: { showToast: () => {} } }, { provide: Router, useValue: { navigate: () => Promise.resolve(true) } }, { provide: PasswordRepromptService, useValue: passwordRepromptService }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index b498e7cd9a..4dfaf7bc66 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, Input } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; -import { BehaviorSubject, combineLatest, firstValueFrom, map, switchMap } from "rxjs"; +import { BehaviorSubject, combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { filter } from "rxjs/operators"; import { CollectionService } from "@bitwarden/admin-console/common"; +import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -17,6 +18,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; @@ -33,6 +35,7 @@ import { } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; @@ -46,7 +49,18 @@ import { @Component({ selector: "app-item-more-options", templateUrl: "./item-more-options.component.html", - imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule], + imports: [ + ItemModule, + IconButtonModule, + MenuModule, + CommonModule, + JslibModule, + RouterModule, + PremiumBadgeComponent, + ], + providers: [ + { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, + ], }) export class ItemMoreOptionsComponent { private _cipher$ = new BehaviorSubject({} as CipherViewLike); @@ -127,18 +141,11 @@ export class ItemMoreOptionsComponent { }), ); - /** Observable Boolean checking if item can show Archive menu option */ - protected canArchive$ = combineLatest([ - this._cipher$, - this.accountService.activeAccount$.pipe( - getUserId, - switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)), - ), - ]).pipe( - filter(([cipher, userId]) => cipher != null && userId != null), - map(([cipher, canArchive]) => { - return canArchive && !CipherViewLikeUtils.isArchived(cipher) && cipher.organizationId == null; - }), + protected showArchive$: Observable = this.cipherArchiveService.hasArchiveFlagEnabled$(); + + protected canArchive$: Observable = this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)), ); protected canDelete$ = this._cipher$.pipe( @@ -377,6 +384,11 @@ export class ItemMoreOptionsComponent { } async archive() { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher); + if (!repromptPassed) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "archiveItem" }, content: { key: "archiveItemConfirmDesc" }, diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index 3c1278b4d4..225640137e 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -34,13 +34,27 @@ - @if (userCanArchive() || showArchiveFilter()) { - - - {{ "archiveNoun" | i18n }} - - - + @if (showArchiveItem()) { + @if (userCanArchive()) { + + + {{ "archiveNoun" | i18n }} + + + + } @else { + + + + {{ "archiveNoun" | i18n }} + @if (!userHasArchivedItems()) { + + } + + + + + } } diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts index ff6e9b4065..c6db820c23 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.ts @@ -2,14 +2,16 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { toSignal } from "@angular/core/rxjs-interop"; import { Router, RouterModule } from "@angular/router"; -import { firstValueFrom, switchMap } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; +import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { NudgesService, NudgeType } from "@bitwarden/angular/vault"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service"; +import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components"; @@ -18,6 +20,7 @@ import BrowserPopupUtils from "../../../platform/browser/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; +import { BrowserPremiumUpgradePromptService } from "../services/browser-premium-upgrade-prompt.service"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -32,20 +35,28 @@ import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.co PopOutComponent, ItemModule, BadgeComponent, + PremiumBadgeComponent, + ], + providers: [ + { provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService }, ], }) export class VaultSettingsV2Component implements OnInit, OnDestroy { lastSync = "--"; private userId$ = this.accountService.activeAccount$.pipe(getUserId); - // Check if user is premium user, they will be able to archive items protected readonly userCanArchive = toSignal( this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))), ); - // Check if user has archived items (does not check if user is premium) - protected readonly showArchiveFilter = toSignal( - this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))), + protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$()); + + protected readonly userHasArchivedItems = toSignal( + this.userId$.pipe( + switchMap((userId) => + this.cipherArchiveService.archivedCiphers$(userId).pipe(map((c) => c.length > 0)), + ), + ), ); protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(