mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 09:13:33 +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:
@@ -1,3 +1,9 @@
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request";
|
||||
import { TokenizedPaymentMethodRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-method.request";
|
||||
import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request";
|
||||
import { PaymentInformationResponse } from "@bitwarden/common/billing/models/response/payment-information.response";
|
||||
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
|
||||
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
||||
@@ -13,23 +19,50 @@ export abstract class BillingApiServiceAbstraction {
|
||||
organizationId: string,
|
||||
request: SubscriptionCancellationRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
|
||||
|
||||
createClientOrganization: (
|
||||
providerId: string,
|
||||
request: CreateClientOrganizationRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
createSetupIntent: (paymentMethodType: PaymentMethodType) => Promise<string>;
|
||||
|
||||
getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
|
||||
|
||||
getOrganizationBillingMetadata: (
|
||||
organizationId: string,
|
||||
) => Promise<OrganizationBillingMetadataResponse>;
|
||||
|
||||
getOrganizationSubscription: (
|
||||
organizationId: string,
|
||||
) => Promise<OrganizationSubscriptionResponse>;
|
||||
|
||||
getPlans: () => Promise<ListResponse<PlanResponse>>;
|
||||
|
||||
getProviderPaymentInformation: (providerId: string) => Promise<PaymentInformationResponse>;
|
||||
|
||||
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
|
||||
|
||||
updateClientOrganization: (
|
||||
providerId: string,
|
||||
organizationId: string,
|
||||
request: UpdateClientOrganizationRequest,
|
||||
) => Promise<any>;
|
||||
|
||||
updateProviderPaymentMethod: (
|
||||
providerId: string,
|
||||
request: TokenizedPaymentMethodRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
updateProviderTaxInformation: (
|
||||
providerId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
verifyProviderBankAccount: (
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
7
libs/common/src/billing/abstractions/index.ts
Normal file
7
libs/common/src/billing/abstractions/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./account/billing-account-profile-state.service";
|
||||
export * from "./billilng-api.service.abstraction";
|
||||
export * from "./organization-billing.service";
|
||||
export * from "./payment-method-warnings-service.abstraction";
|
||||
export * from "./payment-processors/braintree.service.abstraction";
|
||||
export * from "./payment-processors/stripe.service.abstraction";
|
||||
export * from "./provider-billing.service.abstraction";
|
||||
@@ -0,0 +1,28 @@
|
||||
export abstract class BraintreeServiceAbstraction {
|
||||
/**
|
||||
* Utilizes the Braintree SDK to create a [Braintree drop-in]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html} instance attached to the container ID specified as part of the {@link loadBraintree} method.
|
||||
*/
|
||||
createDropin: () => void;
|
||||
|
||||
/**
|
||||
* Loads the Bitwarden dropin.js script in the <head> element of the current page.
|
||||
* This script attaches the Braintree SDK to the window.
|
||||
* @param containerId - The ID of the HTML element where the Braintree drop-in will be loaded at.
|
||||
* @param autoCreateDropin - Specifies whether the Braintree drop-in should be created when dropin.js loads.
|
||||
*/
|
||||
loadBraintree: (containerId: string, autoCreateDropin: boolean) => void;
|
||||
|
||||
/**
|
||||
* Invokes the Braintree [requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} method
|
||||
* in order to generate a payment method token using the active Braintree drop-in.
|
||||
*/
|
||||
requestPaymentMethod: () => Promise<string>;
|
||||
|
||||
/**
|
||||
* Removes the following elements from the <head> of the current page:
|
||||
* - The Bitwarden dropin.js script
|
||||
* - Any <script> elements that contain the word "paypal"
|
||||
* - The Braintree drop-in stylesheet
|
||||
*/
|
||||
unloadBraintree: () => void;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { BankAccount } from "@bitwarden/common/billing/models/domain";
|
||||
|
||||
export abstract class StripeServiceAbstraction {
|
||||
/**
|
||||
* Loads [Stripe JS]{@link https://docs.stripe.com/js} in the <head> element of the current page and mounts
|
||||
* Stripe credit card [elements]{@link https://docs.stripe.com/js/elements_object/create} into the HTML elements with the provided element IDS.
|
||||
* We do this to avoid having to load the Stripe JS SDK on every page of the Web Vault given many pages contain sensitive information.
|
||||
* @param elementIds - The ID attributes of the HTML elements used to load the Stripe JS credit card elements.
|
||||
*/
|
||||
loadStripe: (
|
||||
elementIds: { cardNumber: string; cardExpiry: string; cardCvc: string },
|
||||
autoMount: boolean,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Re-mounts previously created Stripe credit card [elements]{@link https://docs.stripe.com/js/elements_object/create} into the HTML elements
|
||||
* specified during the {@link loadStripe} call. This is useful for when those HTML elements are removed from the DOM by Angular.
|
||||
*/
|
||||
mountElements: () => void;
|
||||
|
||||
/**
|
||||
* Creates a Stripe [SetupIntent]{@link https://docs.stripe.com/api/setup_intents} and uses the resulting client secret
|
||||
* to invoke the Stripe JS [confirmUsBankAccountSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_us_bank_account_setup} method,
|
||||
* thereby creating and storing a Stripe [PaymentMethod]{@link https://docs.stripe.com/api/payment_methods}.
|
||||
* @returns The ID of the newly created PaymentMethod.
|
||||
*/
|
||||
setupBankAccountPaymentMethod: (
|
||||
clientSecret: string,
|
||||
bankAccount: BankAccount,
|
||||
) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Creates a Stripe [SetupIntent]{@link https://docs.stripe.com/api/setup_intents} and uses the resulting client secret
|
||||
* to invoke the Stripe JS [confirmCardSetup]{@link https://docs.stripe.com/js/setup_intents/confirm_card_setup} method,
|
||||
* thereby creating and storing a Stripe [PaymentMethod]{@link https://docs.stripe.com/api/payment_methods}.
|
||||
* @returns The ID of the newly created PaymentMethod.
|
||||
*/
|
||||
setupCardPaymentMethod: (clientSecret: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Removes {@link https://docs.stripe.com/js} from the <head> element of the current page as well as all
|
||||
* Stripe-managed <iframe> elements.
|
||||
*/
|
||||
unloadStripe: () => void;
|
||||
}
|
||||
6
libs/common/src/billing/models/domain/bank-account.ts
Normal file
6
libs/common/src/billing/models/domain/bank-account.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type BankAccount = {
|
||||
accountHolderName: string;
|
||||
routingNumber: string;
|
||||
accountNumber: string;
|
||||
accountHolderType: string;
|
||||
};
|
||||
5
libs/common/src/billing/models/domain/index.ts
Normal file
5
libs/common/src/billing/models/domain/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from "./bank-account";
|
||||
export * from "./masked-payment-method";
|
||||
export * from "./payment-method-warning";
|
||||
export * from "./tax-information";
|
||||
export * from "./tokenized-payment-method";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { MaskedPaymentMethodResponse } from "@bitwarden/common/billing/models/response/masked-payment-method.response";
|
||||
|
||||
export class MaskedPaymentMethod {
|
||||
type: PaymentMethodType;
|
||||
description: string;
|
||||
needsVerification: boolean;
|
||||
|
||||
static from(response: MaskedPaymentMethodResponse | undefined) {
|
||||
if (response === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
};
|
||||
}
|
||||
}
|
||||
32
libs/common/src/billing/models/domain/tax-information.ts
Normal file
32
libs/common/src/billing/models/domain/tax-information.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
export class TaxInformation {
|
||||
country: string;
|
||||
postalCode: string;
|
||||
taxId: string;
|
||||
line1: string;
|
||||
line2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
|
||||
static empty(): TaxInformation {
|
||||
return {
|
||||
country: null,
|
||||
postalCode: null,
|
||||
taxId: null,
|
||||
line1: null,
|
||||
line2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
};
|
||||
}
|
||||
|
||||
static from(response: TaxInfoResponse | null): TaxInformation {
|
||||
if (response === null) {
|
||||
return TaxInformation.empty();
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
|
||||
export type TokenizedPaymentMethod = {
|
||||
type: PaymentMethodType;
|
||||
token: string;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export class BitPayInvoiceRequest {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
providerId: string;
|
||||
credit: boolean;
|
||||
amount: number;
|
||||
returnUrl: string;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information";
|
||||
|
||||
import { TaxInfoUpdateRequest } from "./tax-info-update.request";
|
||||
|
||||
export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest {
|
||||
@@ -6,4 +8,16 @@ export class ExpandedTaxInfoUpdateRequest extends TaxInfoUpdateRequest {
|
||||
line2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
|
||||
static From(taxInformation: TaxInformation): ExpandedTaxInfoUpdateRequest {
|
||||
const request = new ExpandedTaxInfoUpdateRequest();
|
||||
request.country = taxInformation.country;
|
||||
request.postalCode = taxInformation.postalCode;
|
||||
request.taxId = taxInformation.taxId;
|
||||
request.line1 = taxInformation.line1;
|
||||
request.line2 = taxInformation.line2;
|
||||
request.city = taxInformation.city;
|
||||
request.state = taxInformation.state;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { TokenizedPaymentMethod } from "@bitwarden/common/billing/models/domain";
|
||||
|
||||
export class TokenizedPaymentMethodRequest {
|
||||
type: PaymentMethodType;
|
||||
token: string;
|
||||
|
||||
static From(tokenizedPaymentMethod: TokenizedPaymentMethod): TokenizedPaymentMethodRequest {
|
||||
const request = new TokenizedPaymentMethodRequest();
|
||||
request.type = tokenizedPaymentMethod.type;
|
||||
request.token = tokenizedPaymentMethod.token;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class VerifyBankAccountRequest {
|
||||
amount1: number;
|
||||
amount2: number;
|
||||
|
||||
constructor(amount1: number, amount2: number) {
|
||||
this.amount1 = amount1;
|
||||
this.amount2 = amount2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PaymentMethodType } from "@bitwarden/common/billing/enums";
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class MaskedPaymentMethodResponse extends BaseResponse {
|
||||
type: PaymentMethodType;
|
||||
description: string;
|
||||
needsVerification: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.description = this.getResponseProperty("Description");
|
||||
this.needsVerification = this.getResponseProperty("NeedsVerification");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { MaskedPaymentMethodResponse } from "./masked-payment-method.response";
|
||||
import { TaxInfoResponse } from "./tax-info.response";
|
||||
|
||||
export class PaymentInformationResponse extends BaseResponse {
|
||||
accountCredit: number;
|
||||
paymentMethod?: MaskedPaymentMethodResponse;
|
||||
taxInformation?: TaxInfoResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.accountCredit = this.getResponseProperty("AccountCredit");
|
||||
|
||||
const paymentMethod = this.getResponseProperty("PaymentMethod");
|
||||
if (paymentMethod) {
|
||||
this.paymentMethod = new MaskedPaymentMethodResponse(paymentMethod);
|
||||
}
|
||||
|
||||
const taxInformation = this.getResponseProperty("TaxInformation");
|
||||
if (taxInformation) {
|
||||
this.taxInformation = new TaxInfoResponse(taxInformation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ export class TaxInfoResponse extends BaseResponse {
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.taxId = this.getResponseProperty("TaxIdNumber");
|
||||
if (!this.taxId) {
|
||||
this.taxId = this.getResponseProperty("TaxId");
|
||||
}
|
||||
this.taxIdType = this.getResponseProperty("TaxIdType");
|
||||
this.line1 = this.getResponseProperty("Line1");
|
||||
this.line2 = this.getResponseProperty("Line2");
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
|
||||
import { BillingApiServiceAbstraction } from "../../billing/abstractions";
|
||||
import { PaymentMethodType } from "../../billing/enums";
|
||||
import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request";
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
import { TokenizedPaymentMethodRequest } from "../../billing/models/request/tokenized-payment-method.request";
|
||||
import { VerifyBankAccountRequest } from "../../billing/models/request/verify-bank-account.request";
|
||||
import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response";
|
||||
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
|
||||
import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response";
|
||||
import { PaymentInformationResponse } from "../../billing/models/response/payment-information.response";
|
||||
import { PlanResponse } from "../../billing/models/response/plan.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
@@ -44,6 +48,21 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async createSetupIntent(type: PaymentMethodType) {
|
||||
const getPath = () => {
|
||||
switch (type) {
|
||||
case PaymentMethodType.BankAccount: {
|
||||
return "/setup-intent/bank-account";
|
||||
}
|
||||
case PaymentMethodType.Card: {
|
||||
return "/setup-intent/card";
|
||||
}
|
||||
}
|
||||
};
|
||||
const response = await this.apiService.send("POST", getPath(), null, true, true);
|
||||
return response as string;
|
||||
}
|
||||
|
||||
async getBillingStatus(id: string): Promise<OrganizationBillingStatusResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
@@ -87,6 +106,17 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
return new ListResponse(r, PlanResponse);
|
||||
}
|
||||
|
||||
async getProviderPaymentInformation(providerId: string): Promise<PaymentInformationResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/payment-information",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PaymentInformationResponse(response);
|
||||
}
|
||||
|
||||
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
||||
const r = await this.apiService.send(
|
||||
"GET",
|
||||
@@ -111,4 +141,37 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: TokenizedPaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async verifyProviderBankAccount(providerId: string, request: VerifyBankAccountRequest) {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { BraintreeServiceAbstraction } from "../../abstractions";
|
||||
|
||||
export class BraintreeService implements BraintreeServiceAbstraction {
|
||||
private braintree: any;
|
||||
private containerId: string;
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
createDropin() {
|
||||
window.setTimeout(() => {
|
||||
const window$ = window as any;
|
||||
window$.braintree.dropin.create(
|
||||
{
|
||||
authorization: process.env.BRAINTREE_KEY,
|
||||
container: this.containerId,
|
||||
paymentOptionPriority: ["paypal"],
|
||||
paypal: {
|
||||
flow: "vault",
|
||||
buttonStyle: {
|
||||
label: "pay",
|
||||
size: "medium",
|
||||
shape: "pill",
|
||||
color: "blue",
|
||||
tagline: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
(error: any, instance: any) => {
|
||||
if (error != null) {
|
||||
this.logService.error(error);
|
||||
return;
|
||||
}
|
||||
this.braintree = instance;
|
||||
},
|
||||
);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
loadBraintree(containerId: string, autoCreateDropin: boolean) {
|
||||
const script = window.document.createElement("script");
|
||||
script.id = "dropin-script";
|
||||
script.src = `scripts/dropin.js?cache=${process.env.CACHE_TAG}`;
|
||||
script.async = true;
|
||||
if (autoCreateDropin) {
|
||||
script.onload = () => this.createDropin();
|
||||
}
|
||||
this.containerId = containerId;
|
||||
window.document.head.appendChild(script);
|
||||
}
|
||||
|
||||
requestPaymentMethod(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.braintree.requestPaymentMethod((error: any, payload: any) => {
|
||||
if (error) {
|
||||
this.logService.error(error);
|
||||
reject(error.message);
|
||||
} else {
|
||||
resolve(payload.nonce as string);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
unloadBraintree() {
|
||||
const script = window.document.getElementById("dropin-script");
|
||||
window.document.head.removeChild(script);
|
||||
window.setTimeout(() => {
|
||||
const scripts = Array.from(window.document.head.querySelectorAll("script")).filter(
|
||||
(script) => script.src != null && script.src.indexOf("paypal") > -1,
|
||||
);
|
||||
scripts.forEach((script) => {
|
||||
try {
|
||||
window.document.head.removeChild(script);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
});
|
||||
const stylesheet = window.document.head.querySelector("#braintree-dropin-stylesheet");
|
||||
if (stylesheet != null) {
|
||||
try {
|
||||
window.document.head.removeChild(stylesheet);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { StripeServiceAbstraction } from "../../abstractions";
|
||||
import { BankAccount } from "../../models/domain";
|
||||
|
||||
export class StripeService implements StripeServiceAbstraction {
|
||||
private stripe: any;
|
||||
private elements: any;
|
||||
private elementIds: {
|
||||
cardNumber: string;
|
||||
cardExpiry: string;
|
||||
cardCvc: string;
|
||||
};
|
||||
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
loadStripe(
|
||||
elementIds: { cardNumber: string; cardExpiry: string; cardCvc: string },
|
||||
autoMount: boolean,
|
||||
) {
|
||||
this.elementIds = elementIds;
|
||||
const script = window.document.createElement("script");
|
||||
script.id = "stripe-script";
|
||||
script.src = "https://js.stripe.com/v3?advancedFraudSignals=false";
|
||||
script.onload = () => {
|
||||
const window$ = window as any;
|
||||
this.stripe = window$.Stripe(process.env.STRIPE_KEY);
|
||||
this.elements = this.stripe.elements();
|
||||
const options = this.getElementOptions();
|
||||
setTimeout(() => {
|
||||
this.elements.create("cardNumber", options);
|
||||
this.elements.create("cardExpiry", options);
|
||||
this.elements.create("cardCvc", options);
|
||||
if (autoMount) {
|
||||
this.mountElements();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
window.document.head.appendChild(script);
|
||||
}
|
||||
|
||||
mountElements() {
|
||||
setTimeout(() => {
|
||||
const cardNumber = this.elements.getElement("cardNumber");
|
||||
const cardExpiry = this.elements.getElement("cardExpiry");
|
||||
const cardCvc = this.elements.getElement("cardCvc");
|
||||
cardNumber.mount(this.elementIds.cardNumber);
|
||||
cardExpiry.mount(this.elementIds.cardExpiry);
|
||||
cardCvc.mount(this.elementIds.cardCvc);
|
||||
});
|
||||
}
|
||||
|
||||
async setupBankAccountPaymentMethod(
|
||||
clientSecret: string,
|
||||
{ accountHolderName, routingNumber, accountNumber, accountHolderType }: BankAccount,
|
||||
): Promise<string> {
|
||||
const result = await this.stripe.confirmUsBankAccountSetup(clientSecret, {
|
||||
payment_method: {
|
||||
us_bank_account: {
|
||||
routing_number: routingNumber,
|
||||
account_number: accountNumber,
|
||||
account_holder_type: accountHolderType,
|
||||
},
|
||||
billing_details: {
|
||||
name: accountHolderName,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.error || (result.setupIntent && result.setupIntent.status !== "requires_action")) {
|
||||
this.logService.error(result.error);
|
||||
throw result.error;
|
||||
}
|
||||
return result.setupIntent.payment_method as string;
|
||||
}
|
||||
|
||||
async setupCardPaymentMethod(clientSecret: string): Promise<string> {
|
||||
const cardNumber = this.elements.getElement("cardNumber");
|
||||
const result = await this.stripe.confirmCardSetup(clientSecret, {
|
||||
payment_method: {
|
||||
card: cardNumber,
|
||||
},
|
||||
});
|
||||
if (result.error || (result.setupIntent && result.setupIntent.status !== "succeeded")) {
|
||||
this.logService.error(result.error);
|
||||
throw result.error;
|
||||
}
|
||||
return result.setupIntent.payment_method as string;
|
||||
}
|
||||
|
||||
unloadStripe() {
|
||||
const script = window.document.getElementById("stripe-script");
|
||||
window.document.head.removeChild(script);
|
||||
window.setTimeout(() => {
|
||||
const iFrames = Array.from(window.document.querySelectorAll("iframe")).filter(
|
||||
(element) => element.src != null && element.src.indexOf("stripe") > -1,
|
||||
);
|
||||
iFrames.forEach((iFrame) => {
|
||||
try {
|
||||
window.document.body.removeChild(iFrame);
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private getElementOptions(): any {
|
||||
const options: any = {
|
||||
style: {
|
||||
base: {
|
||||
color: null,
|
||||
fontFamily:
|
||||
'"Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif, ' +
|
||||
'"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
||||
fontSize: "14px",
|
||||
fontSmoothing: "antialiased",
|
||||
"::placeholder": {
|
||||
color: null,
|
||||
},
|
||||
},
|
||||
invalid: {
|
||||
color: null,
|
||||
},
|
||||
},
|
||||
classes: {
|
||||
focus: "is-focused",
|
||||
empty: "is-empty",
|
||||
invalid: "is-invalid",
|
||||
},
|
||||
};
|
||||
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
options.style.base.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
|
||||
options.style.base["::placeholder"].color = `rgb(${style.getPropertyValue(
|
||||
"--color-text-muted",
|
||||
)})`;
|
||||
options.style.invalid.color = `rgb(${style.getPropertyValue("--color-text-main")})`;
|
||||
options.style.invalid.borderColor = `rgb(${style.getPropertyValue("--color-danger-600")})`;
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user