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 }}
+
+ +

{{ "addons" | i18n }}

+
+ + {{ "additionalStorageGb" | i18n }} + + {{ + "additionalStorageIntervalDesc" + | i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | 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 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",