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;
}),
);
}