mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
PM-24535 Web premium upgrade path for archive (#16854)
* add premium badge to web filter when the user does not have access to premium * remove feature flag pass through in favor of showing/hiding archive vault observable * refactor archive observable to be more generic * add archive premium badge for the web * show premium badge inline for archive filter * show premium subscription ended message when user has archived ciphers * fix missing refactor * remove unneeded can archive check * reference observable directly * reduce the number of firstValueFroms by combining observables into a single stream * fix failing tests * add import to storybook * update variable naming for premium filters * pass event to `promptForPremium` * remove check for organization * fix footer variable reference * refactor back to `hasArchiveFlagEnabled$` - more straight forward to the underlying logic * update archive service test with new feature flag format
This commit is contained in:
@@ -108,7 +108,7 @@ describe("ItemMoreOptionsComponent", () => {
|
|||||||
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
{ provide: RestrictedItemTypesService, useValue: { restricted$: of([]) } },
|
||||||
{
|
{
|
||||||
provide: CipherArchiveService,
|
provide: CipherArchiveService,
|
||||||
useValue: { userCanArchive$: () => of(true), hasArchiveFlagEnabled$: () => of(true) },
|
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) } },
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export class ItemMoreOptionsComponent {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$();
|
protected showArchive$: Observable<boolean> = this.cipherArchiveService.hasArchiveFlagEnabled$;
|
||||||
|
|
||||||
protected canArchive$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
protected canArchive$: Observable<boolean> = this.accountService.activeAccount$.pipe(
|
||||||
getUserId,
|
getUserId,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {
|
|||||||
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
this.userId$.pipe(switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId))),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
protected readonly showArchiveItem = toSignal(this.cipherArchiveService.hasArchiveFlagEnabled$);
|
||||||
|
|
||||||
protected readonly userHasArchivedItems = toSignal(
|
protected readonly userHasArchivedItems = toSignal(
|
||||||
this.userId$.pipe(
|
this.userId$.pipe(
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
|
|||||||
switchMap((id) =>
|
switchMap((id) =>
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.cipherArchiveService.userCanArchive$(id),
|
this.cipherArchiveService.userCanArchive$(id),
|
||||||
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
@@ -59,6 +60,7 @@ export class VaultFilterComponent
|
|||||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
protected cipherArchiveService: CipherArchiveService,
|
protected cipherArchiveService: CipherArchiveService,
|
||||||
|
premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
vaultFilterService,
|
vaultFilterService,
|
||||||
@@ -72,6 +74,7 @@ export class VaultFilterComponent
|
|||||||
restrictedItemTypesService,
|
restrictedItemTypesService,
|
||||||
cipherService,
|
cipherService,
|
||||||
cipherArchiveService,
|
cipherArchiveService,
|
||||||
|
premiumUpgradePromptService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,10 +203,22 @@
|
|||||||
{{ "eventLogs" | i18n }}
|
{{ "eventLogs" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
@if (showArchiveButton) {
|
@if (showArchiveButton) {
|
||||||
<button bitMenuItem (click)="archive()" type="button">
|
@if (userCanArchive) {
|
||||||
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
<button bitMenuItem (click)="archive()" type="button">
|
||||||
{{ "archiveVerb" | i18n }}
|
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||||
</button>
|
{{ "archiveVerb" | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (!userCanArchive) {
|
||||||
|
<button bitMenuItem (click)="badge.promptForPremium($event)" type="button">
|
||||||
|
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
|
||||||
|
{{ "archiveVerb" | i18n }}
|
||||||
|
<!-- Hide app-premium badge from accessibility tools as it results in a button within a button -->
|
||||||
|
<div slot="end" class="-tw-mt-0.5" aria-hidden>
|
||||||
|
<app-premium-badge #badge></app-premium-badge>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (showUnArchiveButton) {
|
@if (showUnArchiveButton) {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ describe("VaultCipherRowComponent", () => {
|
|||||||
|
|
||||||
fixture = TestBed.createComponent(VaultCipherRowComponent);
|
fixture = TestBed.createComponent(VaultCipherRowComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
|
fixture.componentRef.setInput("archiveEnabled", false);
|
||||||
overlayContainer = TestBed.inject(OverlayContainer);
|
overlayContainer = TestBed.inject(OverlayContainer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
|
input,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
@@ -101,8 +102,10 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||||
@Input() userCanArchive: boolean;
|
@Input() userCanArchive: boolean;
|
||||||
|
/** Archive feature is enabled */
|
||||||
|
readonly archiveEnabled = input.required<boolean>();
|
||||||
/**
|
/**
|
||||||
* Enforge Org Data Ownership Policy Status
|
* Enforce Org Data Ownership Policy Status
|
||||||
*/
|
*/
|
||||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||||
@@ -142,16 +145,21 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected get showArchiveButton() {
|
protected get showArchiveButton() {
|
||||||
|
if (!this.archiveEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.userCanArchive &&
|
!CipherViewLikeUtils.isArchived(this.cipher) && !CipherViewLikeUtils.isDeleted(this.cipher)
|
||||||
!CipherViewLikeUtils.isArchived(this.cipher) &&
|
|
||||||
!CipherViewLikeUtils.isDeleted(this.cipher) &&
|
|
||||||
!this.cipher.organizationId
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If item is archived always show unarchive button, even if user is not premium
|
// If item is archived always show unarchive button, even if user is not premium
|
||||||
protected get showUnArchiveButton() {
|
protected get showUnArchiveButton() {
|
||||||
|
if (!this.archiveEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return CipherViewLikeUtils.isArchived(this.cipher);
|
return CipherViewLikeUtils.isArchived(this.cipher);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,7 @@
|
|||||||
(onEvent)="event($event)"
|
(onEvent)="event($event)"
|
||||||
[userCanArchive]="userCanArchive"
|
[userCanArchive]="userCanArchive"
|
||||||
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy"
|
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy"
|
||||||
|
[archiveEnabled]="archiveFeatureEnabled$ | async"
|
||||||
></tr>
|
></tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { of } from "rxjs";
|
|||||||
|
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
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";
|
||||||
@@ -54,6 +55,12 @@ describe("VaultItemsComponent", () => {
|
|||||||
t: (key: string) => key,
|
t: (key: string) => key,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useValue: {
|
||||||
|
hasArchiveFlagEnabled$: of(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
|||||||
|
|
||||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||||
import {
|
import {
|
||||||
RestrictedCipherType,
|
RestrictedCipherType,
|
||||||
@@ -145,9 +146,12 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
|||||||
protected disableMenu$: Observable<boolean>;
|
protected disableMenu$: Observable<boolean>;
|
||||||
private restrictedTypes: RestrictedCipherType[] = [];
|
private restrictedTypes: RestrictedCipherType[] = [];
|
||||||
|
|
||||||
|
protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
|
protected cipherArchiveService: CipherArchiveService,
|
||||||
) {
|
) {
|
||||||
this.canDeleteSelected$ = this.selection.changed.pipe(
|
this.canDeleteSelected$ = this.selection.changed.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { CommonModule } from "@angular/common";
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule } from "@angular/router";
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
|
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||||
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
|
||||||
import { CopyCipherFieldDirective } from "@bitwarden/vault";
|
import { CopyCipherFieldDirective } from "@bitwarden/vault";
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ import { VaultItemsComponent } from "./vault-items.component";
|
|||||||
PipesModule,
|
PipesModule,
|
||||||
CopyCipherFieldDirective,
|
CopyCipherFieldDirective,
|
||||||
ScrollLayoutDirective,
|
ScrollLayoutDirective,
|
||||||
|
PremiumBadgeComponent,
|
||||||
],
|
],
|
||||||
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
|
||||||
exports: [VaultItemsComponent],
|
exports: [VaultItemsComponent],
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
|
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@@ -143,6 +144,12 @@ export default {
|
|||||||
isCipherRestricted: () => false, // No restrictions for this story
|
isCipherRestricted: () => false, // No restrictions for this story
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CipherArchiveService,
|
||||||
|
useValue: {
|
||||||
|
hasArchiveFlagEnabled$: of(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
applicationConfig({
|
applicationConfig({
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
|||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { 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 { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||||
@@ -170,6 +172,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
protected cipherService: CipherService,
|
protected cipherService: CipherService,
|
||||||
protected cipherArchiveService: CipherArchiveService,
|
protected cipherArchiveService: CipherArchiveService,
|
||||||
|
private premiumUpgradePromptService: PremiumUpgradePromptService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
@@ -252,14 +255,20 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async buildAllFilters(): Promise<VaultFilterList> {
|
async buildAllFilters(): Promise<VaultFilterList> {
|
||||||
const hasArchiveFlag = await firstValueFrom(this.cipherArchiveService.hasArchiveFlagEnabled$());
|
const [userId, showArchive] = await firstValueFrom(
|
||||||
|
combineLatest([
|
||||||
|
this.accountService.activeAccount$.pipe(getUserId),
|
||||||
|
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
const builderFilter = {} as VaultFilterList;
|
const builderFilter = {} as VaultFilterList;
|
||||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||||
builderFilter.typeFilter = await this.addTypeFilter();
|
builderFilter.typeFilter = await this.addTypeFilter();
|
||||||
builderFilter.folderFilter = await this.addFolderFilter();
|
builderFilter.folderFilter = await this.addFolderFilter();
|
||||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||||
if (hasArchiveFlag) {
|
if (showArchive) {
|
||||||
builderFilter.archiveFilter = await this.addArchiveFilter();
|
builderFilter.archiveFilter = await this.addArchiveFilter(userId);
|
||||||
}
|
}
|
||||||
builderFilter.trashFilter = await this.addTrashFilter();
|
builderFilter.trashFilter = await this.addTrashFilter();
|
||||||
return builderFilter;
|
return builderFilter;
|
||||||
@@ -419,7 +428,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
return trashFilterSection;
|
return trashFilterSection;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async addArchiveFilter(): Promise<VaultFilterSection> {
|
protected async addArchiveFilter(userId: UserId): Promise<VaultFilterSection> {
|
||||||
|
const [hasArchivedCiphers, userHasPremium] = await firstValueFrom(
|
||||||
|
combineLatest([
|
||||||
|
this.cipherArchiveService
|
||||||
|
.archivedCiphers$(userId)
|
||||||
|
.pipe(map((archivedCiphers) => archivedCiphers.length > 0)),
|
||||||
|
this.cipherArchiveService.userHasPremium$(userId),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const promptForPremiumOnFilter = !userHasPremium && !hasArchivedCiphers;
|
||||||
|
|
||||||
const archiveFilterSection: VaultFilterSection = {
|
const archiveFilterSection: VaultFilterSection = {
|
||||||
data$: this.vaultFilterService.buildTypeTree(
|
data$: this.vaultFilterService.buildTypeTree(
|
||||||
{
|
{
|
||||||
@@ -442,6 +462,12 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
|||||||
isSelectable: true,
|
isSelectable: true,
|
||||||
},
|
},
|
||||||
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||||
|
premiumOptions: {
|
||||||
|
showBadgeForNonPremium: true,
|
||||||
|
blockFilterAction: promptForPremiumOnFilter
|
||||||
|
? async () => await this.premiumUpgradePromptService.promptForPremium()
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return archiveFilterSection;
|
return archiveFilterSection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,9 @@
|
|||||||
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
|
||||||
></ng-container>
|
></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<ng-container *ngIf="premiumFeature">
|
||||||
|
<app-premium-badge></app-premium-badge>
|
||||||
|
</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<ul
|
<ul
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
|
async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
|
||||||
|
if (this.section?.premiumOptions?.blockFilterAction) {
|
||||||
|
await this.section.premiumOptions.blockFilterAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.section?.action(filterNode);
|
await this.section?.action(filterNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +128,10 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
|
|||||||
return this.section?.options;
|
return this.section?.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get premiumFeature() {
|
||||||
|
return this.section?.premiumOptions?.showBadgeForNonPremium;
|
||||||
|
}
|
||||||
|
|
||||||
get divider() {
|
get divider() {
|
||||||
return this.section?.divider;
|
return this.section?.divider;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@ export type VaultFilterSection = {
|
|||||||
component: any;
|
component: any;
|
||||||
};
|
};
|
||||||
divider?: boolean;
|
divider?: boolean;
|
||||||
|
premiumOptions?: {
|
||||||
|
/** When true, the premium badge will show on the filter for non-premium users. */
|
||||||
|
showBadgeForNonPremium?: true;
|
||||||
|
/**
|
||||||
|
* Action to be called instead of applying the filter.
|
||||||
|
* Useful when the user does not have access to a filter (e.g., premium feature)
|
||||||
|
* and custom behavior is needed when invoking the filter.
|
||||||
|
*/
|
||||||
|
blockFilterAction?: () => Promise<void>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VaultFilterList = {
|
export type VaultFilterList = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||||
import { SearchModule } from "@bitwarden/components";
|
import { SearchModule } from "@bitwarden/components";
|
||||||
|
|
||||||
import { SharedModule } from "../../../../shared";
|
import { SharedModule } from "../../../../shared";
|
||||||
@@ -7,7 +8,7 @@ import { SharedModule } from "../../../../shared";
|
|||||||
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
|
import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, SearchModule],
|
imports: [SharedModule, SearchModule, PremiumBadgeComponent],
|
||||||
declarations: [VaultFilterSectionComponent],
|
declarations: [VaultFilterSectionComponent],
|
||||||
exports: [SharedModule, VaultFilterSectionComponent, SearchModule],
|
exports: [SharedModule, VaultFilterSectionComponent, SearchModule],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
||||||
{{ trashCleanupWarning }}
|
{{ trashCleanupWarning }}
|
||||||
</bit-callout>
|
</bit-callout>
|
||||||
|
<bit-callout
|
||||||
|
type="info"
|
||||||
|
[title]="'premiumSubscriptionEnded' | i18n"
|
||||||
|
*ngIf="showSubscriptionEndedMessaging$ | async"
|
||||||
|
>
|
||||||
|
<p>{{ "premiumSubscriptionEndedDesc" | i18n }}</p>
|
||||||
|
<a routerLink="/settings/subscription/premium" bitButton buttonType="primary">{{
|
||||||
|
"restartPremium" | i18n
|
||||||
|
}}</a>
|
||||||
|
</bit-callout>
|
||||||
<app-vault-items
|
<app-vault-items
|
||||||
#vaultItems
|
#vaultItems
|
||||||
[ciphers]="ciphers"
|
[ciphers]="ciphers"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ import {
|
|||||||
CipherViewLikeUtils,
|
CipherViewLikeUtils,
|
||||||
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
import { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
|
||||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
import { DialogRef, DialogService, ToastService, BannerComponent } from "@bitwarden/components";
|
||||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||||
import {
|
import {
|
||||||
AddEditFolderDialogComponent,
|
AddEditFolderDialogComponent,
|
||||||
@@ -177,6 +177,7 @@ type EmptyStateMap = Record<EmptyStateType, EmptyStateItem>;
|
|||||||
VaultItemsModule,
|
VaultItemsModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
OrganizationWarningsModule,
|
OrganizationWarningsModule,
|
||||||
|
BannerComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
RoutedVaultFilterService,
|
RoutedVaultFilterService,
|
||||||
@@ -230,13 +231,6 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
.pipe(map((a) => a?.id))
|
.pipe(map((a) => a?.id))
|
||||||
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
||||||
|
|
||||||
protected userCanArchive$ = this.accountService.activeAccount$.pipe(
|
|
||||||
getUserId,
|
|
||||||
switchMap((userId) => {
|
|
||||||
return this.cipherArchiveService.userCanArchive$(userId);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
emptyState$ = combineLatest([
|
emptyState$ = combineLatest([
|
||||||
this.currentSearchText$,
|
this.currentSearchText$,
|
||||||
this.routedVaultFilterService.filter$,
|
this.routedVaultFilterService.filter$,
|
||||||
@@ -295,14 +289,28 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
protected enforceOrgDataOwnershipPolicy$ = this.accountService.activeAccount$.pipe(
|
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
||||||
getUserId,
|
|
||||||
|
protected enforceOrgDataOwnershipPolicy$ = this.userId$.pipe(
|
||||||
switchMap((userId) =>
|
switchMap((userId) =>
|
||||||
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
this.policyService.policyAppliesToUser$(PolicyType.OrganizationDataOwnership, userId),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
|
protected userCanArchive$ = this.userId$.pipe(
|
||||||
|
switchMap((userId) => {
|
||||||
|
return this.cipherArchiveService.userCanArchive$(userId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected showSubscriptionEndedMessaging$ = this.userId$.pipe(
|
||||||
|
switchMap((userId) =>
|
||||||
|
combineLatest([
|
||||||
|
this.routedVaultFilterBridgeService.activeFilter$,
|
||||||
|
this.cipherArchiveService.showSubscriptionEndedMessaging$(userId),
|
||||||
|
]).pipe(map(([activeFilter, showMessaging]) => activeFilter.isArchived && showMessaging)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
@@ -438,13 +446,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
|||||||
allowedCiphers$,
|
allowedCiphers$,
|
||||||
filter$,
|
filter$,
|
||||||
this.currentSearchText$,
|
this.currentSearchText$,
|
||||||
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
|
||||||
concatMap(async ([ciphers, filter, searchText, archiveEnabled]) => {
|
concatMap(async ([ciphers, filter, searchText, showArchiveVault]) => {
|
||||||
const failedCiphers =
|
const failedCiphers =
|
||||||
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
|
||||||
const filterFunction = createFilterFunction(filter, archiveEnabled);
|
const filterFunction = createFilterFunction(filter, showArchiveVault);
|
||||||
// Append any failed to decrypt ciphers to the top of the cipher list
|
// Append any failed to decrypt ciphers to the top of the cipher list
|
||||||
const allCiphers = [...failedCiphers, ...ciphers];
|
const allCiphers = [...failedCiphers, ...ciphers];
|
||||||
|
|
||||||
|
|||||||
@@ -3133,6 +3133,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"premiumSubscriptionEnded": {
|
||||||
|
"message": "Your Premium subscription ended"
|
||||||
|
},
|
||||||
|
"premiumSubscriptionEndedDesc": {
|
||||||
|
"message": "To regain access to your archive, restart your Premium subscription. If you edit details for an archived item before restarting, it'll be moved back into your vault."
|
||||||
|
},
|
||||||
|
"restartPremium": {
|
||||||
|
"message": "Restart Premium"
|
||||||
|
},
|
||||||
"additionalStorageGb": {
|
"additionalStorageGb": {
|
||||||
"message": "Additional storage (GB)"
|
"message": "Additional storage (GB)"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export class VaultFilterComponent implements OnInit {
|
|||||||
this.collections = await this.initCollections();
|
this.collections = await this.initCollections();
|
||||||
|
|
||||||
this.showArchiveVaultFilter = await firstValueFrom(
|
this.showArchiveVaultFilter = await firstValueFrom(
|
||||||
this.cipherArchiveService.hasArchiveFlagEnabled$(),
|
this.cipherArchiveService.hasArchiveFlagEnabled$,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
|||||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||||
|
|
||||||
export abstract class CipherArchiveService {
|
export abstract class CipherArchiveService {
|
||||||
abstract hasArchiveFlagEnabled$(): Observable<boolean>;
|
abstract hasArchiveFlagEnabled$: Observable<boolean>;
|
||||||
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
||||||
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
||||||
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
abstract userHasPremium$(userId: UserId): Observable<boolean>;
|
||||||
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||||
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
|
||||||
|
abstract showSubscriptionEndedMessaging$(userId: UserId): Observable<boolean>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { of, firstValueFrom } from "rxjs";
|
import { of, firstValueFrom, BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||||
@@ -24,12 +24,14 @@ describe("DefaultCipherArchiveService", () => {
|
|||||||
|
|
||||||
const userId = "user-id" as UserId;
|
const userId = "user-id" as UserId;
|
||||||
const cipherId = "123" as CipherId;
|
const cipherId = "123" as CipherId;
|
||||||
|
const featureFlag = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockCipherService = mock<CipherService>();
|
mockCipherService = mock<CipherService>();
|
||||||
mockApiService = mock<ApiService>();
|
mockApiService = mock<ApiService>();
|
||||||
mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
mockBillingAccountProfileStateService = mock<BillingAccountProfileStateService>();
|
||||||
mockConfigService = mock<ConfigService>();
|
mockConfigService = mock<ConfigService>();
|
||||||
|
mockConfigService.getFeatureFlag$.mockReturnValue(featureFlag.asObservable());
|
||||||
|
|
||||||
service = new DefaultCipherArchiveService(
|
service = new DefaultCipherArchiveService(
|
||||||
mockCipherService,
|
mockCipherService,
|
||||||
@@ -86,7 +88,7 @@ describe("DefaultCipherArchiveService", () => {
|
|||||||
describe("userCanArchive$", () => {
|
describe("userCanArchive$", () => {
|
||||||
it("should return true when user has premium and feature flag is enabled", async () => {
|
it("should return true when user has premium and feature flag is enabled", async () => {
|
||||||
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(true));
|
featureFlag.next(true);
|
||||||
|
|
||||||
const result = await firstValueFrom(service.userCanArchive$(userId));
|
const result = await firstValueFrom(service.userCanArchive$(userId));
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ describe("DefaultCipherArchiveService", () => {
|
|||||||
|
|
||||||
it("should return false when feature flag is disabled", async () => {
|
it("should return false when feature flag is disabled", async () => {
|
||||||
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||||
mockConfigService.getFeatureFlag$.mockReturnValue(of(false));
|
featureFlag.next(false);
|
||||||
|
|
||||||
const result = await firstValueFrom(service.userCanArchive$(userId));
|
const result = await firstValueFrom(service.userCanArchive$(userId));
|
||||||
|
|
||||||
@@ -109,6 +111,93 @@ describe("DefaultCipherArchiveService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("hasArchiveFlagEnabled$", () => {
|
||||||
|
it("returns true when feature flag is enabled", async () => {
|
||||||
|
featureFlag.next(true);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.hasArchiveFlagEnabled$);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockConfigService.getFeatureFlag$).toHaveBeenCalledWith(
|
||||||
|
FeatureFlag.PM19148_InnovationArchive,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when feature flag is disabled", async () => {
|
||||||
|
featureFlag.next(false);
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.hasArchiveFlagEnabled$);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userHasPremium$", () => {
|
||||||
|
it("returns true when user has premium", async () => {
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.userHasPremium$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockBillingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when user does not have premium", async () => {
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.userHasPremium$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("showSubscriptionEndedMessaging$", () => {
|
||||||
|
it("returns true when user has archived ciphers but no premium", async () => {
|
||||||
|
const mockCiphers: CipherListView[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
type: "identity",
|
||||||
|
} as unknown as CipherListView,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.showSubscriptionEndedMessaging$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when user has archived ciphers and has premium", async () => {
|
||||||
|
const mockCiphers: CipherListView[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||||
|
type: "identity",
|
||||||
|
} as unknown as CipherListView,
|
||||||
|
];
|
||||||
|
|
||||||
|
mockCipherService.cipherListViews$.mockReturnValue(of(mockCiphers));
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.showSubscriptionEndedMessaging$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when user has no archived ciphers and no premium", async () => {
|
||||||
|
mockCipherService.cipherListViews$.mockReturnValue(of([]));
|
||||||
|
mockBillingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
const result = await firstValueFrom(service.showSubscriptionEndedMessaging$(userId));
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("archiveWithServer", () => {
|
describe("archiveWithServer", () => {
|
||||||
const mockResponse = {
|
const mockResponse = {
|
||||||
data: [
|
data: [
|
||||||
|
|||||||
@@ -27,10 +27,6 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
hasArchiveFlagEnabled$(): Observable<boolean> {
|
|
||||||
return this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that contains the list of ciphers that have been archived.
|
* Observable that contains the list of ciphers that have been archived.
|
||||||
*/
|
*/
|
||||||
@@ -61,23 +57,22 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns true when the archive features should be shown. */
|
||||||
* User can access the archive vault if:
|
hasArchiveFlagEnabled$: Observable<boolean> = this.configService
|
||||||
* Feature Flag is enabled
|
.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive)
|
||||||
* There is at least one archived item
|
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
* ///////////// NOTE /////////////
|
|
||||||
* This is separated from userCanArchive because a user that loses premium status, but has archived items,
|
/** Returns true when the user has premium from any means. */
|
||||||
* should still be able to access their archive vault. The items will be read-only, and can be restored.
|
userHasPremium$(userId: UserId): Observable<boolean> {
|
||||||
*/
|
return this.billingAccountProfileStateService
|
||||||
showArchiveVault$(userId: UserId): Observable<boolean> {
|
.hasPremiumFromAnySource$(userId)
|
||||||
return combineLatest([
|
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive),
|
}
|
||||||
this.archivedCiphers$(userId),
|
|
||||||
]).pipe(
|
/** Returns true when the user has previously archived ciphers but lost their premium membership. */
|
||||||
map(
|
showSubscriptionEndedMessaging$(userId: UserId): Observable<boolean> {
|
||||||
([archiveFlagEnabled, hasArchivedItems]) =>
|
return combineLatest([this.archivedCiphers$(userId), this.userHasPremium$(userId)]).pipe(
|
||||||
archiveFlagEnabled && hasArchivedItems.length > 0,
|
map(([archivedCiphers, hasPremium]) => archivedCiphers.length > 0 && !hasPremium),
|
||||||
),
|
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user