mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 16:53:34 +00:00
[AC-1939] Manage provider payment information (#9415)
* Added select-payment-method.component in shared lib Because we're going to be implementing the same functionality for providers and orgs/users, I wanted to start moving some of this shared functionality into libs so it can be accessed in both web and bit-web. Additionally, the Stripe and Braintree functionality has been moved into their own services for more central management. * Added generalized manage-tax-information component to shared lib * Added generalized add-account-credit-dialog component to shared libs * Added generalized verify-bank-account component to shared libs * Added dialog for selection of provider payment method * Added provider-payment-method component * Added provider-payment-method component to provider layout
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
*ngIf="canAccessBilling$ | async"
|
||||
>
|
||||
<bit-nav-item [text]="'subscription' | i18n" route="billing/subscription"></bit-nav-item>
|
||||
<bit-nav-item [text]="'paymentMethod' | i18n" route="billing/payment-method"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-cogs"
|
||||
|
||||
@@ -7,8 +7,12 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi
|
||||
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
|
||||
|
||||
import { ProviderSubscriptionComponent, hasConsolidatedBilling } from "../../billing/providers";
|
||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients";
|
||||
import {
|
||||
ManageClientOrganizationsComponent,
|
||||
ProviderSubscriptionComponent,
|
||||
hasConsolidatedBilling,
|
||||
ProviderPaymentMethodComponent,
|
||||
} from "../../billing/providers";
|
||||
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
@@ -118,6 +122,13 @@ const routes: Routes = [
|
||||
titleId: "subscription",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "payment-method",
|
||||
component: ProviderPaymentMethodComponent,
|
||||
data: {
|
||||
titleId: "paymentMethod",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,13 +9,15 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
|
||||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||
|
||||
import { ProviderSubscriptionComponent } from "../../billing/providers";
|
||||
import {
|
||||
CreateClientOrganizationComponent,
|
||||
ManageClientOrganizationsComponent,
|
||||
ManageClientOrganizationNameComponent,
|
||||
ManageClientOrganizationsComponent,
|
||||
ManageClientOrganizationSubscriptionComponent,
|
||||
} from "../../billing/providers/clients";
|
||||
ProviderPaymentMethodComponent,
|
||||
ProviderSelectPaymentMethodDialogComponent,
|
||||
ProviderSubscriptionComponent,
|
||||
} from "../../billing/providers";
|
||||
|
||||
import { AddOrganizationComponent } from "./clients/add-organization.component";
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
@@ -66,6 +68,8 @@ import { SetupComponent } from "./setup/setup.component";
|
||||
ManageClientOrganizationNameComponent,
|
||||
ManageClientOrganizationSubscriptionComponent,
|
||||
ProviderSubscriptionComponent,
|
||||
ProviderSelectPaymentMethodDialogComponent,
|
||||
ProviderPaymentMethodComponent,
|
||||
],
|
||||
providers: [WebProviderService, ProviderPermissionsGuard],
|
||||
})
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
export * from "./clients/create-client-organization.component";
|
||||
export * from "./clients/manage-client-organization-name.component";
|
||||
export * from "./clients/manage-client-organization-subscription.component";
|
||||
export * from "./clients/manage-client-organizations.component";
|
||||
export * from "./guards/has-consolidated-billing.guard";
|
||||
export * from "./provider-subscription.component";
|
||||
export * from "./payment-method/provider-select-payment-method-dialog.component";
|
||||
export * from "./payment-method/provider-payment-method.component";
|
||||
export * from "./subscription/provider-subscription.component";
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<app-header></app-header>
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-container *ngIf="!loading">
|
||||
<!-- Account Credit -->
|
||||
<ng-container>
|
||||
<h2 bitTypography="h2">
|
||||
{{ "accountCredit" | i18n }}
|
||||
</h2>
|
||||
<p class="tw-text-lg tw-font-bold">{{ accountCredit | currency: "$" }}</p>
|
||||
<p bitTypography="body1">{{ "creditAppliedDesc" | i18n }}</p>
|
||||
<button type="button" bitButton buttonType="secondary" [bitAction]="addAccountCredit">
|
||||
{{ "addCredit" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<!-- Payment Method -->
|
||||
<ng-container>
|
||||
<h2 class="spaced-header">{{ "paymentMethod" | i18n }}</h2>
|
||||
<p *ngIf="!hasPaymentMethod">{{ "noPaymentMethod" | i18n }}</p>
|
||||
<app-verify-bank-account
|
||||
[onSubmit]="verifyBankAccount"
|
||||
(verificationSubmitted)="onDataUpdated()"
|
||||
*ngIf="hasUnverifiedPaymentMethod"
|
||||
/>
|
||||
<ng-container *ngIf="hasPaymentMethod">
|
||||
<p>
|
||||
<i class="bwi bwi-fw" [ngClass]="paymentMethodClass"></i>
|
||||
{{ paymentMethodDescription }}
|
||||
</p>
|
||||
</ng-container>
|
||||
<button type="button" bitButton buttonType="secondary" [bitAction]="changePaymentMethod">
|
||||
{{ (hasPaymentMethod ? "changePaymentMethod" : "addPaymentMethod") | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<!-- Tax Information -->
|
||||
<ng-container>
|
||||
<h2 class="spaced-header">{{ "taxInformation" | i18n }}</h2>
|
||||
<p>{{ "taxInformationDesc" | i18n }}</p>
|
||||
<app-manage-tax-information
|
||||
*ngIf="taxInformation"
|
||||
[taxInformation]="taxInformation"
|
||||
[onSubmit]="updateTaxInformation"
|
||||
(taxInformationUpdated)="onDataUpdated()"
|
||||
/>
|
||||
</ng-container>
|
||||
</bit-container>
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { from, lastValueFrom, Subject, switchMap } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
|
||||
import { openAddAccountCreditDialog } from "@bitwarden/angular/billing/components";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { MaskedPaymentMethod, TaxInformation } from "@bitwarden/common/billing/models/domain";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
openProviderSelectPaymentMethodDialog,
|
||||
ProviderSelectPaymentMethodDialogResultType,
|
||||
} from "./provider-select-payment-method-dialog.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-provider-payment-method",
|
||||
templateUrl: "./provider-payment-method.component.html",
|
||||
})
|
||||
export class ProviderPaymentMethodComponent implements OnInit, OnDestroy {
|
||||
protected providerId: string;
|
||||
protected loading: boolean;
|
||||
|
||||
protected accountCredit: number;
|
||||
protected maskedPaymentMethod: MaskedPaymentMethod;
|
||||
protected taxInformation: TaxInformation;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
addAccountCredit = () =>
|
||||
openAddAccountCreditDialog(this.dialogService, {
|
||||
data: {
|
||||
providerId: this.providerId,
|
||||
},
|
||||
});
|
||||
|
||||
changePaymentMethod = async () => {
|
||||
const dialogRef = openProviderSelectPaymentMethodDialog(this.dialogService, {
|
||||
data: {
|
||||
providerId: this.providerId,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result == ProviderSelectPaymentMethodDialogResultType.Submitted) {
|
||||
await this.load();
|
||||
}
|
||||
};
|
||||
|
||||
async load() {
|
||||
this.loading = true;
|
||||
const paymentInformation = await this.billingApiService.getProviderPaymentInformation(
|
||||
this.providerId,
|
||||
);
|
||||
this.accountCredit = paymentInformation.accountCredit;
|
||||
this.maskedPaymentMethod = MaskedPaymentMethod.from(paymentInformation.paymentMethod);
|
||||
this.taxInformation = TaxInformation.from(paymentInformation.taxInformation);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
onDataUpdated = async () => await this.load();
|
||||
|
||||
updateTaxInformation = async (taxInformation: TaxInformation) => {
|
||||
const request = ExpandedTaxInfoUpdateRequest.From(taxInformation);
|
||||
await this.billingApiService.updateProviderTaxInformation(this.providerId, request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedTaxInformation"),
|
||||
});
|
||||
};
|
||||
|
||||
verifyBankAccount = async (amount1: number, amount2: number) => {
|
||||
const request = new VerifyBankAccountRequest(amount1, amount2);
|
||||
await this.billingApiService.verifyProviderBankAccount(this.providerId, request);
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedRoute.params
|
||||
.pipe(
|
||||
switchMap(({ providerId }) => {
|
||||
this.providerId = providerId;
|
||||
return from(this.load());
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
protected get hasPaymentMethod(): boolean {
|
||||
return !!this.maskedPaymentMethod;
|
||||
}
|
||||
|
||||
protected get hasUnverifiedPaymentMethod(): boolean {
|
||||
return !!this.maskedPaymentMethod && this.maskedPaymentMethod.needsVerification;
|
||||
}
|
||||
|
||||
protected get paymentMethodClass(): string[] {
|
||||
switch (this.maskedPaymentMethod.type) {
|
||||
case PaymentMethodType.Card:
|
||||
return ["bwi-credit-card"];
|
||||
case PaymentMethodType.BankAccount:
|
||||
return ["bwi-bank"];
|
||||
case PaymentMethodType.PayPal:
|
||||
return ["bwi-paypal tw-text-primary"];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected get paymentMethodDescription(): string {
|
||||
let description = this.maskedPaymentMethod.description;
|
||||
if (this.maskedPaymentMethod.type === PaymentMethodType.BankAccount) {
|
||||
if (this.hasUnverifiedPaymentMethod) {
|
||||
description += " - " + this.i18nService.t("unverified");
|
||||
} else {
|
||||
description += " - " + this.i18nService.t("verified");
|
||||
}
|
||||
}
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle class="tw-font-semibold">
|
||||
{{ "addPaymentMethod" | i18n }}
|
||||
</span>
|
||||
<ng-container bitDialogContent>
|
||||
<app-select-payment-method [showAccountCredit]="false" />
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton bitFormButton buttonType="primary" type="submit">
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,60 @@
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, EventEmitter, Inject, Output, ViewChild } from "@angular/core";
|
||||
import { FormGroup } from "@angular/forms";
|
||||
|
||||
import { SelectPaymentMethodComponent } from "@bitwarden/angular/billing/components";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
type ProviderSelectPaymentMethodDialogParams = {
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
export enum ProviderSelectPaymentMethodDialogResultType {
|
||||
Closed = "closed",
|
||||
Submitted = "submitted",
|
||||
}
|
||||
|
||||
export const openProviderSelectPaymentMethodDialog = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<ProviderSelectPaymentMethodDialogParams>,
|
||||
) =>
|
||||
dialogService.open<
|
||||
ProviderSelectPaymentMethodDialogResultType,
|
||||
ProviderSelectPaymentMethodDialogParams
|
||||
>(ProviderSelectPaymentMethodDialogComponent, dialogConfig);
|
||||
|
||||
@Component({
|
||||
templateUrl: "provider-select-payment-method-dialog.component.html",
|
||||
})
|
||||
export class ProviderSelectPaymentMethodDialogComponent {
|
||||
@ViewChild(SelectPaymentMethodComponent)
|
||||
selectPaymentMethodComponent: SelectPaymentMethodComponent;
|
||||
@Output() providerPaymentMethodUpdated = new EventEmitter();
|
||||
|
||||
protected readonly formGroup = new FormGroup({});
|
||||
protected readonly ResultType = ProviderSelectPaymentMethodDialogResultType;
|
||||
|
||||
constructor(
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
@Inject(DIALOG_DATA) private dialogParams: ProviderSelectPaymentMethodDialogParams,
|
||||
private dialogRef: DialogRef<ProviderSelectPaymentMethodDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
submit = async () => {
|
||||
const tokenizedPaymentMethod = await this.selectPaymentMethodComponent.tokenizePaymentMethod();
|
||||
const request = TokenizedPaymentMethodRequest.From(tokenizedPaymentMethod);
|
||||
await this.billingApiService.updateProviderPaymentMethod(this.dialogParams.providerId, request);
|
||||
this.providerPaymentMethodUpdated.emit();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("updatedPaymentMethod"),
|
||||
});
|
||||
this.dialogRef.close(this.ResultType.Submitted);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user