2 ChargebeeSubmitEventPayload,
3 ChargebeeVerifySavedCardEventPayload,
5 } from '@proton/chargebee/lib';
7 type BackendPaymentIntent,
8 type CreatePaymentIntentData,
10 type FetchPaymentIntentV5Response,
12 fetchPaymentIntentForExistingV5,
14 } from '@proton/shared/lib/api/payments';
15 import { isProduction } from '@proton/shared/lib/helpers/sentry';
16 import type { Api } from '@proton/shared/lib/interfaces';
18 import { PAYMENT_METHOD_TYPES, PAYMENT_TOKEN_STATUS } from './constants';
21 AuthorizedV5PaymentToken,
23 ChargeablePaymentToken,
24 ChargeableV5PaymentToken,
25 ChargebeeFetchedPaymentToken,
26 ChargebeeIframeEvents,
27 ChargebeeIframeHandles,
29 ExistingPaymentMethod,
31 NonAuthorizedV5PaymentToken,
32 NonChargeablePaymentToken,
33 NonChargeableV5PaymentToken,
36 PlainPaymentMethodType,
41 import { toV5PaymentToken } from './utils';
44 * Prepares the parameters and makes the API call to create the payment token.
48 * @param amountAndCurrency
50 const fetchPaymentToken = async (
51 params: WrappedCardPayment | ExistingPayment,
53 amountAndCurrency?: AmountAndCurrency
54 ): Promise<PaymentTokenResult> => {
55 const data: CreateTokenData = { ...amountAndCurrency, ...params };
57 return api<PaymentTokenResult>({
58 ...createTokenV4(data),
59 notificationExpiration: 10000,
63 export const formatToken = (
64 { Token, Status, ApprovalURL, ReturnHost }: PaymentTokenResult,
65 type: PlainPaymentMethodType,
66 amountAndCurrency?: AmountAndCurrency
67 ): ChargeablePaymentToken | NonChargeablePaymentToken => {
68 const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
69 const paymentToken = toV5PaymentToken(Token);
79 return base as ChargeablePaymentToken;
84 approvalURL: ApprovalURL,
85 returnHost: ReturnHost,
86 } as NonChargeablePaymentToken;
90 export const createPaymentTokenForCard = async (
91 params: WrappedCardPayment,
93 amountAndCurrency?: AmountAndCurrency
94 ): Promise<ChargeablePaymentToken | NonChargeablePaymentToken> => {
95 const paymentTokenResult = await fetchPaymentToken(params, api, amountAndCurrency);
96 return formatToken(paymentTokenResult, PAYMENT_METHOD_TYPES.CARD, amountAndCurrency);
99 export function convertPaymentIntentData(paymentIntentData: BackendPaymentIntent): PaymentIntent;
100 export function convertPaymentIntentData(paymentIntentData: BackendPaymentIntent | null): PaymentIntent | null;
101 export function convertPaymentIntentData(paymentIntentData: BackendPaymentIntent | null): PaymentIntent | null {
102 if (!paymentIntentData) {
106 const Data: PaymentIntent = {
107 id: paymentIntentData.ID,
108 status: paymentIntentData.Status,
109 amount: paymentIntentData.Amount,
110 gateway_account_id: paymentIntentData.GatewayAccountID,
111 expires_at: paymentIntentData.ExpiresAt,
112 payment_method_type: paymentIntentData.PaymentMethodType,
113 created_at: paymentIntentData.CreatedAt,
114 modified_at: paymentIntentData.ModifiedAt,
115 updated_at: paymentIntentData.UpdatedAt,
116 resource_version: paymentIntentData.ResourceVersion,
117 object: paymentIntentData.Object,
118 customer_id: paymentIntentData.CustomerID,
119 currency_code: paymentIntentData.CurrencyCode,
120 gateway: paymentIntentData.Gateway,
121 reference_id: paymentIntentData.ReferenceID,
127 export const createPaymentTokenForExistingPayment = async (
128 PaymentMethodID: ExistingPaymentMethod,
129 type: PAYMENT_METHOD_TYPES.CARD | PAYMENT_METHOD_TYPES.PAYPAL | PAYMENT_METHOD_TYPES.CHARGEBEE_SEPA_DIRECT_DEBIT,
131 amountAndCurrency: AmountAndCurrency
132 ): Promise<ChargeablePaymentToken | NonChargeablePaymentToken> => {
133 const paymentTokenResult = await fetchPaymentToken(
141 return formatToken(paymentTokenResult, type, amountAndCurrency);
144 export type PaymentVerificator = (params: {
145 addCardMode?: boolean;
146 Payment?: CardPayment | PaypalPayment;
148 ApprovalURL?: string;
150 }) => Promise<V5PaymentToken>;
152 export type PaymentVerificatorV5Params = {
153 token: ChargebeeFetchedPaymentToken;
155 events: ChargebeeIframeEvents;
156 addCardMode?: boolean;
159 export type PaymentVerificatorV5 = (params: PaymentVerificatorV5Params) => Promise<V5PaymentToken>;
161 export type ChargebeeCardParams = {
162 type: PAYMENT_METHOD_TYPES.CHARGEBEE_CARD;
163 amountAndCurrency: AmountAndCurrency;
168 type ChargebeePaypalParams = {
169 type: PAYMENT_METHOD_TYPES.CHARGEBEE_PAYPAL;
170 amountAndCurrency: AmountAndCurrency;
173 type Dependencies = {
175 handles: ChargebeeIframeHandles;
176 events: ChargebeeIframeEvents;
177 forceEnableChargebee: ForceEnableChargebee;
180 function submitChargebeeCard(
181 handles: ChargebeeIframeHandles,
182 events: ChargebeeIframeEvents,
183 payload: ChargebeeSubmitEventPayload
185 const removeEventListeners: RemoveEventListener[] = [];
187 const challenge = new Promise<{
191 const listener = events.onThreeDsChallenge((data) =>
194 approvalUrl: data.url,
197 removeEventListeners.push(listener);
200 const finalResult = handles.submitCreditCard(payload).then((result) => result.data);
201 const challengeOrAuthorizedPaymentIntent = Promise.race([challenge, finalResult]);
203 void challengeOrAuthorizedPaymentIntent.finally(() => {
204 removeEventListeners.forEach((removeListener) => removeListener());
207 return challengeOrAuthorizedPaymentIntent;
210 function submitSavedChargebeeCard(
211 handles: ChargebeeIframeHandles,
212 events: ChargebeeIframeEvents,
213 payload: ChargebeeVerifySavedCardEventPayload
215 const removeEventListeners: RemoveEventListener[] = [];
217 const challenge = new Promise<{
221 const listener = events.onCardVeririfcation3dsChallenge((data) => {
224 approvalUrl: data.url,
227 removeEventListeners.push(listener);
230 const finalResult = handles.validateSavedCreditCard(payload).then((result) => result.data);
231 const challengeOrAuthorizedPaymentIntent = Promise.race([challenge, finalResult]);
233 void challengeOrAuthorizedPaymentIntent.finally(() => {
234 removeEventListeners.forEach((removeListener) => removeListener());
237 return challengeOrAuthorizedPaymentIntent;
240 export async function createPaymentTokenV5CreditCard(
241 params: ChargebeeCardParams,
242 { api, handles, events, forceEnableChargebee }: Dependencies,
243 abortController?: AbortController
244 ): Promise<ChargebeeFetchedPaymentToken> {
245 const { type, amountAndCurrency } = params;
247 const binResponse = await handles.getBin();
248 // Can the response even be a failure? Wouldn't it throw an error?
249 if (binResponse.status === 'failure') {
250 throw new Error(binResponse.error);
253 let Bin: string | undefined = binResponse.data?.bin;
254 const allowBinFallback = !isProduction(window.location.host);
255 if (!Bin && allowBinFallback) {
259 const data: CreatePaymentIntentData = {
260 ...amountAndCurrency,
272 Data: paymentIntentData,
273 } = await fetchPaymentIntentV5(api, data, abortController?.signal);
274 forceEnableChargebee();
276 let Data = convertPaymentIntentData(paymentIntentData);
277 let authorizedStatus: AuthorizedV5PaymentToken | NonAuthorizedV5PaymentToken;
278 const result = await submitChargebeeCard(handles, events, {
280 countryCode: params.countryCode,
284 if (!result.authorized) {
287 approvalUrl: result.approvalUrl,
295 const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
298 ...amountAndCurrency,
307 export async function createPaymentTokenV5Paypal(
308 params: ChargebeePaypalParams,
309 { api, forceEnableChargebee }: Dependencies,
310 abortController?: AbortController
313 paymentIntent: PaymentIntent;
314 } & ChargebeeFetchedPaymentToken
316 const { type, amountAndCurrency } = params;
318 let data: CreatePaymentIntentData = {
319 ...amountAndCurrency,
328 Data: paymentIntentData,
329 } = await fetchPaymentIntentV5(api, data, abortController?.signal);
330 forceEnableChargebee();
332 let paymentIntent = convertPaymentIntentData(paymentIntentData);
333 let authorizedStatus: AuthorizedV5PaymentToken = {
337 const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
340 ...amountAndCurrency,
350 export const formatTokenV5 = (
351 { Token, Status }: FetchPaymentIntentV5Response,
352 type: PAYMENT_METHOD_TYPES.CHARGEBEE_CARD | PAYMENT_METHOD_TYPES.CHARGEBEE_PAYPAL,
353 amountAndCurrency: AmountAndCurrency
354 ): ChargeableV5PaymentToken | NonChargeableV5PaymentToken => {
355 const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
356 const paymentToken = toV5PaymentToken(Token);
358 const base: ChargeableV5PaymentToken | NonChargeableV5PaymentToken = {
360 ...amountAndCurrency,
368 export const createPaymentTokenForExistingChargebeePayment = async (
369 PaymentMethodID: ExistingPaymentMethod,
371 | PAYMENT_METHOD_TYPES.CHARGEBEE_CARD
372 | PAYMENT_METHOD_TYPES.CHARGEBEE_PAYPAL
373 | PAYMENT_METHOD_TYPES.CARD
374 | PAYMENT_METHOD_TYPES.PAYPAL
375 | PAYMENT_METHOD_TYPES.CHARGEBEE_SEPA_DIRECT_DEBIT,
377 handles: ChargebeeIframeHandles,
378 events: ChargebeeIframeEvents,
379 amountAndCurrency: AmountAndCurrency
380 // or ChargebeeFetchedPaymentToken
382 ChargeableV5PaymentToken | NonChargeableV5PaymentToken // | ChargebeeFetchedPaymentToken
385 Data: paymentIntentBackend,
388 } = await fetchPaymentIntentForExistingV5(api, {
389 ...amountAndCurrency,
393 const paymentIntent = convertPaymentIntentData(paymentIntentBackend);
394 let authorizedStatus: AuthorizedV5PaymentToken | NonAuthorizedV5PaymentToken;
396 // CARD is allowed for v4-v5 migration
397 if (type === PAYMENT_METHOD_TYPES.CHARGEBEE_CARD || type === PAYMENT_METHOD_TYPES.CARD) {
398 const result = await submitSavedChargebeeCard(handles, events, {
399 paymentIntent: paymentIntent as PaymentIntent,
402 if (!result.authorized) {
405 approvalUrl: result.approvalUrl,
418 const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
421 | PAYMENT_METHOD_TYPES.CHARGEBEE_CARD
422 | PAYMENT_METHOD_TYPES.CHARGEBEE_PAYPAL
423 | PAYMENT_METHOD_TYPES.CHARGEBEE_SEPA_DIRECT_DEBIT;
425 if (type === PAYMENT_METHOD_TYPES.CARD) {
426 convertedType = PAYMENT_METHOD_TYPES.CHARGEBEE_CARD;
427 } else if (type === PAYMENT_METHOD_TYPES.PAYPAL) {
428 convertedType = PAYMENT_METHOD_TYPES.CHARGEBEE_PAYPAL;
430 convertedType = type;
434 ...amountAndCurrency,