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 5c5c28a6efe..2d55a5a0199 100644
--- a/apps/web/src/app/billing/individual/premium/premium.component.html
+++ b/apps/web/src/app/billing/individual/premium/premium.component.html
@@ -5,13 +5,15 @@
class="tw-inline-block tw-bg-background-alt tw-border-[0.5px] tw-border-secondary-700 tw-rounded-full tw-px-2 tw-py-1 tw-mt-8 tw-mb-6"
>
- You have the Bitwarden Free plan
+ {{ "bitwardenFreeplanMessage" | i18n }}
-
Upgrade for complete security
+
+ {{ "upgradeCompleteSecurity" | i18n }}
+
- Unlock more security features with Premium, or start sharing items with Families
+ {{ "unlockPremiumFeatures" | i18n }}
@@ -21,13 +23,13 @@
@if (premiumCardData$ | async; as premiumData) {
- Premium
+ {{ "premium" | i18n }}
}
@@ -36,13 +38,13 @@
@if (familiesCardData$ | async; as familiesData) {
- Families
+ {{ "families" | i18n }}
}
@@ -50,7 +52,7 @@
-
Prices exclude tax and are billed annually
+
{{ "individualUpgradeTaxInformationMessage" | i18n }}
- View business plans
+ {{ "viewbusinessplans" | i18n }}
+
-
-
-
+
+
+
+
{{ "goPremium" | i18n }}
{{
"premiumPriceWithFamilyPlan"
- | i18n
- : (((getPremiumPrice() | async) || 0) * 12 | currency: "$")
- : familyPlanMaxUserCount
+ | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
}}
+
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 1fe40f0f547..50f7c7917a7 100644
--- a/apps/web/src/app/billing/individual/premium/premium.component.ts
+++ b/apps/web/src/app/billing/individual/premium/premium.component.ts
@@ -1,17 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { Component } from "@angular/core";
+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 { map, shareReplay } from "rxjs/operators";
+import { debounceTime, map, shareReplay } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
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";
@@ -19,12 +21,14 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/platform/sync";
import { DialogService, ToastService } from "@bitwarden/components";
-import { SubscriptionPricingService } from "../../services/subscription-pricing.service";
+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,
@@ -35,6 +39,9 @@ import {
standalone: false,
})
export class PremiumComponent {
+ @ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
+ @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
+
protected hasPremiumFromAnyOrganization$: Observable;
protected hasPremiumPersonally$: Observable;
protected shouldShowNewDesign$: Observable;
@@ -60,11 +67,12 @@ export class PremiumComponent {
protected cloudWebVaultURL: string;
protected isSelfHost = false;
- protected providerId: string;
protected subscriber: BitwardenSubscriber;
protected estimatedTax: number = 0;
protected readonly familyPlanMaxUserCount = 6;
+ protected readonly premiumPrice = 10;
+ protected readonly storageGBPrice = 4;
constructor(
private activatedRoute: ActivatedRoute,
@@ -106,7 +114,13 @@ export class PremiumComponent {
this.shouldShowNewDesign$ = combineLatest([
this.hasPremiumFromAnyOrganization$,
this.hasPremiumPersonally$,
- ]).pipe(map(([hasOrgPremium, hasPersonalPremium]) => !hasOrgPremium && !hasPersonalPremium));
+ this.configService.getFeatureFlag$(FeatureFlag.PremiumUpgradeNewDesign),
+ ]).pipe(
+ map(
+ ([hasOrgPremium, hasPersonalPremium, isNewDesignEnabled]) =>
+ isNewDesignEnabled && !hasOrgPremium && !hasPersonalPremium,
+ ),
+ );
this.personalPricingTiers$ =
this.subscriptionPricingService.getPersonalSubscriptionPricingTiers$();
@@ -162,10 +176,10 @@ export class PremiumComponent {
)
.subscribe();
- this.activatedRoute.parent.parent.parent.params
- .pipe(takeUntilDestroyed())
- .subscribe((params) => {
- this.providerId = params.providerId;
+ this.addOnFormGroup.controls.additionalStorage.valueChanges
+ .pipe(debounceTime(1000), takeUntilDestroyed())
+ .subscribe(() => {
+ this.refreshSalesTax();
});
}
@@ -219,14 +233,78 @@ export class PremiumComponent {
await this.postFinalizeUpgrade();
};
+ submitPayment = async (): Promise => {
+ this.taxInfoComponent.taxFormGroup.markAllAsTouched();
+ if (this.taxInfoComponent.taxFormGroup.invalid) {
+ return;
+ }
+
+ const { type, token } = await this.paymentComponent.tokenize();
+
+ const formData = new FormData();
+ formData.append("paymentMethodType", type.toString());
+ formData.append("paymentToken", token);
+ formData.append("additionalStorageGb", this.addOnFormGroup.value.additionalStorage.toString());
+ formData.append("country", this.taxInfoComponent.country);
+ formData.append("postalCode", this.taxInfoComponent.postalCode);
+
+ await this.apiService.postPremium(formData);
+ await this.finalizeUpgrade();
+ await this.postFinalizeUpgrade();
+ };
+
+ protected get additionalStorageCost(): number {
+ return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
+ }
+
protected get premiumURL(): string {
return `${this.cloudWebVaultURL}/#/settings/subscription/premium`;
}
+ protected get subtotal(): number {
+ return this.premiumPrice + this.additionalStorageCost;
+ }
+
+ protected get total(): number {
+ return this.subtotal + this.estimatedTax;
+ }
+
protected async onLicenseFileSelectedChanged(): Promise {
await this.postFinalizeUpgrade();
}
+ private refreshSalesTax(): void {
+ if (!this.taxInfoComponent.country || !this.taxInfoComponent.postalCode) {
+ return;
+ }
+ const request: PreviewIndividualInvoiceRequest = {
+ passwordManager: {
+ additionalStorage: this.addOnFormGroup.value.additionalStorage,
+ },
+ taxInformation: {
+ postalCode: this.taxInfoComponent.postalCode,
+ country: this.taxInfoComponent.country,
+ },
+ };
+
+ this.taxService
+ .previewIndividualInvoice(request)
+ .then((invoice) => {
+ this.estimatedTax = invoice.taxAmount;
+ })
+ .catch((error) => {
+ this.toastService.showToast({
+ title: "",
+ variant: "error",
+ message: this.i18nService.t(error.message),
+ });
+ });
+ }
+
+ protected onTaxInformationChanged(): void {
+ this.refreshSalesTax();
+ }
+
protected async openUpgradeDialog(type: "Premium" | "Families"): Promise {
try {
const planId =
@@ -283,29 +361,4 @@ export class PremiumComponent {
break;
}
}
-
- // Helper methods for backward compatibility (if needed elsewhere)
- protected getPremiumTier(): Observable {
- return this.premiumCardData$.pipe(map((data) => data.tier));
- }
-
- protected getFamiliesTier(): Observable {
- return this.familiesCardData$.pipe(map((data) => data.tier));
- }
-
- protected getPremiumPrice(): Observable {
- return this.premiumCardData$.pipe(map((data) => data.price));
- }
-
- protected getFamiliesPrice(): Observable {
- return this.familiesCardData$.pipe(map((data) => data.price));
- }
-
- protected getPremiumFeatures(): Observable {
- return this.premiumCardData$.pipe(map((data) => data.features));
- }
-
- protected getFamiliesFeatures(): Observable {
- return this.familiesCardData$.pipe(map((data) => data.features));
- }
}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index eaf162186f8..81cafed422b 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -11521,5 +11521,17 @@
},
"organizationNameDescription": {
"message": "Your organization name will appear in invitations you send to members."
+ },
+ "bitwardenFreeplanMessage": {
+ "message": "You have the Bitwarden Free plan"
+ },
+ "upgradeCompleteSecurity": {
+ "message": "Upgrade for complete security"
+ },
+ "unlockPremiumFeatures": {
+ "message": "Unlock more security features with Premium, or start sharing items with Families"
+ },
+ "viewbusinessplans": {
+ "message": "View business plans"
}
}
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 5dcc83303fa..3e096b22c56 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 = "premium-upgrade-new-design",
+ PremiumUpgradeNewDesign = "pm-24033-updat-premium-subscription-page",
/* Key Management */
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",