mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-06 23:51:28 +01:00
[PM-11730] Remove feature flag: AC-2476-deprecate-stripe-sources-api (#13032)
* Remove FF from trial-billing-step.component * Remove FF from user-subscription.component * Remove FF from individual-billing-routing.module * Remove FF from organization-billing.service * Remove FF from organization-subscription-cloud.component * Remove FF from organization-billing-routing.mdoule * Remove FF from organization-plans.component * Remove FF from change-plan-dialog.component * Remove FF * Remove legacy payment.component * Rename V2: adjust-payment-dialog.component * Rename V2: adjust-storage-dialog.component * Rename V2: payment-label.component * Rename V2: payment.component * Rename V2: premium.component * Patrick's feedback
This commit is contained in:
parent
315e1338d5
commit
f630ee5f4e
@ -10,7 +10,7 @@ import { RegisterFormModule } from "../../auth/register-form/register-form.modul
|
||||
import { SecretsManagerTrialFreeStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-free-stepper.component";
|
||||
import { SecretsManagerTrialPaidStepperComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial-paid-stepper.component";
|
||||
import { SecretsManagerTrialComponent } from "../../auth/trial-initiation/secrets-manager/secrets-manager-trial.component";
|
||||
import { PaymentComponent, TaxInfoComponent } from "../../billing";
|
||||
import { TaxInfoComponent } from "../../billing";
|
||||
import { TrialBillingStepComponent } from "../../billing/accounts/trial-initiation/trial-billing-step.component";
|
||||
import { EnvironmentSelectorModule } from "../../components/environment-selector/environment-selector.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
@ -51,7 +51,6 @@ import { VerticalStepperModule } from "./vertical-stepper/vertical-stepper.modul
|
||||
RegisterFormModule,
|
||||
OrganizationCreateModule,
|
||||
EnvironmentSelectorModule,
|
||||
PaymentComponent,
|
||||
TaxInfoComponent,
|
||||
TrialBillingStepComponent,
|
||||
InputPasswordComponent,
|
||||
|
@ -49,15 +49,7 @@
|
||||
</div>
|
||||
<div class="tw-mb-4">
|
||||
<h2 class="tw-mb-3 tw-text-base tw-font-semibold">{{ "paymentType" | i18n }}</h2>
|
||||
<app-payment
|
||||
*ngIf="!deprecateStripeSourcesAPI"
|
||||
[hideCredit]="true"
|
||||
[trialFlow]="true"
|
||||
></app-payment>
|
||||
<app-payment-v2
|
||||
*ngIf="deprecateStripeSourcesAPI"
|
||||
[showAccountCredit]="false"
|
||||
></app-payment-v2>
|
||||
<app-payment [showAccountCredit]="false"></app-payment>
|
||||
<app-tax-info [trialFlow]="true" (countryChanged)="changedCountry()"></app-tax-info>
|
||||
</div>
|
||||
<div class="tw-flex tw-space-x-2">
|
||||
|
@ -13,14 +13,12 @@ import {
|
||||
} from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BillingSharedModule, PaymentComponent, TaxInfoComponent } from "../../shared";
|
||||
import { PaymentV2Component } from "../../shared/payment/payment-v2.component";
|
||||
import { BillingSharedModule, TaxInfoComponent } from "../../shared";
|
||||
import { PaymentComponent } from "../../shared/payment/payment.component";
|
||||
|
||||
export type TrialOrganizationType = Exclude<ProductTierType, ProductTierType.Free>;
|
||||
|
||||
@ -53,7 +51,6 @@ export enum SubscriptionProduct {
|
||||
})
|
||||
export class TrialBillingStepComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component;
|
||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||
@Input() organizationInfo: OrganizationInfo;
|
||||
@Input() subscriptionProduct: SubscriptionProduct = SubscriptionProduct.PasswordManager;
|
||||
@ -74,11 +71,8 @@ export class TrialBillingStepComponent implements OnInit {
|
||||
annualPlan?: PlanResponse;
|
||||
monthlyPlan?: PlanResponse;
|
||||
|
||||
deprecateStripeSourcesAPI: boolean;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private configService: ConfigService,
|
||||
private i18nService: I18nService,
|
||||
private formBuilder: FormBuilder,
|
||||
private messagingService: MessagingService,
|
||||
@ -87,9 +81,6 @@ export class TrialBillingStepComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
);
|
||||
const plans = await this.apiService.getPlans();
|
||||
this.applicablePlans = plans.data.filter(this.isApplicable);
|
||||
this.annualPlan = this.findPlanFor(SubscriptionCadence.Annual);
|
||||
@ -124,23 +115,12 @@ export class TrialBillingStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
protected changedCountry() {
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
this.paymentV2Component.showBankAccount = this.taxInfoComponent.country === "US";
|
||||
if (
|
||||
!this.paymentV2Component.showBankAccount &&
|
||||
this.paymentV2Component.selected === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentV2Component.select(PaymentMethodType.Card);
|
||||
}
|
||||
} else {
|
||||
this.paymentComponent.hideBank = this.taxInfoComponent.taxFormGroup.value.country !== "US";
|
||||
if (
|
||||
this.paymentComponent.hideBank &&
|
||||
this.paymentComponent.method === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
this.paymentComponent.showBankAccount = this.taxInfoComponent.country === "US";
|
||||
if (
|
||||
!this.paymentComponent.showBankAccount &&
|
||||
this.paymentComponent.selected === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentComponent.select(PaymentMethodType.Card);
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,14 +142,8 @@ export class TrialBillingStepComponent implements OnInit {
|
||||
private async createOrganization(): Promise<string> {
|
||||
const planResponse = this.findPlanFor(this.formGroup.value.cadence);
|
||||
|
||||
let paymentMethod: [string, PaymentMethodType];
|
||||
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
const { type, token } = await this.paymentV2Component.tokenize();
|
||||
paymentMethod = [token, type];
|
||||
} else {
|
||||
paymentMethod = await this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
const paymentMethod: [string, PaymentMethodType] = [token, type];
|
||||
|
||||
const organization: OrganizationInformation = {
|
||||
name: this.organizationInfo.name,
|
||||
|
@ -1,2 +1,2 @@
|
||||
export { OrganizationPlansComponent } from "./organizations";
|
||||
export { PaymentComponent, TaxInfoComponent } from "./shared";
|
||||
export { TaxInfoComponent } from "./shared";
|
||||
|
@ -1,13 +1,9 @@
|
||||
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 { PremiumV2Component } from "./premium/premium-v2.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
@ -24,15 +20,11 @@ const routes: Routes = [
|
||||
component: UserSubscriptionComponent,
|
||||
data: { titleId: "premiumMembership" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: PremiumComponent,
|
||||
flaggedComponent: PremiumV2Component,
|
||||
featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
routeOptions: {
|
||||
path: "premium",
|
||||
data: { titleId: "goPremium" },
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "premium",
|
||||
component: PremiumComponent,
|
||||
data: { titleId: "goPremium" },
|
||||
},
|
||||
{
|
||||
path: "payment-method",
|
||||
component: PaymentMethodComponent,
|
||||
|
@ -5,7 +5,6 @@ import { BillingSharedModule } from "../shared";
|
||||
|
||||
import { BillingHistoryViewComponent } from "./billing-history-view.component";
|
||||
import { IndividualBillingRoutingModule } from "./individual-billing-routing.module";
|
||||
import { PremiumV2Component } from "./premium/premium-v2.component";
|
||||
import { PremiumComponent } from "./premium/premium.component";
|
||||
import { SubscriptionComponent } from "./subscription.component";
|
||||
import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
@ -17,7 +16,6 @@ import { UserSubscriptionComponent } from "./user-subscription.component";
|
||||
BillingHistoryViewComponent,
|
||||
UserSubscriptionComponent,
|
||||
PremiumComponent,
|
||||
PremiumV2Component,
|
||||
],
|
||||
})
|
||||
export class IndividualBillingModule {}
|
||||
|
@ -1,149 +0,0 @@
|
||||
<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">
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<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>
|
||||
</ng-container>
|
||||
<individual-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</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 (taxInformationChanged)="onTaxInformationChanged()"></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>
|
||||
<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>
|
@ -1,228 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
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, switchMap } 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 { 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";
|
||||
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 useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
protected estimatedTax: number = 0;
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected readonly premiumPrice = 10;
|
||||
protected readonly storageGBPrice = 4;
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private syncService: SyncService,
|
||||
private toastService: ToastService,
|
||||
private tokenService: TokenService,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.isSelfHost = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
|
||||
),
|
||||
),
|
||||
this.environmentService.cloudWebVaultUrl$,
|
||||
])
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
|
||||
if (hasPremiumPersonally) {
|
||||
return from(this.navigateToSubscriptionPage());
|
||||
}
|
||||
|
||||
this.cloudWebVaultURL = cloudWebVaultURL;
|
||||
return of(true);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.addOnFormGroup.controls.additionalStorage.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntilDestroyed())
|
||||
.subscribe(() => {
|
||||
this.refreshSalesTax();
|
||||
});
|
||||
}
|
||||
|
||||
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<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();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
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();
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
<bit-section>
|
||||
<h2 *ngIf="!selfHosted" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<h2 *ngIf="!isSelfHost" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="canAccessPremium$ | async"
|
||||
*ngIf="hasPremiumFromAnyOrganization$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
@ -40,7 +40,7 @@
|
||||
{{ "premiumSignUpFuture" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !selfHosted }">
|
||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !isSelfHost }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
@ -49,49 +49,58 @@
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>{{ "bitwardenFamiliesPlan" | i18n }}</a
|
||||
>
|
||||
{{ "bitwardenFamiliesPlan" | i18n }}
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
|
||||
href="{{ premiumURL }}}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
buttonType="secondary"
|
||||
*ngIf="selfHosted"
|
||||
*ngIf="isSelfHost"
|
||||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="selfHosted">
|
||||
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form [formGroup]="licenseForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div class="tw-pt-2 tw-pb-1">
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }}
|
||||
</div>
|
||||
<input
|
||||
bitInput
|
||||
#fileSelector
|
||||
type="file"
|
||||
formControlName="file"
|
||||
(change)="setSelectedFile($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 *ngIf="isSelfHost">
|
||||
<ng-container *ngIf="!(useLicenseUploaderComponent$ | async)">
|
||||
<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>
|
||||
</ng-container>
|
||||
<individual-self-hosting-license-uploader
|
||||
*ngIf="useLicenseUploaderComponent$ | async"
|
||||
(onLicenseFileUploaded)="onLicenseFileSelectedChanged()"
|
||||
/>
|
||||
</bit-section>
|
||||
<form [formGroup]="addonForm" [bitSubmit]="submit" *ngIf="!selfHosted">
|
||||
<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">
|
||||
@ -106,7 +115,7 @@
|
||||
/>
|
||||
<bit-hint>{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
|
||||
| i18n: "1 GB" : (storageGBPrice | currency: "$") : ("year" | i18n)
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
@ -114,30 +123,26 @@
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB ×
|
||||
{{ storageGbPrice | currency: "$" }} =
|
||||
{{ additionalStorageTotal | currency: "$" }}
|
||||
{{ "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 [hideBank]="true"></app-payment>
|
||||
<app-tax-info (taxInformationChanged)="onTaxInformationChanged()" />
|
||||
<div id="price" class="tw-my-4">
|
||||
<div class="tw-text-muted tw-text-sm">
|
||||
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
|
||||
<br />
|
||||
<ng-container>
|
||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||
</ng-container>
|
||||
<app-payment [showBankAccount]="false"></app-payment>
|
||||
<app-tax-info (taxInformationChanged)="onTaxInformationChanged()"></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>
|
||||
<span>{{ "estimatedTax" | i18n }}: {{ estimatedTax | currency: "USD $" }}</span>
|
||||
</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>
|
||||
</div>
|
||||
<p bitTypography="body2">{{ "paymentChargedAnnually" | i18n }}</p>
|
||||
<button type="submit" bitButton bitFormButton>
|
||||
<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>
|
||||
|
@ -1,187 +1,197 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { Component, ViewChild } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, Observable, switchMap } from "rxjs";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, concatMap, from, Observable, of, switchMap } 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 { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.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";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent, TaxInfoComponent } from "../../shared";
|
||||
import { PaymentComponent } from "../../shared/payment/payment.component";
|
||||
import { TaxInfoComponent } from "../../shared/tax-info.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "premium.component.html",
|
||||
templateUrl: "./premium.component.html",
|
||||
})
|
||||
export class PremiumComponent implements OnInit {
|
||||
export class PremiumComponent {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
canAccessPremium$: Observable<boolean>;
|
||||
selfHosted = false;
|
||||
premiumPrice = 10;
|
||||
familyPlanMaxUserCount = 6;
|
||||
storageGbPrice = 4;
|
||||
cloudWebVaultUrl: string;
|
||||
licenseFile: File = null;
|
||||
protected hasPremiumFromAnyOrganization$: Observable<boolean>;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
protected licenseForm = new FormGroup({
|
||||
file: new FormControl(null, [Validators.required]),
|
||||
});
|
||||
protected addonForm = new FormGroup({
|
||||
additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]),
|
||||
protected addOnFormGroup = new FormGroup({
|
||||
additionalStorage: new FormControl<number>(0, [Validators.min(0), Validators.max(99)]),
|
||||
});
|
||||
|
||||
private estimatedTax: number = 0;
|
||||
protected licenseFormGroup = new FormGroup({
|
||||
file: new FormControl<File>(null, [Validators.required]),
|
||||
});
|
||||
|
||||
protected cloudWebVaultURL: string;
|
||||
protected isSelfHost = false;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
);
|
||||
|
||||
protected estimatedTax: number = 0;
|
||||
protected readonly familyPlanMaxUserCount = 6;
|
||||
protected readonly premiumPrice = 10;
|
||||
protected readonly storageGBPrice = 4;
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private configService: ConfigService,
|
||||
private environmentService: EnvironmentService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private tokenService: TokenService,
|
||||
private router: Router,
|
||||
private messagingService: MessagingService,
|
||||
private syncService: SyncService,
|
||||
private environmentService: EnvironmentService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
private tokenService: TokenService,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
this.isSelfHost = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id),
|
||||
),
|
||||
);
|
||||
|
||||
this.addonForm.controls.additionalStorage.valueChanges
|
||||
combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumPersonally$(account.id),
|
||||
),
|
||||
),
|
||||
this.environmentService.cloudWebVaultUrl$,
|
||||
])
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
concatMap(([hasPremiumPersonally, cloudWebVaultURL]) => {
|
||||
if (hasPremiumPersonally) {
|
||||
return from(this.navigateToSubscriptionPage());
|
||||
}
|
||||
|
||||
this.cloudWebVaultURL = cloudWebVaultURL;
|
||||
return of(true);
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.addOnFormGroup.controls.additionalStorage.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntilDestroyed())
|
||||
.subscribe(() => {
|
||||
this.refreshSalesTax();
|
||||
});
|
||||
}
|
||||
protected setSelectedFile(event: Event) {
|
||||
const fileInputEl = <HTMLInputElement>event.target;
|
||||
const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||
this.licenseFile = file;
|
||||
}
|
||||
async ngOnInit() {
|
||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (
|
||||
await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.id))
|
||||
) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
submit = async () => {
|
||||
if (this.taxInfoComponent) {
|
||||
if (!this.taxInfoComponent?.taxFormGroup.valid) {
|
||||
this.taxInfoComponent.taxFormGroup.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.licenseForm.markAllAsTouched();
|
||||
this.addonForm.markAllAsTouched();
|
||||
if (this.selfHosted) {
|
||||
if (this.licenseFile == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("selectFile"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selfHosted) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
if (!this.tokenService.getEmailVerified()) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("verifyEmailFirst"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("license", this.licenseFile);
|
||||
await this.apiService.postAccountLicense(fd).then(() => {
|
||||
return this.finalizePremium();
|
||||
});
|
||||
} else {
|
||||
await this.paymentComponent
|
||||
.createPaymentToken()
|
||||
.then((result) => {
|
||||
const fd = new FormData();
|
||||
fd.append("paymentMethodType", result[1].toString());
|
||||
if (result[0] != null) {
|
||||
fd.append("paymentToken", result[0]);
|
||||
}
|
||||
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
|
||||
fd.append("country", this.taxInfoComponent?.taxFormGroup?.value.country);
|
||||
fd.append("postalCode", this.taxInfoComponent?.taxFormGroup?.value.postalCode);
|
||||
return this.apiService.postPremium(fd);
|
||||
})
|
||||
.then((paymentResponse) => {
|
||||
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
|
||||
return this.paymentComponent.handleStripeCardPayment(
|
||||
paymentResponse.paymentIntentClientSecret,
|
||||
() => this.finalizePremium(),
|
||||
);
|
||||
} else {
|
||||
return this.finalizePremium();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async finalizePremium() {
|
||||
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.router.navigate(["/settings/subscription/user-subscription"]);
|
||||
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();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
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();
|
||||
await this.postFinalizeUpgrade();
|
||||
};
|
||||
|
||||
protected get additionalStorageCost(): number {
|
||||
return this.storageGBPrice * this.addOnFormGroup.value.additionalStorage;
|
||||
}
|
||||
|
||||
get additionalStorage(): number {
|
||||
return this.addonForm.get("additionalStorage").value;
|
||||
}
|
||||
get additionalStorageTotal(): number {
|
||||
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
||||
protected get premiumURL(): string {
|
||||
return `${this.cloudWebVaultURL}/#/settings/subscription/premium`;
|
||||
}
|
||||
|
||||
get subtotal(): number {
|
||||
return this.premiumPrice + this.additionalStorageTotal;
|
||||
protected get subtotal(): number {
|
||||
return this.premiumPrice + this.additionalStorageCost;
|
||||
}
|
||||
|
||||
get taxCharges(): number {
|
||||
return this.estimatedTax;
|
||||
protected get total(): number {
|
||||
return this.subtotal + this.estimatedTax;
|
||||
}
|
||||
|
||||
get total(): number {
|
||||
return this.subtotal + this.taxCharges || 0;
|
||||
protected async onLicenseFileSelectedChanged(): Promise<void> {
|
||||
await this.postFinalizeUpgrade();
|
||||
}
|
||||
|
||||
private refreshSalesTax(): void {
|
||||
@ -190,7 +200,7 @@ export class PremiumComponent implements OnInit {
|
||||
}
|
||||
const request: PreviewIndividualInvoiceRequest = {
|
||||
passwordManager: {
|
||||
additionalStorage: this.addonForm.value.additionalStorage,
|
||||
additionalStorage: this.addOnFormGroup.value.additionalStorage,
|
||||
},
|
||||
taxInformation: {
|
||||
postalCode: this.taxInfoComponent.postalCode,
|
||||
|
@ -8,8 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
|
||||
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 { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@ -18,12 +16,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
AdjustStorageDialogV2Component,
|
||||
AdjustStorageDialogV2ResultType,
|
||||
} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component";
|
||||
import {
|
||||
AdjustStorageDialogResult,
|
||||
openAdjustStorageDialog,
|
||||
AdjustStorageDialogComponent,
|
||||
AdjustStorageDialogResultType,
|
||||
} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component";
|
||||
import {
|
||||
OffboardingSurveyDialogResultType,
|
||||
@ -45,10 +39,6 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@ -60,7 +50,6 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
private environmentService: EnvironmentService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
@ -166,33 +155,18 @@ export class UserSubscriptionComponent implements OnInit {
|
||||
};
|
||||
|
||||
adjustStorage = async (add: boolean) => {
|
||||
const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$);
|
||||
const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
price: 4,
|
||||
cadence: "year",
|
||||
type: add ? "Add" : "Remove",
|
||||
},
|
||||
});
|
||||
|
||||
if (deprecateStripeSourcesAPI) {
|
||||
const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, {
|
||||
data: {
|
||||
price: 4,
|
||||
cadence: "year",
|
||||
type: add ? "Add" : "Remove",
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result === AdjustStorageDialogV2ResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
} else {
|
||||
const dialogRef = openAdjustStorageDialog(this.dialogService, {
|
||||
data: {
|
||||
storageGbPrice: 4,
|
||||
add: add,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustStorageDialogResult.Adjusted) {
|
||||
await this.load();
|
||||
}
|
||||
if (result === AdjustStorageDialogResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -346,25 +346,20 @@
|
||||
"
|
||||
>
|
||||
<i class="bwi bwi-fw" [ngClass]="paymentSourceClasses"></i>
|
||||
{{
|
||||
deprecateStripeSourcesAPI
|
||||
? paymentSource?.description
|
||||
: billing?.paymentSource?.description
|
||||
}}
|
||||
{{ paymentSource?.description }}
|
||||
<span class="ml-2 tw-text-primary-600 tw-cursor-pointer" (click)="toggleShowPayment()">
|
||||
{{ "changePaymentMethod" | i18n }}
|
||||
</span>
|
||||
<a></a>
|
||||
</p>
|
||||
<ng-container *ngIf="canUpdatePaymentInformation()">
|
||||
<app-payment *ngIf="!deprecateStripeSourcesAPI" [hideCredit]="true" />
|
||||
<app-payment-v2 *ngIf="deprecateStripeSourcesAPI" [showAccountCredit]="false" />
|
||||
<app-payment [showAccountCredit]="false" />
|
||||
<app-manage-tax-information
|
||||
[startWith]="taxInformation"
|
||||
(taxInformationChanged)="taxInformationChanged($event)"
|
||||
></app-manage-tax-information>
|
||||
</ng-container>
|
||||
<div id="price" class="tw-mt-4">
|
||||
<div class="tw-mt-4">
|
||||
<p class="tw-text-lg tw-mb-1">
|
||||
<span class="tw-font-semibold"
|
||||
>{{ "total" | i18n }}:
|
||||
@ -962,7 +957,7 @@
|
||||
</p>
|
||||
</bit-hint>
|
||||
</div>
|
||||
<div *ngIf="totalOpened" id="price" class="row tw-mt-4">
|
||||
<div *ngIf="totalOpened" class="row tw-mt-4">
|
||||
<bit-hint class="col-6">
|
||||
<p
|
||||
class="tw-flex tw-justify-between tw-border-0 tw-border-solid tw-border-t tw-border-secondary-300 tw-pt-2 tw-mb-0"
|
||||
|
@ -45,16 +45,13 @@ import {
|
||||
} from "@bitwarden/common/billing/enums";
|
||||
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
|
||||
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
|
||||
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
@ -62,7 +59,6 @@ import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BillingSharedModule } from "../shared/billing-shared.module";
|
||||
import { PaymentV2Component } from "../shared/payment/payment-v2.component";
|
||||
import { PaymentComponent } from "../shared/payment/payment.component";
|
||||
|
||||
type ChangePlanDialogParams = {
|
||||
@ -107,7 +103,6 @@ interface OnSuccessArgs {
|
||||
})
|
||||
export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component;
|
||||
@ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent;
|
||||
|
||||
@Input() acceptingSponsorship = false;
|
||||
@ -183,13 +178,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
showPayment: boolean = false;
|
||||
totalOpened: boolean = false;
|
||||
currentPlan: PlanResponse;
|
||||
currentFocusIndex = 0;
|
||||
isCardStateDisabled = false;
|
||||
focusedIndex: number | null = null;
|
||||
accountCredit: number;
|
||||
paymentSource?: PaymentSourceResponse;
|
||||
plans: ListResponse<PlanResponse>;
|
||||
deprecateStripeSourcesAPI: boolean;
|
||||
isSubscriptionCanceled: boolean = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
@ -210,7 +203,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
private messagingService: MessagingService,
|
||||
private formBuilder: FormBuilder,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private taxService: TaxServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
@ -218,10 +210,6 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
);
|
||||
|
||||
if (this.dialogParams.organizationId) {
|
||||
this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType);
|
||||
this.sub =
|
||||
@ -239,14 +227,10 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
} else {
|
||||
this.billing = await this.organizationApiService.getBilling(this.organizationId);
|
||||
}
|
||||
const { accountCredit, paymentSource } =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(this.organizationId);
|
||||
this.accountCredit = accountCredit;
|
||||
this.paymentSource = paymentSource;
|
||||
}
|
||||
|
||||
if (!this.selfHosted) {
|
||||
@ -333,16 +317,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
return this.selectableProducts.find((product) => product.productTier === productTier);
|
||||
}
|
||||
|
||||
secretsManagerTrialDiscount() {
|
||||
return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone")
|
||||
? this.discountPercentage
|
||||
: this.discountPercentageFromSub + this.discountPercentage;
|
||||
}
|
||||
|
||||
isPaymentSourceEmpty() {
|
||||
return this.deprecateStripeSourcesAPI
|
||||
? this.paymentSource === null || this.paymentSource === undefined
|
||||
: this.billing?.paymentSource === null || this.billing?.paymentSource === undefined;
|
||||
return this.paymentSource === null || this.paymentSource === undefined;
|
||||
}
|
||||
|
||||
isSecretsManagerTrial(): boolean {
|
||||
@ -486,9 +462,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
get upgradeRequiresPaymentMethod() {
|
||||
const isFreeTier = this.organization?.productTierType === ProductTierType.Free;
|
||||
const shouldHideFree = !this.showFree;
|
||||
const hasNoPaymentSource = this.deprecateStripeSourcesAPI
|
||||
? !this.paymentSource
|
||||
: !this.billing?.paymentSource;
|
||||
const hasNoPaymentSource = !this.paymentSource;
|
||||
|
||||
return isFreeTier && shouldHideFree && hasNoPaymentSource;
|
||||
}
|
||||
@ -721,25 +695,13 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
changedCountry() {
|
||||
if (this.deprecateStripeSourcesAPI && this.paymentV2Component) {
|
||||
this.paymentV2Component.showBankAccount = this.taxInformation.country === "US";
|
||||
this.paymentComponent.showBankAccount = this.taxInformation.country === "US";
|
||||
|
||||
if (
|
||||
!this.paymentV2Component.showBankAccount &&
|
||||
this.paymentV2Component.selected === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentV2Component.select(PaymentMethodType.Card);
|
||||
}
|
||||
} else if (this.paymentComponent && this.taxInformation) {
|
||||
this.paymentComponent!.hideBank = this.taxInformation.country !== "US";
|
||||
// Bank Account payments are only available for US customers
|
||||
if (
|
||||
this.paymentComponent.hideBank &&
|
||||
this.paymentComponent.method === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
if (
|
||||
!this.paymentComponent.showBankAccount &&
|
||||
this.paymentComponent.selected === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentComponent.select(PaymentMethodType.Card);
|
||||
}
|
||||
}
|
||||
|
||||
@ -821,14 +783,8 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
plan.secretsManagerSeats = org.smSeats;
|
||||
}
|
||||
|
||||
let paymentMethod: [string, PaymentMethodType];
|
||||
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
const { type, token } = await this.paymentV2Component.tokenize();
|
||||
paymentMethod = [token, type];
|
||||
} else {
|
||||
paymentMethod = await this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
const paymentMethod: [string, PaymentMethodType] = [token, type];
|
||||
|
||||
const payment: PaymentInformation = {
|
||||
paymentMethod,
|
||||
@ -864,27 +820,17 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
this.buildSecretsManagerRequest(request);
|
||||
|
||||
if (this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty()) {
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
const tokenizedPaymentSource = await this.paymentV2Component.tokenize();
|
||||
const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
|
||||
updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource;
|
||||
updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
|
||||
this.taxInformation,
|
||||
);
|
||||
const tokenizedPaymentSource = await this.paymentComponent.tokenize();
|
||||
const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
|
||||
updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource;
|
||||
updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
|
||||
this.taxInformation,
|
||||
);
|
||||
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(
|
||||
this.organizationId,
|
||||
updatePaymentMethodRequest,
|
||||
);
|
||||
} else {
|
||||
const tokenResult = await this.paymentComponent.createPaymentToken();
|
||||
const paymentRequest = new PaymentRequest();
|
||||
paymentRequest.paymentToken = tokenResult[0];
|
||||
paymentRequest.paymentMethodType = tokenResult[1];
|
||||
paymentRequest.country = this.taxInformation.country;
|
||||
paymentRequest.postalCode = this.taxInformation.postalCode;
|
||||
await this.organizationApiService.updatePayment(this.organizationId, paymentRequest);
|
||||
}
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(
|
||||
this.organizationId,
|
||||
updatePaymentMethodRequest,
|
||||
);
|
||||
}
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
@ -894,10 +840,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
}
|
||||
|
||||
const result = await this.organizationApiService.upgrade(this.organizationId, request);
|
||||
if (!result.success && result.paymentIntentClientSecret != null) {
|
||||
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||
}
|
||||
await this.organizationApiService.upgrade(this.organizationId, request);
|
||||
return this.organizationId;
|
||||
}
|
||||
|
||||
@ -994,38 +937,20 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
get paymentSourceClasses() {
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
if (this.paymentSource == null) {
|
||||
if (this.paymentSource == null) {
|
||||
return [];
|
||||
}
|
||||
switch (this.paymentSource.type) {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
switch (this.paymentSource.type) {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
if (this.billing.paymentSource == null) {
|
||||
return [];
|
||||
}
|
||||
switch (this.billing.paymentSource.type) {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.Check:
|
||||
return ["bwi-money"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal text-primary"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { canAccessBillingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
|
||||
import { organizationIsUnmanaged } from "../../billing/guards/organization-is-unmanaged.guard";
|
||||
import { WebPlatformUtilsService } from "../../core/web-platform-utils.service";
|
||||
import { PaymentMethodComponent } from "../shared";
|
||||
|
||||
import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component";
|
||||
import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component";
|
||||
@ -28,21 +25,17 @@ const routes: Routes = [
|
||||
: OrganizationSubscriptionCloudComponent,
|
||||
data: { titleId: "subscription" },
|
||||
},
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: PaymentMethodComponent,
|
||||
flaggedComponent: OrganizationPaymentMethodComponent,
|
||||
featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
routeOptions: {
|
||||
path: "payment-method",
|
||||
canActivate: [
|
||||
organizationPermissionsGuard((org) => org.canEditPaymentMethods),
|
||||
organizationIsUnmanaged,
|
||||
],
|
||||
data: {
|
||||
titleId: "paymentMethod",
|
||||
},
|
||||
{
|
||||
path: "payment-method",
|
||||
component: OrganizationPaymentMethodComponent,
|
||||
canActivate: [
|
||||
organizationPermissionsGuard((org) => org.canEditPaymentMethods),
|
||||
organizationIsUnmanaged,
|
||||
],
|
||||
data: {
|
||||
titleId: "paymentMethod",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: "history",
|
||||
component: OrgBillingHistoryViewComponent,
|
||||
|
@ -433,13 +433,7 @@
|
||||
<p class="tw-text-muted tw-italic tw-mb-3 tw-block" bitTypography="body2">
|
||||
{{ paymentDesc }}
|
||||
</p>
|
||||
<app-payment
|
||||
*ngIf="!deprecateStripeSourcesAPI && (createOrganization || upgradeRequiresPaymentMethod)"
|
||||
[hideCredit]="true"
|
||||
></app-payment>
|
||||
<app-payment-v2
|
||||
*ngIf="deprecateStripeSourcesAPI && (createOrganization || upgradeRequiresPaymentMethod)"
|
||||
></app-payment-v2>
|
||||
<app-payment *ngIf="createOrganization || upgradeRequiresPaymentMethod"></app-payment>
|
||||
<app-manage-tax-information
|
||||
class="tw-my-4"
|
||||
[showTaxIdField]="showTaxIdField"
|
||||
@ -465,9 +459,6 @@
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<ng-container *ngIf="!createOrganization">
|
||||
<app-payment [showMethods]="false"></app-payment>
|
||||
</ng-container>
|
||||
</bit-section>
|
||||
<bit-section *ngIf="singleOrgPolicyBlock">
|
||||
<bit-callout type="danger" [title]="'error' | i18n">
|
||||
|
@ -36,7 +36,6 @@ import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/ta
|
||||
import { PaymentMethodType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request";
|
||||
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
|
||||
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
||||
@ -57,7 +56,6 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module";
|
||||
import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared";
|
||||
import { PaymentV2Component } from "../shared/payment/payment-v2.component";
|
||||
import { PaymentComponent } from "../shared/payment/payment.component";
|
||||
|
||||
interface OnSuccessArgs {
|
||||
@ -79,7 +77,6 @@ const Allowed2020PlansForLegacyProviders = [
|
||||
})
|
||||
export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component;
|
||||
@ViewChild(ManageTaxInformationComponent) taxComponent: ManageTaxInformationComponent;
|
||||
|
||||
@Input() organizationId?: string;
|
||||
@ -128,7 +125,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
singleOrgPolicyAppliesToActiveUser = false;
|
||||
isInTrialFlow = false;
|
||||
discount = 0;
|
||||
deprecateStripeSourcesAPI: boolean;
|
||||
|
||||
protected useLicenseUploaderComponent$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader,
|
||||
@ -189,10 +185,6 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
);
|
||||
|
||||
if (this.organizationId) {
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
@ -580,23 +572,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
protected changedCountry(): void {
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
this.paymentV2Component.showBankAccount = this.taxInformation?.country === "US";
|
||||
if (
|
||||
!this.paymentV2Component.showBankAccount &&
|
||||
this.paymentV2Component.selected === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentV2Component.select(PaymentMethodType.Card);
|
||||
}
|
||||
} else {
|
||||
this.paymentComponent.hideBank = this.taxInformation?.country !== "US";
|
||||
if (
|
||||
this.paymentComponent.hideBank &&
|
||||
this.paymentComponent.method === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
this.paymentComponent.showBankAccount = this.taxInformation?.country === "US";
|
||||
if (
|
||||
!this.paymentComponent.showBankAccount &&
|
||||
this.paymentComponent.selected === PaymentMethodType.BankAccount
|
||||
) {
|
||||
this.paymentComponent.select(PaymentMethodType.Card);
|
||||
}
|
||||
}
|
||||
|
||||
@ -751,25 +732,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
this.buildSecretsManagerRequest(request);
|
||||
|
||||
if (this.upgradeRequiresPaymentMethod) {
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
|
||||
updatePaymentMethodRequest.paymentSource = await this.paymentV2Component.tokenize();
|
||||
updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
|
||||
this.taxInformation,
|
||||
);
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(
|
||||
this.organizationId,
|
||||
updatePaymentMethodRequest,
|
||||
);
|
||||
} else {
|
||||
const [paymentToken, paymentMethodType] = await this.paymentComponent.createPaymentToken();
|
||||
const paymentRequest = new PaymentRequest();
|
||||
paymentRequest.paymentToken = paymentToken;
|
||||
paymentRequest.paymentMethodType = paymentMethodType;
|
||||
paymentRequest.country = this.taxInformation?.country;
|
||||
paymentRequest.postalCode = this.taxInformation?.postalCode;
|
||||
await this.organizationApiService.updatePayment(this.organizationId, paymentRequest);
|
||||
}
|
||||
const updatePaymentMethodRequest = new UpdatePaymentMethodRequest();
|
||||
updatePaymentMethodRequest.paymentSource = await this.paymentComponent.tokenize();
|
||||
updatePaymentMethodRequest.taxInformation = ExpandedTaxInfoUpdateRequest.From(
|
||||
this.taxInformation,
|
||||
);
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(
|
||||
this.organizationId,
|
||||
updatePaymentMethodRequest,
|
||||
);
|
||||
}
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
@ -779,10 +750,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
}
|
||||
|
||||
const result = await this.organizationApiService.upgrade(this.organizationId, request);
|
||||
if (!result.success && result.paymentIntentClientSecret != null) {
|
||||
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||
}
|
||||
await this.organizationApiService.upgrade(this.organizationId, request);
|
||||
return this.organizationId;
|
||||
}
|
||||
|
||||
@ -803,14 +771,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
if (this.selectedPlan.type === PlanType.Free) {
|
||||
request.planType = PlanType.Free;
|
||||
} else {
|
||||
let type: PaymentMethodType;
|
||||
let token: string;
|
||||
|
||||
if (this.deprecateStripeSourcesAPI) {
|
||||
({ type, token } = await this.paymentV2Component.tokenize());
|
||||
} else {
|
||||
[token, type] = await this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
|
||||
request.paymentToken = token;
|
||||
request.paymentMethodType = type;
|
||||
|
@ -25,12 +25,8 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
AdjustStorageDialogV2Component,
|
||||
AdjustStorageDialogV2ResultType,
|
||||
} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component";
|
||||
import {
|
||||
AdjustStorageDialogResult,
|
||||
openAdjustStorageDialog,
|
||||
AdjustStorageDialogComponent,
|
||||
AdjustStorageDialogResultType,
|
||||
} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component";
|
||||
import {
|
||||
OffboardingSurveyDialogResultType,
|
||||
@ -55,7 +51,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
organizationId: string;
|
||||
userOrg: Organization;
|
||||
showChangePlan = false;
|
||||
showDownloadLicense = false;
|
||||
hasBillingSyncToken: boolean;
|
||||
showAdjustSecretsManager = false;
|
||||
showSecretsManagerSubscribe = false;
|
||||
@ -70,10 +65,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
protected readonly subscriptionHiddenIcon = SubscriptionHiddenIcon;
|
||||
protected readonly teamsStarter = ProductTierType.TeamsStarter;
|
||||
|
||||
protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@ -426,36 +417,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy
|
||||
|
||||
adjustStorage = (add: boolean) => {
|
||||
return async () => {
|
||||
const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$);
|
||||
const dialogRef = AdjustStorageDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
price: this.storageGbPrice,
|
||||
cadence: this.billingInterval,
|
||||
type: add ? "Add" : "Remove",
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (deprecateStripeSourcesAPI) {
|
||||
const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, {
|
||||
data: {
|
||||
price: this.storageGbPrice,
|
||||
cadence: this.billingInterval,
|
||||
type: add ? "Add" : "Remove",
|
||||
organizationId: this.organizationId,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result === AdjustStorageDialogV2ResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
} else {
|
||||
const dialogRef = openAdjustStorageDialog(this.dialogService, {
|
||||
data: {
|
||||
storageGbPrice: this.storageGbPrice,
|
||||
add: add,
|
||||
organizationId: this.organizationId,
|
||||
interval: this.billingInterval,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustStorageDialogResult.Adjusted) {
|
||||
await this.load();
|
||||
}
|
||||
if (result === AdjustStorageDialogResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -30,9 +30,9 @@ import {
|
||||
openAddCreditDialog,
|
||||
} from "../../shared/add-credit-dialog.component";
|
||||
import {
|
||||
AdjustPaymentDialogV2Component,
|
||||
AdjustPaymentDialogV2ResultType,
|
||||
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog-v2.component";
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustPaymentDialogResultType,
|
||||
} from "../../shared/adjust-payment-dialog/adjust-payment-dialog.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "./organization-payment-method.component.html",
|
||||
@ -159,7 +159,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
};
|
||||
|
||||
protected updatePaymentMethod = async (): Promise<void> => {
|
||||
const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, {
|
||||
const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
initialPaymentMethod: this.paymentSource?.type,
|
||||
organizationId: this.organizationId,
|
||||
@ -169,13 +169,13 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result === AdjustPaymentDialogV2ResultType.Submitted) {
|
||||
if (result === AdjustPaymentDialogResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
|
||||
changePayment = async () => {
|
||||
const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, {
|
||||
const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
initialPaymentMethod: this.paymentSource?.type,
|
||||
organizationId: this.organizationId,
|
||||
@ -183,7 +183,7 @@ export class OrganizationPaymentMethodComponent implements OnDestroy {
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustPaymentDialogV2ResultType.Submitted) {
|
||||
if (result === AdjustPaymentDialogResultType.Submitted) {
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
if (this.launchPaymentModalAutomatically && !this.organization.enabled) {
|
||||
await this.syncService.fullSync(true);
|
||||
|
@ -1,29 +0,0 @@
|
||||
<bit-dialog dialogSize="large" [title]="dialogHeader">
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment-v2
|
||||
[showAccountCredit]="false"
|
||||
[showBankAccount]="!!organizationId"
|
||||
[initialPaymentMethod]="initialPaymentMethod"
|
||||
></app-payment-v2>
|
||||
<app-manage-tax-information
|
||||
*ngIf="taxInformation"
|
||||
[showTaxIdField]="showTaxIdField"
|
||||
[startWith]="taxInformation"
|
||||
(taxInformationChanged)="taxInformationChanged($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [bitAction]="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
@ -1,179 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
|
||||
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentV2Component } from "../payment/payment-v2.component";
|
||||
|
||||
export interface AdjustPaymentDialogV2Params {
|
||||
initialPaymentMethod?: PaymentMethodType;
|
||||
organizationId?: string;
|
||||
productTier?: ProductTierType;
|
||||
}
|
||||
|
||||
export enum AdjustPaymentDialogV2ResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./adjust-payment-dialog-v2.component.html",
|
||||
})
|
||||
export class AdjustPaymentDialogV2Component implements OnInit {
|
||||
@ViewChild(PaymentV2Component) paymentComponent: PaymentV2Component;
|
||||
@ViewChild(forwardRef(() => ManageTaxInformationComponent))
|
||||
taxInfoComponent: ManageTaxInformationComponent;
|
||||
|
||||
protected readonly PaymentMethodType = PaymentMethodType;
|
||||
protected readonly ResultType = AdjustPaymentDialogV2ResultType;
|
||||
|
||||
protected dialogHeader: string;
|
||||
protected initialPaymentMethod: PaymentMethodType;
|
||||
protected organizationId?: string;
|
||||
protected productTier?: ProductTierType;
|
||||
|
||||
protected taxInformation: TaxInformation;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogV2Params,
|
||||
private dialogRef: DialogRef<AdjustPaymentDialogV2ResultType>,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod";
|
||||
this.dialogHeader = this.i18nService.t(key);
|
||||
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
|
||||
this.organizationId = this.dialogParams.organizationId;
|
||||
this.productTier = this.dialogParams.productTier;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.organizationId) {
|
||||
this.organizationApiService
|
||||
.getTaxInfo(this.organizationId)
|
||||
.then((response: TaxInfoResponse) => {
|
||||
this.taxInformation = TaxInformation.from(response);
|
||||
})
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
});
|
||||
} else {
|
||||
this.apiService
|
||||
.getTaxInfo()
|
||||
.then((response: TaxInfoResponse) => {
|
||||
this.taxInformation = TaxInformation.from(response);
|
||||
})
|
||||
.catch(() => {
|
||||
this.taxInformation = new TaxInformation();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
taxInformationChanged(event: TaxInformation) {
|
||||
this.taxInformation = event;
|
||||
if (event.country === "US") {
|
||||
this.paymentComponent.showBankAccount = !!this.organizationId;
|
||||
} else {
|
||||
this.paymentComponent.showBankAccount = false;
|
||||
if (this.paymentComponent.selected === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.select(PaymentMethodType.Card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (!this.taxInfoComponent.validate()) {
|
||||
this.taxInfoComponent.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.organizationId) {
|
||||
await this.updatePremiumUserPaymentMethod();
|
||||
} else {
|
||||
await this.updateOrganizationPaymentMethod();
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedPaymentMethod"),
|
||||
});
|
||||
|
||||
this.dialogRef.close(AdjustPaymentDialogV2ResultType.Submitted);
|
||||
} catch (error) {
|
||||
const msg = typeof error == "object" ? error.message : error;
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t(msg) || msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private updateOrganizationPaymentMethod = async () => {
|
||||
const paymentSource = await this.paymentComponent.tokenize();
|
||||
|
||||
const request = new UpdatePaymentMethodRequest();
|
||||
request.paymentSource = paymentSource;
|
||||
request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation);
|
||||
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request);
|
||||
};
|
||||
|
||||
protected get showTaxIdField(): boolean {
|
||||
if (!this.organizationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Free:
|
||||
case ProductTierType.Families:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private updatePremiumUserPaymentMethod = async () => {
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
|
||||
const request = new PaymentRequest();
|
||||
request.paymentMethodType = type;
|
||||
request.paymentToken = token;
|
||||
request.country = this.taxInformation.country;
|
||||
request.postalCode = this.taxInformation.postalCode;
|
||||
request.taxId = this.taxInformation.taxId;
|
||||
request.state = this.taxInformation.state;
|
||||
request.line1 = this.taxInformation.line1;
|
||||
request.line2 = this.taxInformation.line2;
|
||||
request.city = this.taxInformation.city;
|
||||
request.state = this.taxInformation.state;
|
||||
await this.apiService.postAccountPayment(request);
|
||||
};
|
||||
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AdjustPaymentDialogV2Params>,
|
||||
) =>
|
||||
dialogService.open<AdjustPaymentDialogV2ResultType>(
|
||||
AdjustPaymentDialogV2Component,
|
||||
dialogConfig,
|
||||
);
|
||||
}
|
@ -1,30 +1,29 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog
|
||||
dialogSize="large"
|
||||
[title]="(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n"
|
||||
>
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
|
||||
<app-manage-tax-information
|
||||
*ngIf="taxInformation"
|
||||
[showTaxIdField]="showTaxIdField"
|
||||
[startWith]="taxInformation"
|
||||
(taxInformationChanged)="taxInformationChanged($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="DialogResult.Cancelled"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<bit-dialog dialogSize="large" [title]="dialogHeader">
|
||||
<ng-container bitDialogContent>
|
||||
<app-payment
|
||||
[showAccountCredit]="false"
|
||||
[showBankAccount]="!!organizationId"
|
||||
[initialPaymentMethod]="initialPaymentMethod"
|
||||
></app-payment>
|
||||
<app-manage-tax-information
|
||||
*ngIf="taxInformation"
|
||||
[showTaxIdField]="showTaxIdField"
|
||||
[startWith]="taxInformation"
|
||||
(taxInformationChanged)="taxInformationChanged($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [bitAction]="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
@ -1,59 +1,66 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormGroup } from "@angular/forms";
|
||||
import { Component, forwardRef, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
|
||||
import { ManageTaxInformationComponent } from "@bitwarden/angular/billing/components";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PaymentMethodType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request";
|
||||
import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request";
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent } from "../payment/payment.component";
|
||||
|
||||
export interface AdjustPaymentDialogData {
|
||||
organizationId: string;
|
||||
currentType: PaymentMethodType;
|
||||
export interface AdjustPaymentDialogParams {
|
||||
initialPaymentMethod?: PaymentMethodType;
|
||||
organizationId?: string;
|
||||
productTier?: ProductTierType;
|
||||
}
|
||||
|
||||
export enum AdjustPaymentDialogResult {
|
||||
Adjusted = "adjusted",
|
||||
Cancelled = "cancelled",
|
||||
export enum AdjustPaymentDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "adjust-payment-dialog.component.html",
|
||||
templateUrl: "./adjust-payment-dialog.component.html",
|
||||
})
|
||||
export class AdjustPaymentDialogComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
@ViewChild(ManageTaxInformationComponent) taxInfoComponent: ManageTaxInformationComponent;
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(forwardRef(() => ManageTaxInformationComponent))
|
||||
taxInfoComponent: ManageTaxInformationComponent;
|
||||
|
||||
organizationId: string;
|
||||
currentType: PaymentMethodType;
|
||||
paymentMethodType = PaymentMethodType;
|
||||
protected readonly PaymentMethodType = PaymentMethodType;
|
||||
protected readonly ResultType = AdjustPaymentDialogResultType;
|
||||
|
||||
protected DialogResult = AdjustPaymentDialogResult;
|
||||
protected formGroup = new FormGroup({});
|
||||
protected dialogHeader: string;
|
||||
protected initialPaymentMethod: PaymentMethodType;
|
||||
protected organizationId?: string;
|
||||
protected productTier?: ProductTierType;
|
||||
|
||||
protected taxInformation: TaxInformation;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: AdjustPaymentDialogData,
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: AdjustPaymentDialogParams,
|
||||
private dialogRef: DialogRef<AdjustPaymentDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.organizationId = data.organizationId;
|
||||
this.currentType = data.currentType;
|
||||
const key = this.dialogParams.initialPaymentMethod ? "changePaymentMethod" : "addPaymentMethod";
|
||||
this.dialogHeader = this.i18nService.t(key);
|
||||
this.initialPaymentMethod = this.dialogParams.initialPaymentMethod ?? PaymentMethodType.Card;
|
||||
this.organizationId = this.dialogParams.organizationId;
|
||||
this.productTier = this.dialogParams.productTier;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -78,65 +85,92 @@ export class AdjustPaymentDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if (!this.taxInfoComponent?.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new PaymentRequest();
|
||||
const response = this.paymentComponent.createPaymentToken().then((result) => {
|
||||
request.paymentToken = result[0];
|
||||
request.paymentMethodType = result[1];
|
||||
request.postalCode = this.taxInformation?.postalCode;
|
||||
request.country = this.taxInformation?.country;
|
||||
request.taxId = this.taxInformation?.taxId;
|
||||
if (this.organizationId == null) {
|
||||
return this.apiService.postAccountPayment(request);
|
||||
} else {
|
||||
request.taxId = this.taxInformation?.taxId;
|
||||
request.state = this.taxInformation?.state;
|
||||
request.line1 = this.taxInformation?.line1;
|
||||
request.line2 = this.taxInformation?.line2;
|
||||
request.city = this.taxInformation?.city;
|
||||
request.state = this.taxInformation?.state;
|
||||
return this.organizationApiService.updatePayment(this.organizationId, request);
|
||||
}
|
||||
});
|
||||
await response;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedPaymentMethod"),
|
||||
});
|
||||
this.dialogRef.close(AdjustPaymentDialogResult.Adjusted);
|
||||
};
|
||||
|
||||
taxInformationChanged(event: TaxInformation) {
|
||||
this.taxInformation = event;
|
||||
if (event.country === "US") {
|
||||
this.paymentComponent.hideBank = !this.organizationId;
|
||||
this.paymentComponent.showBankAccount = !!this.organizationId;
|
||||
} else {
|
||||
this.paymentComponent.hideBank = true;
|
||||
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
this.paymentComponent.showBankAccount = false;
|
||||
if (this.paymentComponent.selected === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.select(PaymentMethodType.Card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected get showTaxIdField(): boolean {
|
||||
return !!this.organizationId;
|
||||
}
|
||||
}
|
||||
submit = async (): Promise<void> => {
|
||||
if (!this.taxInfoComponent.validate()) {
|
||||
this.taxInfoComponent.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open a AdjustPaymentDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openAdjustPaymentDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AdjustPaymentDialogData>,
|
||||
) {
|
||||
return dialogService.open<AdjustPaymentDialogResult>(AdjustPaymentDialogComponent, config);
|
||||
try {
|
||||
if (!this.organizationId) {
|
||||
await this.updatePremiumUserPaymentMethod();
|
||||
} else {
|
||||
await this.updateOrganizationPaymentMethod();
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedPaymentMethod"),
|
||||
});
|
||||
|
||||
this.dialogRef.close(AdjustPaymentDialogResultType.Submitted);
|
||||
} catch (error) {
|
||||
const msg = typeof error == "object" ? error.message : error;
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t(msg) || msg,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private updateOrganizationPaymentMethod = async () => {
|
||||
const paymentSource = await this.paymentComponent.tokenize();
|
||||
|
||||
const request = new UpdatePaymentMethodRequest();
|
||||
request.paymentSource = paymentSource;
|
||||
request.taxInformation = ExpandedTaxInfoUpdateRequest.From(this.taxInformation);
|
||||
|
||||
await this.billingApiService.updateOrganizationPaymentMethod(this.organizationId, request);
|
||||
};
|
||||
|
||||
protected get showTaxIdField(): boolean {
|
||||
if (!this.organizationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.productTier) {
|
||||
case ProductTierType.Free:
|
||||
case ProductTierType.Families:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private updatePremiumUserPaymentMethod = async () => {
|
||||
const { type, token } = await this.paymentComponent.tokenize();
|
||||
|
||||
const request = new PaymentRequest();
|
||||
request.paymentMethodType = type;
|
||||
request.paymentToken = token;
|
||||
request.country = this.taxInformation.country;
|
||||
request.postalCode = this.taxInformation.postalCode;
|
||||
request.taxId = this.taxInformation.taxId;
|
||||
request.state = this.taxInformation.state;
|
||||
request.line1 = this.taxInformation.line1;
|
||||
request.line2 = this.taxInformation.line2;
|
||||
request.city = this.taxInformation.city;
|
||||
request.state = this.taxInformation.state;
|
||||
await this.apiService.postAccountPayment(request);
|
||||
};
|
||||
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AdjustPaymentDialogParams>,
|
||||
) =>
|
||||
dialogService.open<AdjustPaymentDialogResultType>(AdjustPaymentDialogComponent, dialogConfig);
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [title]="title">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ body }}</p>
|
||||
<div class="tw-grid two-grid-cols-12">
|
||||
<bit-form-field class="tw-col-span-7">
|
||||
<bit-label>{{ storageFieldLabel }}</bit-label>
|
||||
<input bitInput type="number" formControlName="storage" />
|
||||
<bit-hint *ngIf="dialogParams.type === 'Add'">
|
||||
<!-- Total: 10 GB × $0.50 = $5.00 /month -->
|
||||
<strong>{{ "total" | i18n }}</strong>
|
||||
{{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} =
|
||||
{{ this.price * this.formGroup.value.storage | currency: "$" }} /
|
||||
{{ this.cadence | i18n }}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
@ -1,106 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { StorageRequest } from "@bitwarden/common/models/request/storage.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
export interface AdjustStorageDialogV2Params {
|
||||
price: number;
|
||||
cadence: "month" | "year";
|
||||
type: "Add" | "Remove";
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
export enum AdjustStorageDialogV2ResultType {
|
||||
Submitted = "submitted",
|
||||
Closed = "closed",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./adjust-storage-dialog-v2.component.html",
|
||||
})
|
||||
export class AdjustStorageDialogV2Component {
|
||||
protected formGroup = new FormGroup({
|
||||
storage: new FormControl<number>(0, [
|
||||
Validators.required,
|
||||
Validators.min(0),
|
||||
Validators.max(99),
|
||||
]),
|
||||
});
|
||||
|
||||
protected organizationId?: string;
|
||||
protected price: number;
|
||||
protected cadence: "month" | "year";
|
||||
|
||||
protected title: string;
|
||||
protected body: string;
|
||||
protected storageFieldLabel: string;
|
||||
|
||||
protected ResultType = AdjustStorageDialogV2ResultType;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogV2Params,
|
||||
private dialogRef: DialogRef<AdjustStorageDialogV2ResultType>,
|
||||
private i18nService: I18nService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.price = this.dialogParams.price;
|
||||
this.cadence = this.dialogParams.cadence;
|
||||
this.organizationId = this.dialogParams.organizationId;
|
||||
switch (this.dialogParams.type) {
|
||||
case "Add":
|
||||
this.title = this.i18nService.t("addStorage");
|
||||
this.body = this.i18nService.t("storageAddNote");
|
||||
this.storageFieldLabel = this.i18nService.t("gbStorageAdd");
|
||||
break;
|
||||
case "Remove":
|
||||
this.title = this.i18nService.t("removeStorage");
|
||||
this.body = this.i18nService.t("storageRemoveNote");
|
||||
this.storageFieldLabel = this.i18nService.t("gbStorageRemove");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const request = new StorageRequest();
|
||||
switch (this.dialogParams.type) {
|
||||
case "Add":
|
||||
request.storageGbAdjustment = this.formGroup.value.storage;
|
||||
break;
|
||||
case "Remove":
|
||||
request.storageGbAdjustment = this.formGroup.value.storage * -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.organizationId) {
|
||||
await this.organizationApiService.updateStorage(this.organizationId, request);
|
||||
} else {
|
||||
await this.apiService.postAccountStorage(request);
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
|
||||
});
|
||||
|
||||
this.dialogRef.close(this.ResultType.Submitted);
|
||||
};
|
||||
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AdjustStorageDialogV2Params>,
|
||||
) =>
|
||||
dialogService.open<AdjustStorageDialogV2ResultType>(
|
||||
AdjustStorageDialogV2Component,
|
||||
dialogConfig,
|
||||
);
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="default" [title]="(add ? 'addStorage' : 'removeStorage') | i18n">
|
||||
<bit-dialog [title]="title">
|
||||
<ng-container bitDialogContent>
|
||||
<p bitTypography="body1">{{ (add ? "storageAddNote" : "storageRemoveNote") | i18n }}</p>
|
||||
<p bitTypography="body1">{{ body }}</p>
|
||||
<div class="tw-grid tw-grid-cols-12">
|
||||
<bit-form-field class="tw-col-span-7">
|
||||
<bit-label>{{ (add ? "gbStorageAdd" : "gbStorageRemove") | i18n }}</bit-label>
|
||||
<input bitInput type="number" formControlName="storageAdjustment" />
|
||||
<bit-hint *ngIf="add">
|
||||
<strong>{{ "total" | i18n }}:</strong>
|
||||
{{ formGroup.get("storageAdjustment").value || 0 }} GB ×
|
||||
{{ storageGbPrice | currency: "$" }} = {{ adjustedStorageTotal | currency: "$" }} /{{
|
||||
interval | i18n
|
||||
}}
|
||||
<bit-label>{{ storageFieldLabel }}</bit-label>
|
||||
<input bitInput type="number" formControlName="storage" />
|
||||
<bit-hint *ngIf="dialogParams.type === 'Add'">
|
||||
<!-- Total: 10 GB × $0.50 = $5.00 /month -->
|
||||
<strong>{{ "total" | i18n }}</strong>
|
||||
{{ this.formGroup.value.storage }} GB × {{ this.price | currency: "$" }} =
|
||||
{{ this.price * this.formGroup.value.storage | currency: "$" }} /
|
||||
{{ this.cadence | i18n }}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
@ -25,11 +25,10 @@
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[bitDialogClose]="DialogResult.Cancelled"
|
||||
[bitDialogClose]="ResultType.Closed"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
<app-payment [showMethods]="false"></app-payment>
|
||||
|
@ -1,132 +1,103 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, ViewChild } from "@angular/core";
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PaymentResponse } from "@bitwarden/common/billing/models/response/payment.response";
|
||||
import { StorageRequest } from "@bitwarden/common/models/request/storage.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { PaymentComponent } from "../payment/payment.component";
|
||||
|
||||
export interface AdjustStorageDialogData {
|
||||
storageGbPrice: number;
|
||||
add: boolean;
|
||||
export interface AdjustStorageDialogParams {
|
||||
price: number;
|
||||
cadence: "month" | "year";
|
||||
type: "Add" | "Remove";
|
||||
organizationId?: string;
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export enum AdjustStorageDialogResult {
|
||||
Adjusted = "adjusted",
|
||||
Cancelled = "cancelled",
|
||||
export enum AdjustStorageDialogResultType {
|
||||
Submitted = "submitted",
|
||||
Closed = "closed",
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "adjust-storage-dialog.component.html",
|
||||
templateUrl: "./adjust-storage-dialog.component.html",
|
||||
})
|
||||
export class AdjustStorageDialogComponent {
|
||||
storageGbPrice: number;
|
||||
add: boolean;
|
||||
organizationId: string;
|
||||
interval: string;
|
||||
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
protected DialogResult = AdjustStorageDialogResult;
|
||||
protected formGroup = new FormGroup({
|
||||
storageAdjustment: new FormControl(0, [
|
||||
storage: new FormControl<number>(0, [
|
||||
Validators.required,
|
||||
Validators.min(0),
|
||||
Validators.max(99),
|
||||
]),
|
||||
});
|
||||
|
||||
protected organizationId?: string;
|
||||
protected price: number;
|
||||
protected cadence: "month" | "year";
|
||||
|
||||
protected title: string;
|
||||
protected body: string;
|
||||
protected storageFieldLabel: string;
|
||||
|
||||
protected ResultType = AdjustStorageDialogResultType;
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) protected data: AdjustStorageDialogData,
|
||||
private apiService: ApiService,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: AdjustStorageDialogParams,
|
||||
private dialogRef: DialogRef<AdjustStorageDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.storageGbPrice = data.storageGbPrice;
|
||||
this.add = data.add;
|
||||
this.organizationId = data.organizationId;
|
||||
this.interval = data.interval || "year";
|
||||
this.price = this.dialogParams.price;
|
||||
this.cadence = this.dialogParams.cadence;
|
||||
this.organizationId = this.dialogParams.organizationId;
|
||||
switch (this.dialogParams.type) {
|
||||
case "Add":
|
||||
this.title = this.i18nService.t("addStorage");
|
||||
this.body = this.i18nService.t("storageAddNote");
|
||||
this.storageFieldLabel = this.i18nService.t("gbStorageAdd");
|
||||
break;
|
||||
case "Remove":
|
||||
this.title = this.i18nService.t("removeStorage");
|
||||
this.body = this.i18nService.t("storageRemoveNote");
|
||||
this.storageFieldLabel = this.i18nService.t("gbStorageRemove");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const request = new StorageRequest();
|
||||
request.storageGbAdjustment = this.formGroup.value.storageAdjustment;
|
||||
if (!this.add) {
|
||||
request.storageGbAdjustment *= -1;
|
||||
switch (this.dialogParams.type) {
|
||||
case "Add":
|
||||
request.storageGbAdjustment = this.formGroup.value.storage;
|
||||
break;
|
||||
case "Remove":
|
||||
request.storageGbAdjustment = this.formGroup.value.storage * -1;
|
||||
break;
|
||||
}
|
||||
|
||||
let paymentFailed = false;
|
||||
const action = async () => {
|
||||
let response: Promise<PaymentResponse>;
|
||||
if (this.organizationId == null) {
|
||||
response = this.apiService.postAccountStorage(request);
|
||||
} else {
|
||||
response = this.organizationApiService.updateStorage(this.organizationId, request);
|
||||
}
|
||||
const result = await response;
|
||||
if (result != null && result.paymentIntentClientSecret != null) {
|
||||
try {
|
||||
await this.paymentComponent.handleStripeCardPayment(
|
||||
result.paymentIntentClientSecret,
|
||||
null,
|
||||
);
|
||||
} catch {
|
||||
paymentFailed = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
await action();
|
||||
this.dialogRef.close(AdjustStorageDialogResult.Adjusted);
|
||||
if (paymentFailed) {
|
||||
this.toastService.showToast({
|
||||
variant: "warning",
|
||||
title: null,
|
||||
message: this.i18nService.t("couldNotChargeCardPayInvoice"),
|
||||
timeout: 10000,
|
||||
});
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["../billing"], { relativeTo: this.activatedRoute });
|
||||
if (this.organizationId) {
|
||||
await this.organizationApiService.updateStorage(this.organizationId, request);
|
||||
} else {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
|
||||
});
|
||||
await this.apiService.postAccountStorage(request);
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("adjustedStorage", request.storageGbAdjustment.toString()),
|
||||
});
|
||||
|
||||
this.dialogRef.close(this.ResultType.Submitted);
|
||||
};
|
||||
|
||||
get adjustedStorageTotal(): number {
|
||||
return this.storageGbPrice * this.formGroup.value.storageAdjustment;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open an AdjustStorageDialog
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param config Configuration for the dialog
|
||||
*/
|
||||
export function openAdjustStorageDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<AdjustStorageDialogData>,
|
||||
) {
|
||||
return dialogService.open<AdjustStorageDialogResult>(AdjustStorageDialogComponent, config);
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<AdjustStorageDialogParams>,
|
||||
) =>
|
||||
dialogService.open<AdjustStorageDialogResultType>(AdjustStorageDialogComponent, dialogConfig);
|
||||
}
|
||||
|
@ -6,13 +6,10 @@ import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { AddCreditDialogComponent } from "./add-credit-dialog.component";
|
||||
import { AdjustPaymentDialogV2Component } from "./adjust-payment-dialog/adjust-payment-dialog-v2.component";
|
||||
import { AdjustPaymentDialogComponent } from "./adjust-payment-dialog/adjust-payment-dialog.component";
|
||||
import { AdjustStorageDialogV2Component } from "./adjust-storage-dialog/adjust-storage-dialog-v2.component";
|
||||
import { AdjustStorageDialogComponent } from "./adjust-storage-dialog/adjust-storage-dialog.component";
|
||||
import { BillingHistoryComponent } from "./billing-history.component";
|
||||
import { OffboardingSurveyComponent } from "./offboarding-survey.component";
|
||||
import { PaymentV2Component } from "./payment/payment-v2.component";
|
||||
import { PaymentComponent } from "./payment/payment.component";
|
||||
import { PaymentMethodComponent } from "./payment-method.component";
|
||||
import { IndividualSelfHostingLicenseUploaderComponent } from "./self-hosting-license-uploader/individual-self-hosting-license-uploader.component";
|
||||
@ -26,40 +23,35 @@ import { VerifyBankAccountComponent } from "./verify-bank-account/verify-bank-ac
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
PaymentComponent,
|
||||
TaxInfoComponent,
|
||||
HeaderModule,
|
||||
BannerModule,
|
||||
PaymentV2Component,
|
||||
PaymentComponent,
|
||||
VerifyBankAccountComponent,
|
||||
],
|
||||
declarations: [
|
||||
AddCreditDialogComponent,
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustStorageDialogComponent,
|
||||
BillingHistoryComponent,
|
||||
PaymentMethodComponent,
|
||||
SecretsManagerSubscribeComponent,
|
||||
UpdateLicenseComponent,
|
||||
UpdateLicenseDialogComponent,
|
||||
OffboardingSurveyComponent,
|
||||
AdjustPaymentDialogV2Component,
|
||||
AdjustStorageDialogV2Component,
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustStorageDialogComponent,
|
||||
IndividualSelfHostingLicenseUploaderComponent,
|
||||
OrganizationSelfHostingLicenseUploaderComponent,
|
||||
],
|
||||
exports: [
|
||||
SharedModule,
|
||||
PaymentComponent,
|
||||
TaxInfoComponent,
|
||||
AdjustStorageDialogComponent,
|
||||
BillingHistoryComponent,
|
||||
SecretsManagerSubscribeComponent,
|
||||
UpdateLicenseComponent,
|
||||
UpdateLicenseDialogComponent,
|
||||
OffboardingSurveyComponent,
|
||||
VerifyBankAccountComponent,
|
||||
PaymentV2Component,
|
||||
PaymentComponent,
|
||||
IndividualSelfHostingLicenseUploaderComponent,
|
||||
OrganizationSelfHostingLicenseUploaderComponent,
|
||||
],
|
||||
|
@ -1,5 +1,4 @@
|
||||
export * from "./billing-shared.module";
|
||||
export * from "./payment-method.component";
|
||||
export * from "./payment/payment.component";
|
||||
export * from "./sm-subscribe.component";
|
||||
export * from "./tax-info.component";
|
||||
|
@ -29,8 +29,8 @@ import { TrialFlowService } from "../services/trial-flow.service";
|
||||
|
||||
import { AddCreditDialogResult, openAddCreditDialog } from "./add-credit-dialog.component";
|
||||
import {
|
||||
AdjustPaymentDialogResult,
|
||||
openAdjustPaymentDialog,
|
||||
AdjustPaymentDialogComponent,
|
||||
AdjustPaymentDialogResultType,
|
||||
} from "./adjust-payment-dialog/adjust-payment-dialog.component";
|
||||
|
||||
@Component({
|
||||
@ -170,14 +170,16 @@ export class PaymentMethodComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
changePayment = async () => {
|
||||
const dialogRef = openAdjustPaymentDialog(this.dialogService, {
|
||||
const dialogRef = AdjustPaymentDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organizationId,
|
||||
currentType: this.paymentSource !== null ? this.paymentSource.type : null,
|
||||
initialPaymentMethod: this.paymentSource !== null ? this.paymentSource.type : null,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === AdjustPaymentDialogResult.Adjusted) {
|
||||
|
||||
if (result === AdjustPaymentDialogResultType.Submitted) {
|
||||
this.location.replaceState(this.location.path(), "", {});
|
||||
if (this.launchPaymentModalAutomatically && !this.organization.enabled) {
|
||||
await this.syncService.fullSync(true);
|
||||
|
@ -15,12 +15,12 @@ import { SharedModule } from "../../../shared";
|
||||
* the `ExtensionRefresh` flag is set.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-payment-label-v2",
|
||||
templateUrl: "./payment-label-v2.component.html",
|
||||
selector: "app-payment-label",
|
||||
templateUrl: "./payment-label.component.html",
|
||||
standalone: true,
|
||||
imports: [FormFieldModule, SharedModule],
|
||||
})
|
||||
export class PaymentLabelV2 implements OnInit {
|
||||
export class PaymentLabelComponent implements OnInit {
|
||||
/** `id` of the associated input */
|
||||
@Input({ required: true }) for: string;
|
||||
/** Displays required text on the label */
|
@ -1,152 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-mb-4 tw-text-lg">
|
||||
<bit-radio-group formControlName="paymentMethod">
|
||||
<bit-radio-button id="card-payment-method" [value]="PaymentMethodType.Card">
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>
|
||||
{{ "creditCard" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
id="bank-payment-method"
|
||||
[value]="PaymentMethodType.BankAccount"
|
||||
*ngIf="showBankAccount"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i>
|
||||
{{ "bankAccount" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
id="paypal-payment-method"
|
||||
[value]="PaymentMethodType.PayPal"
|
||||
*ngIf="showPayPal"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i>
|
||||
{{ "payPal" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
id="credit-payment-method"
|
||||
[value]="PaymentMethodType.Credit"
|
||||
*ngIf="showAccountCredit"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i>
|
||||
{{ "accountCredit" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<!-- Card -->
|
||||
<ng-container *ngIf="usingCard">
|
||||
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4">
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label-v2 for="stripe-card-number" required>
|
||||
{{ "number" | i18n }}
|
||||
</app-payment-label-v2>
|
||||
<div id="stripe-card-number" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="tw-col-span-1 tw-flex tw-items-end">
|
||||
<img
|
||||
src="../../../images/cards.png"
|
||||
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
|
||||
class="tw-max-w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label-v2 for="stripe-card-expiry" required>
|
||||
{{ "expiration" | i18n }}
|
||||
</app-payment-label-v2>
|
||||
<div id="stripe-card-expiry" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label-v2 for="stripe-card-cvc" required>
|
||||
{{ "securityCodeSlashCVV" | i18n }}
|
||||
<a
|
||||
href="https://www.cvvnumber.com/cvv.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
class="hover:tw-no-underline"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</app-payment-label-v2>
|
||||
<div id="stripe-card-cvc" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Bank Account -->
|
||||
<ng-container *ngIf="showBankAccount && usingBankAccount">
|
||||
<bit-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
|
||||
{{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4" formGroupName="bankInformation">
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "routingNumber" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
id="routingNumber"
|
||||
type="text"
|
||||
formControlName="routingNumber"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "accountNumber" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
id="accountNumber"
|
||||
type="text"
|
||||
formControlName="accountNumber"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "accountHolderName" | i18n }}</bit-label>
|
||||
<input
|
||||
id="accountHolderName"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="accountHolderName"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "bankAccountType" | i18n }}</bit-label>
|
||||
<bit-select id="accountHolderType" formControlName="accountHolderType" required>
|
||||
<bit-option [value]="''" label="-- {{ 'select' | i18n }} --"></bit-option>
|
||||
<bit-option
|
||||
[value]="'company'"
|
||||
label="{{ 'bankAccountTypeCompany' | i18n }}"
|
||||
></bit-option>
|
||||
<bit-option
|
||||
[value]="'individual'"
|
||||
label="{{ 'bankAccountTypeIndividual' | i18n }}"
|
||||
></bit-option>
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- PayPal -->
|
||||
<ng-container *ngIf="showPayPal && usingPayPal">
|
||||
<div class="tw-mb-3">
|
||||
<div id="braintree-container" class="tw-mb-1 tw-content-center"></div>
|
||||
<small class="tw-text-muted">{{ "paypalClickSubmit" | i18n }}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- Account Credit -->
|
||||
<ng-container *ngIf="showAccountCredit && usingAccountCredit">
|
||||
<app-callout type="info">
|
||||
{{ "makeSureEnoughCredit" | i18n }}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
<button *ngIf="!!onSubmit" bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
@ -1,205 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Subject } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { BillingServicesModule, BraintreeService, StripeService } from "../../services";
|
||||
|
||||
import { PaymentLabelV2 } from "./payment-label-v2.component";
|
||||
|
||||
/**
|
||||
* Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and,
|
||||
* optionally, submit it using the {@link onSubmit} function if it is provided.
|
||||
*
|
||||
* This component is meant to replace the existing {@link PaymentComponent} which is using the deprecated Stripe Sources API.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-payment-v2",
|
||||
templateUrl: "./payment-v2.component.html",
|
||||
standalone: true,
|
||||
imports: [BillingServicesModule, SharedModule, PaymentLabelV2],
|
||||
})
|
||||
export class PaymentV2Component implements OnInit, OnDestroy {
|
||||
/** Show account credit as a payment option. */
|
||||
@Input() showAccountCredit: boolean = true;
|
||||
/** Show bank account as a payment option. */
|
||||
@Input() showBankAccount: boolean = true;
|
||||
/** Show PayPal as a payment option. */
|
||||
@Input() showPayPal: boolean = true;
|
||||
|
||||
/** The payment method selected by default when the component renders. */
|
||||
@Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card;
|
||||
/** If provided, will be invoked with the tokenized payment source during form submission. */
|
||||
@Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise<void>;
|
||||
|
||||
@Output() submitted = new EventEmitter<PaymentMethodType>();
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
paymentMethod: new FormControl<PaymentMethodType>(null),
|
||||
bankInformation: new FormGroup({
|
||||
routingNumber: new FormControl<string>("", [Validators.required]),
|
||||
accountNumber: new FormControl<string>("", [Validators.required]),
|
||||
accountHolderName: new FormControl<string>("", [Validators.required]),
|
||||
accountHolderType: new FormControl<string>("", [Validators.required]),
|
||||
}),
|
||||
});
|
||||
|
||||
protected PaymentMethodType = PaymentMethodType;
|
||||
|
||||
constructor(
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private braintreeService: BraintreeService,
|
||||
private stripeService: StripeService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod);
|
||||
|
||||
this.stripeService.loadStripe(
|
||||
{
|
||||
cardNumber: "#stripe-card-number",
|
||||
cardExpiry: "#stripe-card-expiry",
|
||||
cardCvc: "#stripe-card-cvc",
|
||||
},
|
||||
this.initialPaymentMethod === PaymentMethodType.Card,
|
||||
);
|
||||
|
||||
if (this.showPayPal) {
|
||||
this.braintreeService.loadBraintree(
|
||||
"#braintree-container",
|
||||
this.initialPaymentMethod === PaymentMethodType.PayPal,
|
||||
);
|
||||
}
|
||||
|
||||
this.formGroup
|
||||
.get("paymentMethod")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((type) => {
|
||||
this.onPaymentMethodChange(type);
|
||||
});
|
||||
}
|
||||
|
||||
/** Programmatically select the provided payment method. */
|
||||
select = (paymentMethod: PaymentMethodType) => {
|
||||
this.formGroup.get("paymentMethod").patchValue(paymentMethod);
|
||||
};
|
||||
|
||||
protected submit = async () => {
|
||||
const { type, token } = await this.tokenize();
|
||||
await this.onSubmit?.({ type, token });
|
||||
this.submitted.emit(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tokenize the payment method information entered by the user against one of our payment providers.
|
||||
*
|
||||
* - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup}
|
||||
* - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup}
|
||||
* - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod}
|
||||
* */
|
||||
async tokenize(): Promise<{ type: PaymentMethodType; token: string }> {
|
||||
const type = this.selected;
|
||||
|
||||
if (this.usingStripe) {
|
||||
const clientSecret = await this.billingApiService.createSetupIntent(type);
|
||||
|
||||
if (this.usingBankAccount) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.valid) {
|
||||
const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, {
|
||||
accountHolderName: this.formGroup.value.bankInformation.accountHolderName,
|
||||
routingNumber: this.formGroup.value.bankInformation.routingNumber,
|
||||
accountNumber: this.formGroup.value.bankInformation.accountNumber,
|
||||
accountHolderType: this.formGroup.value.bankInformation.accountHolderType,
|
||||
});
|
||||
return {
|
||||
type,
|
||||
token,
|
||||
};
|
||||
} else {
|
||||
throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again.";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.usingCard) {
|
||||
const token = await this.stripeService.setupCardPaymentMethod(clientSecret);
|
||||
return {
|
||||
type,
|
||||
token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.usingPayPal) {
|
||||
const token = await this.braintreeService.requestPaymentMethod();
|
||||
return {
|
||||
type,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.usingAccountCredit) {
|
||||
return {
|
||||
type: PaymentMethodType.Credit,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.stripeService.unloadStripe();
|
||||
if (this.showPayPal) {
|
||||
this.braintreeService.unloadBraintree();
|
||||
}
|
||||
}
|
||||
|
||||
private onPaymentMethodChange(type: PaymentMethodType): void {
|
||||
switch (type) {
|
||||
case PaymentMethodType.Card: {
|
||||
this.stripeService.mountElements();
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal: {
|
||||
this.braintreeService.createDropin();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get selected(): PaymentMethodType {
|
||||
return this.formGroup.value.paymentMethod;
|
||||
}
|
||||
|
||||
protected get usingAccountCredit(): boolean {
|
||||
return this.selected === PaymentMethodType.Credit;
|
||||
}
|
||||
|
||||
protected get usingBankAccount(): boolean {
|
||||
return this.selected === PaymentMethodType.BankAccount;
|
||||
}
|
||||
|
||||
protected get usingCard(): boolean {
|
||||
return this.selected === PaymentMethodType.Card;
|
||||
}
|
||||
|
||||
protected get usingPayPal(): boolean {
|
||||
return this.selected === PaymentMethodType.PayPal;
|
||||
}
|
||||
|
||||
private get usingStripe(): boolean {
|
||||
return this.usingBankAccount || this.usingCard;
|
||||
}
|
||||
}
|
@ -1,96 +1,125 @@
|
||||
<div [formGroup]="paymentForm">
|
||||
<div class="tw-mb-4 tw-text-lg" *ngIf="showOptions && showMethods">
|
||||
<bit-radio-group formControlName="method">
|
||||
<bit-radio-button id="method-card" [value]="paymentMethodType.Card">
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<div class="tw-mb-4 tw-text-lg">
|
||||
<bit-radio-group formControlName="paymentMethod">
|
||||
<bit-radio-button id="card-payment-method" [value]="PaymentMethodType.Card">
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>
|
||||
{{ "creditCard" | i18n }}</bit-label
|
||||
>
|
||||
{{ "creditCard" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="method-bank" [value]="paymentMethodType.BankAccount" *ngIf="!hideBank">
|
||||
<bit-radio-button
|
||||
id="bank-payment-method"
|
||||
[value]="PaymentMethodType.BankAccount"
|
||||
*ngIf="showBankAccount"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-bank" aria-hidden="true"></i>
|
||||
{{ "bankAccount" | i18n }}</bit-label
|
||||
>
|
||||
{{ "bankAccount" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="method-paypal" [value]="paymentMethodType.PayPal" *ngIf="!hidePaypal">
|
||||
<bit-label> <i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i> PayPal</bit-label>
|
||||
<bit-radio-button
|
||||
id="paypal-payment-method"
|
||||
[value]="PaymentMethodType.PayPal"
|
||||
*ngIf="showPayPal"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-paypal" aria-hidden="true"></i>
|
||||
{{ "payPal" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button id="method-credit" [value]="paymentMethodType.Credit" *ngIf="!hideCredit">
|
||||
<bit-radio-button
|
||||
id="credit-payment-method"
|
||||
[value]="PaymentMethodType.Credit"
|
||||
*ngIf="showAccountCredit"
|
||||
>
|
||||
<bit-label>
|
||||
<i class="bwi bwi-fw bwi-dollar" aria-hidden="true"></i>
|
||||
{{ "accountCredit" | i18n }}</bit-label
|
||||
>
|
||||
{{ "accountCredit" | i18n }}
|
||||
</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.Card">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4">
|
||||
<div [ngClass]="trialFlow ? 'tw-col-span-12' : 'tw-col-span-6'">
|
||||
<app-payment-label-v2 for="stripe-card-number-element">{{
|
||||
"number" | i18n
|
||||
}}</app-payment-label-v2>
|
||||
<div id="stripe-card-number-element" class="form-control stripe-form-control"></div>
|
||||
<!-- Card -->
|
||||
<ng-container *ngIf="usingCard">
|
||||
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4">
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label for="stripe-card-number" required>
|
||||
{{ "number" | i18n }}
|
||||
</app-payment-label>
|
||||
<div id="stripe-card-number" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div *ngIf="!trialFlow" class="tw-col-span-8 tw-flex tw-items-end">
|
||||
<div class="tw-col-span-1 tw-flex tw-items-end">
|
||||
<img
|
||||
src="../../images/cards.png"
|
||||
src="../../../images/cards.png"
|
||||
alt="Visa, MasterCard, Discover, AmEx, JCB, Diners Club, UnionPay"
|
||||
width="323"
|
||||
height="32"
|
||||
class="tw-max-w-full"
|
||||
/>
|
||||
</div>
|
||||
<div [ngClass]="trialFlow ? 'tw-col-span-6' : 'tw-col-span-6'">
|
||||
<app-payment-label-v2 for="stripe-card-expiry-element">{{
|
||||
"expiration" | i18n
|
||||
}}</app-payment-label-v2>
|
||||
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label for="stripe-card-expiry" required>
|
||||
{{ "expiration" | i18n }}
|
||||
</app-payment-label>
|
||||
<div id="stripe-card-expiry" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div [ngClass]="trialFlow ? 'tw-col-span-6' : 'tw-col-span-6'">
|
||||
<app-payment-label-v2 for="stripe-card-cvc-element">
|
||||
<div class="tw-col-span-1">
|
||||
<app-payment-label for="stripe-card-cvc" required>
|
||||
{{ "securityCodeSlashCVV" | i18n }}
|
||||
<a
|
||||
href="https://www.cvvnumber.com/cvv.html"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
class="hover:tw-no-underline"
|
||||
appA11yTitle="{{ 'whatIsACvvNumber' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</app-payment-label-v2>
|
||||
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
|
||||
</app-payment-label>
|
||||
<div id="stripe-card-cvc" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.BankAccount">
|
||||
<!-- Bank Account -->
|
||||
<ng-container *ngIf="showBankAccount && usingBankAccount">
|
||||
<bit-callout type="warning" title="{{ 'verifyBankAccount' | i18n }}">
|
||||
{{ "verifyBankAccountInitialDesc" | i18n }} {{ "verifyBankAccountFailureWarning" | i18n }}
|
||||
{{ "verifyBankAccountWithStatementDescriptorWarning" | i18n }}
|
||||
</bit-callout>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4" formGroupName="bank">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<div class="tw-grid tw-grid-cols-2 tw-gap-4 tw-mb-4" formGroupName="bankInformation">
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "routingNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="routing_number" required appInputVerbatim />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "accountNumber" | i18n }}</bit-label>
|
||||
<input bitInput type="text" formControlName="account_number" required appInputVerbatim />
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "accountHolderName" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
id="routingNumber"
|
||||
type="text"
|
||||
formControlName="account_holder_name"
|
||||
formControlName="routingNumber"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "accountNumber" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
id="accountNumber"
|
||||
type="text"
|
||||
formControlName="accountNumber"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "accountHolderName" | i18n }}</bit-label>
|
||||
<input
|
||||
id="accountHolderName"
|
||||
bitInput
|
||||
type="text"
|
||||
formControlName="accountHolderName"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</bit-form-field>
|
||||
<bit-form-field class="tw-col-span-1" disableMargin>
|
||||
<bit-label>{{ "bankAccountType" | i18n }}</bit-label>
|
||||
<bit-select formControlName="account_holder_type" required>
|
||||
<bit-select id="accountHolderType" formControlName="accountHolderType" required>
|
||||
<bit-option value="" label="-- {{ 'select' | i18n }} --"></bit-option>
|
||||
<bit-option value="company" label="{{ 'bankAccountTypeCompany' | i18n }}"></bit-option>
|
||||
<bit-option
|
||||
@ -101,15 +130,20 @@
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.PayPal">
|
||||
<!-- PayPal -->
|
||||
<ng-container *ngIf="showPayPal && usingPayPal">
|
||||
<div class="tw-mb-3">
|
||||
<div id="bt-dropin-container" class="tw-mb-1"></div>
|
||||
<div id="braintree-container" class="tw-mb-1 tw-content-center"></div>
|
||||
<small class="tw-text-muted">{{ "paypalClickSubmit" | i18n }}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showMethods && method === paymentMethodType.Credit">
|
||||
<bit-callout>
|
||||
<!-- Account Credit -->
|
||||
<ng-container *ngIf="showAccountCredit && usingAccountCredit">
|
||||
<app-callout type="info">
|
||||
{{ "makeSureEnoughCredit" | i18n }}
|
||||
</bit-callout>
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
</div>
|
||||
<button *ngIf="!!onSubmit" bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</form>
|
||||
|
@ -1,330 +1,203 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Subject } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { BillingServicesModule, BraintreeService, StripeService } from "../../services";
|
||||
|
||||
import { PaymentLabelV2 } from "./payment-label-v2.component";
|
||||
import { PaymentLabelComponent } from "./payment-label.component";
|
||||
|
||||
/**
|
||||
* Render a form that allows the user to enter their payment method, tokenize it against one of our payment providers and,
|
||||
* optionally, submit it using the {@link onSubmit} function if it is provided.
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-payment",
|
||||
templateUrl: "payment.component.html",
|
||||
templateUrl: "./payment.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, PaymentLabelV2],
|
||||
imports: [BillingServicesModule, SharedModule, PaymentLabelComponent],
|
||||
})
|
||||
export class PaymentComponent implements OnInit, OnDestroy {
|
||||
@Input() showMethods = true;
|
||||
@Input() showOptions = true;
|
||||
@Input() hideBank = false;
|
||||
@Input() hidePaypal = false;
|
||||
@Input() hideCredit = false;
|
||||
@Input() trialFlow = false;
|
||||
/** Show account credit as a payment option. */
|
||||
@Input() showAccountCredit: boolean = true;
|
||||
/** Show bank account as a payment option. */
|
||||
@Input() showBankAccount: boolean = true;
|
||||
/** Show PayPal as a payment option. */
|
||||
@Input() showPayPal: boolean = true;
|
||||
|
||||
@Input()
|
||||
set method(value: PaymentMethodType) {
|
||||
this._method = value;
|
||||
this.paymentForm?.controls.method.setValue(value, { emitEvent: false });
|
||||
}
|
||||
/** The payment method selected by default when the component renders. */
|
||||
@Input() private initialPaymentMethod: PaymentMethodType = PaymentMethodType.Card;
|
||||
/** If provided, will be invoked with the tokenized payment source during form submission. */
|
||||
@Input() protected onSubmit?: (request: TokenizedPaymentSourceRequest) => Promise<void>;
|
||||
|
||||
get method(): PaymentMethodType {
|
||||
return this._method;
|
||||
}
|
||||
private _method: PaymentMethodType = PaymentMethodType.Card;
|
||||
@Output() submitted = new EventEmitter<PaymentMethodType>();
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
protected paymentForm = new FormGroup({
|
||||
method: new FormControl(this.method),
|
||||
bank: new FormGroup({
|
||||
routing_number: new FormControl(null, [Validators.required]),
|
||||
account_number: new FormControl(null, [Validators.required]),
|
||||
account_holder_name: new FormControl(null, [Validators.required]),
|
||||
account_holder_type: new FormControl("", [Validators.required]),
|
||||
currency: new FormControl("USD"),
|
||||
country: new FormControl("US"),
|
||||
|
||||
protected formGroup = new FormGroup({
|
||||
paymentMethod: new FormControl<PaymentMethodType>(null),
|
||||
bankInformation: new FormGroup({
|
||||
routingNumber: new FormControl<string>("", [Validators.required]),
|
||||
accountNumber: new FormControl<string>("", [Validators.required]),
|
||||
accountHolderName: new FormControl<string>("", [Validators.required]),
|
||||
accountHolderType: new FormControl<string>("", [Validators.required]),
|
||||
}),
|
||||
});
|
||||
paymentMethodType = PaymentMethodType;
|
||||
|
||||
private btScript: HTMLScriptElement;
|
||||
private btInstance: any = null;
|
||||
private stripeScript: HTMLScriptElement;
|
||||
private stripe: any = null;
|
||||
private stripeElements: any = null;
|
||||
private stripeCardNumberElement: any = null;
|
||||
private stripeCardExpiryElement: any = null;
|
||||
private stripeCardCvcElement: any = null;
|
||||
private StripeElementStyle: any;
|
||||
private StripeElementClasses: any;
|
||||
protected PaymentMethodType = PaymentMethodType;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private themingService: AbstractThemingService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.stripeScript = window.document.createElement("script");
|
||||
this.stripeScript.src = "https://js.stripe.com/v3/?advancedFraudSignals=false";
|
||||
this.stripeScript.async = true;
|
||||
this.stripeScript.onload = async () => {
|
||||
this.stripe = (window as any).Stripe(process.env.STRIPE_KEY);
|
||||
this.stripeElements = this.stripe.elements();
|
||||
await this.setStripeElement();
|
||||
};
|
||||
this.btScript = window.document.createElement("script");
|
||||
this.btScript.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`;
|
||||
this.btScript.async = true;
|
||||
this.StripeElementStyle = {
|
||||
base: {
|
||||
color: null,
|
||||
fontFamily:
|
||||
'"DM Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
|
||||
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||
fontSize: "16px",
|
||||
fontSmoothing: "antialiased",
|
||||
"::placeholder": {
|
||||
color: null,
|
||||
},
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private braintreeService: BraintreeService,
|
||||
private stripeService: StripeService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.formGroup.controls.paymentMethod.patchValue(this.initialPaymentMethod);
|
||||
|
||||
this.stripeService.loadStripe(
|
||||
{
|
||||
cardNumber: "#stripe-card-number",
|
||||
cardExpiry: "#stripe-card-expiry",
|
||||
cardCvc: "#stripe-card-cvc",
|
||||
},
|
||||
invalid: {
|
||||
color: null,
|
||||
},
|
||||
};
|
||||
this.StripeElementClasses = {
|
||||
focus: "is-focused",
|
||||
empty: "is-empty",
|
||||
invalid: "is-invalid",
|
||||
};
|
||||
}
|
||||
async ngOnInit() {
|
||||
if (!this.showOptions) {
|
||||
this.hidePaypal = this.method !== PaymentMethodType.PayPal;
|
||||
this.hideBank = this.method !== PaymentMethodType.BankAccount;
|
||||
this.hideCredit = this.method !== PaymentMethodType.Credit;
|
||||
}
|
||||
this.subscribeToTheme();
|
||||
window.document.head.appendChild(this.stripeScript);
|
||||
if (!this.hidePaypal) {
|
||||
window.document.head.appendChild(this.btScript);
|
||||
}
|
||||
this.paymentForm
|
||||
.get("method")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((v) => {
|
||||
this.method = v;
|
||||
this.changeMethod();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
window.document.head.removeChild(this.stripeScript);
|
||||
window.setTimeout(() => {
|
||||
Array.from(window.document.querySelectorAll("iframe")).forEach((el) => {
|
||||
if (el.src != null && el.src.indexOf("stripe") > -1) {
|
||||
try {
|
||||
window.document.body.removeChild(el);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
if (!this.hidePaypal) {
|
||||
window.document.head.removeChild(this.btScript);
|
||||
window.setTimeout(() => {
|
||||
Array.from(window.document.head.querySelectorAll("script")).forEach((el) => {
|
||||
if (el.src != null && el.src.indexOf("paypal") > -1) {
|
||||
try {
|
||||
window.document.head.removeChild(el);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
const btStylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet");
|
||||
if (btStylesheet != null) {
|
||||
try {
|
||||
window.document.head.removeChild(btStylesheet);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
changeMethod() {
|
||||
this.btInstance = null;
|
||||
if (this.method === PaymentMethodType.PayPal) {
|
||||
window.setTimeout(() => {
|
||||
(window as any).braintree.dropin.create(
|
||||
{
|
||||
authorization: process.env.BRAINTREE_KEY,
|
||||
container: "#bt-dropin-container",
|
||||
paymentOptionPriority: ["paypal"],
|
||||
paypal: {
|
||||
flow: "vault",
|
||||
buttonStyle: {
|
||||
label: "pay",
|
||||
size: "medium",
|
||||
shape: "pill",
|
||||
color: "blue",
|
||||
tagline: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
(createErr: any, instance: any) => {
|
||||
if (createErr != null) {
|
||||
// eslint-disable-next-line
|
||||
console.error(createErr);
|
||||
return;
|
||||
}
|
||||
this.btInstance = instance;
|
||||
},
|
||||
);
|
||||
}, 250);
|
||||
} else {
|
||||
void this.setStripeElement();
|
||||
}
|
||||
}
|
||||
|
||||
createPaymentToken(): Promise<[string, PaymentMethodType]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.method === PaymentMethodType.Credit) {
|
||||
resolve([null, this.method]);
|
||||
} else if (this.method === PaymentMethodType.PayPal) {
|
||||
this.btInstance
|
||||
.requestPaymentMethod()
|
||||
.then((payload: any) => {
|
||||
resolve([payload.nonce, this.method]);
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(err.message);
|
||||
});
|
||||
} else if (
|
||||
this.method === PaymentMethodType.Card ||
|
||||
this.method === PaymentMethodType.BankAccount
|
||||
) {
|
||||
if (this.method === PaymentMethodType.Card) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.apiService
|
||||
.postSetupPayment()
|
||||
.then((clientSecret) =>
|
||||
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement),
|
||||
)
|
||||
.then((result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
} else if (result.setupIntent && result.setupIntent.status === "succeeded") {
|
||||
resolve([result.setupIntent.payment_method, this.method]);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.stripe
|
||||
.createToken("bank_account", this.paymentForm.get("bank").value)
|
||||
.then((result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
} else if (result.token && result.token.id != null) {
|
||||
resolve([result.token.id, this.method]);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleStripeCardPayment(clientSecret: string, successCallback: () => Promise<any>): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.showMethods && this.stripeCardNumberElement == null) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
const handleCardPayment = () =>
|
||||
this.showMethods
|
||||
? this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement)
|
||||
: this.stripe.handleCardSetup(clientSecret);
|
||||
return handleCardPayment().then(async (result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
} else if (result.paymentIntent && result.paymentIntent.status === "succeeded") {
|
||||
if (successCallback != null) {
|
||||
await successCallback();
|
||||
}
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async setStripeElement() {
|
||||
const extensionRefreshFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.ExtensionRefresh,
|
||||
this.initialPaymentMethod === PaymentMethodType.Card,
|
||||
);
|
||||
|
||||
// Apply unique styles for extension refresh
|
||||
if (extensionRefreshFlag) {
|
||||
this.StripeElementStyle.base.fontWeight = "500";
|
||||
this.StripeElementClasses.base = "v2";
|
||||
if (this.showPayPal) {
|
||||
this.braintreeService.loadBraintree(
|
||||
"#braintree-container",
|
||||
this.initialPaymentMethod === PaymentMethodType.PayPal,
|
||||
);
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (this.showMethods && this.method === PaymentMethodType.Card) {
|
||||
if (this.stripeCardNumberElement == null) {
|
||||
this.stripeCardNumberElement = this.stripeElements.create("cardNumber", {
|
||||
style: this.StripeElementStyle,
|
||||
classes: this.StripeElementClasses,
|
||||
placeholder: "",
|
||||
});
|
||||
}
|
||||
if (this.stripeCardExpiryElement == null) {
|
||||
this.stripeCardExpiryElement = this.stripeElements.create("cardExpiry", {
|
||||
style: this.StripeElementStyle,
|
||||
classes: this.StripeElementClasses,
|
||||
});
|
||||
}
|
||||
if (this.stripeCardCvcElement == null) {
|
||||
this.stripeCardCvcElement = this.stripeElements.create("cardCvc", {
|
||||
style: this.StripeElementStyle,
|
||||
classes: this.StripeElementClasses,
|
||||
placeholder: "",
|
||||
});
|
||||
}
|
||||
this.stripeCardNumberElement.mount("#stripe-card-number-element");
|
||||
this.stripeCardExpiryElement.mount("#stripe-card-expiry-element");
|
||||
this.stripeCardCvcElement.mount("#stripe-card-cvc-element");
|
||||
}
|
||||
}, 50);
|
||||
this.formGroup
|
||||
.get("paymentMethod")
|
||||
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((type) => {
|
||||
this.onPaymentMethodChange(type);
|
||||
});
|
||||
}
|
||||
|
||||
private subscribeToTheme() {
|
||||
this.themingService.theme$.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
this.StripeElementStyle.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
|
||||
this.StripeElementStyle.base["::placeholder"].color = `rgb(${style.getPropertyValue(
|
||||
"--color-text-muted",
|
||||
)})`;
|
||||
this.StripeElementStyle.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
|
||||
this.StripeElementStyle.invalid.borderColor = `rgb(${style.getPropertyValue(
|
||||
"--color-danger-600",
|
||||
)})`;
|
||||
});
|
||||
/** Programmatically select the provided payment method. */
|
||||
select = (paymentMethod: PaymentMethodType) => {
|
||||
this.formGroup.get("paymentMethod").patchValue(paymentMethod);
|
||||
};
|
||||
|
||||
protected submit = async () => {
|
||||
const { type, token } = await this.tokenize();
|
||||
await this.onSubmit?.({ type, token });
|
||||
this.submitted.emit(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tokenize the payment method information entered by the user against one of our payment providers.
|
||||
*
|
||||
* - {@link PaymentMethodType.Card} => [Stripe.confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup}
|
||||
* - {@link PaymentMethodType.BankAccount} => [Stripe.confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup}
|
||||
* - {@link PaymentMethodType.PayPal} => [Braintree.requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod}
|
||||
* */
|
||||
async tokenize(): Promise<{ type: PaymentMethodType; token: string }> {
|
||||
const type = this.selected;
|
||||
|
||||
if (this.usingStripe) {
|
||||
const clientSecret = await this.billingApiService.createSetupIntent(type);
|
||||
|
||||
if (this.usingBankAccount) {
|
||||
this.formGroup.markAllAsTouched();
|
||||
if (this.formGroup.valid) {
|
||||
const token = await this.stripeService.setupBankAccountPaymentMethod(clientSecret, {
|
||||
accountHolderName: this.formGroup.value.bankInformation.accountHolderName,
|
||||
routingNumber: this.formGroup.value.bankInformation.routingNumber,
|
||||
accountNumber: this.formGroup.value.bankInformation.accountNumber,
|
||||
accountHolderType: this.formGroup.value.bankInformation.accountHolderType,
|
||||
});
|
||||
return {
|
||||
type,
|
||||
token,
|
||||
};
|
||||
} else {
|
||||
throw "Invalid input provided, Please ensure all required fields are filled out correctly and try again.";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.usingCard) {
|
||||
const token = await this.stripeService.setupCardPaymentMethod(clientSecret);
|
||||
return {
|
||||
type,
|
||||
token,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (this.usingPayPal) {
|
||||
const token = await this.braintreeService.requestPaymentMethod();
|
||||
return {
|
||||
type,
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.usingAccountCredit) {
|
||||
return {
|
||||
type: PaymentMethodType.Credit,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.stripeService.unloadStripe();
|
||||
if (this.showPayPal) {
|
||||
this.braintreeService.unloadBraintree();
|
||||
}
|
||||
}
|
||||
|
||||
private onPaymentMethodChange(type: PaymentMethodType): void {
|
||||
switch (type) {
|
||||
case PaymentMethodType.Card: {
|
||||
this.stripeService.mountElements();
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal: {
|
||||
this.braintreeService.createDropin();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get selected(): PaymentMethodType {
|
||||
return this.formGroup.value.paymentMethod;
|
||||
}
|
||||
|
||||
protected get usingAccountCredit(): boolean {
|
||||
return this.selected === PaymentMethodType.Credit;
|
||||
}
|
||||
|
||||
protected get usingBankAccount(): boolean {
|
||||
return this.selected === PaymentMethodType.BankAccount;
|
||||
}
|
||||
|
||||
protected get usingCard(): boolean {
|
||||
return this.selected === PaymentMethodType.Card;
|
||||
}
|
||||
|
||||
protected get usingPayPal(): boolean {
|
||||
return this.selected === PaymentMethodType.PayPal;
|
||||
}
|
||||
|
||||
private get usingStripe(): boolean {
|
||||
return this.usingBankAccount || this.usingCard;
|
||||
}
|
||||
}
|
||||
|
@ -1236,7 +1236,6 @@ const safeProviders: SafeProvider[] = [
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
BillingApiServiceAbstraction,
|
||||
ConfigService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
|
@ -4,7 +4,6 @@
|
||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||
import { InitiationPath } from "../../models/request/reference-event.request";
|
||||
import { PaymentMethodType, PlanType } from "../enums";
|
||||
import { BillingSourceResponse } from "../models/response/billing.response";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
export type OrganizationInformation = {
|
||||
@ -46,9 +45,7 @@ export type SubscriptionInformation = {
|
||||
};
|
||||
|
||||
export abstract class OrganizationBillingServiceAbstraction {
|
||||
getPaymentSource: (
|
||||
organizationId: string,
|
||||
) => Promise<BillingSourceResponse | PaymentSourceResponse>;
|
||||
getPaymentSource: (organizationId: string) => Promise<PaymentSourceResponse>;
|
||||
|
||||
purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
|
||||
|
||||
|
@ -7,8 +7,6 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request";
|
||||
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
|
||||
import { FeatureFlag } from "../../enums/feature-flag.enum";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { EncryptService } from "../../platform/abstractions/encrypt.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
@ -24,7 +22,6 @@ import {
|
||||
} from "../abstractions";
|
||||
import { PlanType } from "../enums";
|
||||
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
|
||||
import { BillingSourceResponse } from "../models/response/billing.response";
|
||||
import { PaymentSourceResponse } from "../models/response/payment-source.response";
|
||||
|
||||
interface OrganizationKeys {
|
||||
@ -38,7 +35,6 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private configService: ConfigService,
|
||||
private keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
private i18nService: I18nService,
|
||||
@ -46,21 +42,9 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
async getPaymentSource(
|
||||
organizationId: string,
|
||||
): Promise<BillingSourceResponse | PaymentSourceResponse> {
|
||||
const deprecateStripeSourcesAPI = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.AC2476_DeprecateStripeSourcesAPI,
|
||||
);
|
||||
|
||||
if (deprecateStripeSourcesAPI) {
|
||||
const paymentMethod =
|
||||
await this.billingApiService.getOrganizationPaymentMethod(organizationId);
|
||||
return paymentMethod.paymentSource;
|
||||
} else {
|
||||
const billing = await this.organizationApiService.getBilling(organizationId);
|
||||
return billing.paymentSource;
|
||||
}
|
||||
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
|
||||
const paymentMethod = await this.billingApiService.getOrganizationPaymentMethod(organizationId);
|
||||
return paymentMethod.paymentSource;
|
||||
}
|
||||
|
||||
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
|
||||
|
@ -34,7 +34,6 @@ export enum FeatureFlag {
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
SSHKeyVaultItem = "ssh-key-vault-item",
|
||||
SSHAgent = "ssh-agent",
|
||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader",
|
||||
CriticalApps = "pm-14466-risk-insights-critical-application",
|
||||
@ -92,7 +91,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
||||
[FeatureFlag.SSHAgent]: FALSE,
|
||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE,
|
||||
[FeatureFlag.CriticalApps]: FALSE,
|
||||
|
Loading…
Reference in New Issue
Block a user