@blocklet/payment-react-headless 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/.eslintrc.js +18 -0
  2. package/build.config.ts +30 -0
  3. package/es/checkout/context/CheckoutProvider.d.ts +6 -0
  4. package/es/checkout/context/CheckoutProvider.js +209 -0
  5. package/es/checkout/context/CustomerFormContext.d.ts +4 -0
  6. package/es/checkout/context/CustomerFormContext.js +9 -0
  7. package/es/checkout/context/ExchangeRateContext.d.ts +11 -0
  8. package/es/checkout/context/ExchangeRateContext.js +9 -0
  9. package/es/checkout/context/PaymentMethodContext.d.ts +26 -0
  10. package/es/checkout/context/PaymentMethodContext.js +9 -0
  11. package/es/checkout/context/SessionContext.d.ts +45 -0
  12. package/es/checkout/context/SessionContext.js +9 -0
  13. package/es/checkout/context/SubmitContext.d.ts +4 -0
  14. package/es/checkout/context/SubmitContext.js +9 -0
  15. package/es/checkout/context/index.d.ts +6 -0
  16. package/es/checkout/context/index.js +6 -0
  17. package/es/checkout/core/billingInterval.d.ts +15 -0
  18. package/es/checkout/core/billingInterval.js +36 -0
  19. package/es/checkout/core/crossSell.d.ts +4 -0
  20. package/es/checkout/core/crossSell.js +30 -0
  21. package/es/checkout/core/customerForm.d.ts +5 -0
  22. package/es/checkout/core/customerForm.js +105 -0
  23. package/es/checkout/core/exchangeRate.d.ts +11 -0
  24. package/es/checkout/core/exchangeRate.js +25 -0
  25. package/es/checkout/core/index.d.ts +10 -0
  26. package/es/checkout/core/index.js +55 -0
  27. package/es/checkout/core/lineItems.d.ts +7 -0
  28. package/es/checkout/core/lineItems.js +59 -0
  29. package/es/checkout/core/paymentMethod.d.ts +23 -0
  30. package/es/checkout/core/paymentMethod.js +85 -0
  31. package/es/checkout/core/pricing.d.ts +32 -0
  32. package/es/checkout/core/pricing.js +221 -0
  33. package/es/checkout/core/promotion.d.ts +10 -0
  34. package/es/checkout/core/promotion.js +39 -0
  35. package/es/checkout/core/session.d.ts +26 -0
  36. package/es/checkout/core/session.js +50 -0
  37. package/es/checkout/core/submit.d.ts +40 -0
  38. package/es/checkout/core/submit.js +66 -0
  39. package/es/checkout/hooks/index.d.ts +34 -0
  40. package/es/checkout/hooks/index.js +19 -0
  41. package/es/checkout/hooks/useBillingInterval.d.ts +14 -0
  42. package/es/checkout/hooks/useBillingInterval.js +50 -0
  43. package/es/checkout/hooks/useCheckout.d.ts +2 -0
  44. package/es/checkout/hooks/useCheckout.js +212 -0
  45. package/es/checkout/hooks/useCheckoutSession.d.ts +58 -0
  46. package/es/checkout/hooks/useCheckoutSession.js +107 -0
  47. package/es/checkout/hooks/useCheckoutStatus.d.ts +10 -0
  48. package/es/checkout/hooks/useCheckoutStatus.js +16 -0
  49. package/es/checkout/hooks/useCrossSell.d.ts +8 -0
  50. package/es/checkout/hooks/useCrossSell.js +57 -0
  51. package/es/checkout/hooks/useCustomerForm.d.ts +14 -0
  52. package/es/checkout/hooks/useCustomerForm.js +116 -0
  53. package/es/checkout/hooks/useCustomerFormFeature.d.ts +2 -0
  54. package/es/checkout/hooks/useCustomerFormFeature.js +4 -0
  55. package/es/checkout/hooks/useExchangeRate.d.ts +11 -0
  56. package/es/checkout/hooks/useExchangeRate.js +15 -0
  57. package/es/checkout/hooks/useLineItems.d.ts +22 -0
  58. package/es/checkout/hooks/useLineItems.js +139 -0
  59. package/es/checkout/hooks/usePaymentMethod.d.ts +26 -0
  60. package/es/checkout/hooks/usePaymentMethod.js +101 -0
  61. package/es/checkout/hooks/usePaymentMethodFeature.d.ts +2 -0
  62. package/es/checkout/hooks/usePaymentMethodFeature.js +4 -0
  63. package/es/checkout/hooks/usePricing.d.ts +57 -0
  64. package/es/checkout/hooks/usePricing.js +174 -0
  65. package/es/checkout/hooks/usePricingFeature.d.ts +28 -0
  66. package/es/checkout/hooks/usePricingFeature.js +36 -0
  67. package/es/checkout/hooks/useProduct.d.ts +32 -0
  68. package/es/checkout/hooks/useProduct.js +5 -0
  69. package/es/checkout/hooks/usePromotion.d.ts +12 -0
  70. package/es/checkout/hooks/usePromotion.js +48 -0
  71. package/es/checkout/hooks/useSlippage.d.ts +8 -0
  72. package/es/checkout/hooks/useSlippage.js +29 -0
  73. package/es/checkout/hooks/useSubmit.d.ts +38 -0
  74. package/es/checkout/hooks/useSubmit.js +493 -0
  75. package/es/checkout/hooks/useSubmitFeature.d.ts +2 -0
  76. package/es/checkout/hooks/useSubmitFeature.js +4 -0
  77. package/es/checkout/hooks/useUpsell.d.ts +5 -0
  78. package/es/checkout/hooks/useUpsell.js +25 -0
  79. package/es/checkout/index.d.ts +37 -0
  80. package/es/checkout/index.js +28 -0
  81. package/es/checkout/types.d.ts +262 -0
  82. package/es/checkout/types.js +0 -0
  83. package/es/index.d.ts +1 -0
  84. package/es/index.js +28 -0
  85. package/es/shared/api.d.ts +41 -0
  86. package/es/shared/api.js +81 -0
  87. package/es/shared/format.d.ts +38 -0
  88. package/es/shared/format.js +229 -0
  89. package/es/shared/polling.d.ts +15 -0
  90. package/es/shared/polling.js +20 -0
  91. package/es/shared/types.d.ts +10 -0
  92. package/es/shared/types.js +0 -0
  93. package/es/shared/validation.d.ts +38 -0
  94. package/es/shared/validation.js +190 -0
  95. package/es/types/checkout-augmented.d.ts +42 -0
  96. package/es/types/checkout-augmented.js +17 -0
  97. package/es/types/external.d.ts +18 -0
  98. package/examples/01-basic-checkout.tsx +159 -0
  99. package/examples/01-credit-recharge.tsx +19 -0
  100. package/examples/02-subscription.tsx +40 -0
  101. package/examples/03-upsell.tsx +60 -0
  102. package/examples/04-cross-sell.tsx +54 -0
  103. package/examples/05-full-checkout.tsx +126 -0
  104. package/jest.config.js +15 -0
  105. package/lib/checkout/context/CheckoutProvider.d.ts +6 -0
  106. package/lib/checkout/context/CheckoutProvider.js +181 -0
  107. package/lib/checkout/context/CustomerFormContext.d.ts +4 -0
  108. package/lib/checkout/context/CustomerFormContext.js +16 -0
  109. package/lib/checkout/context/ExchangeRateContext.d.ts +11 -0
  110. package/lib/checkout/context/ExchangeRateContext.js +16 -0
  111. package/lib/checkout/context/PaymentMethodContext.d.ts +26 -0
  112. package/lib/checkout/context/PaymentMethodContext.js +16 -0
  113. package/lib/checkout/context/SessionContext.d.ts +45 -0
  114. package/lib/checkout/context/SessionContext.js +16 -0
  115. package/lib/checkout/context/SubmitContext.d.ts +4 -0
  116. package/lib/checkout/context/SubmitContext.js +16 -0
  117. package/lib/checkout/context/index.d.ts +6 -0
  118. package/lib/checkout/context/index.js +77 -0
  119. package/lib/checkout/core/billingInterval.d.ts +15 -0
  120. package/lib/checkout/core/billingInterval.js +42 -0
  121. package/lib/checkout/core/crossSell.d.ts +4 -0
  122. package/lib/checkout/core/crossSell.js +43 -0
  123. package/lib/checkout/core/customerForm.d.ts +5 -0
  124. package/lib/checkout/core/customerForm.js +106 -0
  125. package/lib/checkout/core/exchangeRate.d.ts +11 -0
  126. package/lib/checkout/core/exchangeRate.js +45 -0
  127. package/lib/checkout/core/index.d.ts +10 -0
  128. package/lib/checkout/core/index.js +297 -0
  129. package/lib/checkout/core/lineItems.d.ts +7 -0
  130. package/lib/checkout/core/lineItems.js +76 -0
  131. package/lib/checkout/core/paymentMethod.d.ts +23 -0
  132. package/lib/checkout/core/paymentMethod.js +114 -0
  133. package/lib/checkout/core/pricing.d.ts +32 -0
  134. package/lib/checkout/core/pricing.js +216 -0
  135. package/lib/checkout/core/promotion.d.ts +10 -0
  136. package/lib/checkout/core/promotion.js +62 -0
  137. package/lib/checkout/core/session.d.ts +26 -0
  138. package/lib/checkout/core/session.js +58 -0
  139. package/lib/checkout/core/submit.d.ts +40 -0
  140. package/lib/checkout/core/submit.js +84 -0
  141. package/lib/checkout/hooks/index.d.ts +34 -0
  142. package/lib/checkout/hooks/index.js +138 -0
  143. package/lib/checkout/hooks/useBillingInterval.d.ts +14 -0
  144. package/lib/checkout/hooks/useBillingInterval.js +63 -0
  145. package/lib/checkout/hooks/useCheckout.d.ts +2 -0
  146. package/lib/checkout/hooks/useCheckout.js +190 -0
  147. package/lib/checkout/hooks/useCheckoutSession.d.ts +58 -0
  148. package/lib/checkout/hooks/useCheckoutSession.js +119 -0
  149. package/lib/checkout/hooks/useCheckoutStatus.d.ts +10 -0
  150. package/lib/checkout/hooks/useCheckoutStatus.js +28 -0
  151. package/lib/checkout/hooks/useCrossSell.d.ts +8 -0
  152. package/lib/checkout/hooks/useCrossSell.js +75 -0
  153. package/lib/checkout/hooks/useCustomerForm.d.ts +14 -0
  154. package/lib/checkout/hooks/useCustomerForm.js +135 -0
  155. package/lib/checkout/hooks/useCustomerFormFeature.d.ts +2 -0
  156. package/lib/checkout/hooks/useCustomerFormFeature.js +10 -0
  157. package/lib/checkout/hooks/useExchangeRate.d.ts +11 -0
  158. package/lib/checkout/hooks/useExchangeRate.js +29 -0
  159. package/lib/checkout/hooks/useLineItems.d.ts +22 -0
  160. package/lib/checkout/hooks/useLineItems.js +142 -0
  161. package/lib/checkout/hooks/usePaymentMethod.d.ts +26 -0
  162. package/lib/checkout/hooks/usePaymentMethod.js +101 -0
  163. package/lib/checkout/hooks/usePaymentMethodFeature.d.ts +2 -0
  164. package/lib/checkout/hooks/usePaymentMethodFeature.js +10 -0
  165. package/lib/checkout/hooks/usePricing.d.ts +57 -0
  166. package/lib/checkout/hooks/usePricing.js +168 -0
  167. package/lib/checkout/hooks/usePricingFeature.d.ts +28 -0
  168. package/lib/checkout/hooks/usePricingFeature.js +48 -0
  169. package/lib/checkout/hooks/useProduct.d.ts +32 -0
  170. package/lib/checkout/hooks/useProduct.js +21 -0
  171. package/lib/checkout/hooks/usePromotion.d.ts +12 -0
  172. package/lib/checkout/hooks/usePromotion.js +57 -0
  173. package/lib/checkout/hooks/useSlippage.d.ts +8 -0
  174. package/lib/checkout/hooks/useSlippage.js +39 -0
  175. package/lib/checkout/hooks/useSubmit.d.ts +38 -0
  176. package/lib/checkout/hooks/useSubmit.js +504 -0
  177. package/lib/checkout/hooks/useSubmitFeature.d.ts +2 -0
  178. package/lib/checkout/hooks/useSubmitFeature.js +10 -0
  179. package/lib/checkout/hooks/useUpsell.d.ts +5 -0
  180. package/lib/checkout/hooks/useUpsell.js +40 -0
  181. package/lib/checkout/index.d.ts +37 -0
  182. package/lib/checkout/index.js +182 -0
  183. package/lib/checkout/types.d.ts +262 -0
  184. package/lib/checkout/types.js +1 -0
  185. package/lib/index.d.ts +1 -0
  186. package/lib/index.js +162 -0
  187. package/lib/shared/api.d.ts +41 -0
  188. package/lib/shared/api.js +88 -0
  189. package/lib/shared/format.d.ts +38 -0
  190. package/lib/shared/format.js +262 -0
  191. package/lib/shared/polling.d.ts +15 -0
  192. package/lib/shared/polling.js +32 -0
  193. package/lib/shared/types.d.ts +10 -0
  194. package/lib/shared/types.js +1 -0
  195. package/lib/shared/validation.d.ts +38 -0
  196. package/lib/shared/validation.js +212 -0
  197. package/lib/types/checkout-augmented.d.ts +42 -0
  198. package/lib/types/checkout-augmented.js +24 -0
  199. package/lib/types/external.d.ts +18 -0
  200. package/package.json +64 -0
  201. package/src/checkout/context/CheckoutProvider.tsx +269 -0
  202. package/src/checkout/context/CustomerFormContext.ts +14 -0
  203. package/src/checkout/context/ExchangeRateContext.ts +21 -0
  204. package/src/checkout/context/PaymentMethodContext.ts +36 -0
  205. package/src/checkout/context/SessionContext.ts +49 -0
  206. package/src/checkout/context/SubmitContext.ts +14 -0
  207. package/src/checkout/context/index.ts +6 -0
  208. package/src/checkout/core/billingInterval.ts +62 -0
  209. package/src/checkout/core/crossSell.ts +52 -0
  210. package/src/checkout/core/customerForm.ts +122 -0
  211. package/src/checkout/core/exchangeRate.ts +38 -0
  212. package/src/checkout/core/index.ts +60 -0
  213. package/src/checkout/core/lineItems.ts +106 -0
  214. package/src/checkout/core/paymentMethod.ts +113 -0
  215. package/src/checkout/core/pricing.ts +347 -0
  216. package/src/checkout/core/promotion.ts +59 -0
  217. package/src/checkout/core/session.ts +62 -0
  218. package/src/checkout/core/submit.ts +109 -0
  219. package/src/checkout/hooks/index.ts +41 -0
  220. package/src/checkout/hooks/useBillingInterval.ts +71 -0
  221. package/src/checkout/hooks/useCheckout.ts +267 -0
  222. package/src/checkout/hooks/useCheckoutSession.ts +217 -0
  223. package/src/checkout/hooks/useCheckoutStatus.ts +31 -0
  224. package/src/checkout/hooks/useCrossSell.ts +80 -0
  225. package/src/checkout/hooks/useCustomerForm.ts +156 -0
  226. package/src/checkout/hooks/useCustomerFormFeature.ts +7 -0
  227. package/src/checkout/hooks/useExchangeRate.ts +28 -0
  228. package/src/checkout/hooks/useLineItems.ts +191 -0
  229. package/src/checkout/hooks/usePaymentMethod.ts +165 -0
  230. package/src/checkout/hooks/usePaymentMethodFeature.ts +8 -0
  231. package/src/checkout/hooks/usePricing.ts +274 -0
  232. package/src/checkout/hooks/usePricingFeature.ts +73 -0
  233. package/src/checkout/hooks/useProduct.ts +32 -0
  234. package/src/checkout/hooks/usePromotion.ts +67 -0
  235. package/src/checkout/hooks/useSlippage.ts +39 -0
  236. package/src/checkout/hooks/useSubmit.ts +684 -0
  237. package/src/checkout/hooks/useSubmitFeature.ts +7 -0
  238. package/src/checkout/hooks/useUpsell.ts +35 -0
  239. package/src/checkout/index.ts +65 -0
  240. package/src/checkout/types.ts +292 -0
  241. package/src/index.ts +64 -0
  242. package/src/shared/api.ts +118 -0
  243. package/src/shared/format.ts +318 -0
  244. package/src/shared/polling.ts +49 -0
  245. package/src/shared/types.ts +13 -0
  246. package/src/shared/validation.ts +254 -0
  247. package/src/types/checkout-augmented.ts +77 -0
  248. package/src/types/external.d.ts +18 -0
  249. package/tools/jest.js +1 -0
  250. package/tsconfig.json +18 -0
@@ -0,0 +1,36 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { TPaymentMethodExpanded, TPaymentCurrency } from '@blocklet/payment-types';
3
+
4
+ export interface PaymentMethodContextValue {
5
+ current: TPaymentMethodExpanded;
6
+ currency: TPaymentCurrency;
7
+ available: TPaymentMethodExpanded[];
8
+ currencies: TPaymentCurrency[];
9
+ isStripe: boolean;
10
+ isCrypto: boolean;
11
+ isCredit: boolean;
12
+ switching: boolean;
13
+ setType: (type: 'stripe' | 'crypto') => Promise<void>;
14
+ types: Array<{
15
+ type: 'stripe' | 'crypto';
16
+ label: string;
17
+ currencies: TPaymentCurrency[];
18
+ active: boolean;
19
+ }>;
20
+ setCurrency: (currencyId: string) => Promise<void>;
21
+ stripe: {
22
+ publishableKey: string | null;
23
+ clientSecret: string | null;
24
+ status: 'idle' | 'ready' | 'processing' | 'succeeded' | 'failed';
25
+ } | null;
26
+ }
27
+
28
+ export const PaymentMethodContext = createContext<PaymentMethodContextValue | null>(null);
29
+
30
+ export function usePaymentMethodContext(): PaymentMethodContextValue {
31
+ const ctx = useContext(PaymentMethodContext);
32
+ if (!ctx) {
33
+ throw new Error('usePaymentMethodContext must be used within <CheckoutProvider>');
34
+ }
35
+ return ctx;
36
+ }
@@ -0,0 +1,49 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { TLineItemExpanded, TCheckoutSessionExpanded } from '@blocklet/payment-types';
3
+ import type { SessionData } from '../hooks/useCheckoutSession';
4
+
5
+ export interface SessionContextValue {
6
+ sessionData: SessionData | null;
7
+ sessionId: string;
8
+ effectiveSessionId: string;
9
+ isLoading: boolean;
10
+ error: string | null;
11
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
12
+ refresh: (forceRefresh?: boolean) => Promise<void>;
13
+ items: TLineItemExpanded[];
14
+ session: TCheckoutSessionExpanded | null | undefined;
15
+ isDonation: boolean;
16
+ vendorCount: number;
17
+ product: {
18
+ name: string;
19
+ description: string;
20
+ images: string[];
21
+ features: Array<{ name: string; icon?: string }>;
22
+ billing: {
23
+ mode: 'payment' | 'subscription';
24
+ interval: 'month' | 'year' | 'week' | 'day' | null;
25
+ intervalCount: number;
26
+ displayInterval: string;
27
+ };
28
+ metadata: Record<string, string>;
29
+ } | null;
30
+ subscription: {
31
+ mode: 'payment' | 'subscription' | 'setup';
32
+ showStake: boolean;
33
+ confirmMessage: string;
34
+ } | null;
35
+ pageInfo: {
36
+ formPurposeDescription?: { en: string; zh: string };
37
+ showProductFeatures: boolean;
38
+ } | null;
39
+ }
40
+
41
+ export const SessionContext = createContext<SessionContextValue | null>(null);
42
+
43
+ export function useSessionContext(): SessionContextValue {
44
+ const ctx = useContext(SessionContext);
45
+ if (!ctx) {
46
+ throw new Error('useSessionContext must be used within <CheckoutProvider>');
47
+ }
48
+ return ctx;
49
+ }
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { UseSubmitReturn } from '../hooks/useSubmit';
3
+
4
+ export type SubmitContextValue = UseSubmitReturn;
5
+
6
+ export const SubmitReactContext = createContext<SubmitContextValue | null>(null);
7
+
8
+ export function useSubmitContext(): SubmitContextValue {
9
+ const ctx = useContext(SubmitReactContext);
10
+ if (!ctx) {
11
+ throw new Error('useSubmitContext must be used within <CheckoutProvider>');
12
+ }
13
+ return ctx;
14
+ }
@@ -0,0 +1,6 @@
1
+ export { CheckoutProvider, type CheckoutProviderProps } from './CheckoutProvider';
2
+ export { SessionContext, useSessionContext, type SessionContextValue } from './SessionContext';
3
+ export { PaymentMethodContext, usePaymentMethodContext, type PaymentMethodContextValue } from './PaymentMethodContext';
4
+ export { ExchangeRateContext, useExchangeRateContext, type ExchangeRateContextValue } from './ExchangeRateContext';
5
+ export { CustomerFormContext, useCustomerFormContext, type CustomerFormContextValue } from './CustomerFormContext';
6
+ export { SubmitReactContext, useSubmitContext, type SubmitContextValue } from './SubmitContext';
@@ -0,0 +1,62 @@
1
+ import type { TLineItemExpanded } from '@blocklet/payment-types';
2
+ import type { UpsellExpanded } from '../../types/checkout-augmented';
3
+
4
+ export type BillingIntervalType = 'month' | 'year' | 'week' | 'day';
5
+
6
+ export interface BillingIntervalOption {
7
+ interval: BillingIntervalType;
8
+ priceId: string;
9
+ amount: string;
10
+ savings: string | null;
11
+ }
12
+
13
+ export interface BillingIntervalInfo {
14
+ current: BillingIntervalType | null;
15
+ available: BillingIntervalOption[];
16
+ /** The first item used to derive billing info */
17
+ firstItem: TLineItemExpanded;
18
+ }
19
+
20
+ export function parseBillingInterval(items: TLineItemExpanded[]): BillingIntervalInfo | null {
21
+ if (!items.length) return null;
22
+
23
+ const firstItem = items[0];
24
+ const price = firstItem.upsell_price || firstItem.price;
25
+ const recurring = price?.recurring;
26
+ if (!recurring) return null;
27
+
28
+ const currentInterval = recurring.interval as BillingIntervalType | null;
29
+
30
+ const upsell = price?.upsell as UpsellExpanded | undefined;
31
+ const upsellTo = upsell?.upsells_to;
32
+ const available: BillingIntervalOption[] = [];
33
+
34
+ if (upsellTo?.recurring?.interval && upsellTo.recurring.interval !== currentInterval) {
35
+ available.push({
36
+ interval: upsellTo.recurring.interval as BillingIntervalType,
37
+ priceId: upsell?.upsells_to_id || '',
38
+ amount: upsellTo.unit_amount || '0',
39
+ savings: null,
40
+ });
41
+ }
42
+
43
+ if (firstItem.upsell_price_id && firstItem.price?.recurring) {
44
+ const origInterval = firstItem.price.recurring.interval;
45
+ if (origInterval && origInterval !== currentInterval) {
46
+ available.push({
47
+ interval: origInterval as BillingIntervalType,
48
+ priceId: firstItem.price_id,
49
+ amount: firstItem.price.unit_amount || '0',
50
+ savings: null,
51
+ });
52
+ }
53
+ }
54
+
55
+ if (available.length === 0) return null;
56
+
57
+ return {
58
+ current: currentInterval,
59
+ available,
60
+ firstItem,
61
+ };
62
+ }
@@ -0,0 +1,52 @@
1
+ import type { TCheckoutSessionExpanded, TPrice } from '@blocklet/payment-types';
2
+
3
+ import api, { API } from '../../shared/api';
4
+ import { recalculatePromotionIfNeeded } from './lineItems';
5
+
6
+ export async function addCrossSellItem(
7
+ sessionId: string,
8
+ crossSellItemId: string,
9
+ session: TCheckoutSessionExpanded | undefined | null,
10
+ currencyId: string | null | undefined,
11
+ refresh: (force?: boolean) => Promise<void>
12
+ ): Promise<void> {
13
+ await api.put(API.CROSS_SELL(sessionId), { to: crossSellItemId });
14
+ await recalculatePromotionIfNeeded(session, sessionId, currencyId);
15
+ await refresh(true);
16
+ }
17
+
18
+ export async function removeCrossSellItem(
19
+ sessionId: string,
20
+ session: TCheckoutSessionExpanded | undefined | null,
21
+ currencyId: string | null | undefined,
22
+ refresh: (force?: boolean) => Promise<void>
23
+ ): Promise<void> {
24
+ await api.delete(API.CROSS_SELL(sessionId));
25
+ await recalculatePromotionIfNeeded(session, sessionId, currencyId);
26
+ await refresh(true);
27
+ }
28
+
29
+ // Dedup concurrent fetchCrossSellItem calls (multiple useLineItems instances share one in-flight request)
30
+ const pendingFetches = new Map<string, Promise<TPrice | null>>();
31
+
32
+ export function fetchCrossSellItem(sessionId: string): Promise<TPrice | null> {
33
+ const existing = pendingFetches.get(sessionId);
34
+ if (existing) return existing;
35
+
36
+ const promise = (async (): Promise<TPrice | null> => {
37
+ try {
38
+ const { data } = await api.get(`${API.CROSS_SELL(sessionId)}?skipError=true`);
39
+ if (data && !data.error) {
40
+ return data as TPrice;
41
+ }
42
+ } catch {
43
+ // Ignore cross-sell fetch errors
44
+ }
45
+ return null;
46
+ })();
47
+
48
+ pendingFetches.set(sessionId, promise);
49
+ promise.finally(() => pendingFetches.delete(sessionId));
50
+
51
+ return promise;
52
+ }
@@ -0,0 +1,122 @@
1
+ import type { TCheckoutSessionExpanded, TCustomer } from '@blocklet/payment-types';
2
+
3
+ import type { FieldConfig, CheckoutFormData } from '../types';
4
+
5
+ export function buildFields(session: TCheckoutSessionExpanded | undefined | null): FieldConfig[] {
6
+ const fields: FieldConfig[] = [];
7
+
8
+ fields.push({
9
+ name: 'customer_name',
10
+ type: 'text',
11
+ required: true,
12
+ });
13
+
14
+ fields.push({
15
+ name: 'customer_email',
16
+ type: 'email',
17
+ required: true,
18
+ });
19
+
20
+ if (session?.phone_number_collection?.enabled) {
21
+ fields.push({
22
+ name: 'customer_phone',
23
+ type: 'phone',
24
+ required: true,
25
+ });
26
+ }
27
+
28
+ const addressMode = session?.billing_address_collection;
29
+ if (addressMode === 'required') {
30
+ // Full address form — all fields required
31
+ fields.push(
32
+ {
33
+ name: 'billing_address.country',
34
+ type: 'country',
35
+ required: true,
36
+ group: 'address',
37
+ },
38
+ {
39
+ name: 'billing_address.line1',
40
+ type: 'text',
41
+ required: true,
42
+ group: 'address',
43
+ },
44
+ {
45
+ name: 'billing_address.line2',
46
+ type: 'text',
47
+ required: false,
48
+ group: 'address',
49
+ },
50
+ {
51
+ name: 'billing_address.city',
52
+ type: 'text',
53
+ required: true,
54
+ group: 'address',
55
+ },
56
+ {
57
+ name: 'billing_address.state',
58
+ type: 'text',
59
+ required: true,
60
+ group: 'address',
61
+ },
62
+ {
63
+ name: 'billing_address.postal_code',
64
+ type: 'text',
65
+ required: true,
66
+ group: 'address',
67
+ }
68
+ );
69
+ } else if (addressMode === 'auto') {
70
+ // Auto mode — only state and postal_code (country shown as prefix in postal code field)
71
+ fields.push(
72
+ {
73
+ name: 'billing_address.state',
74
+ type: 'text',
75
+ required: false,
76
+ group: 'address',
77
+ },
78
+ {
79
+ name: 'billing_address.postal_code',
80
+ type: 'text',
81
+ required: false,
82
+ group: 'address',
83
+ }
84
+ );
85
+ }
86
+
87
+ return fields;
88
+ }
89
+
90
+ export function createInitialValues(
91
+ session: TCheckoutSessionExpanded | undefined | null,
92
+ customer: TCustomer | undefined | null
93
+ ): CheckoutFormData {
94
+ return {
95
+ customer_name: customer?.name || '',
96
+ customer_email: (session?.customer_details as { email?: string } | undefined)?.email || customer?.email || '',
97
+ customer_phone: customer?.phone || '',
98
+ payment_method: '',
99
+ payment_currency: session?.currency_id || '',
100
+ billing_address: {
101
+ country: customer?.address?.country || '',
102
+ state: customer?.address?.state || '',
103
+ city: customer?.address?.city || '',
104
+ line1: customer?.address?.line1 || '',
105
+ line2: customer?.address?.line2 || '',
106
+ postal_code: customer?.address?.postal_code || '',
107
+ },
108
+ };
109
+ }
110
+
111
+ export function setNestedValue<T extends Record<string, unknown>>(obj: T, path: string, value: unknown): T {
112
+ const parts = path.split('.');
113
+ if (parts.length === 1) {
114
+ return { ...obj, [path]: value };
115
+ }
116
+
117
+ const [head, ...rest] = parts;
118
+ return {
119
+ ...obj,
120
+ [head]: setNestedValue((obj[head] || {}) as Record<string, unknown>, rest.join('.'), value),
121
+ } as T;
122
+ }
@@ -0,0 +1,38 @@
1
+ import type { TLineItemExpanded } from '@blocklet/payment-types';
2
+
3
+ import api, { API } from '../../shared/api';
4
+
5
+ // Rate polling intervals
6
+ export const BASE_INTERVAL = 30_000;
7
+ export const MAX_INTERVAL = 120_000;
8
+
9
+ export function checkHasDynamicPricing(items: TLineItemExpanded[]): boolean {
10
+ return items.some((item) => (item.upsell_price || item.price)?.pricing_type === 'dynamic');
11
+ }
12
+
13
+ export async function fetchExchangeRate(
14
+ sessionId: string,
15
+ currencyId: string | null | undefined
16
+ ): Promise<{ rate: string | null; provider: string | null; providerDisplay: string | null; fetchedAt: number | null }> {
17
+ const { data } = await api.get(API.EXCHANGE_RATE(sessionId), {
18
+ params: currencyId ? { currency_id: currencyId } : undefined,
19
+ });
20
+
21
+ const rateValue = data.rate || data.exchange_rate;
22
+ if (rateValue) {
23
+ // provider_display is preferred (e.g., "token-data (2 sources)")
24
+ // fallback: provider_name → provider_id → null
25
+ const providerDisplay = data.provider_display || data.provider_name || data.provider_id || null;
26
+ return {
27
+ rate: rateValue,
28
+ provider: data.provider_name || data.rate_provider_name || null,
29
+ providerDisplay,
30
+ fetchedAt: data.fetched_at || data.timestamp_ms || null,
31
+ };
32
+ }
33
+ return { rate: null, provider: null, providerDisplay: null, fetchedAt: null };
34
+ }
35
+
36
+ export function getBackoffInterval(failCount: number): number {
37
+ return Math.min(BASE_INTERVAL * 2 ** failCount, MAX_INTERVAL);
38
+ }
@@ -0,0 +1,60 @@
1
+ export { parseProduct, parseSubscription, parsePageInfo } from './session';
2
+ export {
3
+ flattenCurrencies,
4
+ hasDidWallet,
5
+ getInitialCurrencyId,
6
+ findMethodAndCurrency,
7
+ buildPaymentTypes,
8
+ } from './paymentMethod';
9
+ export {
10
+ BASE_INTERVAL,
11
+ MAX_INTERVAL,
12
+ checkHasDynamicPricing,
13
+ fetchExchangeRate,
14
+ getBackoffInterval,
15
+ } from './exchangeRate';
16
+ export {
17
+ calculateAmounts,
18
+ calculateQuoteMeta,
19
+ calculateTrial,
20
+ type CalculatedAmounts,
21
+ type QuoteMeta,
22
+ } from './pricing';
23
+ export {
24
+ extractPromotionCodeFromSession,
25
+ applyPromotionCode,
26
+ removePromotionCode,
27
+ recalculatePromotion,
28
+ isPromotionActive,
29
+ hasAppliedDiscounts,
30
+ } from './promotion';
31
+ export {
32
+ recalculatePromotionIfNeeded,
33
+ adjustQuantity,
34
+ performUpsell,
35
+ performDownsell,
36
+ changeDonationAmount,
37
+ getCrossSellItem,
38
+ } from './lineItems';
39
+ export { addCrossSellItem, removeCrossSellItem, fetchCrossSellItem } from './crossSell';
40
+ export {
41
+ parseBillingInterval,
42
+ type BillingIntervalType,
43
+ type BillingIntervalOption,
44
+ type BillingIntervalInfo,
45
+ } from './billingInterval';
46
+ export { buildFields, createInitialValues, setNestedValue } from './customerForm';
47
+ export {
48
+ QUOTE_ERROR_CODES,
49
+ RELAY_SOCKET_PREFIX,
50
+ getAppId,
51
+ getRelayChannel,
52
+ getRelayProtocol,
53
+ getSocketHost,
54
+ buildSubmitPayload,
55
+ isQuoteError,
56
+ abortStripePayment,
57
+ submitCheckout,
58
+ confirmFastCheckout,
59
+ updateSlippage,
60
+ } from './submit';
@@ -0,0 +1,106 @@
1
+ import type { TCheckoutSessionExpanded, TLineItemExpanded, TPrice } from '@blocklet/payment-types';
2
+
3
+ import api, { API } from '../../shared/api';
4
+ import { recalculatePromotion, hasAppliedDiscounts } from './promotion';
5
+ import type { PriceWithCrossSell } from '../../types/checkout-augmented';
6
+
7
+ export async function recalculatePromotionIfNeeded(
8
+ session: TCheckoutSessionExpanded | undefined | null,
9
+ sessionId: string,
10
+ currencyId: string | null | undefined
11
+ ): Promise<void> {
12
+ if (!hasAppliedDiscounts(session)) return;
13
+ try {
14
+ await recalculatePromotion(sessionId, currencyId);
15
+ } catch {
16
+ // Ignore recalculation error
17
+ }
18
+ }
19
+
20
+ export async function adjustQuantity(
21
+ sessionId: string,
22
+ itemId: string,
23
+ qty: number,
24
+ currencyId: string | null | undefined,
25
+ session: TCheckoutSessionExpanded | undefined | null,
26
+ refresh: (force?: boolean) => Promise<void>
27
+ ): Promise<void> {
28
+ await api.put(API.ADJUST_QUANTITY(sessionId), {
29
+ itemId,
30
+ quantity: qty,
31
+ currency_id: currencyId,
32
+ });
33
+ await recalculatePromotionIfNeeded(session, sessionId, currencyId);
34
+ await refresh(true);
35
+ }
36
+
37
+ export async function performUpsell(
38
+ sessionId: string,
39
+ fromId: string,
40
+ toId: string,
41
+ session: TCheckoutSessionExpanded | undefined | null,
42
+ currencyId: string | null | undefined,
43
+ refresh: (force?: boolean) => Promise<void>
44
+ ): Promise<void> {
45
+ // Backend rejects upsell with multiple line items — auto-remove cross-sell first
46
+ // Always try DELETE when multiple items exist (cross_sell flag may not be set)
47
+ if ((session?.line_items?.length || 0) > 1) {
48
+ try {
49
+ await api.delete(API.CROSS_SELL(sessionId));
50
+ } catch {
51
+ // No cross-sell to remove — will proceed and let upsell API decide
52
+ }
53
+ }
54
+ await api.put(API.UPSELL(sessionId), { from: fromId, to: toId });
55
+ await recalculatePromotionIfNeeded(session, sessionId, currencyId);
56
+ await refresh(true);
57
+ }
58
+
59
+ export async function performDownsell(
60
+ sessionId: string,
61
+ priceId: string,
62
+ session: TCheckoutSessionExpanded | undefined | null,
63
+ currencyId: string | null | undefined,
64
+ refresh: (force?: boolean) => Promise<void>
65
+ ): Promise<void> {
66
+ // Auto-remove cross-sell to keep interval consistency (cross-sell is interval-aware)
67
+ if ((session?.line_items?.length || 0) > 1) {
68
+ try {
69
+ await api.delete(API.CROSS_SELL(sessionId));
70
+ } catch {
71
+ // No cross-sell to remove — will proceed and let downsell API decide
72
+ }
73
+ }
74
+ await api.put(API.DOWNSELL(sessionId), { from: priceId });
75
+ await recalculatePromotionIfNeeded(session, sessionId, currencyId);
76
+ await refresh(true);
77
+ }
78
+
79
+ export async function changeDonationAmount(
80
+ sessionId: string,
81
+ priceId: string,
82
+ amount: string,
83
+ session: TCheckoutSessionExpanded | undefined | null,
84
+ currencyId: string | null | undefined,
85
+ refresh: (force?: boolean) => Promise<void>
86
+ ): Promise<void> {
87
+ const { data } = await api.put(API.CHANGE_AMOUNT(sessionId), {
88
+ priceId,
89
+ amount,
90
+ });
91
+ if (data?.discounts?.length) {
92
+ await recalculatePromotionIfNeeded(session, sessionId, currencyId);
93
+ }
94
+ await refresh(true);
95
+ }
96
+
97
+ // Extract cross-sell price from items
98
+ export function getCrossSellItem(items: TLineItemExpanded[]): TPrice | null {
99
+ for (const item of items) {
100
+ const crossSell = (item.price as PriceWithCrossSell)?.cross_sell;
101
+ if (crossSell?.cross_sells_to) {
102
+ return crossSell.cross_sells_to as TPrice;
103
+ }
104
+ }
105
+ return null;
106
+ }
@@ -0,0 +1,113 @@
1
+ import type { TPaymentMethodExpanded, TPaymentCurrency, TCheckoutSessionExpanded } from '@blocklet/payment-types';
2
+
3
+ import type { CheckoutSessionRuntime, CheckoutSessionUser } from '../../types/checkout-augmented';
4
+
5
+ export type CurrencyWithMethod = TPaymentCurrency & {
6
+ method?: { id: string; name: string; type: string };
7
+ };
8
+
9
+ export function flattenCurrencies(methods: TPaymentMethodExpanded[]): CurrencyWithMethod[] {
10
+ const out: CurrencyWithMethod[] = [];
11
+ methods.forEach((method) => {
12
+ const currencies = method.payment_currencies || [];
13
+ currencies.forEach((currency) => {
14
+ const enriched: CurrencyWithMethod = {
15
+ ...currency,
16
+ method: { id: method.id, name: method.name, type: method.type },
17
+ };
18
+ out.push(enriched);
19
+ });
20
+ });
21
+ return out;
22
+ }
23
+
24
+ export function hasDidWallet(user: CheckoutSessionUser | undefined): boolean {
25
+ const connected = user?.connectedAccounts || user?.extraConfigs?.connectedAccounts || [];
26
+ return connected.some((x) => x.provider === 'wallet');
27
+ }
28
+
29
+ const CURRENCY_PREFERENCE_KEY = 'payment-currency-preference';
30
+
31
+ export function getCurrencyStorageKey(did?: string): string {
32
+ return did ? `${CURRENCY_PREFERENCE_KEY}:${did}` : CURRENCY_PREFERENCE_KEY;
33
+ }
34
+
35
+ export function getInitialCurrencyId(
36
+ session: TCheckoutSessionExpanded | undefined | null,
37
+ methods: TPaymentMethodExpanded[]
38
+ ): string | null {
39
+ if (typeof window !== 'undefined') {
40
+ try {
41
+ const params = new URLSearchParams(window.location.search);
42
+ const urlCurrency = params.get('currencyId') || params.get('currency_id');
43
+ if (urlCurrency) return urlCurrency;
44
+ } catch {
45
+ // Ignore
46
+ }
47
+
48
+ const user = (session as CheckoutSessionRuntime | undefined)?.user;
49
+ if (user && !hasDidWallet(user)) {
50
+ const stripeMethod = methods.find((m) => m.type === 'stripe');
51
+ const stripeCurrency = stripeMethod?.payment_currencies?.[0];
52
+ if (stripeCurrency) return stripeCurrency.id;
53
+ }
54
+
55
+ try {
56
+ const did = (session as CheckoutSessionRuntime | undefined)?.user?.did;
57
+ const stored = localStorage.getItem(getCurrencyStorageKey(did));
58
+ if (stored) return stored;
59
+ } catch {
60
+ // Ignore
61
+ }
62
+ }
63
+ return session?.currency_id || null;
64
+ }
65
+
66
+ export function findMethodAndCurrency(
67
+ methods: TPaymentMethodExpanded[],
68
+ currencyId: string | null
69
+ ): { method: TPaymentMethodExpanded | null; currency: TPaymentCurrency | null } {
70
+ if (!currencyId) {
71
+ const first = methods[0];
72
+ const firstCurrency = first?.payment_currencies?.[0] || null;
73
+ return { method: first || null, currency: firstCurrency };
74
+ }
75
+
76
+ for (const method of methods) {
77
+ for (const currency of method.payment_currencies || []) {
78
+ if (currency.id === currencyId) {
79
+ return { method, currency };
80
+ }
81
+ }
82
+ }
83
+
84
+ const first = methods[0];
85
+ return { method: first || null, currency: first?.payment_currencies?.[0] || null };
86
+ }
87
+
88
+ export function buildPaymentTypes(methods: TPaymentMethodExpanded[], currentMethod: TPaymentMethodExpanded | null) {
89
+ const typeMap = new Map<string, { currencies: CurrencyWithMethod[]; active: boolean }>();
90
+
91
+ methods.forEach((method) => {
92
+ const type = method.type === 'stripe' ? 'stripe' : 'crypto';
93
+ if (!typeMap.has(type)) {
94
+ typeMap.set(type, { currencies: [], active: false });
95
+ }
96
+ const entry = typeMap.get(type)!;
97
+ const enriched: CurrencyWithMethod[] = (method.payment_currencies || []).map((cur) => ({
98
+ ...cur,
99
+ method: { id: method.id, name: method.name, type: method.type },
100
+ }));
101
+ entry.currencies.push(...enriched);
102
+ if (method === currentMethod) {
103
+ entry.active = true;
104
+ }
105
+ });
106
+
107
+ return Array.from(typeMap.entries()).map(([type, data]) => ({
108
+ type: type as 'stripe' | 'crypto',
109
+ label: type === 'stripe' ? 'Card' : 'Crypto',
110
+ currencies: data.currencies,
111
+ active: data.active,
112
+ }));
113
+ }