1
0
mirror of https://github.com/bitwarden/browser synced 2026-03-01 11:01:17 +00:00

[PM-32612] Only show subscription menu option when premium user has subscription (#19209) (#19225)

* fix(billing): only show Subscription menu option when premium user has subscription

* fix(billing): missed state service invocation changes

(cherry picked from commit b964cfc8e4)
This commit is contained in:
Alex Morask
2026-02-25 09:23:56 -06:00
committed by GitHub
parent 270f45ae72
commit 35229eb021
8 changed files with 75 additions and 144 deletions

View File

@@ -3,6 +3,7 @@ import { Component, DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import {
catchError,
combineLatest,
firstValueFrom,
from,
@@ -32,6 +33,7 @@ import {
} from "@bitwarden/components";
import { PricingCardComponent } from "@bitwarden/pricing";
import { I18nPipe } from "@bitwarden/ui-common";
import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients";
import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
import {
@@ -63,10 +65,12 @@ const RouteParamValues = {
I18nPipe,
PricingCardComponent,
],
providers: [AccountBillingClient],
})
export class CloudHostedPremiumComponent {
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
protected hasPremiumPersonally$: Observable<boolean>;
protected hasSubscription$: Observable<boolean>;
protected shouldShowNewDesign$: Observable<boolean>;
protected shouldShowUpgradeDialogOnInit$: Observable<boolean>;
protected personalPricingTiers$: Observable<PersonalSubscriptionPricingTier[]>;
@@ -84,6 +88,7 @@ export class CloudHostedPremiumComponent {
private destroyRef = inject(DestroyRef);
constructor(
private accountBillingClient: AccountBillingClient,
private accountService: AccountService,
private apiService: ApiService,
private dialogService: DialogService,
@@ -109,6 +114,17 @@ export class CloudHostedPremiumComponent {
),
);
this.hasSubscription$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
account
? from(this.accountBillingClient.getSubscription()).pipe(
map((subscription) => !!subscription),
catchError(() => of(false)),
)
: of(false),
),
);
this.accountService.activeAccount$
.pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef))
.subscribe((subscriber) => {
@@ -122,11 +138,15 @@ export class CloudHostedPremiumComponent {
// redirect to user subscription page if they already have premium personally
// redirect to individual vault if they already have premium from an org
combineLatest([this.hasPremiumFromAnyOrganization$, this.hasPremiumPersonally$])
combineLatest([
this.hasPremiumFromAnyOrganization$,
this.hasPremiumPersonally$,
this.hasSubscription$,
])
.pipe(
takeUntilDestroyed(this.destroyRef),
switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => {
if (hasPremiumPersonally) {
switchMap(([hasPremiumFromOrg, hasPremiumPersonally, hasSubscription]) => {
if (hasPremiumPersonally && hasSubscription) {
return from(this.navigateToSubscriptionPage());
}
if (hasPremiumFromOrg) {

View File

@@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common";
import { Component, OnInit, Signal } from "@angular/core";
import { toSignal } from "@angular/core/rxjs-interop";
import { RouterModule } from "@angular/router";
import { map, Observable, switchMap } from "rxjs";
import { catchError, combineLatest, from, map, Observable, of, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PasswordManagerLogo } from "@bitwarden/assets/svg";
@@ -18,6 +18,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { SvgModule } from "@bitwarden/components";
import { UserId } from "@bitwarden/user-core";
import { AccountBillingClient } from "@bitwarden/web-vault/app/billing/clients";
import { BillingFreeFamiliesNavItemComponent } from "../billing/shared/billing-free-families-nav-item.component";
@@ -36,12 +38,11 @@ import { WebLayoutModule } from "./web-layout.module";
SvgModule,
BillingFreeFamiliesNavItemComponent,
],
providers: [AccountBillingClient],
})
export class UserLayoutComponent implements OnInit {
protected readonly logo = PasswordManagerLogo;
protected readonly showEmergencyAccess: Signal<boolean>;
protected hasFamilySponsorshipAvailable$: Observable<boolean>;
protected showSponsoredFamilies$: Observable<boolean>;
protected showSubscription$: Observable<boolean>;
protected readonly sendEnabled$: Observable<boolean> = this.accountService.activeAccount$.pipe(
getUserId,
@@ -49,6 +50,9 @@ export class UserLayoutComponent implements OnInit {
map((isDisabled) => !isDisabled),
);
protected consolidatedSessionTimeoutComponent$: Observable<boolean>;
protected hasPremiumPersonally$: Observable<boolean>;
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
protected hasSubscription$: Observable<boolean>;
constructor(
private syncService: SyncService,
@@ -56,13 +60,8 @@ export class UserLayoutComponent implements OnInit {
private accountService: AccountService,
private policyService: PolicyService,
private configService: ConfigService,
private accountBillingClient: AccountBillingClient,
) {
this.showSubscription$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.canViewSubscription$(account.id),
),
);
this.showEmergencyAccess = toSignal(
this.accountService.activeAccount$.pipe(
getUserId,
@@ -75,10 +74,44 @@ export class UserLayoutComponent implements OnInit {
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
);
this.hasPremiumPersonally$ = this.ifAccountExistsCheck((userId) =>
this.billingAccountProfileStateService.hasPremiumPersonally$(userId),
);
this.hasPremiumFromAnyOrganization$ = this.ifAccountExistsCheck((userId) =>
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(userId),
);
this.hasSubscription$ = this.ifAccountExistsCheck(() =>
from(this.accountBillingClient.getSubscription()).pipe(
map((subscription) => !!subscription),
catchError(() => of(false)),
),
);
this.showSubscription$ = combineLatest([
this.hasPremiumPersonally$,
this.hasPremiumFromAnyOrganization$,
this.hasSubscription$,
]).pipe(
map(([hasPremiumPersonally, hasPremiumFromAnyOrganization, hasSubscription]) => {
if (hasPremiumFromAnyOrganization && !hasPremiumPersonally) {
return false;
}
return hasSubscription;
}),
);
}
async ngOnInit() {
document.body.classList.remove("layout_frontend");
await this.syncService.fullSync(false);
}
private ifAccountExistsCheck(predicate$: (userId: UserId) => Observable<boolean>) {
return this.accountService.activeAccount$.pipe(
switchMap((account) => (account ? predicate$(account.id) : of(false))),
);
}
}