1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-27 10:03:23 +00:00

refactor(billing): simplify subscription visibility and fix redirect race condition (#19255)

This commit is contained in:
Alex Morask
2026-02-26 10:13:47 -06:00
committed by GitHub
parent 3092b4bcf7
commit abbfda124f
7 changed files with 48 additions and 47 deletions

View File

@@ -1,5 +1,5 @@
<div class="tw-max-w-3xl tw-mx-auto">
<bit-section *ngIf="shouldShowNewDesign$ | async">
<bit-section>
<div class="tw-text-center">
<div class="tw-mt-8 tw-mb-6">
<span bitBadge variant="secondary" [truncate]="false">

View File

@@ -71,7 +71,6 @@ 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[]>;
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);

View File

@@ -1,9 +1,10 @@
<app-header>
@if (!selfHosted) {
<bit-tab-nav-bar slot="tabs">
<bit-tab-link [route]="(hasPremium$ | async) ? 'user-subscription' : 'premium'">{{
"subscription" | i18n
}}</bit-tab-link>
<bit-tab-link
[route]="(showSubscriptionPageLink$ | async) ? 'user-subscription' : 'premium'"
>{{ "subscription" | i18n }}</bit-tab-link
>
<bit-tab-link route="payment-details">{{ "paymentDetails" | i18n }}</bit-tab-link>
<bit-tab-link route="billing-history">{{ "billingHistory" | i18n }}</bit-tab-link>
</bit-tab-nav-bar>

View File

@@ -19,7 +19,7 @@ import { AccountBillingClient } from "../clients/account-billing.client";
providers: [AccountBillingClient],
})
export class SubscriptionComponent implements OnInit {
hasPremium$: Observable<boolean>;
showSubscriptionPageLink$: Observable<boolean>;
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(

View File

@@ -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<null> => {
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;
},

View File

@@ -20,11 +20,10 @@
} @else {
<bit-nav-item [text]="'preferences' | i18n" route="settings/preferences"></bit-nav-item>
}
<bit-nav-item
[text]="'subscription' | i18n"
route="settings/subscription"
*ngIf="showSubscription$ | async"
></bit-nav-item>
@let subscriptionRoute = subscriptionRoute$ | async;
@if (subscriptionRoute) {
<bit-nav-item [text]="'subscription' | i18n" [route]="subscriptionRoute"></bit-nav-item>
}
<bit-nav-item [text]="'domainRules' | i18n" route="settings/domain-rules"></bit-nav-item>
@if (showEmergencyAccess()) {
<bit-nav-item

View File

@@ -43,16 +43,15 @@ import { WebLayoutModule } from "./web-layout.module";
export class UserLayoutComponent implements OnInit {
protected readonly logo = PasswordManagerLogo;
protected readonly showEmergencyAccess: Signal<boolean>;
protected showSubscription$: Observable<boolean>;
protected readonly sendEnabled$: Observable<boolean> = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId)),
map((isDisabled) => !isDisabled),
);
protected consolidatedSessionTimeoutComponent$: Observable<boolean>;
protected hasPremiumPersonally$: Observable<boolean>;
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
protected hasSubscription$: Observable<boolean>;
protected subscriptionRoute$: Observable<string | null>;
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;
}),
);
}