Merge branch 'INDA-330-pii-update' into 'main'
[ProtonMail-WebClient.git] / packages / payments / core / createPaymentToken.ts
blob83fd1032d2b84fb9a2c3d867dd20391edff701d8
1 import type {
2     ChargebeeSubmitEventPayload,
3     ChargebeeVerifySavedCardEventPayload,
4     PaymentIntent,
5 } from '@proton/chargebee/lib';
6 import {
7     type BackendPaymentIntent,
8     type CreatePaymentIntentData,
9     type CreateTokenData,
10     type FetchPaymentIntentV5Response,
11     createTokenV4,
12     fetchPaymentIntentForExistingV5,
13     fetchPaymentIntentV5,
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';
19 import type {
20     AmountAndCurrency,
21     AuthorizedV5PaymentToken,
22     CardPayment,
23     ChargeablePaymentToken,
24     ChargeableV5PaymentToken,
25     ChargebeeFetchedPaymentToken,
26     ChargebeeIframeEvents,
27     ChargebeeIframeHandles,
28     ExistingPayment,
29     ExistingPaymentMethod,
30     ForceEnableChargebee,
31     NonAuthorizedV5PaymentToken,
32     NonChargeablePaymentToken,
33     NonChargeableV5PaymentToken,
34     PaymentTokenResult,
35     PaypalPayment,
36     PlainPaymentMethodType,
37     RemoveEventListener,
38     V5PaymentToken,
39     WrappedCardPayment,
40 } from './interface';
41 import { toV5PaymentToken } from './utils';
43 /**
44  * Prepares the parameters and makes the API call to create the payment token.
45  *
46  * @param params
47  * @param api
48  * @param amountAndCurrency
49  */
50 const fetchPaymentToken = async (
51     params: WrappedCardPayment | ExistingPayment,
52     api: Api,
53     amountAndCurrency?: AmountAndCurrency
54 ): Promise<PaymentTokenResult> => {
55     const data: CreateTokenData = { ...amountAndCurrency, ...params };
57     return api<PaymentTokenResult>({
58         ...createTokenV4(data),
59         notificationExpiration: 10000,
60     });
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);
71     const base = {
72         type,
73         chargeable,
74         ...amountAndCurrency,
75         ...paymentToken,
76     };
78     if (chargeable) {
79         return base as ChargeablePaymentToken;
80     } else {
81         return {
82             ...base,
83             status: Status,
84             approvalURL: ApprovalURL,
85             returnHost: ReturnHost,
86         } as NonChargeablePaymentToken;
87     }
90 export const createPaymentTokenForCard = async (
91     params: WrappedCardPayment,
92     api: Api,
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) {
103         return null;
104     }
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,
122     };
124     return Data;
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,
130     api: Api,
131     amountAndCurrency: AmountAndCurrency
132 ): Promise<ChargeablePaymentToken | NonChargeablePaymentToken> => {
133     const paymentTokenResult = await fetchPaymentToken(
134         {
135             PaymentMethodID,
136         },
137         api,
138         amountAndCurrency
139     );
141     return formatToken(paymentTokenResult, type, amountAndCurrency);
144 export type PaymentVerificator = (params: {
145     addCardMode?: boolean;
146     Payment?: CardPayment | PaypalPayment;
147     Token: string;
148     ApprovalURL?: string;
149     ReturnHost?: string;
150 }) => Promise<V5PaymentToken>;
152 export type PaymentVerificatorV5Params = {
153     token: ChargebeeFetchedPaymentToken;
154     v: 5;
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;
164     countryCode: string;
165     zip: string;
168 type ChargebeePaypalParams = {
169     type: PAYMENT_METHOD_TYPES.CHARGEBEE_PAYPAL;
170     amountAndCurrency: AmountAndCurrency;
173 type Dependencies = {
174     api: Api;
175     handles: ChargebeeIframeHandles;
176     events: ChargebeeIframeEvents;
177     forceEnableChargebee: ForceEnableChargebee;
180 function submitChargebeeCard(
181     handles: ChargebeeIframeHandles,
182     events: ChargebeeIframeEvents,
183     payload: ChargebeeSubmitEventPayload
184 ) {
185     const removeEventListeners: RemoveEventListener[] = [];
187     const challenge = new Promise<{
188         authorized: false;
189         approvalUrl: string;
190     }>((resolve) => {
191         const listener = events.onThreeDsChallenge((data) =>
192             resolve({
193                 authorized: false,
194                 approvalUrl: data.url,
195             })
196         );
197         removeEventListeners.push(listener);
198     });
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());
205     });
207     return challengeOrAuthorizedPaymentIntent;
210 function submitSavedChargebeeCard(
211     handles: ChargebeeIframeHandles,
212     events: ChargebeeIframeEvents,
213     payload: ChargebeeVerifySavedCardEventPayload
214 ) {
215     const removeEventListeners: RemoveEventListener[] = [];
217     const challenge = new Promise<{
218         authorized: false;
219         approvalUrl: string;
220     }>((resolve) => {
221         const listener = events.onCardVeririfcation3dsChallenge((data) => {
222             resolve({
223                 authorized: false,
224                 approvalUrl: data.url,
225             });
226         });
227         removeEventListeners.push(listener);
228     });
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());
235     });
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);
251     }
253     let Bin: string | undefined = binResponse.data?.bin;
254     const allowBinFallback = !isProduction(window.location.host);
255     if (!Bin && allowBinFallback) {
256         Bin = '424242';
257     }
259     const data: CreatePaymentIntentData = {
260         ...amountAndCurrency,
261         Payment: {
262             Type: 'card',
263             Details: {
264                 Bin: Bin as string,
265             },
266         },
267     };
269     const {
270         Token: PaymentToken,
271         Status,
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, {
279         paymentIntent: Data,
280         countryCode: params.countryCode,
281         zip: params.zip,
282     });
284     if (!result.authorized) {
285         authorizedStatus = {
286             authorized: false,
287             approvalUrl: result.approvalUrl,
288         };
289     } else {
290         authorizedStatus = {
291             authorized: true,
292         };
293     }
295     const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
297     return {
298         ...amountAndCurrency,
299         ...authorizedStatus,
300         type,
301         v: 5,
302         PaymentToken,
303         chargeable,
304     };
307 export async function createPaymentTokenV5Paypal(
308     params: ChargebeePaypalParams,
309     { api, forceEnableChargebee }: Dependencies,
310     abortController?: AbortController
311 ): Promise<
312     {
313         paymentIntent: PaymentIntent;
314     } & ChargebeeFetchedPaymentToken
315 > {
316     const { type, amountAndCurrency } = params;
318     let data: CreatePaymentIntentData = {
319         ...amountAndCurrency,
320         Payment: {
321             Type: 'paypal',
322         },
323     };
325     const {
326         Token: PaymentToken,
327         Status,
328         Data: paymentIntentData,
329     } = await fetchPaymentIntentV5(api, data, abortController?.signal);
330     forceEnableChargebee();
332     let paymentIntent = convertPaymentIntentData(paymentIntentData);
333     let authorizedStatus: AuthorizedV5PaymentToken = {
334         authorized: true,
335     };
337     const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
339     return {
340         ...amountAndCurrency,
341         ...authorizedStatus,
342         type,
343         v: 5,
344         PaymentToken,
345         chargeable,
346         paymentIntent,
347     };
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 = {
359         ...paymentToken,
360         ...amountAndCurrency,
361         chargeable,
362         type,
363     };
365     return base;
368 export const createPaymentTokenForExistingChargebeePayment = async (
369     PaymentMethodID: ExistingPaymentMethod,
370     type:
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,
376     api: Api,
377     handles: ChargebeeIframeHandles,
378     events: ChargebeeIframeEvents,
379     amountAndCurrency: AmountAndCurrency
380     // or ChargebeeFetchedPaymentToken
381 ): Promise<
382     ChargeableV5PaymentToken | NonChargeableV5PaymentToken // | ChargebeeFetchedPaymentToken
383 > => {
384     const {
385         Data: paymentIntentBackend,
386         Status,
387         Token: PaymentToken,
388     } = await fetchPaymentIntentForExistingV5(api, {
389         ...amountAndCurrency,
390         PaymentMethodID,
391     });
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,
400         });
402         if (!result.authorized) {
403             authorizedStatus = {
404                 authorized: false,
405                 approvalUrl: result.approvalUrl,
406             };
407         } else {
408             authorizedStatus = {
409                 authorized: true,
410             };
411         }
412     } else {
413         authorizedStatus = {
414             authorized: true,
415         };
416     }
418     const chargeable = Status === PAYMENT_TOKEN_STATUS.STATUS_CHARGEABLE;
420     let convertedType:
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;
429     } else {
430         convertedType = type;
431     }
433     return {
434         ...amountAndCurrency,
435         ...authorizedStatus,
436         type: convertedType,
437         v: 5,
438         PaymentToken,
439         chargeable,
440     };