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:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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()">
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user