From 93e70d5186c320e588fc541afd1b81b34963ce4c Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:42:21 -0600 Subject: [PATCH] refactor(billing): simplify subscription visibility and fix redirect race condition (#19255) (#19257) (cherry picked from commit abbfda124fe11777497f272efa7ee96281e14d05) --- .../cloud-hosted-premium.component.html | 2 +- .../premium/cloud-hosted-premium.component.ts | 21 ++++---------- .../individual/subscription.component.html | 7 +++-- .../individual/subscription.component.ts | 6 ++-- .../account-subscription.component.ts | 28 +++++++++++++++---- .../app/layouts/user-layout.component.html | 9 +++--- .../src/app/layouts/user-layout.component.ts | 22 ++++++--------- 7 files changed, 48 insertions(+), 47 deletions(-) diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html index e182659acbb..095c721d9d8 100644 --- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html +++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html @@ -1,5 +1,5 @@
- +
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts index 94f75c5ecd2..8c7a407eee5 100644 --- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.ts @@ -71,7 +71,6 @@ export class CloudHostedPremiumComponent { protected hasPremiumFromAnyOrganization$: Observable; protected hasPremiumPersonally$: Observable; protected hasSubscription$: Observable; - protected shouldShowNewDesign$: Observable; protected shouldShowUpgradeDialogOnInit$: Observable; protected personalPricingTiers$: Observable; protected premiumCardData$: Observable<{ @@ -131,25 +130,15 @@ export class CloudHostedPremiumComponent { this.subscriber = subscriber; }); - this.shouldShowNewDesign$ = combineLatest([ - this.hasPremiumFromAnyOrganization$, - this.hasPremiumPersonally$, - ]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium)); - - // 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$, - this.hasSubscription$, - ]) + combineLatest([this.hasSubscription$, this.hasPremiumFromAnyOrganization$]) .pipe( takeUntilDestroyed(this.destroyRef), - switchMap(([hasPremiumFromOrg, hasPremiumPersonally, hasSubscription]) => { - if (hasPremiumPersonally && hasSubscription) { + take(1), + switchMap(([hasSubscription, hasPremiumFromAnyOrganization]) => { + if (hasSubscription) { return from(this.navigateToSubscriptionPage()); } - if (hasPremiumFromOrg) { + if (hasPremiumFromAnyOrganization) { return from(this.navigateToIndividualVault()); } return of(true); diff --git a/apps/web/src/app/billing/individual/subscription.component.html b/apps/web/src/app/billing/individual/subscription.component.html index 7fd7beff109..cf9344996cb 100644 --- a/apps/web/src/app/billing/individual/subscription.component.html +++ b/apps/web/src/app/billing/individual/subscription.component.html @@ -1,9 +1,10 @@ @if (!selfHosted) { - {{ - "subscription" | i18n - }} + {{ "subscription" | i18n }} {{ "paymentDetails" | i18n }} {{ "billingHistory" | i18n }} diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index 4f52f3c2ea2..454a4d6aa6d 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -19,7 +19,7 @@ import { AccountBillingClient } from "../clients/account-billing.client"; providers: [AccountBillingClient], }) export class SubscriptionComponent implements OnInit { - hasPremium$: Observable; + showSubscriptionPageLink$: Observable; selfHosted: boolean; constructor( @@ -27,9 +27,9 @@ export class SubscriptionComponent implements OnInit { billingAccountProfileStateService: BillingAccountProfileStateService, accountService: AccountService, configService: ConfigService, - private accountBillingClient: AccountBillingClient, + accountBillingClient: AccountBillingClient, ) { - this.hasPremium$ = combineLatest([ + this.showSubscriptionPageLink$ = combineLatest([ configService.getFeatureFlag$(FeatureFlag.PM29594_UpdateIndividualSubscriptionPage), accountService.activeAccount$, ]).pipe( diff --git a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts index 7fdc830effd..ed3ac56c968 100644 --- a/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription/account-subscription.component.ts @@ -89,18 +89,34 @@ export class AccountSubscriptionComponent { { initialValue: false }, ); + readonly hasPremiumFromAnyOrganization = toSignal( + this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (!account) { + return of(false); + } + return this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id); + }), + ), + { initialValue: false }, + ); + readonly subscription = resource({ - loader: async () => { - const redirectToPremiumPage = async (): Promise => { + params: () => ({ + account: this.account(), + }), + loader: async ({ params: { account } }) => { + if (!account) { await this.router.navigate(["/settings/subscription/premium"]); return null; - }; - if (!this.account()) { - return await redirectToPremiumPage(); } const subscription = await this.accountBillingClient.getSubscription(); if (!subscription) { - return await redirectToPremiumPage(); + const hasPremiumFromAnyOrganization = this.hasPremiumFromAnyOrganization(); + await this.router.navigate([ + hasPremiumFromAnyOrganization ? "/vault" : "/settings/subscription/premium", + ]); + return null; } return subscription; }, diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html index 57b8cf047c4..ffc3f57babe 100644 --- a/apps/web/src/app/layouts/user-layout.component.html +++ b/apps/web/src/app/layouts/user-layout.component.html @@ -20,11 +20,10 @@ } @else { } - + @let subscriptionRoute = subscriptionRoute$ | async; + @if (subscriptionRoute) { + + } @if (showEmergencyAccess()) { ; - protected showSubscription$: Observable; protected readonly sendEnabled$: Observable = this.accountService.activeAccount$.pipe( getUserId, switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId)), map((isDisabled) => !isDisabled), ); protected consolidatedSessionTimeoutComponent$: Observable; - protected hasPremiumPersonally$: Observable; protected hasPremiumFromAnyOrganization$: Observable; protected hasSubscription$: Observable; + protected subscriptionRoute$: Observable; constructor( private syncService: SyncService, @@ -75,10 +74,6 @@ export class UserLayoutComponent implements OnInit { FeatureFlag.ConsolidatedSessionTimeoutComponent, ); - this.hasPremiumPersonally$ = this.ifAccountExistsCheck((userId) => - this.billingAccountProfileStateService.hasPremiumPersonally$(userId), - ); - this.hasPremiumFromAnyOrganization$ = this.ifAccountExistsCheck((userId) => this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(userId), ); @@ -90,16 +85,17 @@ export class UserLayoutComponent implements OnInit { ), ); - this.showSubscription$ = combineLatest([ - this.hasPremiumPersonally$, - this.hasPremiumFromAnyOrganization$, + this.subscriptionRoute$ = combineLatest([ this.hasSubscription$, + this.hasPremiumFromAnyOrganization$, ]).pipe( - map(([hasPremiumPersonally, hasPremiumFromAnyOrganization, hasSubscription]) => { - if (hasPremiumFromAnyOrganization && !hasPremiumPersonally) { - return false; + map(([hasSubscription, hasPremiumFromAnyOrganization]) => { + if (!hasPremiumFromAnyOrganization || hasSubscription) { + return hasSubscription + ? "settings/subscription/user-subscription" + : "settings/subscription/premium"; } - return hasSubscription; + return null; }), ); }