Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
eda7252
add premium badge to web filter when the user does not have access toโ€ฆ
nick-livefront Oct 10, 2025
6a23f3a
remove feature flag pass through in favor of showing/hiding archive vโ€ฆ
nick-livefront Oct 10, 2025
5329317
refactor archive observable to be more generic
nick-livefront Oct 10, 2025
09fce37
add archive premium badge for the web
nick-livefront Oct 13, 2025
9dbc599
show premium badge inline for archive filter
nick-livefront Oct 13, 2025
cbcdeb5
show premium subscription ended message when user has archived ciphers
nick-livefront Oct 13, 2025
d623e65
fix missing refactor
nick-livefront Oct 13, 2025
b463790
remove unneeded can archive check
nick-livefront Oct 14, 2025
59e3097
reference observable directly
nick-livefront Oct 14, 2025
abb1e9d
reduce the number of firstValueFroms by combining observables into a โ€ฆ
nick-livefront Oct 14, 2025
1f0b113
fix failing tests
nick-livefront Oct 15, 2025
79d4871
add import to storybook
nick-livefront Oct 15, 2025
84d3c01
update variable naming for premium filters
nick-livefront Oct 17, 2025
eef48ad
Merge branch 'main' of github.com:bitwarden/clients into vault/pm-245โ€ฆ
nick-livefront Nov 19, 2025
a02977f
pass event to `promptForPremium`
nick-livefront Nov 19, 2025
385f5fa
remove check for organization
nick-livefront Nov 19, 2025
2c8b51c
Merge branch 'main' of github.com:bitwarden/clients into vault/pm-245โ€ฆ
nick-livefront Nov 19, 2025
f3ef1ca
fix footer variable reference
nick-livefront Nov 19, 2025
a55201e
refactor back to `hasArchiveFlagEnabled$` - more straight forward to โ€ฆ
nick-livefront Nov 19, 2025
b914a54
update archive service test with new feature flag format
nick-livefront Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { JslibModule } from "@bitwarden/angular/jslib.module";
import { NudgesService, NudgeType } from "@bitwarden/angular/vault";
Expand Down Expand Up @@ -45,7 +45,13 @@ export class VaultSettingsV2Component implements OnInit, OnDestroy {

// 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))),
this.userId$.pipe(
switchMap((userId) =>
this.cipherArchiveService
.archivedCiphers$(userId)
.pipe(map((ciphers) => ciphers.length > 0)),
),
),
);

protected emptyVaultImportBadge$ = this.accountService.activeAccount$.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
switchMap((id) =>
combineLatest([
this.cipherArchiveService.userCanArchive$(id),
this.cipherArchiveService.hasArchiveFlagEnabled$(),
this.cipherArchiveService.hasArchiveFlagEnabled$,
]),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogService, ToastService } from "@bitwarden/components";
Expand Down Expand Up @@ -59,6 +60,7 @@ export class VaultFilterComponent
protected restrictedItemTypesService: RestrictedItemTypesService,
protected cipherService: CipherService,
protected cipherArchiveService: CipherArchiveService,
premiumUpgradePromptService: PremiumUpgradePromptService,
) {
super(
vaultFilterService,
Expand All @@ -72,6 +74,7 @@ export class VaultFilterComponent
restrictedItemTypesService,
cipherService,
cipherArchiveService,
premiumUpgradePromptService,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,22 @@
{{ "eventLogs" | i18n }}
</button>
@if (showArchiveButton) {
<button bitMenuItem (click)="archive()" type="button">
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
{{ "archiveVerb" | i18n }}
</button>
@if (userCanArchive) {
<button bitMenuItem (click)="archive()" type="button">
<i class="bwi bwi-fw bwi-archive" aria-hidden="true"></i>
{{ "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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
OnInit,
Output,
ViewChild,
input,
} from "@angular/core";

import { CollectionView } from "@bitwarden/admin-console/common";
Expand Down Expand Up @@ -101,8 +102,10 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
// eslint-disable-next-line @angular-eslint/prefer-signals
@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
// eslint-disable-next-line @angular-eslint/prefer-signals
Expand Down Expand Up @@ -142,16 +145,21 @@ export class VaultCipherRowComponent<C extends CipherViewLike> implements OnInit
}

protected get showArchiveButton() {
if (!this.archiveEnabled()) {
return false;
}

return (
this.userCanArchive &&
!CipherViewLikeUtils.isArchived(this.cipher) &&
!CipherViewLikeUtils.isDeleted(this.cipher) &&
!this.cipher.organizationId
!CipherViewLikeUtils.isArchived(this.cipher) && !CipherViewLikeUtils.isDeleted(this.cipher)
);
}

// If item is archived always show unarchive button, even if user is not premium
protected get showUnArchiveButton() {
if (!this.archiveEnabled()) {
return false;
}

return CipherViewLikeUtils.isArchived(this.cipher);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
(onEvent)="event($event)"
[userCanArchive]="userCanArchive"
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy"
[archiveEnabled]="archiveFeatureEnabled$ | async"
></tr>
</ng-container>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { of } from "rxjs";

import { CollectionView } from "@bitwarden/admin-console/common";
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 { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
Expand Down Expand Up @@ -54,6 +55,12 @@ describe("VaultItemsComponent", () => {
t: (key: string) => key,
},
},
{
provide: CipherArchiveService,
useValue: {
hasArchiveFlagEnabled$: of(true),
},
},
],
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";

import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
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 {
RestrictedCipherType,
Expand Down Expand Up @@ -145,9 +146,12 @@ export class VaultItemsComponent<C extends CipherViewLike> {
protected disableMenu$: Observable<boolean>;
private restrictedTypes: RestrictedCipherType[] = [];

protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$;

constructor(
protected cipherAuthorizationService: CipherAuthorizationService,
protected restrictedItemTypesService: RestrictedItemTypesService,
protected cipherArchiveService: CipherArchiveService,
) {
this.canDeleteSelected$ = this.selection.changed.pipe(
startWith(null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";

import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { ScrollLayoutDirective, TableModule } from "@bitwarden/components";
import { CopyCipherFieldDirective } from "@bitwarden/vault";

Expand All @@ -29,6 +30,7 @@ import { VaultItemsComponent } from "./vault-items.component";
PipesModule,
CopyCipherFieldDirective,
ScrollLayoutDirective,
PremiumBadgeComponent,
],
declarations: [VaultItemsComponent, VaultCipherRowComponent, VaultCollectionRowComponent],
exports: [VaultItemsComponent],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
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 { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
Expand Down Expand Up @@ -143,6 +144,12 @@ export default {
isCipherRestricted: () => false, // No restrictions for this story
},
},
{
provide: CipherArchiveService,
useValue: {
hasArchiveFlagEnabled$: of(true),
},
},
],
}),
applicationConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { 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 { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
Expand Down Expand Up @@ -170,6 +172,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
protected restrictedItemTypesService: RestrictedItemTypesService,
protected cipherService: CipherService,
protected cipherArchiveService: CipherArchiveService,
private premiumUpgradePromptService: PremiumUpgradePromptService,
) {}

async ngOnInit(): Promise<void> {
Expand Down Expand Up @@ -252,14 +255,20 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
};

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;
builderFilter.organizationFilter = await this.addOrganizationFilter();
builderFilter.typeFilter = await this.addTypeFilter();
builderFilter.folderFilter = await this.addFolderFilter();
builderFilter.collectionFilter = await this.addCollectionFilter();
if (hasArchiveFlag) {
builderFilter.archiveFilter = await this.addArchiveFilter();
if (showArchive) {
builderFilter.archiveFilter = await this.addArchiveFilter(userId);
}
builderFilter.trashFilter = await this.addTrashFilter();
return builderFilter;
Expand Down Expand Up @@ -419,7 +428,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
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 = {
data$: this.vaultFilterService.buildTypeTree(
{
Expand All @@ -442,6 +462,12 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
isSelectable: true,
},
action: this.applyTypeFilter as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
premiumOptions: {
showBadgeForNonPremium: true,
blockFilterAction: promptForPremiumOnFilter
? async () => await this.premiumUpgradePromptService.promptForPremium()
: undefined,
},
};
return archiveFilterSection;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ <h3 *ngIf="!headerInfo.isSelectable" class="filter-title">
*ngComponentOutlet="optionsInfo.component; injector: createInjector(f.node)"
></ng-container>
</ng-container>
<ng-container *ngIf="premiumFeature">
<app-premium-badge></app-premium-badge>
</ng-container>
</span>
</span>
<ul
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
}

async onFilterSelect(filterNode: TreeNode<VaultFilterType>) {
if (this.section?.premiumOptions?.blockFilterAction) {
await this.section.premiumOptions.blockFilterAction();
return;
}

await this.section?.action(filterNode);
}

Expand Down Expand Up @@ -123,6 +128,10 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
return this.section?.options;
}

get premiumFeature() {
return this.section?.premiumOptions?.showBadgeForNonPremium;
}

get divider() {
return this.section?.divider;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ export type VaultFilterSection = {
component: any;
};
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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { NgModule } from "@angular/core";

import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { SearchModule } from "@bitwarden/components";

import { SharedModule } from "../../../../shared";

import { VaultFilterSectionComponent } from "./components/vault-filter-section.component";

@NgModule({
imports: [SharedModule, SearchModule],
imports: [SharedModule, SearchModule, PremiumBadgeComponent],
declarations: [VaultFilterSectionComponent],
exports: [SharedModule, VaultFilterSectionComponent, SearchModule],
})
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/app/vault/individual-vault/vault.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
{{ trashCleanupWarning }}
</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
#vaultItems
[ciphers]="ciphers"
Expand Down
Loading
Loading