@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,15 @@
1
+ import type { CheckoutResult } from '../checkout/types';
2
+ /**
3
+ * Poll for checkout session completion.
4
+ * Extracted from react/src/payment/form/index.tsx:72-99
5
+ *
6
+ * Uses p-wait-for with 2s interval and 3 minute timeout.
7
+ * Completion condition: session.status === 'complete' && payment_status in ['paid', 'no_payment_required']
8
+ * Throws if paymentIntent has last_payment_error.
9
+ */
10
+ export declare function waitForCheckoutComplete(sessionId: string): Promise<CheckoutResult>;
11
+ /**
12
+ * Generate unique idempotency key for submit (Final Freeze Architecture).
13
+ * Format: ${sessionId}-${currencyId}-${timestamp}-${random}
14
+ */
15
+ export declare function generateIdempotencyKey(sessionId: string, currencyId: string): string;
@@ -0,0 +1,20 @@
1
+ import pWaitFor from "p-wait-for";
2
+ import api, { API } from "./api.js";
3
+ export async function waitForCheckoutComplete(sessionId) {
4
+ let result;
5
+ await pWaitFor(
6
+ async () => {
7
+ const { data } = await api.get(API.RETRIEVE_SESSION(sessionId));
8
+ if (data.paymentIntent && data.paymentIntent.status === "requires_action" && data.paymentIntent.last_payment_error) {
9
+ throw new Error(data.paymentIntent.last_payment_error.message);
10
+ }
11
+ result = data;
12
+ return data.checkoutSession?.status === "complete" && ["paid", "no_payment_required"].includes(data.checkoutSession?.payment_status);
13
+ },
14
+ { interval: 2e3, timeout: 3 * 60 * 1e3 }
15
+ );
16
+ return result;
17
+ }
18
+ export function generateIdempotencyKey(sessionId, currencyId) {
19
+ return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
20
+ }
@@ -0,0 +1,10 @@
1
+ export interface ApiConfig {
2
+ baseUrl?: string;
3
+ authToken?: string;
4
+ locale?: string;
5
+ livemode?: boolean;
6
+ }
7
+ export interface LoadingState {
8
+ isLoading: boolean;
9
+ error: string | null;
10
+ }
File without changes
@@ -0,0 +1,38 @@
1
+ import type { CheckoutFormData } from '../checkout/types';
2
+ export declare const postalCodePatterns: Record<string, RegExp>;
3
+ export declare function validatePostalCode(postalCode: string, country?: string): boolean;
4
+ export declare function getFieldValidation(fieldName: string, validations?: Record<string, {
5
+ pattern?: string;
6
+ pattern_message?: Record<string, string>;
7
+ }>, locale?: string): {
8
+ pattern?: {
9
+ value: RegExp;
10
+ message: string;
11
+ };
12
+ };
13
+ export declare function validatePhone(phone: string): Promise<boolean>;
14
+ export declare function validateEmail(email: string): boolean;
15
+ export interface ValidateFormOptions {
16
+ phoneEnabled: boolean;
17
+ addressMode: 'auto' | 'required' | null;
18
+ fieldValidation?: Record<string, {
19
+ pattern?: string;
20
+ pattern_message?: Record<string, string>;
21
+ }>;
22
+ locale?: string;
23
+ }
24
+ export interface FormErrors {
25
+ customer_name?: string;
26
+ customer_email?: string;
27
+ customer_phone?: string;
28
+ 'billing_address.country'?: string;
29
+ 'billing_address.state'?: string;
30
+ 'billing_address.line1'?: string;
31
+ 'billing_address.city'?: string;
32
+ 'billing_address.postal_code'?: string;
33
+ [key: string]: string | undefined;
34
+ }
35
+ export declare function validateForm(values: CheckoutFormData | null, options: ValidateFormOptions): Promise<{
36
+ valid: boolean;
37
+ errors: FormErrors;
38
+ }>;
@@ -0,0 +1,190 @@
1
+ import isEmail from "validator/lib/isEmail";
2
+ const fourDigit = /^\d{4}$/;
3
+ const fiveDigit = /^\d{5}$/;
4
+ const sixDigit = /^\d{6}$/;
5
+ export const postalCodePatterns = {
6
+ GB: /^GIR[ ]?0AA|((AB|AL|B|BA|BB|BD|BH|BL|BN|BR|BS|BT|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}))|BFPO[ ]?\d{1,4}$/i,
7
+ JE: /^JE\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$/i,
8
+ GG: /^GY\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$/i,
9
+ IM: /^IM\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$/i,
10
+ US: /^\d{5}([ -]\d{4})?$/,
11
+ CA: /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJ-NPRSTV-Z][ ]?\d[ABCEGHJ-NPRSTV-Z]\d$/i,
12
+ DE: fiveDigit,
13
+ JP: /^\d{3}-?\d{4}$/,
14
+ FR: /^\d{2}[ ]?\d{3}$/,
15
+ AU: fourDigit,
16
+ IT: fiveDigit,
17
+ CH: fourDigit,
18
+ AT: fourDigit,
19
+ ES: fiveDigit,
20
+ NL: /^\d{4}[ ]?[A-Z]{2}$/i,
21
+ BE: fourDigit,
22
+ DK: fourDigit,
23
+ SE: /^\d{3}[ ]?\d{2}$/,
24
+ NO: fourDigit,
25
+ BR: /^\d{5}-?\d{3}$/,
26
+ PT: /^\d{4}(-\d{3})?$/,
27
+ FI: fiveDigit,
28
+ KR: fiveDigit,
29
+ CN: sixDigit,
30
+ TW: /^\d{3}(\d{2})?$/,
31
+ SG: sixDigit,
32
+ IN: /^[1-9]\d{5}$/,
33
+ RU: sixDigit,
34
+ PL: /^\d{2}-\d{3}$/,
35
+ CZ: /^\d{3}[ ]?\d{2}$/,
36
+ HU: fourDigit,
37
+ RO: sixDigit,
38
+ BG: fourDigit,
39
+ HR: fiveDigit,
40
+ SK: /^\d{3}[ ]?\d{2}$/,
41
+ SI: fourDigit,
42
+ TR: fiveDigit,
43
+ IL: fiveDigit,
44
+ ZA: fourDigit,
45
+ MX: fiveDigit,
46
+ AR: /^([A-HJ-NP-Z])?\d{4}([A-Z]{3})?$/i,
47
+ CL: /^\d{7}$/,
48
+ CO: sixDigit,
49
+ NZ: fourDigit,
50
+ MY: fiveDigit,
51
+ TH: fiveDigit,
52
+ PH: fourDigit,
53
+ ID: fiveDigit,
54
+ VN: sixDigit
55
+ };
56
+ export function validatePostalCode(postalCode, country) {
57
+ if (!postalCode) return true;
58
+ try {
59
+ const countryUpper = country?.toUpperCase();
60
+ if (!countryUpper) return false;
61
+ const pattern = postalCodePatterns[countryUpper];
62
+ return !pattern || pattern.test(postalCode);
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+ export function getFieldValidation(fieldName, validations, locale = "en") {
68
+ if (!validations || !validations[fieldName]) return {};
69
+ const fieldValidation = validations[fieldName];
70
+ const rules = {};
71
+ if (fieldValidation.pattern) {
72
+ rules.pattern = {
73
+ value: new RegExp(fieldValidation.pattern),
74
+ message: fieldValidation.pattern_message?.[locale] || "Invalid format"
75
+ };
76
+ }
77
+ return rules;
78
+ }
79
+ let phoneUtil = null;
80
+ async function getPhoneUtil() {
81
+ if (!phoneUtil) {
82
+ const result = await import("google-libphonenumber");
83
+ const PhoneNumberUtil = (result.default || result)?.PhoneNumberUtil;
84
+ if (!PhoneNumberUtil) {
85
+ throw new Error("PhoneNumberUtil not found");
86
+ }
87
+ phoneUtil = PhoneNumberUtil.getInstance();
88
+ }
89
+ return phoneUtil;
90
+ }
91
+ const PHONE_REGEX_FALLBACK = /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/im;
92
+ export async function validatePhone(phone) {
93
+ if (!phone || phone.trim() === "") return false;
94
+ try {
95
+ const util = await getPhoneUtil();
96
+ const parsed = util.parseAndKeepRawInput(phone);
97
+ return util.isValidNumber(parsed);
98
+ } catch {
99
+ return PHONE_REGEX_FALLBACK.test(phone);
100
+ }
101
+ }
102
+ export function validateEmail(email) {
103
+ if (!email || email.trim() === "") return false;
104
+ return isEmail(email);
105
+ }
106
+ export async function validateForm(values, options) {
107
+ const errors = {};
108
+ const { phoneEnabled, addressMode, fieldValidation, locale = "en" } = options;
109
+ if (!values) {
110
+ return { valid: false, errors: { customer_name: "Required" } };
111
+ }
112
+ const customerName = values.customer_name;
113
+ if (!customerName || customerName.trim() === "") {
114
+ errors.customer_name = "Required";
115
+ } else {
116
+ const nameRule = getFieldValidation("customer_name", fieldValidation, locale);
117
+ if (nameRule.pattern && !nameRule.pattern.value.test(customerName)) {
118
+ errors.customer_name = nameRule.pattern.message;
119
+ }
120
+ }
121
+ const customerEmail = values.customer_email;
122
+ if (!customerEmail || !validateEmail(customerEmail)) {
123
+ errors.customer_email = !customerEmail ? "Required" : "Invalid email";
124
+ } else {
125
+ const emailRule = getFieldValidation("customer_email", fieldValidation, locale);
126
+ if (emailRule.pattern && !emailRule.pattern.value.test(customerEmail)) {
127
+ errors.customer_email = emailRule.pattern.message;
128
+ }
129
+ }
130
+ if (phoneEnabled) {
131
+ const customerPhone = values.customer_phone;
132
+ if (!customerPhone || customerPhone.trim() === "") {
133
+ errors.customer_phone = "Required";
134
+ } else if (!await validatePhone(customerPhone)) {
135
+ errors.customer_phone = "Invalid phone number";
136
+ } else {
137
+ const phoneRule = getFieldValidation("customer_phone", fieldValidation, locale);
138
+ if (phoneRule.pattern && !phoneRule.pattern.value.test(customerPhone)) {
139
+ errors.customer_phone = phoneRule.pattern.message;
140
+ }
141
+ }
142
+ }
143
+ const billingAddress = values.billing_address;
144
+ const postalCode = billingAddress?.postal_code;
145
+ const country = billingAddress?.country;
146
+ const state = billingAddress?.state;
147
+ const line1 = billingAddress?.line1;
148
+ const city = billingAddress?.city;
149
+ if (!postalCode) {
150
+ errors["billing_address.postal_code"] = "Required";
151
+ } else if (!validatePostalCode(postalCode, country)) {
152
+ errors["billing_address.postal_code"] = "Invalid postal code";
153
+ } else {
154
+ const postalRule = getFieldValidation("billing_address.postal_code", fieldValidation, locale);
155
+ if (postalRule.pattern && !postalRule.pattern.value.test(postalCode)) {
156
+ errors["billing_address.postal_code"] = postalRule.pattern.message;
157
+ }
158
+ }
159
+ if (!state) {
160
+ errors["billing_address.state"] = "Required";
161
+ } else {
162
+ const stateRule = getFieldValidation("billing_address.state", fieldValidation, locale);
163
+ if (stateRule.pattern && !stateRule.pattern.value.test(state)) {
164
+ errors["billing_address.state"] = stateRule.pattern.message;
165
+ }
166
+ }
167
+ if (addressMode === "required") {
168
+ if (!country) errors["billing_address.country"] = "Required";
169
+ if (!line1) {
170
+ errors["billing_address.line1"] = "Required";
171
+ } else {
172
+ const line1Rule = getFieldValidation("billing_address.line1", fieldValidation, locale);
173
+ if (line1Rule.pattern && !line1Rule.pattern.value.test(line1)) {
174
+ errors["billing_address.line1"] = line1Rule.pattern.message;
175
+ }
176
+ }
177
+ if (!city) {
178
+ errors["billing_address.city"] = "Required";
179
+ } else {
180
+ const cityRule = getFieldValidation("billing_address.city", fieldValidation, locale);
181
+ if (cityRule.pattern && !cityRule.pattern.value.test(city)) {
182
+ errors["billing_address.city"] = cityRule.pattern.message;
183
+ }
184
+ }
185
+ }
186
+ return {
187
+ valid: Object.keys(errors).length === 0,
188
+ errors
189
+ };
190
+ }
@@ -0,0 +1,42 @@
1
+ import type { TCheckoutSessionExpanded, TPrice, TPriceExpanded } from '@blocklet/payment-types';
2
+ export interface CheckoutSessionUser {
3
+ did?: string;
4
+ sourceAppPid?: string;
5
+ connectedAccounts?: Array<{
6
+ provider: string;
7
+ }>;
8
+ extraConfigs?: {
9
+ connectedAccounts?: Array<{
10
+ provider: string;
11
+ }>;
12
+ };
13
+ }
14
+ export type CheckoutSessionRuntime = TCheckoutSessionExpanded & {
15
+ user?: CheckoutSessionUser;
16
+ quote_locked_at?: number;
17
+ };
18
+ export type PriceWithCrossSell = TPriceExpanded & {
19
+ cross_sell?: {
20
+ cross_sells_to?: TPrice;
21
+ cross_sells_to_id?: string;
22
+ };
23
+ };
24
+ export interface UpsellExpanded {
25
+ upsells_to_id?: string;
26
+ upsells_to?: TPrice;
27
+ }
28
+ /**
29
+ * Get human-readable message from unknown error.
30
+ * Works for standard Error, AxiosError, and string errors.
31
+ */
32
+ export declare function getErrorMessage(err: unknown): string;
33
+ /**
34
+ * Extract API error details from an unknown axios-like error.
35
+ * Returns message, error code, and raw response data.
36
+ */
37
+ export declare function getAxiosErrorDetails(err: unknown): {
38
+ message: string;
39
+ code: string | undefined;
40
+ data: Record<string, unknown> | undefined;
41
+ isCancelled: boolean;
42
+ };
@@ -0,0 +1,17 @@
1
+ export function getErrorMessage(err) {
2
+ const axErr = err;
3
+ if (axErr?.response?.data?.error) return axErr.response.data.error;
4
+ if (err instanceof Error) return err.message;
5
+ if (typeof err === "string") return err;
6
+ return axErr?.message || "Unknown error";
7
+ }
8
+ export function getAxiosErrorDetails(err) {
9
+ const axErr = err;
10
+ const data = axErr?.response?.data;
11
+ return {
12
+ message: data?.error || axErr?.message || "Unknown error",
13
+ code: data?.code,
14
+ data,
15
+ isCancelled: axErr?.name === "CanceledError" || axErr?.name === "AbortError"
16
+ };
17
+ }
@@ -0,0 +1,18 @@
1
+ declare module 'google-libphonenumber' {
2
+ export class PhoneNumberUtil {
3
+ static getInstance(): PhoneNumberUtil;
4
+ parseAndKeepRawInput(numberToParse: string, defaultRegion?: string): any;
5
+ isValidNumber(number: any): boolean;
6
+ }
7
+ }
8
+
9
+ declare module '@arcblock/ws' {
10
+ export class WsClient {
11
+ constructor(url: string, options?: Record<string, any>);
12
+ connect(): void;
13
+ disconnect(): void;
14
+ isConnected(): boolean;
15
+ subscribe(channel: string): any;
16
+ unsubscribe(channel: string): void;
17
+ }
18
+ }
@@ -0,0 +1,159 @@
1
+ /* eslint-disable react/button-has-type */
2
+ /**
3
+ * Basic Checkout — minimal integration using CheckoutProvider + feature hooks.
4
+ *
5
+ * This is how payment-react's CheckoutV2 component uses the headless package internally.
6
+ * The CheckoutProvider sets up layered contexts (session → payment method → exchange rate →
7
+ * customer form → submit), and feature hooks consume them.
8
+ *
9
+ * Usage:
10
+ * <CheckoutProvider sessionId="cs_xxx">
11
+ * <MyCheckoutUI />
12
+ * </CheckoutProvider>
13
+ */
14
+ import {
15
+ CheckoutProvider,
16
+ useSessionContext,
17
+ usePaymentMethodContext,
18
+ usePricingFeature,
19
+ useCustomerFormFeature,
20
+ useSubmitFeature,
21
+ useLineItems,
22
+ useProduct,
23
+ } from '../src';
24
+
25
+ function CheckoutUI() {
26
+ const { isLoading, error } = useSessionContext();
27
+ const { product } = useProduct();
28
+ const lineItems = useLineItems();
29
+ const { currency, currencies, setCurrency, types, setType } = usePaymentMethodContext();
30
+ const pricing = usePricingFeature();
31
+ const form = useCustomerFormFeature();
32
+ const submit = useSubmitFeature();
33
+
34
+ if (isLoading || !product) return <div>Loading...</div>;
35
+ if (error) return <div>Error: {error}</div>;
36
+
37
+ return (
38
+ <div style={{ display: 'flex', gap: 32 }}>
39
+ {/* Left: Product info */}
40
+ <div style={{ flex: 1 }}>
41
+ <h2>{product.name}</h2>
42
+ <p>{product.description}</p>
43
+ {product.features.length > 0 && (
44
+ <ul>
45
+ {product.features.map((f: { name: string }) => (
46
+ <li key={f.name}>{f.name}</li>
47
+ ))}
48
+ </ul>
49
+ )}
50
+
51
+ {/* Line items */}
52
+ {lineItems.items.map((item) => {
53
+ const price = (item as any).upsell_price || item.price;
54
+ return (
55
+ <div key={item.id}>
56
+ <span>{price?.product?.name}</span>
57
+ <span> × {item.quantity}</span>
58
+ </div>
59
+ );
60
+ })}
61
+ </div>
62
+
63
+ {/* Right: Payment */}
64
+ <div style={{ flex: 1 }}>
65
+ {/* Payment method tabs */}
66
+ {types.length > 1 &&
67
+ types.map((t) => (
68
+ <button key={t.type} onClick={() => setType(t.type)} style={{ fontWeight: t.active ? 'bold' : 'normal' }}>
69
+ {t.label}
70
+ </button>
71
+ ))}
72
+
73
+ {/* Currency selector */}
74
+ {currencies.length > 1 && (
75
+ <select value={currency?.id || ''} onChange={(e) => setCurrency(e.target.value)}>
76
+ {currencies.map((c) => (
77
+ <option key={c.id} value={c.id}>
78
+ {c.symbol}
79
+ </option>
80
+ ))}
81
+ </select>
82
+ )}
83
+
84
+ {/* Customer form */}
85
+ {form.fields.map((f) => {
86
+ const inputId = `customer-field-${String(f.name).replace(/[^a-zA-Z0-9\-_:.]/g, '-')}`;
87
+ const hasError = Boolean(form.touched[f.name] && form.errors[f.name]);
88
+
89
+ return (
90
+ <div key={f.name}>
91
+ <label htmlFor={inputId}>
92
+ {f.name}
93
+ {f.required ? ' *' : ''}
94
+ </label>
95
+ <input
96
+ id={inputId}
97
+ name={f.name}
98
+ type={f.type === 'email' ? 'email' : 'text'}
99
+ value={String((form.values as any)[f.name] || '')}
100
+ aria-invalid={hasError}
101
+ onChange={(e) => form.onChange(f.name, e.target.value)}
102
+ />
103
+ {hasError && <span style={{ color: 'red' }}>{form.errors[f.name]}</span>}
104
+ </div>
105
+ );
106
+ })}
107
+
108
+ {/* Pricing summary */}
109
+ <div>
110
+ <p>Subtotal: {pricing.subtotal}</p>
111
+ {pricing.discount && <p>Discount: -{pricing.discount}</p>}
112
+ {pricing.tax && (
113
+ <p>
114
+ Tax ({pricing.tax.rate}%): {pricing.tax.amount}
115
+ </p>
116
+ )}
117
+ {pricing.trial.active && <p>Trial: {pricing.trial.days} days free</p>}
118
+ <p>
119
+ <strong>Total: {pricing.total}</strong>
120
+ </p>
121
+ {pricing.usdEquivalent && <p>≈ {pricing.usdEquivalent}</p>}
122
+ </div>
123
+
124
+ {/* Submit */}
125
+ <button onClick={submit.execute} disabled={submit.status === 'submitting'}>
126
+ {submit.status === 'submitting' ? 'Processing...' : `Pay ${pricing.total}`}
127
+ </button>
128
+
129
+ {/* Status feedback */}
130
+ {submit.status === 'confirming_fast_pay' && submit.context?.type === 'fast_pay' && (
131
+ <div>
132
+ <p>
133
+ Confirm: {submit.context.amount} via {submit.context.payType}
134
+ </p>
135
+ <button onClick={submit.confirm}>Confirm</button>
136
+ <button onClick={submit.cancel}>Cancel</button>
137
+ </div>
138
+ )}
139
+ {submit.status === 'waiting_did' && <p>Confirm in DID Wallet...</p>}
140
+ {submit.status === 'waiting_stripe' && <p>Complete card payment...</p>}
141
+ {submit.status === 'completed' && <p>Payment successful!</p>}
142
+ {submit.status === 'failed' && submit.context?.type === 'error' && (
143
+ <p style={{ color: 'red' }}>
144
+ {submit.context.message} <button onClick={submit.retry}>Retry</button>
145
+ </p>
146
+ )}
147
+ </div>
148
+ </div>
149
+ );
150
+ }
151
+
152
+ /** Wrap with CheckoutProvider — this is the entry point */
153
+ export function BasicCheckout({ sessionId }: { sessionId: string }) {
154
+ return (
155
+ <CheckoutProvider sessionId={sessionId}>
156
+ <CheckoutUI />
157
+ </CheckoutProvider>
158
+ );
159
+ }
@@ -0,0 +1,19 @@
1
+ // Scenario 1: Credit Recharge (< 20 lines)
2
+ import React from 'react';
3
+ import { useCheckout } from '../src';
4
+
5
+ export function CreditRecharge({ sessionId }: { sessionId: string }) {
6
+ const { product, pricing, submit, canSubmit } = useCheckout(sessionId);
7
+ if (!product) return <div>Loading...</div>;
8
+ return (
9
+ <div>
10
+ <h2>{product.name}</h2>
11
+ <p>Total: {pricing.total}{pricing.usdEquivalent && ` (${pricing.usdEquivalent})`}</p>
12
+ <button onClick={submit.execute} disabled={!canSubmit}>
13
+ {submit.status === 'submitting' ? 'Processing...' : 'Pay'}
14
+ </button>
15
+ {submit.status === 'completed' && <p>Success!</p>}
16
+ {submit.context?.type === 'error' && <p>{submit.context.message}</p>}
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,40 @@
1
+ // Scenario 2: Subscription Purchase (< 50 lines)
2
+ import React from 'react';
3
+ import { useCheckout } from '../src';
4
+
5
+ export function SubscriptionCheckout({ sessionId }: { sessionId: string }) {
6
+ const { product, pricing, form, submit, lineItems, canSubmit, isLoading } = useCheckout(sessionId);
7
+ if (isLoading || !product) return <div>Loading...</div>;
8
+
9
+ return (
10
+ <div>
11
+ <h2>{product.name}</h2>
12
+ <p>{product.description}</p>
13
+ <ul>{product.features.map((f) => <li key={f.name}>{f.name}</li>)}</ul>
14
+
15
+ {lineItems.billingInterval && lineItems.billingInterval.available.map((opt) => (
16
+ <button key={opt.interval} onClick={() => lineItems.billingInterval!.switch(opt.interval)}>
17
+ {opt.interval === 'year' ? 'Annual' : 'Monthly'}
18
+ </button>
19
+ ))}
20
+
21
+ {pricing.trial.active && <p>Free trial: {pricing.trial.days} days</p>}
22
+
23
+ {form.fields.filter((f) => f.group !== 'address').map((field) => (
24
+ <input key={field.name} placeholder={field.name}
25
+ value={String((form.values as any)[field.name] || '')}
26
+ onChange={(e) => form.onChange(field.name, e.target.value)} />
27
+ ))}
28
+ {Object.entries(form.errors).map(([k, v]) => <p key={k} style={{ color: 'red' }}>{v}</p>)}
29
+
30
+ <p>Total: {pricing.total}</p>
31
+ {pricing.trial.active && <p>After trial: {pricing.trial.afterTrialPrice}/{product.billing.displayInterval}</p>}
32
+
33
+ <button onClick={submit.execute} disabled={!canSubmit}>
34
+ {submit.status === 'submitting' ? 'Processing...' : `Subscribe ${product.billing.displayInterval}`}
35
+ </button>
36
+ {submit.status === 'waiting_stripe' && <p>Complete payment with card...</p>}
37
+ {submit.status === 'completed' && <p>Subscribed!</p>}
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,60 @@
1
+ // Scenario 3: Upsell (< 60 lines)
2
+ import React from 'react';
3
+ import { useCheckout } from '../src';
4
+
5
+ export function UpsellCheckout({ sessionId }: { sessionId: string }) {
6
+ const { product, pricing, lineItems, submit, canSubmit, isLoading } = useCheckout(sessionId);
7
+
8
+ if (isLoading || !product) return <div>Loading...</div>;
9
+
10
+ return (
11
+ <div>
12
+ <h2>{product.name}</h2>
13
+
14
+ {/* Line items with upsell */}
15
+ {lineItems.items.map((item) => {
16
+ const price = item.upsell_price || item.price;
17
+ const upsellTarget = (item.price as any)?.upsell?.upsells_to;
18
+
19
+ return (
20
+ <div key={item.id}>
21
+ <span>{price?.product?.name}</span>
22
+ <span> - {price?.unit_amount}</span>
23
+
24
+ {/* Show upsell toggle if not yet upselled */}
25
+ {!item.upsell_price_id && upsellTarget && (
26
+ <button onClick={() => lineItems.upsell(item.price_id, (item.price as any).upsell.upsells_to_id)}>
27
+ Upgrade to {upsellTarget.recurring?.interval === 'year' ? 'Annual' : 'Premium'}
28
+ </button>
29
+ )}
30
+
31
+ {/* Show downsell if currently upselled */}
32
+ {item.upsell_price_id && (
33
+ <button onClick={() => lineItems.downsell(item.upsell_price_id!)}>
34
+ Revert to original
35
+ </button>
36
+ )}
37
+
38
+ {/* Quantity adjust */}
39
+ {(item as any).adjustable_quantity?.enabled && (
40
+ <span>
41
+ <button onClick={() => lineItems.updateQuantity(item.id, item.quantity - 1)}>-</button>
42
+ <span>{item.quantity}</span>
43
+ <button onClick={() => lineItems.updateQuantity(item.id, item.quantity + 1)}>+</button>
44
+ </span>
45
+ )}
46
+ </div>
47
+ );
48
+ })}
49
+
50
+ <p>Total: {pricing.total}</p>
51
+ {pricing.discount && <p>Discount: -{pricing.discount}</p>}
52
+
53
+ <button onClick={submit.execute} disabled={!canSubmit}>
54
+ {submit.status === 'submitting' ? 'Processing...' : 'Pay'}
55
+ </button>
56
+ {submit.status === 'completed' && <p>Done!</p>}
57
+ {submit.context?.type === 'error' && <p style={{ color: 'red' }}>{submit.context.message}</p>}
58
+ </div>
59
+ );
60
+ }