Remove payments API routing initialization
[ProtonMail-WebClient.git] / packages / shared / lib / api / payments.ts
blob18b72fe62a0d722daaf566ac6bca01a7eb1195f7
1 import type {
2     AmountAndCurrency,
3     Autopay,
4     BillingAddress,
5     BillingAddressProperty,
6     ChargeablePaymentParameters,
7     ExistingPayment,
8     PAYMENT_TOKEN_STATUS,
9     PlanIDs,
10     SavedPaymentMethod,
11     TokenPayment,
12     TokenPaymentMethod,
13     V5PaymentToken,
14     WrappedCardPayment,
15     WrappedCryptoPayment,
16     WrappedPaypalPayment,
17 } from '@proton/payments';
18 import {
19     type Currency,
20     type INVOICE_STATE,
21     type INVOICE_TYPE,
22     PAYMENT_METHOD_TYPES,
23     PLANS,
24     isTokenPaymentMethod,
25     isV5PaymentToken,
26 } from '@proton/payments';
27 import type { INVOICE_OWNER } from '@proton/shared/lib/constants';
28 import { FREE_PLAN } from '@proton/shared/lib/subscription/freePlans';
30 import type { ProductParam } from '../apps/product';
31 import { getProductHeaders } from '../apps/product';
32 import { getPlanNameFromIDs, isLifetimePlanSelected } from '../helpers/planIDs';
33 import type { Api, Cycle, FreePlanDefault, Renew, Subscription } from '../interfaces';
35 export type PaymentsVersion = 'v4' | 'v5';
36 let paymentsVersion: PaymentsVersion = 'v5';
38 export function setPaymentsVersion(version: PaymentsVersion) {
39     paymentsVersion = version;
42 export function getPaymentsVersion(): PaymentsVersion {
43     return paymentsVersion;
46 export const queryFreePlan = (params?: QueryPlansParams) => ({
47     url: `payments/${paymentsVersion}/plans/default`,
48     method: 'get',
49     params,
50 });
52 export const getFreePlan = ({ api, currency }: { api: Api; currency?: Currency }) =>
53     api<{ Plans: FreePlanDefault }>(queryFreePlan(currency ? { Currency: currency } : undefined))
54         .then(({ Plans }): FreePlanDefault => {
55             return {
56                 ...Plans,
57                 MaxBaseSpace: Plans.MaxBaseSpace ?? Plans.MaxSpace,
58                 MaxBaseRewardSpace: Plans.MaxBaseRewardSpace ?? Plans.MaxRewardSpace,
59                 MaxDriveSpace: Plans.MaxDriveSpace ?? Plans.MaxSpace,
60                 MaxDriveRewardSpace: Plans.MaxDriveRewardSpace ?? Plans.MaxRewardSpace,
61             };
62         })
63         .catch(() => FREE_PLAN);
65 export const getSubscription = (forceVersion?: PaymentsVersion) => ({
66     url: `payments/${forceVersion ?? paymentsVersion}/subscription`,
67     method: 'get',
68 });
70 export interface FeedbackDowngradeData {
71     Reason?: string;
72     Feedback?: string;
73     ReasonDetails?: string;
74     Context?: 'vpn' | 'mail';
77 export const deleteSubscription = (data: FeedbackDowngradeData, version: PaymentsVersion) => ({
78     url: `payments/${version}/subscription`,
79     method: 'delete',
80     data,
81 });
83 export enum ProrationMode {
84     Default = 0,
85     Exact = 1,
88 export type CheckSubscriptionData = {
89     Plans: PlanIDs;
90     Currency: Currency;
91     Cycle: Cycle;
92     CouponCode?: string;
93     Codes?: string[];
94     /**
95      * For taxes
96      */
97     BillingAddress?: BillingAddress;
98     ProrationMode?: ProrationMode;
101 type CommonSubscribeData = {
102     Plans: PlanIDs;
103     Currency: Currency;
104     Cycle: Cycle;
105     Codes?: string[];
106 } & AmountAndCurrency;
108 type SubscribeDataV4 = CommonSubscribeData & TokenPaymentMethod & BillingAddressProperty;
109 type SubscribeDataV5 = CommonSubscribeData & V5PaymentToken & BillingAddressProperty;
110 type SubscribeDataNoPayment = CommonSubscribeData;
111 export type SubscribeData = SubscribeDataV4 | SubscribeDataV5 | SubscribeDataNoPayment;
113 function isCommonSubscribeData(data: any): data is CommonSubscribeData {
114     return !!data.Plans && !!data.Currency && !!data.Cycle && !!data.Amount && !!data.Currency;
117 function isSubscribeDataV4(data: any): data is SubscribeDataV4 {
118     return isCommonSubscribeData(data) && isTokenPaymentMethod(data);
121 function isSubscribeDataV5(data: any): data is SubscribeDataV5 {
122     return isCommonSubscribeData(data) && isV5PaymentToken(data);
125 function isSubscribeDataNoPayment(data: any): data is SubscribeDataNoPayment {
126     return isCommonSubscribeData(data);
129 export function isSubscribeData(data: any): data is SubscribeData {
130     return isSubscribeDataV4(data) || isSubscribeDataV5(data) || isSubscribeDataNoPayment(data);
133 function prepareSubscribeDataPayload(data: SubscribeData): SubscribeData {
134     const allowedProps: (keyof SubscribeDataV4 | keyof SubscribeDataV5)[] = [
135         'Plans',
136         'Currency',
137         'Cycle',
138         'Codes',
139         'PaymentToken',
140         'Payment',
141         'Amount',
142         'Currency',
143         'BillingAddress',
144     ];
145     const payload: any = {};
146     Object.keys(data).forEach((key: any) => {
147         if (allowedProps.includes(key)) {
148             payload[key] = (data as any)[key];
149         }
150     });
152     return payload as SubscribeData;
155 function getPaymentTokenFromSubscribeData(data: SubscribeData): string | undefined {
156     return (data as any)?.PaymentToken ?? (data as any)?.Payment?.Details?.Token;
159 export function getLifetimeProductType(data: Pick<SubscribeData, 'Plans'>) {
160     const planName = getPlanNameFromIDs(data.Plans);
161     if (planName === PLANS.PASS_LIFETIME) {
162         return 'pass-lifetime' as const;
163     }
166 export const buyProduct = (rawData: SubscribeData, product: ProductParam) => {
167     const sanitizedData = prepareSubscribeDataPayload(rawData) as SubscribeDataV5;
169     const url = 'payments/v5/products';
170     const config = {
171         url,
172         method: 'post',
173         data: {
174             Quantity: 1,
175             PaymentToken: getPaymentTokenFromSubscribeData(sanitizedData),
176             ProductType: getLifetimeProductType(sanitizedData),
177             Amount: sanitizedData.Amount,
178             Currency: sanitizedData.Currency,
179             BillingAddress: sanitizedData.BillingAddress,
180         },
181         headers: getProductHeaders(product, {
182             endpoint: url,
183             product,
184         }),
185         timeout: 60000 * 2,
186     };
188     return config;
191 export const subscribe = (rawData: SubscribeData, product: ProductParam, version: PaymentsVersion) => {
192     const sanitizedData = prepareSubscribeDataPayload(rawData);
194     if (isLifetimePlanSelected(sanitizedData.Plans)) {
195         return buyProduct(sanitizedData, product);
196     }
198     let data: SubscribeData = sanitizedData;
199     if (version === 'v5' && isSubscribeDataV4(sanitizedData)) {
200         const v5Data: SubscribeDataV5 = {
201             ...sanitizedData,
202             PaymentToken: sanitizedData.Payment.Details.Token,
203             v: 5,
204         };
206         data = v5Data;
207         delete (data as any).Payment;
208     } else if (version === 'v4' && isSubscribeDataV5(sanitizedData)) {
209         const v4Data: SubscribeDataV4 = {
210             ...sanitizedData,
211             Payment: {
212                 Type: PAYMENT_METHOD_TYPES.TOKEN,
213                 Details: {
214                     Token: sanitizedData.PaymentToken,
215                 },
216             },
217         };
219         data = v4Data;
220         delete (data as any).PaymentToken;
221     }
223     const config = {
224         url: `payments/${version}/subscription`,
225         method: 'post',
226         data,
227         headers: getProductHeaders(product, {
228             endpoint: `payments/${version}/subscription`,
229             product,
230         }),
231         timeout: 60000 * 2,
232     };
234     return config;
237 export enum InvoiceDocument {
238     Invoice = 'invoice',
239     CreditNote = 'credit_note',
240     CurrencyConversion = 'currency_conversion',
243 export interface QueryInvoicesParams {
244     /**
245      * Starts with 0
246      */
247     Page: number;
248     PageSize: number;
249     Owner: INVOICE_OWNER;
250     State?: INVOICE_STATE;
251     Type?: INVOICE_TYPE;
252     Document?: InvoiceDocument;
256  * Query list of invoices for the current user. The response is {@link InvoiceResponse}
257  */
258 export const queryInvoices = (params: QueryInvoicesParams, version?: PaymentsVersion) => ({
259     url: `payments/${version ?? paymentsVersion}/invoices`,
260     method: 'get',
261     params,
264 export interface QueryPlansParams {
265     Currency?: Currency;
268 export const queryPlans = (params?: QueryPlansParams) => ({
269     url: `payments/${paymentsVersion}/plans`,
270     method: 'get',
271     params,
274 export const getInvoice = (invoiceID: string, version: PaymentsVersion) => ({
275     url: `payments/${version}/invoices/${invoiceID}`,
276     method: 'get',
277     output: 'arrayBuffer',
280 export const checkInvoice = (invoiceID: string, version?: PaymentsVersion, GiftCode?: string) => ({
281     url: `payments/${version ?? paymentsVersion}/invoices/${invoiceID}/check`,
282     method: 'put',
283     data: { GiftCode },
286 export const queryPaymentMethods = (forceVersion?: PaymentsVersion) => ({
287     url: `payments/${forceVersion ?? paymentsVersion}/methods`,
288     method: 'get',
291 export type SetPaymentMethodDataV4 = TokenPayment & { Autopay?: Autopay };
293 export const setPaymentMethodV4 = (data: SetPaymentMethodDataV4) => ({
294     url: 'payments/v4/methods',
295     method: 'post',
296     data,
299 export type SetPaymentMethodDataV5 = V5PaymentToken & { Autopay?: Autopay };
300 export const setPaymentMethodV5 = (data: SetPaymentMethodDataV5) => ({
301     url: 'payments/v5/methods',
302     method: 'post',
303     data,
306 export interface UpdatePaymentMethodsData {
307     Autopay: Autopay;
310 export const updatePaymentMethod = (methodId: string, data: UpdatePaymentMethodsData, version: PaymentsVersion) => ({
311     url: `payments/${version}/methods/${methodId}`,
312     method: 'put',
313     data,
316 export const deletePaymentMethod = (methodID: string, version: PaymentsVersion) => ({
317     url: `payments/${version}/methods/${methodID}`,
318     method: 'delete',
322  * @param invoiceID
323  * @param data – does not have to include the payment token if user pays from the credits balance. In this case Amount
324  * must be set to 0 and payment token must not be supplied.
325  */
326 export const payInvoice = (
327     invoiceID: string,
328     data: (TokenPaymentMethod & AmountAndCurrency) | AmountAndCurrency,
329     version: PaymentsVersion
330 ) => ({
331     url: `payments/${version}/invoices/${invoiceID}`,
332     method: 'post',
333     data,
336 export const orderPaymentMethods = (PaymentMethodIDs: string[], version: PaymentsVersion) => ({
337     url: `payments/${version}/methods/order`,
338     method: 'put',
339     data: { PaymentMethodIDs },
342 export interface GiftCodeData {
343     GiftCode: string;
344     Amount: number;
347 export const buyCredit = (
348     data: (TokenPaymentMethod & AmountAndCurrency) | GiftCodeData | ChargeablePaymentParameters,
349     forceVersion: PaymentsVersion
350 ) => ({
351     url: `payments/${forceVersion ?? paymentsVersion}/credit`,
352     method: 'post',
353     data,
356 export interface ValidateCreditData {
357     GiftCode: string;
360 export const validateCredit = (data: ValidateCreditData, version: PaymentsVersion) => ({
361     url: `payments/${version ?? paymentsVersion}/credit/check`,
362     method: 'post',
363     data,
366 export type CreateBitcoinTokenData = AmountAndCurrency & WrappedCryptoPayment;
368 export type CreateTokenData =
369     | ((AmountAndCurrency | {}) & (WrappedPaypalPayment | WrappedCardPayment | ExistingPayment))
370     | CreateBitcoinTokenData;
372 export const createToken = (data: CreateTokenData, version: PaymentsVersion) => ({
373     url: `payments/${version}/tokens`,
374     method: 'post',
375     data,
378 export const createTokenV4 = (data: CreateTokenData) => createToken(data, 'v4');
379 export const createTokenV5 = (data: CreateTokenData) => createToken(data, 'v5');
381 export const getTokenStatus = (paymentToken: string, version: PaymentsVersion) => ({
382     url: `payments/${version}/tokens/${paymentToken}`,
383     method: 'get',
386 export const getTokenStatusV4 = (paymentToken: string) => getTokenStatus(paymentToken, 'v4');
387 export const getTokenStatusV5 = (paymentToken: string) => getTokenStatus(paymentToken, 'v5');
389 export const getLastCancelledSubscription = () => ({
390     url: `payments/${paymentsVersion}/subscription/latest`,
391     method: 'get',
394 export type RenewalStateData =
395     | {
396           RenewalState: Renew.Enabled;
397       }
398     | {
399           RenewalState: Renew.Disabled;
400           CancellationFeedback: FeedbackDowngradeData;
401       };
403 export const changeRenewState = (data: RenewalStateData, version: PaymentsVersion) => ({
404     url: `payments/${version}/subscription/renew`,
405     method: 'put',
406     data,
409 export type SubscribeV5Data = {
410     PaymentToken?: string;
411     Plans: PlanIDs;
412     Amount: number;
413     Currency: Currency;
414     Cycle: Cycle;
415     Codes?: string[];
418 export type CreatePaymentIntentPaypalData = AmountAndCurrency & {
419     Payment: {
420         Type: 'paypal';
421     };
424 export type CreatePaymentIntentCardData = AmountAndCurrency & {
425     Payment: {
426         Type: 'card';
427         Details: {
428             Bin: string;
429         };
430     };
433 export type CreatePaymentIntentSavedCardData = AmountAndCurrency & {
434     PaymentMethodID: string;
437 export type CreatePaymentIntentDirectDebitData = AmountAndCurrency & {
438     Payment: {
439         Type: 'sepa_direct_debit';
440         Details: {
441             Email: string;
442         };
443     };
446 export type CreatePaymentIntentData =
447     | CreatePaymentIntentPaypalData
448     | CreatePaymentIntentCardData
449     | CreatePaymentIntentSavedCardData
450     | CreatePaymentIntentDirectDebitData;
452 export const createPaymentIntentV5 = (data: CreatePaymentIntentData) => ({
453     url: `payments/v5/tokens`,
454     method: 'post',
455     data,
458 export type BackendPaymentIntent = {
459     ID: string;
460     Status: 'inited' | 'authorized';
461     Amount: number;
462     GatewayAccountID: string;
463     ExpiresAt: number;
464     PaymentMethodType: 'card' | 'paypal';
465     CreatedAt: number;
466     ModifiedAt: number;
467     UpdatedAt: number;
468     ResourceVersion: number;
469     Object: 'payment_intent';
470     CustomerID: string;
471     CurrencyCode: Currency;
472     Gateway: string;
473     ReferenceID: string;
476 export type FetchPaymentIntentV5Response = {
477     Token: string;
478     Status: PAYMENT_TOKEN_STATUS;
479     Data: BackendPaymentIntent;
482 export const fetchPaymentIntentV5 = (
483     api: Api,
484     data: CreatePaymentIntentData,
485     signal?: AbortSignal
486 ): Promise<FetchPaymentIntentV5Response> => {
487     return api<FetchPaymentIntentV5Response>({
488         ...createPaymentIntentV5(data),
489         signal,
490     });
493 export type FetchPaymentIntentForExistingV5Response = {
494     Token: string;
495     Status: PAYMENT_TOKEN_STATUS;
496     Data: BackendPaymentIntent | null;
499 export const fetchPaymentIntentForExistingV5 = (
500     api: Api,
501     data: CreatePaymentIntentData,
502     signal?: AbortSignal
503 ): Promise<FetchPaymentIntentForExistingV5Response> => {
504     return api<FetchPaymentIntentForExistingV5Response>({
505         ...createPaymentIntentV5(data),
506         signal,
507     });
510 export interface GetChargebeeConfigurationResponse {
511     Code: number;
512     Site: string;
513     PublishableKey: string;
514     Domain: string;
517 export const getChargebeeConfiguration = () => ({
518     url: `payments/v5/web-configuration`,
519     method: 'get',
522 // returns the ID. Or is it user's ID? hopefully.
523 // Call only if ChargebeeEnabled is set to 0 (the system already supports cb but this user was not migrated yet)
524 // Do not call for signups.
525 // Do not call if ChargebeeEnabled is undefined.
526 // If ChargebeeEnabled === 1 then always go to v5 and do not call this.
527 export const importAccount = () => ({
528     url: 'payments/v5/import',
529     method: 'post',
532 export const checkImport = () => ({
533     url: 'payments/v5/import',
534     method: 'head',
537 // no parameter, ideally. Always call before importAccount.
538 export const cleanupImport = () => ({
539     url: 'payments/v5/import',
540     method: 'delete',
543 export type GetSubscriptionResponse = {
544     Subscription: Subscription;
545     UpcomingSubscription?: Subscription;
548 export type GetPaymentMethodsResponse = {
549     PaymentMethods: SavedPaymentMethod[];