From 2a1d9b7f31e83ed7d14bbeecc952fdceffd21ef2 Mon Sep 17 00:00:00 2001
From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com>
Date: Thu, 5 Sep 2024 11:20:23 -0400
Subject: [PATCH] [AC-2963] Add premium-v2.component for individual users
(#10885)
* Add premium-v2.component
* (No Logic) Move existing premium.component into new premium folder
* Add new premium-v2.component to /premium route behind FF
---
.../individual-billing-routing.module.ts | 20 ++-
.../individual/individual-billing.module.ts | 4 +-
.../premium/premium-v2.component.html | 144 +++++++++++++++
.../premium/premium-v2.component.ts | 164 ++++++++++++++++++
.../{ => premium}/premium.component.html | 0
.../{ => premium}/premium.component.ts | 2 +-
.../billing/shared/billing-shared.module.ts | 1 +
7 files changed, 327 insertions(+), 8 deletions(-)
create mode 100644 apps/web/src/app/billing/individual/premium/premium-v2.component.html
create mode 100644 apps/web/src/app/billing/individual/premium/premium-v2.component.ts
rename apps/web/src/app/billing/individual/{ => premium}/premium.component.html (100%)
rename apps/web/src/app/billing/individual/{ => premium}/premium.component.ts (98%)
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 ff45ca75ac..585d9b418c 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,10 +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 { PaymentMethodComponent } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
-import { PremiumComponent } from "./premium.component";
+import { PremiumV2Component } from "./premium/premium-v2.component";
+import { PremiumComponent } from "./premium/premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -20,11 +24,15 @@ const routes: Routes = [
component: UserSubscriptionComponent,
data: { titleId: "premiumMembership" },
},
- {
- path: "premium",
- component: PremiumComponent,
- data: { titleId: "goPremium" },
- },
+ ...featureFlaggedRoute({
+ defaultComponent: PremiumComponent,
+ flaggedComponent: PremiumV2Component,
+ featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
+ routeOptions: {
+ path: "premium",
+ data: { titleId: "goPremium" },
+ },
+ }),
{
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 dbae28858f..0dbbc8c683 100644
--- a/apps/web/src/app/billing/individual/individual-billing.module.ts
+++ b/apps/web/src/app/billing/individual/individual-billing.module.ts
@@ -5,7 +5,8 @@ import { BillingSharedModule } from "../shared";
import { BillingHistoryViewComponent } from "./billing-history-view.component";
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
-import { PremiumComponent } from "./premium.component";
+import { PremiumV2Component } from "./premium/premium-v2.component";
+import { PremiumComponent } from "./premium/premium.component";
import { SubscriptionComponent } from "./subscription.component";
import { UserSubscriptionComponent } from "./user-subscription.component";
@@ -16,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
BillingHistoryViewComponent,
UserSubscriptionComponent,
PremiumComponent,
+ PremiumV2Component,
],
})
export class IndividualBillingModule {}
diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.html b/apps/web/src/app/billing/individual/premium/premium-v2.component.html
new file mode 100644
index 0000000000..bdf6ff87d1
--- /dev/null
+++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.html
@@ -0,0 +1,144 @@
+
+ {{ "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 }}
+
+
+
+
+ {{ "uploadLicenseFilePremium" | i18n }}
+
+
+
diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts
new file mode 100644
index 0000000000..cf66dac2f7
--- /dev/null
+++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts
@@ -0,0 +1,164 @@
+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, from, Observable, of } from "rxjs";
+
+import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
+import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
+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 { ToastService } from "@bitwarden/components";
+
+import { PaymentV2Component } from "../../shared/payment/payment-v2.component";
+import { TaxInfoComponent } from "../../shared/tax-info.component";
+
+@Component({
+ templateUrl: "./premium-v2.component.html",
+})
+export class PremiumV2Component {
+ @ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component;
+ @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
+
+ protected hasPremiumFromAnyOrganization$: Observable;
+
+ protected addOnFormGroup = new FormGroup({
+ additionalStorage: new FormControl(0, [Validators.min(0), Validators.max(99)]),
+ });
+
+ protected licenseFormGroup = new FormGroup({
+ file: new FormControl(null, [Validators.required]),
+ });
+
+ protected cloudWebVaultURL: string;
+ protected isSelfHost = false;
+
+ protected readonly familyPlanMaxUserCount = 6;
+ protected readonly premiumPrice = 10;
+ protected readonly storageGBPrice = 4;
+
+ constructor(
+ private activatedRoute: ActivatedRoute,
+ private apiService: ApiService,
+ private billingAccountProfileStateService: BillingAccountProfileStateService,
+ private environmentService: EnvironmentService,
+ private i18nService: I18nService,
+ private platformUtilsService: PlatformUtilsService,
+ private router: Router,
+ private syncService: SyncService,
+ private toastService: ToastService,
+ private tokenService: TokenService,
+ ) {
+ this.isSelfHost = this.platformUtilsService.isSelfHost();
+
+ this.hasPremiumFromAnyOrganization$ =
+ this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$;
+
+ combineLatest([
+ this.billingAccountProfileStateService.hasPremiumPersonally$,
+ this.environmentService.cloudWebVaultUrl$,
+ ])
+ .pipe(
+ takeUntilDestroyed(),
+ concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
+ if (hasPremiumPersonally) {
+ return from(this.navigateToSubscriptionPage());
+ }
+
+ this.cloudWebVaultURL = cloudWebVaultURL;
+ return of(true);
+ }),
+ )
+ .subscribe();
+ }
+
+ finalizeUpgrade = async () => {
+ await this.apiService.refreshIdentityToken();
+ await this.syncService.fullSync(true);
+ 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 });
+
+ onLicenseFileSelected = (event: Event): void => {
+ const element = event.target as HTMLInputElement;
+ this.licenseFormGroup.value.file = element.files.length > 0 ? element.files[0] : null;
+ };
+
+ submitPremiumLicense = async (): Promise => {
+ this.licenseFormGroup.markAllAsTouched();
+
+ if (this.licenseFormGroup.invalid) {
+ return this.toastService.showToast({
+ variant: "error",
+ title: this.i18nService.t("errorOccurred"),
+ message: this.i18nService.t("selectFile"),
+ });
+ }
+
+ const emailVerified = await this.tokenService.getEmailVerified();
+ if (!emailVerified) {
+ return this.toastService.showToast({
+ variant: "error",
+ title: this.i18nService.t("errorOccurred"),
+ message: this.i18nService.t("verifyEmailFirst"),
+ });
+ }
+
+ const formData = new FormData();
+ formData.append("license", this.licenseFormGroup.value.file);
+
+ await this.apiService.postAccountLicense(formData);
+ await this.finalizeUpgrade();
+ };
+
+ 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();
+ };
+
+ protected get additionalStorageCost(): number {
+ return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
+ }
+
+ protected get estimatedTax(): number {
+ return this.taxInfoComponent?.taxRate != null
+ ? (this.taxInfoComponent.taxRate / 100) * this.subtotal
+ : 0;
+ }
+
+ 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;
+ }
+}
diff --git a/apps/web/src/app/billing/individual/premium.component.html b/apps/web/src/app/billing/individual/premium/premium.component.html
similarity index 100%
rename from apps/web/src/app/billing/individual/premium.component.html
rename to apps/web/src/app/billing/individual/premium/premium.component.html
diff --git a/apps/web/src/app/billing/individual/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts
similarity index 98%
rename from apps/web/src/app/billing/individual/premium.component.ts
rename to apps/web/src/app/billing/individual/premium/premium.component.ts
index 79a5c5e2ed..c45b6b882d 100644
--- a/apps/web/src/app/billing/individual/premium.component.ts
+++ b/apps/web/src/app/billing/individual/premium/premium.component.ts
@@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ToastService } from "@bitwarden/components";
-import { PaymentComponent, TaxInfoComponent } from "../shared";
+import { PaymentComponent, TaxInfoComponent } from "../../shared";
@Component({
templateUrl: "premium.component.html",
diff --git a/apps/web/src/app/billing/shared/billing-shared.module.ts b/apps/web/src/app/billing/shared/billing-shared.module.ts
index c9b3f2de85..b966729c1d 100644
--- a/apps/web/src/app/billing/shared/billing-shared.module.ts
+++ b/apps/web/src/app/billing/shared/billing-shared.module.ts
@@ -52,6 +52,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
UpdateLicenseDialogComponent,
OffboardingSurveyComponent,
VerifyBankAccountComponent,
+ PaymentV2Component,
],
})
export class BillingSharedModule {}