5 BillingAddressProperty,
6 ChargeablePaymentParameters,
17 } from '@proton/payments';
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`,
52 export const getFreePlan = ({ api, currency }: { api: Api; currency?: Currency }) =>
53 api<{ Plans: FreePlanDefault }>(queryFreePlan(currency ? { Currency: currency } : undefined))
54 .then(({ Plans }): FreePlanDefault => {
57 MaxBaseSpace: Plans.MaxBaseSpace ?? Plans.MaxSpace,
58 MaxBaseRewardSpace: Plans.MaxBaseRewardSpace ?? Plans.MaxRewardSpace,
59 MaxDriveSpace: Plans.MaxDriveSpace ?? Plans.MaxSpace,
60 MaxDriveRewardSpace: Plans.MaxDriveRewardSpace ?? Plans.MaxRewardSpace,
63 .catch(() => FREE_PLAN);
65 export const getSubscription = (forceVersion?: PaymentsVersion) => ({
66 url: `payments/${forceVersion ?? paymentsVersion}/subscription`,
70 export interface FeedbackDowngradeData {
73 ReasonDetails?: string;
74 Context?: 'vpn' | 'mail';
77 export const deleteSubscription = (data: FeedbackDowngradeData, version: PaymentsVersion) => ({
78 url: `payments/${version}/subscription`,
83 export enum ProrationMode {
88 export type CheckSubscriptionData = {
97 BillingAddress?: BillingAddress;
98 ProrationMode?: ProrationMode;
101 type CommonSubscribeData = {
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)[] = [
145 const payload: any = {};
146 Object.keys(data).forEach((key: any) => {
147 if (allowedProps.includes(key)) {
148 payload[key] = (data as any)[key];
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;
166 export const buyProduct = (rawData: SubscribeData, product: ProductParam) => {
167 const sanitizedData = prepareSubscribeDataPayload(rawData) as SubscribeDataV5;
169 const url = 'payments/v5/products';
175 PaymentToken: getPaymentTokenFromSubscribeData(sanitizedData),
176 ProductType: getLifetimeProductType(sanitizedData),
177 Amount: sanitizedData.Amount,
178 Currency: sanitizedData.Currency,
179 BillingAddress: sanitizedData.BillingAddress,
181 headers: getProductHeaders(product, {
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);
198 let data: SubscribeData = sanitizedData;
199 if (version === 'v5' && isSubscribeDataV4(sanitizedData)) {
200 const v5Data: SubscribeDataV5 = {
202 PaymentToken: sanitizedData.Payment.Details.Token,
207 delete (data as any).Payment;
208 } else if (version === 'v4' && isSubscribeDataV5(sanitizedData)) {
209 const v4Data: SubscribeDataV4 = {
212 Type: PAYMENT_METHOD_TYPES.TOKEN,
214 Token: sanitizedData.PaymentToken,
220 delete (data as any).PaymentToken;
224 url: `payments/${version}/subscription`,
227 headers: getProductHeaders(product, {
228 endpoint: `payments/${version}/subscription`,
237 export enum InvoiceDocument {
239 CreditNote = 'credit_note',
240 CurrencyConversion = 'currency_conversion',
243 export interface QueryInvoicesParams {
249 Owner: INVOICE_OWNER;
250 State?: INVOICE_STATE;
252 Document?: InvoiceDocument;
256 * Query list of invoices for the current user. The response is {@link InvoiceResponse}
258 export const queryInvoices = (params: QueryInvoicesParams, version?: PaymentsVersion) => ({
259 url: `payments/${version ?? paymentsVersion}/invoices`,
264 export interface QueryPlansParams {
268 export const queryPlans = (params?: QueryPlansParams) => ({
269 url: `payments/${paymentsVersion}/plans`,
274 export const getInvoice = (invoiceID: string, version: PaymentsVersion) => ({
275 url: `payments/${version}/invoices/${invoiceID}`,
277 output: 'arrayBuffer',
280 export const checkInvoice = (invoiceID: string, version?: PaymentsVersion, GiftCode?: string) => ({
281 url: `payments/${version ?? paymentsVersion}/invoices/${invoiceID}/check`,
286 export const queryPaymentMethods = (forceVersion?: PaymentsVersion) => ({
287 url: `payments/${forceVersion ?? paymentsVersion}/methods`,
291 export type SetPaymentMethodDataV4 = TokenPayment & { Autopay?: Autopay };
293 export const setPaymentMethodV4 = (data: SetPaymentMethodDataV4) => ({
294 url: 'payments/v4/methods',
299 export type SetPaymentMethodDataV5 = V5PaymentToken & { Autopay?: Autopay };
300 export const setPaymentMethodV5 = (data: SetPaymentMethodDataV5) => ({
301 url: 'payments/v5/methods',
306 export interface UpdatePaymentMethodsData {
310 export const updatePaymentMethod = (methodId: string, data: UpdatePaymentMethodsData, version: PaymentsVersion) => ({
311 url: `payments/${version}/methods/${methodId}`,
316 export const deletePaymentMethod = (methodID: string, version: PaymentsVersion) => ({
317 url: `payments/${version}/methods/${methodID}`,
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.
326 export const payInvoice = (
328 data: (TokenPaymentMethod & AmountAndCurrency) | AmountAndCurrency,
329 version: PaymentsVersion
331 url: `payments/${version}/invoices/${invoiceID}`,
336 export const orderPaymentMethods = (PaymentMethodIDs: string[], version: PaymentsVersion) => ({
337 url: `payments/${version}/methods/order`,
339 data: { PaymentMethodIDs },
342 export interface GiftCodeData {
347 export const buyCredit = (
348 data: (TokenPaymentMethod & AmountAndCurrency) | GiftCodeData | ChargeablePaymentParameters,
349 forceVersion: PaymentsVersion
351 url: `payments/${forceVersion ?? paymentsVersion}/credit`,
356 export interface ValidateCreditData {
360 export const validateCredit = (data: ValidateCreditData, version: PaymentsVersion) => ({
361 url: `payments/${version ?? paymentsVersion}/credit/check`,
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`,
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}`,
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`,
394 export type RenewalStateData =
396 RenewalState: Renew.Enabled;
399 RenewalState: Renew.Disabled;
400 CancellationFeedback: FeedbackDowngradeData;
403 export const changeRenewState = (data: RenewalStateData, version: PaymentsVersion) => ({
404 url: `payments/${version}/subscription/renew`,
409 export type SubscribeV5Data = {
410 PaymentToken?: string;
418 export type CreatePaymentIntentPaypalData = AmountAndCurrency & {
424 export type CreatePaymentIntentCardData = AmountAndCurrency & {
433 export type CreatePaymentIntentSavedCardData = AmountAndCurrency & {
434 PaymentMethodID: string;
437 export type CreatePaymentIntentDirectDebitData = AmountAndCurrency & {
439 Type: 'sepa_direct_debit';
446 export type CreatePaymentIntentData =
447 | CreatePaymentIntentPaypalData
448 | CreatePaymentIntentCardData
449 | CreatePaymentIntentSavedCardData
450 | CreatePaymentIntentDirectDebitData;
452 export const createPaymentIntentV5 = (data: CreatePaymentIntentData) => ({
453 url: `payments/v5/tokens`,
458 export type BackendPaymentIntent = {
460 Status: 'inited' | 'authorized';
462 GatewayAccountID: string;
464 PaymentMethodType: 'card' | 'paypal';
468 ResourceVersion: number;
469 Object: 'payment_intent';
471 CurrencyCode: Currency;
476 export type FetchPaymentIntentV5Response = {
478 Status: PAYMENT_TOKEN_STATUS;
479 Data: BackendPaymentIntent;
482 export const fetchPaymentIntentV5 = (
484 data: CreatePaymentIntentData,
486 ): Promise<FetchPaymentIntentV5Response> => {
487 return api<FetchPaymentIntentV5Response>({
488 ...createPaymentIntentV5(data),
493 export type FetchPaymentIntentForExistingV5Response = {
495 Status: PAYMENT_TOKEN_STATUS;
496 Data: BackendPaymentIntent | null;
499 export const fetchPaymentIntentForExistingV5 = (
501 data: CreatePaymentIntentData,
503 ): Promise<FetchPaymentIntentForExistingV5Response> => {
504 return api<FetchPaymentIntentForExistingV5Response>({
505 ...createPaymentIntentV5(data),
510 export interface GetChargebeeConfigurationResponse {
513 PublishableKey: string;
517 export const getChargebeeConfiguration = () => ({
518 url: `payments/v5/web-configuration`,
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',
532 export const checkImport = () => ({
533 url: 'payments/v5/import',
537 // no parameter, ideally. Always call before importAccount.
538 export const cleanupImport = () => ({
539 url: 'payments/v5/import',
543 export type GetSubscriptionResponse = {
544 Subscription: Subscription;
545 UpcomingSubscription?: Subscription;
548 export type GetPaymentMethodsResponse = {
549 PaymentMethods: SavedPaymentMethod[];