mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 06:43:35 +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": {
|
"archiveItemConfirmDesc": {
|
||||||
"message": "Archived items are excluded from general search results and autofill suggestions. Are you sure you want to archive this item?"
|
"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": {
|
"edit": {
|
||||||
"message": "Edit"
|
"message": "Edit"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,10 +51,26 @@
|
|||||||
{{ "assignToCollections" | i18n }}
|
{{ "assignToCollections" | i18n }}
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@if (canArchive$ | async) {
|
@if (showArchive$ | async) {
|
||||||
<button type="button" bitMenuItem (click)="archive()">
|
@if (canArchive$ | async) {
|
||||||
{{ "archiveVerb" | i18n }}
|
<button type="button" bitMenuItem (click)="archive()">
|
||||||
</button>
|
{{ "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) {
|
@if (canDelete$ | async) {
|
||||||
<button type="button" bitMenuItem (click)="delete()">
|
<button type="button" bitMenuItem (click)="delete()">
|
||||||
|
|||||||
@@ -106,7 +106,10 @@ describe("ItemMoreOptionsComponent", () => {
|
|||||||
},
|
},
|
||||||
{ provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } },
|
{ provide: CollectionService, useValue: { decryptedCollections$: () => of([]) } },
|
||||||
{ provide: RestrictedItemTypesService, useValue: { restricted$: 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: ToastService, useValue: { showToast: () => {} } },
|
||||||
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
{ provide: Router, useValue: { navigate: () => Promise.resolve(true) } },
|
||||||
{ provide: PasswordRepromptService, useValue: passwordRepromptService },
|
{ provide: PasswordRepromptService, useValue: passwordRepromptService },
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { booleanAttribute, Component, Input } from "@angular/core";
|
import { booleanAttribute, Component, Input } from "@angular/core";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
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 { filter } from "rxjs/operators";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
|
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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 { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||||
|
|
||||||
|
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||||
@@ -46,7 +49,18 @@ import {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: "app-item-more-options",
|
selector: "app-item-more-options",
|
||||||
templateUrl: "./item-more-options.component.html",
|
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 {
|
export class ItemMoreOptionsComponent {
|
||||||
private _cipher$ = new BehaviorSubject<CipherViewLike>({} as CipherViewLike);
|
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 showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$();
|
||||||
protected canArchive$ = combineLatest([
|
|
||||||
this._cipher$,
|
protected canArchive$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||||
this.accountService.activeAccount$.pipe(
|
getUserId,
|
||||||
getUserId,
|
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||||
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 canDelete$ = this._cipher$.pipe(
|
protected canDelete$ = this._cipher$.pipe(
|
||||||
@@ -377,6 +384,11 @@ export class ItemMoreOptionsComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async archive() {
|
async archive() {
|
||||||
|
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(this.cipher);
|
||||||
|
if (!repromptPassed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const confirmed = await this.dialogService.openSimpleDialog({
|
const confirmed = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "archiveItem" },
|
title: { key: "archiveItem" },
|
||||||
content: { key: "archiveItemConfirmDesc" },
|
content: { key: "archiveItemConfirmDesc" },
|
||||||
|
|||||||
@@ -34,13 +34,27 @@
|
|||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</bit-item>
|
</bit-item>
|
||||||
@if (userCanArchive() || showArchiveFilter()) {
|
@if (showArchiveItem()) {
|
||||||
<bit-item>
|
@if (userCanArchive()) {
|
||||||
<a bit-item-content routerLink="/archive">
|
<bit-item>
|
||||||
{{ "archiveNoun" | i18n }}
|
<a bit-item-content routerLink="/archive">
|
||||||
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
{{ "archiveNoun" | i18n }}
|
||||||
</a>
|
<i slot="end" class="bwi bwi-angle-right" aria-hidden="true"></i>
|
||||||
</bit-item>
|
</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>
|
<bit-item>
|
||||||
<a bit-item-content routerLink="/trash">
|
<a bit-item-content routerLink="/trash">
|
||||||
|
|||||||
@@ -2,14 +2,16 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { toSignal } from "@angular/core/rxjs-interop";
|
import { toSignal } from "@angular/core/rxjs-interop";
|
||||||
import { Router, RouterModule } from "@angular/router";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
|
||||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.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 { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { BadgeComponent, ItemModule, ToastOptions, ToastService } from "@bitwarden/components";
|
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 { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.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
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
// 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,
|
PopOutComponent,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
BadgeComponent,
|
BadgeComponent,
|
||||||
|
PremiumBadgeComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: PremiumUpgradePromptService, useClass: BrowserPremiumUpgradePromptService },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
||||||
lastSync = "--";
|
lastSync = "--";
|
||||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
|
|
||||||
// Check if user is premium user, they will be able to archive items
|
|
||||||
protected readonly userCanArchive = toSignal(
|
protected readonly userCanArchive = toSignal(
|
||||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if user has archived items (does not check if user is premium)
|
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
||||||
protected readonly showArchiveFilter = toSignal(
|
|
||||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.showArchiveVault$(userId))),
|
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(
|
protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
|
||||||
|
|||||||
Reference in New Issue
Block a user