1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[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
This commit is contained in:
Nick Krantz
2025-11-25 11:28:34 -06:00
committed by GitHub
parent 17ae78ea83
commit 441783627b
6 changed files with 90 additions and 31 deletions

View File

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

View File

@@ -51,10 +51,26 @@
{{ "assignToCollections" | i18n }}
</a>
</ng-container>
@if (canArchive$ | async) {
<button type="button" bitMenuItem (click)="archive()">
{{ "archiveVerb" | i18n }}
</button>
@if (showArchive$ | async) {
@if (canArchive$ | async) {
<button type="button" bitMenuItem (click)="archive()">
{{ "archiveVerb" | i18n }}
</button>
} @else {
<button
type="button"
bitMenuItem
(click)="badge.promptForPremium($event)"
[attr.aria-label]="'upgradeToUseArchive' | i18n"
>
<div class="tw-flex tw-flex-nowrap tw-items-center tw-gap-2">
{{ "archiveVerb" | i18n }}
<div aria-hidden>
<app-premium-badge #badge></app-premium-badge>
</div>
</div>
</button>
}
}
@if (canDelete$ | async) {
<button type="button" bitMenuItem (click)="delete()">

View File

@@ -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 },

View File

@@ -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<CipherViewLike>({} 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<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$();
protected canArchive$: Observable<boolean> = 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" },

View File

@@ -34,13 +34,27 @@
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
@if (userCanArchive() || showArchiveFilter()) {
<bit-item>
<a bit-item-content routerLink="/archive">
{{ "archiveNoun" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
@if (showArchiveItem()) {
@if (userCanArchive()) {
<bit-item>
<a bit-item-content routerLink="/archive">
{{ "archiveNoun" | i18n }}
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
} @else {
<bit-item>
<a bit-item-content [routerLink]="userHasArchivedItems() ? '/archive' : '/premium'">
<span class="tw-flex tw-items-center tw-gap-2">
{{ "archiveNoun" | i18n }}
@if (!userHasArchivedItems()) {
<app-premium-badge></app-premium-badge>
}
</span>
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
</a>
</bit-item>
}
}
<bit-item>
<a bit-item-content routerLink="/trash">

View File

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