@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,54 @@
1
+ // Scenario 4: Cross-sell (< 60 lines)
2
+ import React from 'react';
3
+ import { useCheckout } from '../src';
4
+
5
+ export function CrossSellCheckout({ sessionId }: { sessionId: string }) {
6
+ const { product, pricing, lineItems, submit, canSubmit, isLoading } = useCheckout(sessionId);
7
+
8
+ if (isLoading || !product) return <div>Loading...</div>;
9
+
10
+ const hasCrossSell = !!lineItems.crossSellItem;
11
+ const crossSellAdded = lineItems.items.length > 1;
12
+
13
+ return (
14
+ <div>
15
+ <h2>{product.name}</h2>
16
+ <p>{product.description}</p>
17
+
18
+ {/* Main item */}
19
+ {lineItems.items.map((item) => (
20
+ <div key={item.id}>
21
+ <span>{(item.upsell_price || item.price)?.product?.name}</span>
22
+ <span> x{item.quantity}</span>
23
+ </div>
24
+ ))}
25
+
26
+ {/* Cross-sell offer */}
27
+ {hasCrossSell && !crossSellAdded && (
28
+ <div style={{ border: '1px solid #ccc', padding: 8 }}>
29
+ <p>Recommended: {(lineItems.crossSellItem as any)?.product?.name}</p>
30
+ <p>+{lineItems.crossSellItem!.unit_amount}</p>
31
+ <button onClick={lineItems.addCrossSell}>Add to order</button>
32
+ </div>
33
+ )}
34
+
35
+ {/* Remove cross-sell */}
36
+ {crossSellAdded && (
37
+ <button onClick={lineItems.removeCrossSell}>Remove add-on</button>
38
+ )}
39
+
40
+ {/* Pricing */}
41
+ <div>
42
+ <p>Subtotal: {pricing.subtotal}</p>
43
+ {pricing.discount && <p>Discount: -{pricing.discount}</p>}
44
+ <p>Total: {pricing.total}</p>
45
+ </div>
46
+
47
+ <button onClick={submit.execute} disabled={!canSubmit}>
48
+ {submit.status === 'submitting' ? 'Processing...' : 'Pay'}
49
+ </button>
50
+ {submit.status === 'completed' && <p>Order placed!</p>}
51
+ {submit.context?.type === 'error' && <p style={{ color: 'red' }}>{submit.context.message}</p>}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,126 @@
1
+ // Scenario 5: Full Checkout (< 150 lines)
2
+ import React, { useState } from 'react';
3
+ import { useCheckout } from '../src';
4
+
5
+ export function FullCheckout({ sessionId }: { sessionId: string }) {
6
+ const c = useCheckout(sessionId);
7
+ const [promo, setPromo] = useState('');
8
+ if (c.isLoading) return <div>Loading...</div>;
9
+ if (c.error || !c.product) return <div>{c.error || 'Not found'}</div>;
10
+
11
+ return (
12
+ <div style={{ display: 'flex', gap: 24 }}>
13
+ {/* Left: Product */}
14
+ <div style={{ flex: 1 }}>
15
+ <h2>{c.product.name}</h2>
16
+ <p>{c.product.description}</p>
17
+ {c.product.features.length > 0 && (
18
+ <ul>{c.product.features.map((f) => <li key={f.name}>{f.name}</li>)}</ul>
19
+ )}
20
+ {/* Billing interval toggle */}
21
+ {c.lineItems.billingInterval && c.lineItems.billingInterval.available.map((opt) => (
22
+ <button key={opt.interval} onClick={() => c.lineItems.billingInterval!.switch(opt.interval)}
23
+ style={{ fontWeight: opt.interval === c.lineItems.billingInterval!.current ? 'bold' : 'normal' }}>
24
+ {opt.interval}
25
+ </button>
26
+ ))}
27
+ {/* Line items */}
28
+ {c.lineItems.items.map((item) => (
29
+ <div key={item.id} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
30
+ <span>{(item.upsell_price || item.price)?.product?.name}</span>
31
+ {(item as any).adjustable_quantity?.enabled && (<>
32
+ <button onClick={() => c.lineItems.updateQuantity(item.id, item.quantity - 1)}>-</button>
33
+ <span>{item.quantity}</span>
34
+ <button onClick={() => c.lineItems.updateQuantity(item.id, item.quantity + 1)}>+</button>
35
+ </>)}
36
+ </div>
37
+ ))}
38
+ {/* Cross-sell */}
39
+ {c.lineItems.crossSellItem && (
40
+ <div>
41
+ <span>Add {(c.lineItems.crossSellItem as any)?.product?.name}?</span>
42
+ <button onClick={c.lineItems.addCrossSell}>Add</button>
43
+ </div>
44
+ )}
45
+ </div>
46
+
47
+ {/* Right: Payment */}
48
+ <div style={{ flex: 1 }}>
49
+ {/* Payment type tabs */}
50
+ {c.paymentMethod.types.length > 1 && c.paymentMethod.types.map((t) => (
51
+ <button key={t.type} onClick={() => c.paymentMethod.setType(t.type)}
52
+ style={{ fontWeight: t.active ? 'bold' : 'normal' }}>{t.label}</button>
53
+ ))}
54
+ {/* Currency selector */}
55
+ {c.paymentMethod.currencies.length > 1 && (
56
+ <select value={c.paymentMethod.currency?.id || ''}
57
+ onChange={(e) => c.paymentMethod.setCurrency(e.target.value)}>
58
+ {c.paymentMethod.currencies.map((cur) => (
59
+ <option key={cur.id} value={cur.id}>{cur.symbol}</option>
60
+ ))}
61
+ </select>
62
+ )}
63
+ {/* Customer form fields */}
64
+ {c.form.fields.map((f) => (
65
+ <div key={f.name}>
66
+ <label>{f.name}{f.required ? ' *' : ''}</label>
67
+ <input type={f.type === 'email' ? 'email' : 'text'}
68
+ value={String((c.form.values as any)[f.name] || '')}
69
+ onChange={(e) => c.form.onChange(f.name, e.target.value)} />
70
+ {c.form.touched[f.name] && c.form.errors[f.name] && (
71
+ <span style={{ color: 'red' }}>{c.form.errors[f.name]}</span>
72
+ )}
73
+ </div>
74
+ ))}
75
+ {/* Promotion */}
76
+ {c.pricing.promotion.active && !c.pricing.promotion.applied && (
77
+ <div>
78
+ <input value={promo} onChange={(e) => setPromo(e.target.value)} placeholder="Promo code" />
79
+ <button onClick={() => c.pricing.promotion.apply(promo)}>Apply</button>
80
+ </div>
81
+ )}
82
+ {c.pricing.promotion.applied && (
83
+ <span>{c.pricing.promotion.code} <button onClick={c.pricing.promotion.remove}>x</button></span>
84
+ )}
85
+ {/* Price summary */}
86
+ <div>
87
+ <p>Subtotal: {c.pricing.subtotal}</p>
88
+ {c.pricing.discount && <p>Discount: -{c.pricing.discount}</p>}
89
+ {c.pricing.tax && <p>Tax ({c.pricing.tax.rate}%): {c.pricing.tax.amount}</p>}
90
+ {c.pricing.trial.active && <p>Trial: {c.pricing.trial.days} days free</p>}
91
+ <p><strong>Total: {c.pricing.total}</strong></p>
92
+ {c.pricing.usdEquivalent && <p>≈ {c.pricing.usdEquivalent}</p>}
93
+ {c.pricing.hasDynamicPricing && c.pricing.rate.display && <p>Rate: {c.pricing.rate.display}</p>}
94
+ </div>
95
+ {/* Submit button */}
96
+ <button onClick={c.submit.execute} disabled={!c.canSubmit}>
97
+ {c.submit.status === 'submitting' ? 'Processing...' : `Pay ${c.pricing.total}`}
98
+ </button>
99
+ {/* Submit status feedback */}
100
+ {c.submit.status === 'confirming_price' && c.submit.context?.type === 'price_change' && (
101
+ <div>
102
+ <p>Price changed by {c.submit.context.changePercent.toFixed(2)}%</p>
103
+ <button onClick={c.submit.confirm}>Accept</button>
104
+ <button onClick={c.submit.cancel}>Cancel</button>
105
+ </div>
106
+ )}
107
+ {c.submit.status === 'confirming_fast_pay' && c.submit.context?.type === 'fast_pay' && (
108
+ <div>
109
+ <p>Pay {c.submit.context.amount} from {c.submit.context.payType}?</p>
110
+ <button onClick={c.submit.confirm}>Confirm</button>
111
+ <button onClick={c.submit.cancel}>Cancel</button>
112
+ </div>
113
+ )}
114
+ {c.submit.status === 'credit_insufficient' && c.submit.context?.type === 'credit_insufficient' && (
115
+ <p>Insufficient: {c.submit.context.available}/{c.submit.context.required}</p>
116
+ )}
117
+ {c.submit.status === 'waiting_stripe' && <p>Complete card payment...</p>}
118
+ {c.submit.status === 'waiting_did' && <p>Confirm in DID Wallet...</p>}
119
+ {c.isCompleted && <p>Payment successful!</p>}
120
+ {c.submit.status === 'failed' && c.submit.context?.type === 'error' && (
121
+ <p style={{ color: 'red' }}>{c.submit.context.message} <button onClick={c.submit.retry}>Retry</button></p>
122
+ )}
123
+ </div>
124
+ </div>
125
+ );
126
+ }
package/jest.config.js ADDED
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ coverageDirectory: 'coverage',
5
+ restoreMocks: true,
6
+ clearMocks: true,
7
+ transform: {
8
+ '^.+\\.ts?$': 'ts-jest',
9
+ },
10
+ testMatch: ['**/tests/**/*.spec.ts'],
11
+ collectCoverageFrom: ['src/**/*.ts', 'src/**/*.tsx'],
12
+ globals: {
13
+ window: {},
14
+ },
15
+ };
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ export interface CheckoutProviderProps {
3
+ sessionId: string;
4
+ children: React.ReactNode;
5
+ }
6
+ export declare function CheckoutProvider({ sessionId, children }: CheckoutProviderProps): JSX.Element;
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CheckoutProvider = CheckoutProvider;
7
+ var _jsxRuntime = require("react/jsx-runtime");
8
+ var _react = require("react");
9
+ var _ahooks = require("ahooks");
10
+ var _useCheckoutSession = require("../hooks/useCheckoutSession");
11
+ var _usePaymentMethod = require("../hooks/usePaymentMethod");
12
+ var _useCustomerForm = require("../hooks/useCustomerForm");
13
+ var _useSubmit = require("../hooks/useSubmit");
14
+ var _exchangeRate = require("../core/exchangeRate");
15
+ var _lineItems = require("../core/lineItems");
16
+ var _SessionContext = require("./SessionContext");
17
+ var _PaymentMethodContext = require("./PaymentMethodContext");
18
+ var _ExchangeRateContext = require("./ExchangeRateContext");
19
+ var _CustomerFormContext = require("./CustomerFormContext");
20
+ var _SubmitContext = require("./SubmitContext");
21
+ function CheckoutProvider({
22
+ sessionId,
23
+ children
24
+ }) {
25
+ const {
26
+ isLoading,
27
+ error,
28
+ errorCode,
29
+ refresh,
30
+ setSessionData,
31
+ sessionData,
32
+ resolvedSessionId,
33
+ vendorCount,
34
+ product,
35
+ subscription,
36
+ pageInfo
37
+ } = (0, _useCheckoutSession.useCheckoutSession)(sessionId);
38
+ const session = sessionData?.checkoutSession;
39
+ const effectiveSessionId = resolvedSessionId || sessionId;
40
+ const items = session?.line_items || [];
41
+ const isDonation = session?.submit_type === "donate";
42
+ const sessionValue = (0, _react.useMemo)(() => ({
43
+ sessionData,
44
+ sessionId,
45
+ effectiveSessionId,
46
+ isLoading,
47
+ error,
48
+ errorCode,
49
+ refresh,
50
+ items,
51
+ session: session || null,
52
+ isDonation,
53
+ vendorCount,
54
+ product,
55
+ subscription,
56
+ pageInfo
57
+ }), [sessionData, sessionId, effectiveSessionId, isLoading, error, errorCode, refresh, items, session, isDonation, vendorCount, product, subscription, pageInfo]);
58
+ const paymentMethodHook = (0, _usePaymentMethod.usePaymentMethod)(sessionData, effectiveSessionId, refresh);
59
+ const prevCurrencyRef = (0, _react.useRef)(null);
60
+ (0, _react.useEffect)(() => {
61
+ const currId = paymentMethodHook.currency?.id || null;
62
+ if (!currId || !session || session.status === "complete") return;
63
+ if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
64
+ prevCurrencyRef.current = currId;
65
+ (0, _lineItems.recalculatePromotionIfNeeded)(session, effectiveSessionId, currId).then(() => refresh(true));
66
+ }
67
+ }, [paymentMethodHook.currency?.id, session?.id]);
68
+ const paymentMethodValue = (0, _react.useMemo)(() => ({
69
+ current: paymentMethodHook.current,
70
+ currency: paymentMethodHook.currency,
71
+ available: paymentMethodHook.available,
72
+ currencies: paymentMethodHook.currencies,
73
+ isStripe: paymentMethodHook.isStripe,
74
+ isCrypto: paymentMethodHook.isCrypto,
75
+ isCredit: paymentMethodHook.isCredit,
76
+ switching: paymentMethodHook.switching,
77
+ setType: paymentMethodHook.setType,
78
+ types: paymentMethodHook.types,
79
+ setCurrency: paymentMethodHook.setCurrency,
80
+ stripe: paymentMethodHook.stripe
81
+ }), [paymentMethodHook.current, paymentMethodHook.currency, paymentMethodHook.available, paymentMethodHook.currencies, paymentMethodHook.isStripe, paymentMethodHook.isCrypto, paymentMethodHook.isCredit, paymentMethodHook.switching, paymentMethodHook.setType, paymentMethodHook.types, paymentMethodHook.setCurrency, paymentMethodHook.stripe]);
82
+ const [exchangeRate, setExchangeRate] = (0, _react.useState)(null);
83
+ const [rateProvider, setRateProvider] = (0, _react.useState)(null);
84
+ const [rateProviderDisplay, setRateProviderDisplay] = (0, _react.useState)(null);
85
+ const [rateFetchedAt, setRateFetchedAt] = (0, _react.useState)(null);
86
+ const [rateStatus, setRateStatus] = (0, _react.useState)("loading");
87
+ const intervalRef = (0, _react.useRef)(null);
88
+ const fetchingRef = (0, _react.useRef)(false);
89
+ const mountedRef = (0, _react.useRef)(true);
90
+ const hasDynamicPricing = (0, _react.useMemo)(() => (0, _exchangeRate.checkHasDynamicPricing)(items), [items]);
91
+ const fetchRate = (0, _ahooks.useMemoizedFn)(async () => {
92
+ if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe) {
93
+ setRateStatus(hasDynamicPricing ? "unavailable" : "available");
94
+ if (paymentMethodHook.isStripe) {
95
+ setExchangeRate(null);
96
+ setRateProvider(null);
97
+ setRateProviderDisplay(null);
98
+ setRateFetchedAt(null);
99
+ }
100
+ return;
101
+ }
102
+ if (fetchingRef.current) return;
103
+ fetchingRef.current = true;
104
+ try {
105
+ setRateStatus("loading");
106
+ const result = await (0, _exchangeRate.fetchExchangeRate)(effectiveSessionId, paymentMethodHook.currency?.id);
107
+ if (!mountedRef.current) return;
108
+ if (result.rate) {
109
+ setExchangeRate(result.rate);
110
+ setRateProvider(result.provider);
111
+ setRateProviderDisplay(result.providerDisplay);
112
+ setRateFetchedAt(result.fetchedAt);
113
+ setRateStatus("available");
114
+ } else {
115
+ setRateStatus("unavailable");
116
+ }
117
+ } catch {
118
+ if (!mountedRef.current) return;
119
+ setRateStatus("unavailable");
120
+ } finally {
121
+ fetchingRef.current = false;
122
+ }
123
+ });
124
+ (0, _react.useEffect)(() => {
125
+ mountedRef.current = true;
126
+ if (!hasDynamicPricing || paymentMethodHook.isStripe || !effectiveSessionId || session?.status === "complete") {
127
+ if (paymentMethodHook.isStripe) {
128
+ setExchangeRate(null);
129
+ setRateProvider(null);
130
+ setRateProviderDisplay(null);
131
+ setRateFetchedAt(null);
132
+ }
133
+ if (session?.status === "complete") {
134
+ setRateStatus("available");
135
+ } else {
136
+ setRateStatus(hasDynamicPricing ? "unavailable" : "available");
137
+ }
138
+ return void 0;
139
+ }
140
+ fetchRate();
141
+ intervalRef.current = setInterval(fetchRate, _exchangeRate.BASE_INTERVAL);
142
+ const handleVisibility = () => {
143
+ if (!document.hidden) {
144
+ fetchRate();
145
+ }
146
+ };
147
+ document.addEventListener("visibilitychange", handleVisibility);
148
+ return () => {
149
+ mountedRef.current = false;
150
+ if (intervalRef.current) clearInterval(intervalRef.current);
151
+ document.removeEventListener("visibilitychange", handleVisibility);
152
+ };
153
+ }, [hasDynamicPricing, paymentMethodHook.isStripe, effectiveSessionId, paymentMethodHook.currency?.id, session?.status]);
154
+ const exchangeRateValue = (0, _react.useMemo)(() => ({
155
+ rate: exchangeRate,
156
+ provider: rateProvider,
157
+ providerDisplay: rateProviderDisplay,
158
+ fetchedAt: rateFetchedAt,
159
+ status: rateStatus,
160
+ hasDynamicPricing,
161
+ refresh: fetchRate
162
+ }), [exchangeRate, rateProvider, rateProviderDisplay, rateFetchedAt, rateStatus, hasDynamicPricing, fetchRate]);
163
+ const customerForm = (0, _useCustomerForm.useCustomerForm)(sessionData, paymentMethodHook.currency?.id || null, paymentMethodHook.current?.id || null);
164
+ const submit = (0, _useSubmit.useSubmit)(sessionData, effectiveSessionId, paymentMethodHook.currency?.id || null, paymentMethodHook.isStripe, paymentMethodHook.isCredit, isDonation, customerForm.values, customerForm.validate, refresh, setSessionData);
165
+ return /* @__PURE__ */(0, _jsxRuntime.jsx)(_SessionContext.SessionContext.Provider, {
166
+ value: sessionValue,
167
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_PaymentMethodContext.PaymentMethodContext.Provider, {
168
+ value: paymentMethodValue,
169
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_ExchangeRateContext.ExchangeRateContext.Provider, {
170
+ value: exchangeRateValue,
171
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_CustomerFormContext.CustomerFormContext.Provider, {
172
+ value: customerForm,
173
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_SubmitContext.SubmitReactContext.Provider, {
174
+ value: submit,
175
+ children
176
+ })
177
+ })
178
+ })
179
+ })
180
+ });
181
+ }
@@ -0,0 +1,4 @@
1
+ import type { UseCustomerFormReturn } from '../hooks/useCustomerForm';
2
+ export type CustomerFormContextValue = UseCustomerFormReturn;
3
+ export declare const CustomerFormContext: import("react").Context<UseCustomerFormReturn | null>;
4
+ export declare function useCustomerFormContext(): CustomerFormContextValue;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.CustomerFormContext = void 0;
7
+ exports.useCustomerFormContext = useCustomerFormContext;
8
+ var _react = require("react");
9
+ const CustomerFormContext = exports.CustomerFormContext = (0, _react.createContext)(null);
10
+ function useCustomerFormContext() {
11
+ const ctx = (0, _react.useContext)(CustomerFormContext);
12
+ if (!ctx) {
13
+ throw new Error("useCustomerFormContext must be used within <CheckoutProvider>");
14
+ }
15
+ return ctx;
16
+ }
@@ -0,0 +1,11 @@
1
+ export interface ExchangeRateContextValue {
2
+ rate: string | null;
3
+ provider: string | null;
4
+ providerDisplay: string | null;
5
+ fetchedAt: number | null;
6
+ status: 'loading' | 'available' | 'unavailable';
7
+ hasDynamicPricing: boolean;
8
+ refresh: () => Promise<void>;
9
+ }
10
+ export declare const ExchangeRateContext: import("react").Context<ExchangeRateContextValue | null>;
11
+ export declare function useExchangeRateContext(): ExchangeRateContextValue;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ExchangeRateContext = void 0;
7
+ exports.useExchangeRateContext = useExchangeRateContext;
8
+ var _react = require("react");
9
+ const ExchangeRateContext = exports.ExchangeRateContext = (0, _react.createContext)(null);
10
+ function useExchangeRateContext() {
11
+ const ctx = (0, _react.useContext)(ExchangeRateContext);
12
+ if (!ctx) {
13
+ throw new Error("useExchangeRateContext must be used within <CheckoutProvider>");
14
+ }
15
+ return ctx;
16
+ }
@@ -0,0 +1,26 @@
1
+ import type { TPaymentMethodExpanded, TPaymentCurrency } from '@blocklet/payment-types';
2
+ export interface PaymentMethodContextValue {
3
+ current: TPaymentMethodExpanded;
4
+ currency: TPaymentCurrency;
5
+ available: TPaymentMethodExpanded[];
6
+ currencies: TPaymentCurrency[];
7
+ isStripe: boolean;
8
+ isCrypto: boolean;
9
+ isCredit: boolean;
10
+ switching: boolean;
11
+ setType: (type: 'stripe' | 'crypto') => Promise<void>;
12
+ types: Array<{
13
+ type: 'stripe' | 'crypto';
14
+ label: string;
15
+ currencies: TPaymentCurrency[];
16
+ active: boolean;
17
+ }>;
18
+ setCurrency: (currencyId: string) => Promise<void>;
19
+ stripe: {
20
+ publishableKey: string | null;
21
+ clientSecret: string | null;
22
+ status: 'idle' | 'ready' | 'processing' | 'succeeded' | 'failed';
23
+ } | null;
24
+ }
25
+ export declare const PaymentMethodContext: import("react").Context<PaymentMethodContextValue | null>;
26
+ export declare function usePaymentMethodContext(): PaymentMethodContextValue;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PaymentMethodContext = void 0;
7
+ exports.usePaymentMethodContext = usePaymentMethodContext;
8
+ var _react = require("react");
9
+ const PaymentMethodContext = exports.PaymentMethodContext = (0, _react.createContext)(null);
10
+ function usePaymentMethodContext() {
11
+ const ctx = (0, _react.useContext)(PaymentMethodContext);
12
+ if (!ctx) {
13
+ throw new Error("usePaymentMethodContext must be used within <CheckoutProvider>");
14
+ }
15
+ return ctx;
16
+ }
@@ -0,0 +1,45 @@
1
+ import type { TLineItemExpanded, TCheckoutSessionExpanded } from '@blocklet/payment-types';
2
+ import type { SessionData } from '../hooks/useCheckoutSession';
3
+ export interface SessionContextValue {
4
+ sessionData: SessionData | null;
5
+ sessionId: string;
6
+ effectiveSessionId: string;
7
+ isLoading: boolean;
8
+ error: string | null;
9
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
10
+ refresh: (forceRefresh?: boolean) => Promise<void>;
11
+ items: TLineItemExpanded[];
12
+ session: TCheckoutSessionExpanded | null | undefined;
13
+ isDonation: boolean;
14
+ vendorCount: number;
15
+ product: {
16
+ name: string;
17
+ description: string;
18
+ images: string[];
19
+ features: Array<{
20
+ name: string;
21
+ icon?: string;
22
+ }>;
23
+ billing: {
24
+ mode: 'payment' | 'subscription';
25
+ interval: 'month' | 'year' | 'week' | 'day' | null;
26
+ intervalCount: number;
27
+ displayInterval: string;
28
+ };
29
+ metadata: Record<string, string>;
30
+ } | null;
31
+ subscription: {
32
+ mode: 'payment' | 'subscription' | 'setup';
33
+ showStake: boolean;
34
+ confirmMessage: string;
35
+ } | null;
36
+ pageInfo: {
37
+ formPurposeDescription?: {
38
+ en: string;
39
+ zh: string;
40
+ };
41
+ showProductFeatures: boolean;
42
+ } | null;
43
+ }
44
+ export declare const SessionContext: import("react").Context<SessionContextValue | null>;
45
+ export declare function useSessionContext(): SessionContextValue;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.SessionContext = void 0;
7
+ exports.useSessionContext = useSessionContext;
8
+ var _react = require("react");
9
+ const SessionContext = exports.SessionContext = (0, _react.createContext)(null);
10
+ function useSessionContext() {
11
+ const ctx = (0, _react.useContext)(SessionContext);
12
+ if (!ctx) {
13
+ throw new Error("useSessionContext must be used within <CheckoutProvider>");
14
+ }
15
+ return ctx;
16
+ }
@@ -0,0 +1,4 @@
1
+ import type { UseSubmitReturn } from '../hooks/useSubmit';
2
+ export type SubmitContextValue = UseSubmitReturn;
3
+ export declare const SubmitReactContext: import("react").Context<UseSubmitReturn | null>;
4
+ export declare function useSubmitContext(): SubmitContextValue;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.SubmitReactContext = void 0;
7
+ exports.useSubmitContext = useSubmitContext;
8
+ var _react = require("react");
9
+ const SubmitReactContext = exports.SubmitReactContext = (0, _react.createContext)(null);
10
+ function useSubmitContext() {
11
+ const ctx = (0, _react.useContext)(SubmitReactContext);
12
+ if (!ctx) {
13
+ throw new Error("useSubmitContext must be used within <CheckoutProvider>");
14
+ }
15
+ return ctx;
16
+ }
@@ -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,77 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "CheckoutProvider", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _CheckoutProvider.CheckoutProvider;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "CustomerFormContext", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _CustomerFormContext.CustomerFormContext;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "ExchangeRateContext", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _ExchangeRateContext.ExchangeRateContext;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "PaymentMethodContext", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _PaymentMethodContext.PaymentMethodContext;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "SessionContext", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _SessionContext.SessionContext;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "SubmitReactContext", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _SubmitContext.SubmitReactContext;
40
+ }
41
+ });
42
+ Object.defineProperty(exports, "useCustomerFormContext", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _CustomerFormContext.useCustomerFormContext;
46
+ }
47
+ });
48
+ Object.defineProperty(exports, "useExchangeRateContext", {
49
+ enumerable: true,
50
+ get: function () {
51
+ return _ExchangeRateContext.useExchangeRateContext;
52
+ }
53
+ });
54
+ Object.defineProperty(exports, "usePaymentMethodContext", {
55
+ enumerable: true,
56
+ get: function () {
57
+ return _PaymentMethodContext.usePaymentMethodContext;
58
+ }
59
+ });
60
+ Object.defineProperty(exports, "useSessionContext", {
61
+ enumerable: true,
62
+ get: function () {
63
+ return _SessionContext.useSessionContext;
64
+ }
65
+ });
66
+ Object.defineProperty(exports, "useSubmitContext", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _SubmitContext.useSubmitContext;
70
+ }
71
+ });
72
+ var _CheckoutProvider = require("./CheckoutProvider");
73
+ var _SessionContext = require("./SessionContext");
74
+ var _PaymentMethodContext = require("./PaymentMethodContext");
75
+ var _ExchangeRateContext = require("./ExchangeRateContext");
76
+ var _CustomerFormContext = require("./CustomerFormContext");
77
+ var _SubmitContext = require("./SubmitContext");