From 75182926d4ef75b564e89df10c84114422405f8d Mon Sep 17 00:00:00 2001
From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Date: Wed, 15 Oct 2025 19:31:10 +0100
Subject: [PATCH] [PM-24033]Implement Subscription Settings UI with Premium and
Families Cards (#16822)
* Add initial changes for thenew premium design
* Add the messages
* Add the new dialog modal
* Resolve the flag issue
* Added changes for redirect
* Fix the unitest errors
* Resolve the badge issue
* refactor the code base pr comments
---
.../individual-billing-routing.module.ts | 17 +-
.../individual/individual-billing.module.ts | 2 +
.../premium/premium-vnext.component.html | 68 +++++
.../premium/premium-vnext.component.ts | 182 +++++++++++++
.../individual/premium/premium.component.html | 257 +++++++++---------
.../individual/subscription.component.html | 4 +-
.../unified-upgrade-dialog.component.ts | 17 ++
apps/web/src/locales/en/messages.json | 9 +
libs/common/src/enums/feature-flag.enum.ts | 2 +
.../pricing-card/pricing-card.component.html | 4 +-
10 files changed, 421 insertions(+), 141 deletions(-)
create mode 100644 apps/web/src/app/billing/individual/premium/premium-vnext.component.html
create mode 100644 apps/web/src/app/billing/individual/premium/premium-vnext.component.ts
diff --git a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts
index bb0ca60b677..0bc6b1effbb 100644
--- a/apps/web/src/app/billing/individual/individual-billing-routing.module.ts
+++ b/apps/web/src/app/billing/individual/individual-billing-routing.module.ts
@@ -1,9 +1,12 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
+import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
+import { PremiumVNextComponent } from "./premium/premium-vnext.component";
import { PremiumComponent } from "./premium/premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -20,11 +23,15 @@ const routes: Routes = [
component: UserSubscriptionComponent,
data: { titleId: "premiumMembership" },
},
- {
- path: "premium",
- component: PremiumComponent,
- data: { titleId: "goPremium" },
- },
+ ...featureFlaggedRoute({
+ defaultComponent: PremiumComponent,
+ flaggedComponent: PremiumVNextComponent,
+ featureFlag: FeatureFlag.PM24033PremiumUpgradeNewDesign,
+ routeOptions: {
+ data: { titleId: "goPremium" },
+ path: "premium",
+ },
+ }),
{
path: "payment-details",
component: AccountPaymentDetailsComponent,
diff --git a/apps/web/src/app/billing/individual/individual-billing.module.ts b/apps/web/src/app/billing/individual/individual-billing.module.ts
index 20f2a6cc143..56c40002f1d 100644
--- a/apps/web/src/app/billing/individual/individual-billing.module.ts
+++ b/apps/web/src/app/billing/individual/individual-billing.module.ts
@@ -1,5 +1,6 @@
import { NgModule } from "@angular/core";
+import { PricingCardComponent } from "@bitwarden/pricing";
import {
EnterBillingAddressComponent,
EnterPaymentMethodComponent,
@@ -21,6 +22,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
HeaderModule,
EnterPaymentMethodComponent,
EnterBillingAddressComponent,
+ PricingCardComponent,
],
declarations: [
SubscriptionComponent,
diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.html b/apps/web/src/app/billing/individual/premium/premium-vnext.component.html
new file mode 100644
index 00000000000..bf5d0f60861
--- /dev/null
+++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+ {{ "bitwardenFreeplanMessage" | i18n }}
+
+
+
+
+ {{ "upgradeCompleteSecurity" | i18n }}
+
+
+ {{ "individualUpgradeDescriptionMessage" | i18n }}
+
+
+
+
+
+
+
+ @if (premiumCardData$ | async; as premiumData) {
+
+ {{ "premium" | i18n }}
+
+ }
+
+
+
+
+ @if (familiesCardData$ | async; as familiesData) {
+
+ {{ "families" | i18n }}
+
+ }
+
+
+
+
+
+
+ {{ "individualUpgradeTaxInformationMessage" | i18n }}
+
+
+ {{ "viewbusinessplans" | i18n }}
+
+
+
+
+
diff --git a/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts
new file mode 100644
index 00000000000..9de9c22d3c3
--- /dev/null
+++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts
@@ -0,0 +1,182 @@
+import { CommonModule } from "@angular/common";
+import { Component, DestroyRef, inject } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { combineLatest, firstValueFrom, map, Observable, of, shareReplay, switchMap } from "rxjs";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { SyncService } from "@bitwarden/common/platform/sync";
+import {
+ DialogService,
+ ToastService,
+ SectionComponent,
+ BadgeModule,
+ TypographyModule,
+ LinkModule,
+} from "@bitwarden/components";
+import { PricingCardComponent } from "@bitwarden/pricing";
+import { I18nPipe } from "@bitwarden/ui-common";
+
+import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
+import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
+import {
+ PersonalSubscriptionPricingTier,
+ PersonalSubscriptionPricingTierIds,
+} from "../../types/subscription-pricing-tier";
+import {
+ UnifiedUpgradeDialogComponent,
+ UnifiedUpgradeDialogParams,
+ UnifiedUpgradeDialogResult,
+ UnifiedUpgradeDialogStatus,
+ UnifiedUpgradeDialogStep,
+} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
+
+@Component({
+ templateUrl: "./premium-vnext.component.html",
+ standalone: true,
+ imports: [
+ CommonModule,
+ SectionComponent,
+ BadgeModule,
+ TypographyModule,
+ LinkModule,
+ I18nPipe,
+ PricingCardComponent,
+ ],
+})
+export class PremiumVNextComponent {
+ protected hasPremiumFromAnyOrganization$: Observable;
+ protected hasPremiumPersonally$: Observable;
+ protected shouldShowNewDesign$: Observable;
+ protected personalPricingTiers$: Observable;
+ protected premiumCardData$: Observable<{
+ tier: PersonalSubscriptionPricingTier | undefined;
+ price: number;
+ features: string[];
+ }>;
+ protected familiesCardData$: Observable<{
+ tier: PersonalSubscriptionPricingTier | undefined;
+ price: number;
+ features: string[];
+ }>;
+
+ protected subscriber!: BitwardenSubscriber;
+ protected isSelfHost = false;
+ private destroyRef = inject(DestroyRef);
+
+ constructor(
+ private accountService: AccountService,
+ private i18nService: I18nService,
+ private apiService: ApiService,
+ private dialogService: DialogService,
+ private platformUtilsService: PlatformUtilsService,
+ private syncService: SyncService,
+ private toastService: ToastService,
+ private billingAccountProfileStateService: BillingAccountProfileStateService,
+ private subscriptionPricingService: SubscriptionPricingService,
+ ) {
+ this.isSelfHost = this.platformUtilsService.isSelfHost();
+
+ this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
+ switchMap((account) =>
+ account
+ ? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
+ : of(false),
+ ),
+ );
+
+ this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
+ switchMap((account) =>
+ account
+ ? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
+ : of(false),
+ ),
+ );
+
+ this.accountService.activeAccount$
+ .pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef))
+ .subscribe((subscriber) => {
+ this.subscriber = subscriber;
+ });
+
+ this.shouldShowNewDesign$ = combineLatest([
+ this.hasPremiumFromAnyOrganization$,
+ this.hasPremiumPersonally$,
+ ]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
+
+ this.personalPricingTiers$ =
+ this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
+
+ this.premiumCardData$ = this.personalPricingTiers$.pipe(
+ map((tiers) => {
+ const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Premium);
+ return {
+ tier,
+ price:
+ tier?.passwordManager.type === "standalone"
+ ? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
+ : 0,
+ features: tier?.passwordManager.features.map((f) => f.value) || [],
+ };
+ }),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
+
+ this.familiesCardData$ = this.personalPricingTiers$.pipe(
+ map((tiers) => {
+ const tier = tiers.find((t) => t.id === PersonalSubscriptionPricingTierIds.Families);
+ return {
+ tier,
+ price:
+ tier?.passwordManager.type === "packaged"
+ ? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
+ : 0,
+ features: tier?.passwordManager.features.map((f) => f.value) || [],
+ };
+ }),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
+ }
+
+ finalizeUpgrade = async () => {
+ await this.apiService.refreshIdentityToken();
+ await this.syncService.fullSync(true);
+ };
+
+ protected async openUpgradeDialog(planType: "Premium" | "Families"): Promise {
+ const account = await firstValueFrom(this.accountService.activeAccount$);
+ if (!account) {
+ return;
+ }
+
+ const selectedPlan =
+ planType === "Premium"
+ ? PersonalSubscriptionPricingTierIds.Premium
+ : PersonalSubscriptionPricingTierIds.Families;
+
+ const dialogParams: UnifiedUpgradeDialogParams = {
+ account,
+ initialStep: UnifiedUpgradeDialogStep.Payment,
+ selectedPlan: selectedPlan,
+ redirectOnCompletion: true,
+ };
+
+ const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, {
+ data: dialogParams,
+ });
+
+ dialogRef.closed
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((result: UnifiedUpgradeDialogResult | undefined) => {
+ if (
+ result?.status === UnifiedUpgradeDialogStatus.UpgradedToPremium ||
+ result?.status === UnifiedUpgradeDialogStatus.UpgradedToFamilies
+ ) {
+ void this.finalizeUpgrade();
+ }
+ });
+ }
+}
diff --git a/apps/web/src/app/billing/individual/premium/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html
index 0a3762a1e41..d08b942ff8b 100644
--- a/apps/web/src/app/billing/individual/premium/premium.component.html
+++ b/apps/web/src/app/billing/individual/premium/premium.component.html
@@ -1,137 +1,132 @@
-
- {{ "goPremium" | i18n }}
-
- {{ "alreadyPremiumFromOrg" | i18n }}
-
-
- {{ "premiumUpgradeUnlockFeatures" | i18n }}
-
- -
-
- {{ "premiumSignUpStorage" | i18n }}
-
- -
-
- {{ "premiumSignUpTwoStepOptions" | i18n }}
-
- -
-
- {{ "premiumSignUpEmergency" | i18n }}
-
- -
-
- {{ "premiumSignUpReports" | i18n }}
-
- -
-
- {{ "premiumSignUpTotp" | i18n }}
-
- -
-
- {{ "premiumSignUpSupport" | i18n }}
-
- -
-
- {{ "premiumSignUpFuture" | i18n }}
-
-
-
- {{
- "premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
- }}
+
+
+ {{ "goPremium" | i18n }}
+
+ {{ "alreadyPremiumFromOrg" | i18n }}
+
+
+ {{ "premiumUpgradeUnlockFeatures" | i18n }}
+
+ -
+
+ {{ "premiumSignUpStorage" | i18n }}
+
+ -
+
+ {{ "premiumSignUpTwoStepOptions" | i18n }}
+
+ -
+
+ {{ "premiumSignUpEmergency" | i18n }}
+
+ -
+
+ {{ "premiumSignUpReports" | i18n }}
+
+ -
+
+ {{ "premiumSignUpTotp" | i18n }}
+
+ -
+
+ {{ "premiumSignUpSupport" | i18n }}
+
+ -
+
+ {{ "premiumSignUpFuture" | i18n }}
+
+
+
+ {{
+ "premiumPriceWithFamilyPlan"
+ | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
+ }}
+
+ {{ "bitwardenFamiliesPlan" | i18n }}
+
+
- {{ "bitwardenFamiliesPlan" | i18n }}
+ {{ "purchasePremium" | i18n }}
-
-
- {{ "purchasePremium" | i18n }}
-
-
-
-
-
-
-