Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / api / payments.ts
blob504d16e6d18f0c06d7fc8db491391a44c19c00e0
1 import type { PlanIDs } from 'proton-account/src/app/signup/interfaces';
3 import type { Autopay, PAYMENT_TOKEN_STATUS, WrappedCryptoPayment } from '@proton/components/payments/core';
4 import { PAYMENT_METHOD_TYPES } from '@proton/components/payments/core';
5 import type {
6     AmountAndCurrency,
7     BillingAddress,
8     BillingAddressProperty,
9     ChargeablePaymentParameters,
10     ExistingPayment,
11     SavedPaymentMethod,
12     TokenPayment,
13     TokenPaymentMethod,
14     V5PaymentToken,
15     WrappedCardPayment,
16     WrappedPaypalPayment,
17 } from '@proton/components/payments/core/interface';
18 import { isTokenPaymentMethod, isV5PaymentToken } from '@proton/components/payments/core/interface';
19 import type { INVOICE_OWNER, INVOICE_STATE, INVOICE_TYPE } from '@proton/shared/lib/constants';
20 import { FREE_PLAN } from '@proton/shared/lib/subscription/freePlans';
22 import type { ProductParam } from '../apps/product';
23 import { getProductHeaders } from '../apps/product';
24 import type { Api, Currency, Cycle, FreePlanDefault, Renew, Subscription } from '../interfaces';
26 export type PaymentsVersion = 'v4' | 'v5';
27 let paymentsVersion: PaymentsVersion = 'v4';
29 export function setPaymentsVersion(version: PaymentsVersion) {
30     paymentsVersion = version;
33 export function getPaymentsVersion(): PaymentsVersion {
34     return paymentsVersion;
37 export const queryFreePlan = (params?: QueryPlansParams) => ({
38     url: `payments/${paymentsVersion}/plans/default`,
39     method: 'get',
40     params,
41 });
43 export const getFreePlan = ({ api, currency }: { api: Api; currency?: Currency }) =>
44     api<{ Plans: FreePlanDefault }>(queryFreePlan(currency ? { Currency: currency } : undefined))
45         .then(({ Plans }): FreePlanDefault => {
46             return {
47                 ...Plans,
48                 MaxBaseSpace: Plans.MaxBaseSpace ?? Plans.MaxSpace,
49                 MaxBaseRewardSpace: Plans.MaxBaseRewardSpace ?? Plans.MaxRewardSpace,
50                 MaxDriveSpace: Plans.MaxDriveSpace ?? Plans.MaxSpace,
51                 MaxDriveRewardSpace: Plans.MaxDriveRewardSpace ?? Plans.MaxRewardSpace,
52             };
53         })
54         .catch(() => FREE_PLAN);
56 export const getSubscription = (forceVersion?: PaymentsVersion) => ({
57     url: `payments/${forceVersion ?? paymentsVersion}/subscription`,
58     method: 'get',
59 });
61 export interface FeedbackDowngradeData {
62     Reason?: string;
63     Feedback?: string;
64     ReasonDetails?: string;
65     Context?: 'vpn' | 'mail';
68 export const deleteSubscription = (data: FeedbackDowngradeData, version: PaymentsVersion) => ({
69     url: `payments/${version}/subscription`,
70     method: 'delete',
71     data,
72 });
74 export type CheckSubscriptionData = {
75     Plans: PlanIDs;
76     Currency: Currency;
77     Cycle: Cycle;
78     CouponCode?: string;
79     Codes?: string[];
80     /**
81      * For taxes
82      */
83     BillingAddress?: BillingAddress;
86 type CommonSubscribeData = {
87     Plans: PlanIDs;
88     Currency: Currency;
89     Cycle: Cycle;
90     Codes?: string[];
91 } & AmountAndCurrency;
93 type SubscribeDataV4 = CommonSubscribeData & TokenPaymentMethod & BillingAddressProperty;
94 type SubscribeDataV5 = CommonSubscribeData & V5PaymentToken & BillingAddressProperty;
95 type SubscribeDataNoPayment = CommonSubscribeData;
96 export type SubscribeData = SubscribeDataV4 | SubscribeDataV5 | SubscribeDataNoPayment;
98 function isCommonSubscribeData(data: any): data is CommonSubscribeData {
99     return !!data.Plans && !!data.Currency && !!data.Cycle && !!data.Amount && !!data.Currency;
102 function isSubscribeDataV4(data: any): data is SubscribeDataV4 {
103     return isCommonSubscribeData(data) && isTokenPaymentMethod(data);
106 function isSubscribeDataV5(data: any): data is SubscribeDataV5 {
107     return isCommonSubscribeData(data) && isV5PaymentToken(data);
110 function isSubscribeDataNoPayment(data: any): data is SubscribeDataNoPayment {
111     return isCommonSubscribeData(data);
114 export function isSubscribeData(data: any): data is SubscribeData {
115     return isSubscribeDataV4(data) || isSubscribeDataV5(data) || isSubscribeDataNoPayment(data);
118 function prepareSubscribeDataPayload(data: SubscribeData): SubscribeData {
119     const allowedProps: (keyof SubscribeDataV4 | keyof SubscribeDataV5)[] = [
120         'Plans',
121         'Currency',
122         'Cycle',
123         'Codes',
124         'PaymentToken',
125         'Payment',
126         'Amount',
127         'Currency',
128         'BillingAddress',
129     ];
130     const payload: any = {};
131     Object.keys(data).forEach((key: any) => {
132         if (allowedProps.includes(key)) {
133             payload[key] = (data as any)[key];
134         }
135     });
137     return payload as SubscribeData;
140 export const subscribe = (rawData: SubscribeData, product: ProductParam, version: PaymentsVersion) => {
141     const sanitizedData = prepareSubscribeDataPayload(rawData);
143     let data: SubscribeData = sanitizedData;
144     if (version === 'v5' && isSubscribeDataV4(sanitizedData)) {
145         const v5Data: SubscribeDataV5 = {
146             ...sanitizedData,
147             PaymentToken: sanitizedData.Payment.Details.Token,
148             v: 5,
149         };
151         data = v5Data;
152         delete (data as any).Payment;
153     } else if (version === 'v4' && isSubscribeDataV5(sanitizedData)) {
154         const v4Data: SubscribeDataV4 = {
155             ...sanitizedData,
156             Payment: {
157                 Type: PAYMENT_METHOD_TYPES.TOKEN,
158                 Details: {
159                     Token: sanitizedData.PaymentToken,
160                 },
161             },
162         };
164         data = v4Data;
165         delete (data as any).PaymentToken;
166     }
168     const config = {
169         url: `payments/${version}/subscription`,
170         method: 'post',
171         data,
172         headers: getProductHeaders(product, {
173             endpoint: `payments/${version}/subscription`,
174             product,
175         }),
176         timeout: 60000 * 2,
177     };
179     return config;
182 export enum InvoiceDocument {
183     Invoice = 'invoice',
184     CreditNote = 'credit_note',
185     CurrencyConversion = 'currency_conversion',
188 export interface QueryInvoicesParams {
189     /**
190      * Starts with 0
191      */
192     Page: number;
193     PageSize: number;
194     Owner: INVOICE_OWNER;
195     State?: INVOICE_STATE;
196     Type?: INVOICE_TYPE;
197     Document?: InvoiceDocument;
201  * Query list of invoices for the current user. The response is {@link InvoiceResponse}
202  */
203 export const queryInvoices = (params: QueryInvoicesParams, version?: PaymentsVersion) => ({
204     url: `payments/${version ?? paymentsVersion}/invoices`,
205     method: 'get',
206     params,
209 export interface QueryPlansParams {
210     Currency?: Currency;
213 export const queryPlans = (params?: QueryPlansParams, forceVersion?: PaymentsVersion) => ({
214     url: `payments/${forceVersion ?? paymentsVersion}/plans`,
215     method: 'get',
216     params,
219 export const getInvoice = (invoiceID: string, version: PaymentsVersion) => ({
220     url: `payments/${version}/invoices/${invoiceID}`,
221     method: 'get',
222     output: 'arrayBuffer',
225 export const checkInvoice = (invoiceID: string, version?: PaymentsVersion, GiftCode?: string) => ({
226     url: `payments/${version ?? paymentsVersion}/invoices/${invoiceID}/check`,
227     method: 'put',
228     data: { GiftCode },
231 export const queryPaymentMethods = (forceVersion?: PaymentsVersion) => ({
232     url: `payments/${forceVersion ?? paymentsVersion}/methods`,
233     method: 'get',
236 export type SetPaymentMethodDataV4 = TokenPayment & { Autopay?: Autopay };
238 export const setPaymentMethodV4 = (data: SetPaymentMethodDataV4) => ({
239     url: 'payments/v4/methods',
240     method: 'post',
241     data,
244 export type SetPaymentMethodDataV5 = V5PaymentToken & { Autopay?: Autopay };
245 export const setPaymentMethodV5 = (data: SetPaymentMethodDataV5) => ({
246     url: 'payments/v5/methods',
247     method: 'post',
248     data,
251 export interface UpdatePaymentMethodsData {
252     Autopay: Autopay;
255 export const updatePaymentMethod = (methodId: string, data: UpdatePaymentMethodsData, version: PaymentsVersion) => ({
256     url: `payments/${version}/methods/${methodId}`,
257     method: 'put',
258     data,
261 export const deletePaymentMethod = (methodID: string, version: PaymentsVersion) => ({
262     url: `payments/${version}/methods/${methodID}`,
263     method: 'delete',
267  * @param invoiceID
268  * @param data – does not have to include the payment token if user pays from the credits balance. In this case Amount
269  * must be set to 0 and payment token must not be supplied.
270  */
271 export const payInvoice = (
272     invoiceID: string,
273     data: (TokenPaymentMethod & AmountAndCurrency) | AmountAndCurrency,
274     version: PaymentsVersion
275 ) => ({
276     url: `payments/${version}/invoices/${invoiceID}`,
277     method: 'post',
278     data,
281 export const orderPaymentMethods = (PaymentMethodIDs: string[], version: PaymentsVersion) => ({
282     url: `payments/${version}/methods/order`,
283     method: 'put',
284     data: { PaymentMethodIDs },
287 export interface GiftCodeData {
288     GiftCode: string;
289     Amount: number;
292 export const buyCredit = (
293     data: (TokenPaymentMethod & AmountAndCurrency) | GiftCodeData | ChargeablePaymentParameters,
294     forceVersion: PaymentsVersion
295 ) => ({
296     url: `payments/${forceVersion ?? paymentsVersion}/credit`,
297     method: 'post',
298     data,
301 export interface ValidateCreditData {
302     GiftCode: string;
305 export const validateCredit = (data: ValidateCreditData, version: PaymentsVersion) => ({
306     url: `payments/${version ?? paymentsVersion}/credit/check`,
307     method: 'post',
308     data,
311 export type CreateBitcoinTokenData = AmountAndCurrency & WrappedCryptoPayment;
313 export type CreateTokenData =
314     | ((AmountAndCurrency | {}) & (WrappedPaypalPayment | WrappedCardPayment | ExistingPayment))
315     | CreateBitcoinTokenData;
317 export const createToken = (data: CreateTokenData, version: PaymentsVersion) => ({
318     url: `payments/${version}/tokens`,
319     method: 'post',
320     data,
323 export const createTokenV4 = (data: CreateTokenData) => createToken(data, 'v4');
324 export const createTokenV5 = (data: CreateTokenData) => createToken(data, 'v5');
326 export const getTokenStatus = (paymentToken: string, version: PaymentsVersion) => ({
327     url: `payments/${version}/tokens/${paymentToken}`,
328     method: 'get',
331 export const getTokenStatusV4 = (paymentToken: string) => getTokenStatus(paymentToken, 'v4');
332 export const getTokenStatusV5 = (paymentToken: string) => getTokenStatus(paymentToken, 'v5');
334 export const getLastCancelledSubscription = () => ({
335     url: `payments/${paymentsVersion}/subscription/latest`,
336     method: 'get',
339 export type RenewalStateData =
340     | {
341           RenewalState: Renew.Enabled;
342       }
343     | {
344           RenewalState: Renew.Disabled;
345           CancellationFeedback: FeedbackDowngradeData;
346       };
348 export const changeRenewState = (data: RenewalStateData, version: PaymentsVersion) => ({
349     url: `payments/${version}/subscription/renew`,
350     method: 'put',
351     data,
354 export type SubscribeV5Data = {
355     PaymentToken?: string;
356     Plans: PlanIDs;
357     Amount: number;
358     Currency: Currency;
359     Cycle: Cycle;
360     Codes?: string[];
363 export type CreatePaymentIntentPaypalData = AmountAndCurrency & {
364     Payment: {
365         Type: 'paypal';
366     };
369 export type CreatePaymentIntentCardData = AmountAndCurrency & {
370     Payment: {
371         Type: 'card';
372         Details: {
373             Bin: string;
374         };
375     };
378 export type CreatePaymentIntentSavedCardData = AmountAndCurrency & {
379     PaymentMethodID: string;
382 export type CreatePaymentIntentData =
383     | CreatePaymentIntentPaypalData
384     | CreatePaymentIntentCardData
385     | CreatePaymentIntentSavedCardData;
387 export const createPaymentIntentV5 = (data: CreatePaymentIntentData) => ({
388     url: `payments/v5/tokens`,
389     method: 'post',
390     data,
393 export type BackendPaymentIntent = {
394     ID: string;
395     Status: 'inited' | 'authorized';
396     Amount: number;
397     GatewayAccountID: string;
398     ExpiresAt: number;
399     PaymentMethodType: 'card' | 'paypal';
400     CreatedAt: number;
401     ModifiedAt: number;
402     UpdatedAt: number;
403     ResourceVersion: number;
404     Object: 'payment_intent';
405     CustomerID: string;
406     CurrencyCode: Currency;
407     Gateway: string;
408     ReferenceID: string;
411 export type FetchPaymentIntentV5Response = {
412     Token: string;
413     Status: PAYMENT_TOKEN_STATUS;
414     Data: BackendPaymentIntent;
417 export const fetchPaymentIntentV5 = (
418     api: Api,
419     data: CreatePaymentIntentData,
420     signal?: AbortSignal
421 ): Promise<FetchPaymentIntentV5Response> => {
422     return api<FetchPaymentIntentV5Response>({
423         ...createPaymentIntentV5(data),
424         signal,
425     });
428 export type FetchPaymentIntentForExistingV5Response = {
429     Token: string;
430     Status: PAYMENT_TOKEN_STATUS;
431     Data: BackendPaymentIntent | null;
434 export const fetchPaymentIntentForExistingV5 = (
435     api: Api,
436     data: CreatePaymentIntentData,
437     signal?: AbortSignal
438 ): Promise<FetchPaymentIntentForExistingV5Response> => {
439     return api<FetchPaymentIntentForExistingV5Response>({
440         ...createPaymentIntentV5(data),
441         signal,
442     });
445 export interface GetChargebeeConfigurationResponse {
446     Code: number;
447     Site: string;
448     PublishableKey: string;
449     Domain: string;
452 export const getChargebeeConfiguration = () => ({
453     url: `payments/v5/web-configuration`,
454     method: 'get',
457 // returns the ID. Or is it user's ID? hopefully.
458 // Call only if ChargebeeEnabled is set to 0 (the system already supports cb but this user was not migrated yet)
459 // Do not call for signups.
460 // Do not call if ChargebeeEnabled is undefined.
461 // If ChargebeeEnabled === 1 then always go to v5 and do not call this.
462 export const importAccount = () => ({
463     url: 'payments/v5/import',
464     method: 'post',
467 export const checkImport = () => ({
468     url: 'payments/v5/import',
469     method: 'head',
472 // no parameter, ideally. Always call before importAccount.
473 export const cleanupImport = () => ({
474     url: 'payments/v5/import',
475     method: 'delete',
478 export type GetSubscriptionResponse = {
479     Subscription: Subscription;
480     UpcomingSubscription?: Subscription;
483 export type GetPaymentMethodsResponse = {
484     PaymentMethods: SavedPaymentMethod[];