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 cdccaaab8ab..fbaf65d1839 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,15 +1,11 @@
import { inject, NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
-import { map } from "rxjs";
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { AccountPaymentDetailsComponent } from "@bitwarden/web-vault/app/billing/individual/payment-details/account-payment-details.component";
import { SelfHostedPremiumComponent } from "@bitwarden/web-vault/app/billing/individual/premium/self-hosted-premium.component";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
-import { CloudHostedPremiumVNextComponent } from "./premium/cloud-hosted-premium-vnext.component";
import { CloudHostedPremiumComponent } from "./premium/cloud-hosted-premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -27,20 +23,15 @@ const routes: Routes = [
data: { titleId: "premiumMembership" },
},
/**
- * Three-Route Matching Strategy for /premium:
+ * Two-Route Matching Strategy for /premium:
*
* Routes are evaluated in order using canMatch guards. The first route that matches will be selected.
*
* 1. Self-Hosted Environment → SelfHostedPremiumComponent
* - Matches when platformUtilsService.isSelfHost() === true
*
- * 2. Cloud-Hosted + Feature Flag Enabled → CloudHostedPremiumVNextComponent
- * - Only evaluated if Route 1 doesn't match (not self-hosted)
- * - Matches when PM24033PremiumUpgradeNewDesign feature flag === true
- *
- * 3. Cloud-Hosted + Feature Flag Disabled → CloudHostedPremiumComponent (Fallback)
- * - No canMatch guard, so this always matches as the fallback route
- * - Used when neither Route 1 nor Route 2 match
+ * 2. Cloud-Hosted (default) → CloudHostedPremiumComponent
+ * - Evaluated when Route 1 doesn't match (not self-hosted)
*/
// Route 1: Self-Hosted -> SelfHostedPremiumComponent
{
@@ -54,22 +45,7 @@ const routes: Routes = [
},
],
},
- // Route 2: Cloud Hosted + FF -> CloudHostedPremiumVNextComponent
- {
- path: "premium",
- component: CloudHostedPremiumVNextComponent,
- data: { titleId: "goPremium" },
- canMatch: [
- () => {
- const configService = inject(ConfigService);
-
- return configService
- .getFeatureFlag$(FeatureFlag.PM24033PremiumUpgradeNewDesign)
- .pipe(map((flagValue) => flagValue === true));
- },
- ],
- },
- // Route 3: Cloud Hosted + FF Disabled -> CloudHostedPremiumComponent (Fallback)
+ // Route 2: Cloud Hosted (default) -> CloudHostedPremiumComponent
{
path: "premium",
component: CloudHostedPremiumComponent,
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 2a529d43416..35c08aa40a2 100644
--- a/apps/web/src/app/billing/individual/individual-billing.module.ts
+++ b/apps/web/src/app/billing/individual/individual-billing.module.ts
@@ -12,7 +12,6 @@ import { BillingSharedModule } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
-import { CloudHostedPremiumComponent } from "./premium/cloud-hosted-premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -26,11 +25,6 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
PricingCardComponent,
BaseCardComponent,
],
- declarations: [
- SubscriptionComponent,
- BillingHistoryViewComponent,
- UserSubscriptionComponent,
- CloudHostedPremiumComponent,
- ],
+ declarations: [SubscriptionComponent, BillingHistoryViewComponent, UserSubscriptionComponent],
})
export class IndividualBillingModule {}
diff --git a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.html b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.html
deleted file mode 100644
index e182659acbb..00000000000
--- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.html
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
- {{ "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/cloud-hosted-premium-vnext.component.ts b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.ts
deleted file mode 100644
index aac7fd3156f..00000000000
--- a/apps/web/src/app/billing/individual/premium/cloud-hosted-premium-vnext.component.ts
+++ /dev/null
@@ -1,242 +0,0 @@
-import { CommonModule } from "@angular/common";
-import { Component, DestroyRef, inject } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
-import { ActivatedRoute, Router } from "@angular/router";
-import {
- combineLatest,
- firstValueFrom,
- from,
- map,
- Observable,
- of,
- shareReplay,
- switchMap,
- take,
-} 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 { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
-import {
- PersonalSubscriptionPricingTier,
- PersonalSubscriptionPricingTierIds,
-} from "@bitwarden/common/billing/types/subscription-pricing-tier";
-import { SyncService } from "@bitwarden/common/platform/sync";
-import {
- BadgeModule,
- DialogService,
- LinkModule,
- SectionComponent,
- TypographyModule,
-} from "@bitwarden/components";
-import { PricingCardComponent } from "@bitwarden/pricing";
-import { I18nPipe } from "@bitwarden/ui-common";
-
-import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
-import {
- UnifiedUpgradeDialogComponent,
- UnifiedUpgradeDialogParams,
- UnifiedUpgradeDialogResult,
- UnifiedUpgradeDialogStatus,
- UnifiedUpgradeDialogStep,
-} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
-
-const RouteParams = {
- callToAction: "callToAction",
-} as const;
-const RouteParamValues = {
- upgradeToPremium: "upgradeToPremium",
-} as const;
-
-// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
-// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
-@Component({
- templateUrl: "./cloud-hosted-premium-vnext.component.html",
- standalone: true,
- imports: [
- CommonModule,
- SectionComponent,
- BadgeModule,
- TypographyModule,
- LinkModule,
- I18nPipe,
- PricingCardComponent,
- ],
-})
-export class CloudHostedPremiumVNextComponent {
- protected hasPremiumFromAnyOrganization$: Observable;
- protected hasPremiumPersonally$: Observable;
- protected shouldShowNewDesign$: Observable;
- protected shouldShowUpgradeDialogOnInit$: 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;
- private destroyRef = inject(DestroyRef);
-
- constructor(
- private accountService: AccountService,
- private apiService: ApiService,
- private dialogService: DialogService,
- private syncService: SyncService,
- private billingAccountProfileStateService: BillingAccountProfileStateService,
- private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
- private router: Router,
- private activatedRoute: ActivatedRoute,
- ) {
- 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));
-
- // 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$])
- .pipe(
- takeUntilDestroyed(this.destroyRef),
- switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => {
- if (hasPremiumPersonally) {
- return from(this.navigateToSubscriptionPage());
- }
- if (hasPremiumFromOrg) {
- return from(this.navigateToIndividualVault());
- }
- return of(true);
- }),
- )
- .subscribe();
-
- this.shouldShowUpgradeDialogOnInit$ = combineLatest([
- this.hasPremiumFromAnyOrganization$,
- this.hasPremiumPersonally$,
- this.activatedRoute.queryParams,
- ]).pipe(
- map(([hasOrgPremium, hasPersonalPremium, queryParams]) => {
- const cta = queryParams[RouteParams.callToAction];
- return !hasOrgPremium && !hasPersonalPremium && cta === RouteParamValues.upgradeToPremium;
- }),
- );
-
- 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" && tier.passwordManager.annualPrice
- ? 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" && tier.passwordManager.annualPrice
- ? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
- : 0,
- features: tier?.passwordManager.features.map((f) => f.value) || [],
- };
- }),
- shareReplay({ bufferSize: 1, refCount: true }),
- );
-
- this.shouldShowUpgradeDialogOnInit$
- .pipe(
- take(1),
- switchMap((shouldShowUpgradeDialogOnInit) => {
- if (shouldShowUpgradeDialogOnInit) {
- return from(this.openUpgradeDialog("Premium"));
- }
- // Return an Observable that completes immediately when dialog should not be shown
- return of(void 0);
- }),
- takeUntilDestroyed(this.destroyRef),
- )
- .subscribe();
- }
-
- private navigateToSubscriptionPage = (): Promise =>
- this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
-
- private navigateToIndividualVault = (): Promise => this.router.navigate(["/vault"]);
-
- 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/cloud-hosted-premium.component.html b/apps/web/src/app/billing/individual/premium/cloud-hosted-premium.component.html
index 33e89f21fc0..e182659acbb 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,141 +1,68 @@
-@if (isLoadingPrices$ | async) {
-
-
- {{ "loading" | i18n }}
-
-} @else {
-
-
- {{ "goPremium" | i18n }}
-
+
+
+
+
+ {{ "bitwardenFreeplanMessage" | i18n }}
+
+
+
+
+ {{ "upgradeCompleteSecurity" | i18n }}
+
+
+ {{ "individualUpgradeDescriptionMessage" | i18n }}
+
+
+
+
+
+
+
+ @if (premiumCardData$ | async; as premiumData) {
+
+ {{ "premium" | i18n }}
+
+ }
+
+
+
+
+ @if (familiesCardData$ | async; as familiesData) {
+
+ {{ "families" | i18n }}
+
+ }
+
+
+
+
+
+
+
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 86a508d2701..7e219c44d90 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
@@ -1,243 +1,242 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Component, ViewChild } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { Component, DestroyRef, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
-import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import {
- catchError,
combineLatest,
- concatMap,
- filter,
+ firstValueFrom,
from,
map,
Observable,
of,
shareReplay,
- startWith,
switchMap,
+ take,
} from "rxjs";
-import { debounceTime } from "rxjs/operators";
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 { SubscriptionPricingServiceAbstraction } from "@bitwarden/common/billing/abstractions/subscription-pricing.service.abstraction";
-import { PaymentMethodType } from "@bitwarden/common/billing/enums";
-import { PersonalSubscriptionPricingTierIds } from "@bitwarden/common/billing/types/subscription-pricing-tier";
-import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import {
+ PersonalSubscriptionPricingTier,
+ PersonalSubscriptionPricingTierIds,
+} from "@bitwarden/common/billing/types/subscription-pricing-tier";
import { SyncService } from "@bitwarden/common/platform/sync";
-import { ToastService } from "@bitwarden/components";
-import { SubscriberBillingClient, TaxClient } from "@bitwarden/web-vault/app/billing/clients";
import {
- EnterBillingAddressComponent,
- EnterPaymentMethodComponent,
- getBillingAddressFromForm,
-} from "@bitwarden/web-vault/app/billing/payment/components";
+ BadgeModule,
+ DialogService,
+ LinkModule,
+ SectionComponent,
+ TypographyModule,
+} from "@bitwarden/components";
+import { PricingCardComponent } from "@bitwarden/pricing";
+import { I18nPipe } from "@bitwarden/ui-common";
+
+import { BitwardenSubscriber, mapAccountToSubscriber } from "../../types";
import {
- NonTokenizablePaymentMethods,
- tokenizablePaymentMethodToLegacyEnum,
-} from "@bitwarden/web-vault/app/billing/payment/types";
-import { mapAccountToSubscriber } from "@bitwarden/web-vault/app/billing/types";
+ UnifiedUpgradeDialogComponent,
+ UnifiedUpgradeDialogParams,
+ UnifiedUpgradeDialogResult,
+ UnifiedUpgradeDialogStatus,
+ UnifiedUpgradeDialogStep,
+} from "../upgrade/unified-upgrade-dialog/unified-upgrade-dialog.component";
+
+const RouteParams = {
+ callToAction: "callToAction",
+} as const;
+const RouteParamValues = {
+ upgradeToPremium: "upgradeToPremium",
+} as const;
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "./cloud-hosted-premium.component.html",
- standalone: false,
- providers: [SubscriberBillingClient, TaxClient],
+ standalone: true,
+ imports: [
+ CommonModule,
+ SectionComponent,
+ BadgeModule,
+ TypographyModule,
+ LinkModule,
+ I18nPipe,
+ PricingCardComponent,
+ ],
})
export class CloudHostedPremiumComponent {
- // FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
- // eslint-disable-next-line @angular-eslint/prefer-signals
- @ViewChild(EnterPaymentMethodComponent) enterPaymentMethodComponent!: EnterPaymentMethodComponent;
-
protected hasPremiumFromAnyOrganization$: Observable;
- protected hasEnoughAccountCredit$: Observable;
-
- protected formGroup = new FormGroup({
- additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]),
- paymentMethod: EnterPaymentMethodComponent.getFormGroup(),
- billingAddress: EnterBillingAddressComponent.getFormGroup(),
- });
-
- premiumPrices$ = this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$().pipe(
- map((tiers) => {
- const premiumPlan = tiers.find(
- (tier) => tier.id === PersonalSubscriptionPricingTierIds.Premium,
- );
-
- if (!premiumPlan) {
- throw new Error("Could not find Premium plan");
- }
-
- return {
- seat: premiumPlan.passwordManager.annualPrice,
- storage: premiumPlan.passwordManager.annualPricePerAdditionalStorageGB,
- providedStorageGb: premiumPlan.passwordManager.providedStorageGB,
- };
- }),
- shareReplay({ bufferSize: 1, refCount: true }),
- );
-
- premiumPrice$ = this.premiumPrices$.pipe(map((prices) => prices.seat));
-
- storagePrice$ = this.premiumPrices$.pipe(map((prices) => prices.storage));
-
- providedStorageGb$ = this.premiumPrices$.pipe(map((prices) => prices.providedStorageGb));
-
- protected isLoadingPrices$ = this.premiumPrices$.pipe(
- map(() => false),
- startWith(true),
- catchError(() => of(false)),
- );
-
- storageCost$ = combineLatest([
- this.storagePrice$,
- this.formGroup.controls.additionalStorage.valueChanges.pipe(
- startWith(this.formGroup.value.additionalStorage),
- ),
- ]).pipe(map(([storagePrice, additionalStorage]) => storagePrice * additionalStorage));
-
- subtotal$ = combineLatest([this.premiumPrice$, this.storageCost$]).pipe(
- map(([premiumPrice, storageCost]) => premiumPrice + storageCost),
- );
-
- tax$ = this.formGroup.valueChanges.pipe(
- filter(() => this.formGroup.valid),
- debounceTime(1000),
- switchMap(async () => {
- const billingAddress = getBillingAddressFromForm(this.formGroup.controls.billingAddress);
- const taxAmounts = await this.taxClient.previewTaxForPremiumSubscriptionPurchase(
- this.formGroup.value.additionalStorage,
- billingAddress,
- );
- return taxAmounts.tax;
- }),
- startWith(0),
- );
-
- total$ = combineLatest([this.subtotal$, this.tax$]).pipe(
- map(([subtotal, tax]) => subtotal + tax),
- );
-
- protected cloudWebVaultURL: string;
- protected readonly familyPlanMaxUserCount = 6;
+ protected hasPremiumPersonally$: Observable;
+ protected shouldShowNewDesign$: Observable;
+ protected shouldShowUpgradeDialogOnInit$: 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;
+ private destroyRef = inject(DestroyRef);
constructor(
- private activatedRoute: ActivatedRoute,
- private apiService: ApiService,
- private billingAccountProfileStateService: BillingAccountProfileStateService,
- private environmentService: EnvironmentService,
- private i18nService: I18nService,
- private router: Router,
- private syncService: SyncService,
- private toastService: ToastService,
private accountService: AccountService,
- private subscriberBillingClient: SubscriberBillingClient,
- private taxClient: TaxClient,
+ private apiService: ApiService,
+ private dialogService: DialogService,
+ private syncService: SyncService,
+ private billingAccountProfileStateService: BillingAccountProfileStateService,
private subscriptionPricingService: SubscriptionPricingServiceAbstraction,
+ private router: Router,
+ private activatedRoute: ActivatedRoute,
) {
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
- this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
+ account
+ ? this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id)
+ : of(false),
),
);
- const accountCredit$ = this.accountService.activeAccount$.pipe(
- mapAccountToSubscriber,
- switchMap((account) => this.subscriberBillingClient.getCredit(account)),
+ this.hasPremiumPersonally$ = this.accountService.activeAccount$.pipe(
+ switchMap((account) =>
+ account
+ ? this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)
+ : of(false),
+ ),
);
- this.hasEnoughAccountCredit$ = combineLatest([
- accountCredit$,
- this.total$,
- this.formGroup.controls.paymentMethod.controls.type.valueChanges.pipe(
- startWith(this.formGroup.value.paymentMethod.type),
- ),
- ]).pipe(
- map(([credit, total, paymentMethod]) => {
- if (paymentMethod !== NonTokenizablePaymentMethods.accountCredit) {
- return true;
- }
- return credit >= total;
- }),
- );
+ this.accountService.activeAccount$
+ .pipe(mapAccountToSubscriber, takeUntilDestroyed(this.destroyRef))
+ .subscribe((subscriber) => {
+ this.subscriber = subscriber;
+ });
- combineLatest([
- this.accountService.activeAccount$.pipe(
- switchMap((account) =>
- this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
- ),
- ),
- this.environmentService.cloudWebVaultUrl$,
- ])
+ 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$])
.pipe(
- takeUntilDestroyed(),
- concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
+ takeUntilDestroyed(this.destroyRef),
+ switchMap(([hasPremiumFromOrg, hasPremiumPersonally]) => {
if (hasPremiumPersonally) {
return from(this.navigateToSubscriptionPage());
}
-
- this.cloudWebVaultURL = cloudWebVaultURL;
+ if (hasPremiumFromOrg) {
+ return from(this.navigateToIndividualVault());
+ }
return of(true);
}),
)
.subscribe();
+
+ this.shouldShowUpgradeDialogOnInit$ = combineLatest([
+ this.hasPremiumFromAnyOrganization$,
+ this.hasPremiumPersonally$,
+ this.activatedRoute.queryParams,
+ ]).pipe(
+ map(([hasOrgPremium, hasPersonalPremium, queryParams]) => {
+ const cta = queryParams[RouteParams.callToAction];
+ return !hasOrgPremium && !hasPersonalPremium && cta === RouteParamValues.upgradeToPremium;
+ }),
+ );
+
+ 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" && tier.passwordManager.annualPrice
+ ? 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" && tier.passwordManager.annualPrice
+ ? Number((tier.passwordManager.annualPrice / 12).toFixed(2))
+ : 0,
+ features: tier?.passwordManager.features.map((f) => f.value) || [],
+ };
+ }),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
+
+ this.shouldShowUpgradeDialogOnInit$
+ .pipe(
+ take(1),
+ switchMap((shouldShowUpgradeDialogOnInit) => {
+ if (shouldShowUpgradeDialogOnInit) {
+ return from(this.openUpgradeDialog("Premium"));
+ }
+ // Return an Observable that completes immediately when dialog should not be shown
+ return of(void 0);
+ }),
+ takeUntilDestroyed(this.destroyRef),
+ )
+ .subscribe();
}
+ private navigateToSubscriptionPage = (): Promise =>
+ this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
+
+ private navigateToIndividualVault = (): Promise => this.router.navigate(["/vault"]);
+
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"),
- });
- await this.navigateToSubscriptionPage();
- };
-
- navigateToSubscriptionPage = (): Promise =>
- this.router.navigate(["../user-subscription"], { relativeTo: this.activatedRoute });
-
- submitPayment = async (): Promise => {
- if (this.formGroup.invalid) {
+ protected async openUpgradeDialog(planType: "Premium" | "Families"): Promise {
+ const account = await firstValueFrom(this.accountService.activeAccount$);
+ if (!account) {
return;
}
- // Check if account credit is selected
- const selectedPaymentType = this.formGroup.value.paymentMethod.type;
+ const selectedPlan =
+ planType === "Premium"
+ ? PersonalSubscriptionPricingTierIds.Premium
+ : PersonalSubscriptionPricingTierIds.Families;
- let paymentMethodType: number;
- let paymentToken: string;
+ const dialogParams: UnifiedUpgradeDialogParams = {
+ account,
+ initialStep: UnifiedUpgradeDialogStep.Payment,
+ selectedPlan: selectedPlan,
+ redirectOnCompletion: true,
+ };
- if (selectedPaymentType === NonTokenizablePaymentMethods.accountCredit) {
- // Account credit doesn't need tokenization
- paymentMethodType = PaymentMethodType.Credit;
- paymentToken = "";
- } else {
- // Tokenize for card, bank account, or PayPal
- const paymentMethod = await this.enterPaymentMethodComponent.tokenize();
- paymentMethodType = tokenizablePaymentMethodToLegacyEnum(paymentMethod.type);
- paymentToken = paymentMethod.token;
- }
+ const dialogRef = UnifiedUpgradeDialogComponent.open(this.dialogService, {
+ data: dialogParams,
+ });
- const formData = new FormData();
- formData.append("paymentMethodType", paymentMethodType.toString());
- formData.append("paymentToken", paymentToken);
- formData.append(
- "additionalStorageGb",
- (this.formGroup.value.additionalStorage ?? 0).toString(),
- );
- formData.append("country", this.formGroup.value.billingAddress.country);
- formData.append("postalCode", this.formGroup.value.billingAddress.postalCode);
-
- await this.apiService.postPremium(formData);
- await this.finalizeUpgrade();
- await this.postFinalizeUpgrade();
- };
+ 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/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 7357e73a89e..15618ab3279 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -30,7 +30,6 @@ export enum FeatureFlag {
PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button",
PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure",
PM24996_ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog",
- PM24033PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
PM26793_FetchPremiumPriceFromPricingService = "pm-26793-fetch-premium-price-from-pricing-service",
PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog",
PM26462_Milestone_3 = "pm-26462-milestone-3",
@@ -140,7 +139,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.PM24032_NewNavigationPremiumUpgradeButton]: FALSE,
[FeatureFlag.PM25379_UseNewOrganizationMetadataStructure]: FALSE,
[FeatureFlag.PM24996_ImplementUpgradeFromFreeDialog]: FALSE,
- [FeatureFlag.PM24033PremiumUpgradeNewDesign]: FALSE,
[FeatureFlag.PM26793_FetchPremiumPriceFromPricingService]: FALSE,
[FeatureFlag.PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog]: FALSE,
[FeatureFlag.PM26462_Milestone_3]: FALSE,