mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-11 10:10:25 +01:00
[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
This commit is contained in:
parent
32903a21f9
commit
2a1d9b7f31
@ -1,10 +1,14 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
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 { PaymentMethodComponent } from "../shared";
|
||||||
|
|
||||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
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 { SubscriptionComponent } from "./subscription.component";
|
||||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||||
|
|
||||||
@ -20,11 +24,15 @@ const routes: Routes = [
|
|||||||
component: UserSubscriptionComponent,
|
component: UserSubscriptionComponent,
|
||||||
data: { titleId: "premiumMembership" },
|
data: { titleId: "premiumMembership" },
|
||||||
},
|
},
|
||||||
{
|
...featureFlaggedRoute({
|
||||||
path: "premium",
|
defaultComponent: PremiumComponent,
|
||||||
component: PremiumComponent,
|
flaggedComponent: PremiumV2Component,
|
||||||
data: { titleId: "goPremium" },
|
featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||||
},
|
routeOptions: {
|
||||||
|
path: "premium",
|
||||||
|
data: { titleId: "goPremium" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
path: "payment-method",
|
path: "payment-method",
|
||||||
component: PaymentMethodComponent,
|
component: PaymentMethodComponent,
|
||||||
|
@ -5,7 +5,8 @@ import { BillingSharedModule } from "../shared";
|
|||||||
|
|
||||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||||
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
|
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 { SubscriptionComponent } from "./subscription.component";
|
||||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
|||||||
BillingHistoryViewComponent,
|
BillingHistoryViewComponent,
|
||||||
UserSubscriptionComponent,
|
UserSubscriptionComponent,
|
||||||
PremiumComponent,
|
PremiumComponent,
|
||||||
|
PremiumV2Component,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class IndividualBillingModule {}
|
export class IndividualBillingModule {}
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
<bit-section>
|
||||||
|
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||||
|
<bit-callout
|
||||||
|
type="info"
|
||||||
|
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||||
|
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||||
|
icon="bwi bwi-star-f"
|
||||||
|
>
|
||||||
|
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||||
|
</bit-callout>
|
||||||
|
<bit-callout type="success">
|
||||||
|
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||||
|
<ul class="bwi-ul">
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpStorage" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpEmergency" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpReports" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpTotp" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpSupport" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpFuture" | i18n }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||||
|
{{
|
||||||
|
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||||
|
}}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
routerLink="/create-organization"
|
||||||
|
[queryParams]="{ plan: 'families' }"
|
||||||
|
>
|
||||||
|
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
bitButton
|
||||||
|
href="{{ premiumURL }}}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
buttonType="secondary"
|
||||||
|
*ngIf="isSelfHost"
|
||||||
|
>
|
||||||
|
{{ "purchasePremium" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-callout>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngIf="isSelfHost">
|
||||||
|
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||||
|
<form [formGroup]="licenseFormGroup" [bitSubmit]="submitPremiumLicense">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||||
|
<div>
|
||||||
|
<button type="button" bitButton buttonType="secondary" (click)="fileSelector.click()">
|
||||||
|
{{ "chooseFile" | i18n }}
|
||||||
|
</button>
|
||||||
|
{{
|
||||||
|
licenseFormGroup.value.file ? licenseFormGroup.value.file.name : ("noFileChosen" | i18n)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
#fileSelector
|
||||||
|
type="file"
|
||||||
|
formControlName="file"
|
||||||
|
(change)="onLicenseFileSelected($event)"
|
||||||
|
hidden
|
||||||
|
class="tw-hidden"
|
||||||
|
/>
|
||||||
|
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||||
|
{{ "submit" | i18n }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</bit-section>
|
||||||
|
<form *ngIf="!isSelfHost" [formGroup]="addOnFormGroup" [bitSubmit]="submitPayment">
|
||||||
|
<bit-section>
|
||||||
|
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||||
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
|
<bit-form-field class="tw-col-span-6">
|
||||||
|
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
formControlName="additionalStorage"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||||
|
/>
|
||||||
|
<bit-hint>{{
|
||||||
|
"additionalStorageIntervalDesc"
|
||||||
|
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||||
|
}}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
</div>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section>
|
||||||
|
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||||
|
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||||
|
{{ "additionalStorageGb" | i18n }}: {{ addOnFormGroup.value.additionalStorage || 0 }} GB ×
|
||||||
|
{{ storageGBPrice | currency: "$" }} =
|
||||||
|
{{ additionalStorageCost | currency: "$" }}
|
||||||
|
<hr class="tw-my-3" />
|
||||||
|
</bit-section>
|
||||||
|
<bit-section>
|
||||||
|
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||||
|
<app-payment-v2 [showBankAccount]="false"></app-payment-v2>
|
||||||
|
<app-tax-info></app-tax-info>
|
||||||
|
<div class="tw-mb-4">
|
||||||
|
<div class="tw-text-muted tw-text-sm tw-flex tw-flex-col">
|
||||||
|
<span>{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}</span>
|
||||||
|
<!-- TODO: Currently incorrect - https://bitwarden.atlassian.net/browse/PM-11525 -->
|
||||||
|
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||||
|
<p bitTypography="body1">
|
||||||
|
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
||||||
|
</p>
|
||||||
|
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||||
|
{{ "submit" | i18n }}
|
||||||
|
</button>
|
||||||
|
</bit-section>
|
||||||
|
</form>
|
@ -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<boolean>;
|
||||||
|
|
||||||
|
protected addOnFormGroup = new FormGroup({
|
||||||
|
additionalStorage: new FormControl<number>(0, [Validators.min(0), Validators.max(99)]),
|
||||||
|
});
|
||||||
|
|
||||||
|
protected licenseFormGroup = new FormGroup({
|
||||||
|
file: new FormControl<File>(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<boolean> =>
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
import { ToastService } from "@bitwarden/components";
|
import { ToastService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { PaymentComponent, TaxInfoComponent } from "../shared";
|
import { PaymentComponent, TaxInfoComponent } from "../../shared";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "premium.component.html",
|
templateUrl: "premium.component.html",
|
@ -52,6 +52,7 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
|||||||
UpdateLicenseDialogComponent,
|
UpdateLicenseDialogComponent,
|
||||||
OffboardingSurveyComponent,
|
OffboardingSurveyComponent,
|
||||||
VerifyBankAccountComponent,
|
VerifyBankAccountComponent,
|
||||||
|
PaymentV2Component,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class BillingSharedModule {}
|
export class BillingSharedModule {}
|
||||||
|
Loading…
Reference in New Issue
Block a user