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 87b342ed997..a5e19a47dcd 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,11 +1,14 @@
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 { PaymentMethodComponent } from "../shared";
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";
@@ -22,11 +25,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-method",
component: PaymentMethodComponent,
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 f6433f0ffdb..0d216d1b655 100644
--- a/apps/web/src/app/billing/individual/individual-billing.module.ts
+++ b/apps/web/src/app/billing/individual/individual-billing.module.ts
@@ -3,10 +3,12 @@ import { NgModule } from "@angular/core";
import { PricingCardComponent, CartSummaryComponent } from "@bitwarden/pricing";
import { HeaderModule } from "../../layouts/header/header.module";
+import { BillingServicesModule } from "../services";
import { BillingSharedModule } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
+import { PremiumVNextComponent } from "./premium/premium-vnext.component";
import { PremiumComponent } from "./premium/premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -15,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
imports: [
IndividualBillingRoutingModule,
BillingSharedModule,
+ BillingServicesModule,
HeaderModule,
PricingCardComponent,
CartSummaryComponent,
@@ -24,6 +27,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
BillingHistoryViewComponent,
UserSubscriptionComponent,
PremiumComponent,
+ PremiumVNextComponent,
],
})
export class IndividualBillingModule {}
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..8c74963a5e0
--- /dev/null
+++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+ {{ "bitwardenFreeplanMessage" | i18n }}
+
+
+
+
+ {{ "upgradeCompleteSecurity" | i18n }}
+
+
+ {{ "unlockPremiumFeatures" | 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..b283b3ca5b3
--- /dev/null
+++ b/apps/web/src/app/billing/individual/premium/premium-vnext.component.ts
@@ -0,0 +1,183 @@
+import { Component } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { combineLatest, firstValueFrom, map, Observable, 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 } from "@bitwarden/components";
+
+import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
+import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
+import {
+ PersonalSubscriptionPricingTier,
+ PersonalSubscriptionPricingTierIds,
+} from "../../types/subscription-pricing-tier";
+import {
+ UpgradePaymentDialogComponent,
+ UpgradePaymentDialogResult,
+} from "../upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component";
+
+@Component({
+ templateUrl: "./premium-vnext.component.html",
+ standalone: false,
+})
+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;
+
+ 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) =>
+ this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
+ ),
+ );
+
+ this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
+ switchMap((account) =>
+ this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
+ ),
+ );
+
+ this.accountService.activeAccount$
+ .pipe(mapAccountToSubscriber, takeUntilDestroyed())
+ .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 === "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 === "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);
+ };
+
+ postFinalizeUpgrade = async () => {
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("premiumUpdated"),
+ });
+ };
+
+ protected async openUpgradeDialog(type: "Premium" | "Families"): Promise {
+ try {
+ const planId =
+ type === "Premium"
+ ? PersonalSubscriptionPricingTierIds.Premium
+ : PersonalSubscriptionPricingTierIds.Families;
+
+ if (!this.subscriber) {
+ throw new Error("No subscriber found");
+ }
+
+ const paymentDialogRef = UpgradePaymentDialogComponent.open(this.dialogService, {
+ data: {
+ plan: planId,
+ subscriber: this.subscriber,
+ },
+ });
+
+ const paymentResult = await firstValueFrom(paymentDialogRef.closed);
+ await this.handleUpgradeResult(paymentResult, planId);
+ } catch {
+ this.toastService.showToast({
+ variant: "error",
+ title: this.i18nService.t("errorOccurred"),
+ message: this.i18nService.t("unexpectedError"),
+ });
+ }
+ }
+
+ private async handleUpgradeResult(
+ result: UpgradePaymentDialogResult | null,
+ plan: string,
+ ): Promise {
+ if (!result) {
+ return;
+ }
+
+ switch (result) {
+ case "upgradedToPremium":
+ await this.finalizeUpgrade();
+ await this.postFinalizeUpgrade();
+ break;
+ case "upgradedToFamilies":
+ this.toastService.showToast({
+ variant: "success",
+ title: null,
+ message: this.i18nService.t("familiesUpgradeSuccess"),
+ });
+ break;
+ case "back":
+ break;
+ }
+ }
+}
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 2d55a5a0199..3f0f97541df 100644
--- a/apps/web/src/app/billing/individual/premium/premium.component.html
+++ b/apps/web/src/app/billing/individual/premium/premium.component.html
@@ -1,191 +1,119 @@
-
-
-
-
-
- {{ "bitwardenFreeplanMessage" | i18n }}
-
-
-
-
- {{ "upgradeCompleteSecurity" | i18n }}
-
-
- {{ "unlockPremiumFeatures" | i18n }}
-
-
-
-
-
-
-
- @if (premiumCardData$ | async; as premiumData) {
-
- {{ "premium" | i18n }}
-
- }
-
-
-
-
- @if (familiesCardData$ | async; as familiesData) {
-
- {{ "families" | i18n }}
-
- }
-
-
-
-
-
-
{{ "individualUpgradeTaxInformationMessage" | i18n }}
+
+ {{ "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
+ }}
- {{ "viewbusinessplans" | i18n }}
-
+ {{ "bitwardenFamiliesPlan" | i18n }}
+
+
+ {{ "purchasePremium" | i18n }}
+
+
+
+
+
+
+
-
-
- {{ "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 }}
-
-
-
- {{ "purchasePremium" | i18n }}
-
-
+ {{ "summary" | i18n }}
+ {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }}
+ {{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB ×
+ {{ storageGBPrice | currency: "$" }} =
+ {{ additionalStorageCost | currency: "$" }}
+
-
-
+
+ {{ "paymentInformation" | i18n }}
+
+
+
+
+ {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
+ {{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}
+
+
+
+
+ {{ "total" | i18n }}: {{ total | currency: "USD $" }}/{{ "year" | i18n }}
+
+
-
-
+
diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts
index 50f7c7917a7..974c22455ff 100644
--- a/apps/web/src/app/billing/individual/premium/premium.component.ts
+++ b/apps/web/src/app/billing/individual/premium/premium.component.ts
@@ -4,8 +4,8 @@ import { Component, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
-import { combineLatest, concatMap, firstValueFrom, from, Observable, of, switchMap } from "rxjs";
-import { debounceTime, map, shareReplay } from "rxjs/operators";
+import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs";
+import { debounceTime } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -13,26 +13,15 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction";
import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
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 } from "@bitwarden/components";
+import { ToastService } from "@bitwarden/components";
import { PaymentComponent } from "../../shared/payment/payment.component";
import { TaxInfoComponent } from "../../shared/tax-info.component";
-import {
- PersonalSubscriptionPricingTier,
- PersonalSubscriptionPricingTierIds,
-} from "../../types/subscription-pricing-tier";
-import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types/bitwarden-subscriber";
-import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
-import {
- UpgradePaymentDialogComponent,
- UpgradePaymentDialogResult,
-} from "../upgrade/upgrade-payment-dialog/upgrade-payment-dialog.component";
@Component({
templateUrl: "./premium.component.html",
@@ -43,19 +32,6 @@ export class PremiumComponent {
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
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 addOnFormGroup = new FormGroup({
additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]),
@@ -67,7 +43,6 @@ export class PremiumComponent {
protected cloudWebVaultURL: string;
protected isSelfHost = false;
- protected subscriber: BitwardenSubscriber;
protected estimatedTax: number = 0;
protected readonly familyPlanMaxUserCount = 6;
@@ -88,8 +63,6 @@ export class PremiumComponent {
private tokenService: TokenService,
private taxService: TaxServiceAbstraction,
private accountService: AccountService,
- private dialogService: DialogService,
- private subscriptionPricingService: SubscriptionPricingService,
) {
this.isSelfHost = this.platformUtilsService.isSelfHost();
@@ -99,62 +72,6 @@ export class PremiumComponent {
),
);
- this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
- switchMap((account) =>
- this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
- ),
- );
-
- this.accountService.activeAccount$
- .pipe(mapAccountToSubscriber, takeUntilDestroyed())
- .subscribe((subscriber) => {
- this.subscriber = subscriber;
- });
-
- this.shouldShowNewDesign$ = combineLatest([
- this.hasPremiumFromAnyOrganization$,
- this.hasPremiumPersonally$,
- this.configService.getFeatureFlag$(FeatureFlag.PremiumUpgradeNewDesign),
- ]).pipe(
- map(
- ([hasOrgPremium, hasPersonalPremium, isNewDesignEnabled]) =>
- isNewDesignEnabled && !hasOrgPremium && !hasPersonalPremium,
- ),
- );
-
- this.personalPricingTiers$ =
- this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
-
- this.premiumCardData$ = this.personalPricingTiers$.pipe(
- map((tiers) => {
- const tier = tiers.find((t) => t.id === "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 === "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 }),
- );
-
combineLatest([
this.accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -304,61 +221,4 @@ export class PremiumComponent {
protected onTaxInformationChanged(): void {
this.refreshSalesTax();
}
-
- protected async openUpgradeDialog(type: "Premium" | "Families"): Promise {
- try {
- const planId =
- type === "Premium"
- ? PersonalSubscriptionPricingTierIds.Premium
- : PersonalSubscriptionPricingTierIds.Families;
-
- if (!this.subscriber) {
- throw new Error("No subscriber found");
- }
-
- const paymentDialogRef = UpgradePaymentDialogComponent.open(this.dialogService, {
- data: {
- plan: planId,
- subscriber: this.subscriber,
- },
- });
-
- const paymentResult = await firstValueFrom(paymentDialogRef.closed);
- await this.handleUpgradeResult(paymentResult, planId);
- } catch (error) {
- this.toastService.showToast({
- variant: "error",
- title: this.i18nService.t("errorOccurred"),
- message: this.i18nService.t("unexpectedError"),
- });
- }
- }
-
- private async handleUpgradeResult(
- result: UpgradePaymentDialogResult | null,
- plan: string,
- ): Promise {
- if (!result) {
- return;
- }
-
- switch (result) {
- case "upgradedToPremium":
- await this.finalizeUpgrade();
- await this.postFinalizeUpgrade();
- break;
- case "upgradedToFamilies":
- this.toastService.showToast({
- variant: "success",
- title: null,
- message: this.i18nService.t("familiesUpgradeSuccess"),
- });
- // Navigate to the organizations page after successful families upgrade
- // Note: We would need the organization ID from the upgrade service to navigate properly
- break;
- case "back":
- // User went back, could re-open the account dialog
- break;
- }
- }
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 3e096b22c56..97a4c06155b 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -28,7 +28,7 @@ export enum FeatureFlag {
PM21881_ManagePaymentDetailsOutsideCheckout = "pm-21881-manage-payment-details-outside-checkout",
PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover",
PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings",
- PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
+ PM24033PremiumUpgradeNewDesign = "pm-24033-update-premium-subscription-page",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
@@ -108,7 +108,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout]: FALSE,
[FeatureFlag.PM21821_ProviderPortalTakeover]: FALSE,
[FeatureFlag.PM22415_TaxIDWarnings]: FALSE,
- [FeatureFlag.PremiumUpgradeNewDesign]: FALSE,
+ [FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
/* Key Management */
[FeatureFlag.PrivateKeyRegeneration]: FALSE,