@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,318 @@
1
+ import { BN, fromUnitToToken, fromTokenToUnit } from '@ocap/util';
2
+ import type { TPaymentCurrency, TPrice, TLineItemExpanded } from '@blocklet/payment-types';
3
+
4
+ // ── Number Formatting ──
5
+
6
+ export function formatNumber(
7
+ n: number | string,
8
+ precision: number = 6,
9
+ trim: boolean = true,
10
+ thousandSeparated: boolean = true
11
+ ): string {
12
+ if (!n || n === '0') {
13
+ return '0';
14
+ }
15
+ const num = Number(n);
16
+ if (!Number.isFinite(num)) {
17
+ return '0';
18
+ }
19
+
20
+ let result: string;
21
+ if (thousandSeparated) {
22
+ result = num.toLocaleString('en-US', {
23
+ minimumFractionDigits: precision,
24
+ maximumFractionDigits: precision,
25
+ });
26
+ } else {
27
+ result = num.toFixed(precision);
28
+ }
29
+
30
+ if (!trim) {
31
+ return result;
32
+ }
33
+ const [left, right] = result.split('.');
34
+ if (!right) return left;
35
+ const trimmed = right.replace(/0+$/, '');
36
+ return trimmed ? `${left}.${trimmed}` : left;
37
+ }
38
+
39
+ export function formatBNStr(
40
+ str: string = '',
41
+ decimals: number = 18,
42
+ precision: number = 6,
43
+ trim: boolean = true,
44
+ thousandSeparated: boolean = true
45
+ ): string {
46
+ if (!str) {
47
+ return '0';
48
+ }
49
+ return formatNumber(fromUnitToToken(str, decimals), precision, trim, thousandSeparated);
50
+ }
51
+
52
+ export function formatDynamicPrice(
53
+ n: number | string,
54
+ isDynamic: boolean,
55
+ precision: number = 6,
56
+ trim: boolean = true,
57
+ thousandSeparated: boolean = true
58
+ ): string {
59
+ if (!isDynamic) {
60
+ return formatNumber(n, precision, trim, thousandSeparated);
61
+ }
62
+ const num = Number(n);
63
+ if (!Number.isFinite(num)) {
64
+ return formatNumber(n, precision, trim, thousandSeparated);
65
+ }
66
+ const abs = Math.abs(num);
67
+ const targetPrecision = abs > 0 && abs < 0.01 ? precision : 2;
68
+ return formatNumber(n, targetPrecision, trim, thousandSeparated);
69
+ }
70
+
71
+ // ── USD Formatting ──
72
+
73
+ const USD_DECIMALS = 8;
74
+
75
+ export function getUsdAmountFromTokenUnits(
76
+ tokenAmount: BN | string,
77
+ tokenDecimals: number,
78
+ exchangeRate?: string | null
79
+ ): string | null {
80
+ if (!exchangeRate && exchangeRate !== '0') {
81
+ return null;
82
+ }
83
+ if (tokenDecimals === undefined || tokenDecimals === null) {
84
+ return null;
85
+ }
86
+
87
+ try {
88
+ const amountBN = tokenAmount instanceof BN ? tokenAmount : new BN(tokenAmount || '0');
89
+ const tokenScale = new BN(10).pow(new BN(tokenDecimals));
90
+ if (tokenScale.isZero()) {
91
+ return null;
92
+ }
93
+
94
+ const rateBN = new BN(exchangeRate.replace('.', ''));
95
+ const rateDecimalPlaces = exchangeRate.includes('.') ? exchangeRate.split('.')[1]?.length || 0 : 0;
96
+ const rateScale = new BN(10).pow(new BN(rateDecimalPlaces));
97
+
98
+ const usdPrecisionScale = new BN(10).pow(new BN(USD_DECIMALS));
99
+ const usdUnit = amountBN.mul(rateBN).mul(usdPrecisionScale).div(tokenScale.mul(rateScale));
100
+
101
+ return fromUnitToToken(usdUnit.toString(), USD_DECIMALS);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ export function formatUsdAmount(amount: string | null, locale: string = 'en'): string | null {
108
+ if (!amount && amount !== '0') {
109
+ return null;
110
+ }
111
+ const num = Number(amount);
112
+ if (!Number.isFinite(num)) {
113
+ return null;
114
+ }
115
+ return new Intl.NumberFormat(locale, {
116
+ minimumFractionDigits: 2,
117
+ maximumFractionDigits: 2,
118
+ }).format(num);
119
+ }
120
+
121
+ export function formatExchangeRate(amount: string | null): string | null {
122
+ if (!amount && amount !== '0') {
123
+ return null;
124
+ }
125
+ const value = String(amount);
126
+ const num = Number(value);
127
+ if (!Number.isFinite(num)) {
128
+ return null;
129
+ }
130
+ return value;
131
+ }
132
+
133
+ export function formatExchangeRateDisplay(
134
+ rate: string | number | null | undefined,
135
+ currency: string = 'USD',
136
+ decimals: number = 2
137
+ ): string | null {
138
+ if (rate === null || rate === undefined) {
139
+ return null;
140
+ }
141
+ const num = Number(rate);
142
+ if (!Number.isFinite(num)) {
143
+ return null;
144
+ }
145
+ const formattedValue = num.toFixed(decimals);
146
+ if (currency === 'USD') {
147
+ return `$${formattedValue}`;
148
+ }
149
+ return `${formattedValue} ${currency}`;
150
+ }
151
+
152
+ // ── Amount Formatting ──
153
+
154
+ export function formatAmount(amount: string, decimals: number, precision: number = 2): string {
155
+ const tokenAmount = fromUnitToToken(amount, decimals);
156
+ const numericValue = Number(tokenAmount);
157
+ if (!Number.isFinite(numericValue)) {
158
+ return formatBNStr(amount, decimals, precision, true, false);
159
+ }
160
+ const abs = Math.abs(numericValue);
161
+ const targetPrecision = abs > 0 && abs < 0.01 ? decimals : 2;
162
+ return formatNumber(tokenAmount, targetPrecision, true, false);
163
+ }
164
+
165
+ // ── Price Utilities ──
166
+
167
+ export function getPriceCurrencyOptions(price: TPrice): Array<{
168
+ currency_id: string;
169
+ unit_amount: string;
170
+ custom_unit_amount: { preset?: string; presets: string[] } | null;
171
+ tiers: unknown;
172
+ }> {
173
+ if (Array.isArray(price.currency_options)) {
174
+ return price.currency_options;
175
+ }
176
+
177
+ return [
178
+ {
179
+ currency_id: price.currency_id,
180
+ unit_amount: price.unit_amount,
181
+ custom_unit_amount: price.custom_unit_amount || null,
182
+ tiers: null,
183
+ },
184
+ ];
185
+ }
186
+
187
+ export function getPriceUnitAmountByCurrency(price: TPrice, currency: TPaymentCurrency): string {
188
+ const options = getPriceCurrencyOptions(price);
189
+ const option = options.find((x) => x.currency_id === currency?.id);
190
+ if (option) {
191
+ if (option.custom_unit_amount) {
192
+ return option.custom_unit_amount.preset || option.custom_unit_amount.presets[0];
193
+ }
194
+ return option.unit_amount;
195
+ }
196
+
197
+ if (price.currency_id === currency?.id) {
198
+ if (price.custom_unit_amount) {
199
+ return price.custom_unit_amount.preset || price.custom_unit_amount.presets[0];
200
+ }
201
+ return price.unit_amount;
202
+ }
203
+
204
+ return '0';
205
+ }
206
+
207
+ export function getLineItemAmounts(
208
+ item: TLineItemExpanded,
209
+ currency: TPaymentCurrency,
210
+ { useUpsell = true, exchangeRate = null }: { useUpsell?: boolean; exchangeRate?: string | null } = {}
211
+ ): { unitAmount: BN; totalAmount: BN; isDynamicQuote: boolean } {
212
+ if (!currency) {
213
+ return { unitAmount: new BN(0), totalAmount: new BN(0), isDynamicQuote: false };
214
+ }
215
+
216
+ const price = useUpsell ? item.upsell_price || item.price : item.price;
217
+ const quantity = new BN(item.quantity || 0);
218
+ const quoteAmount = item.quoted_amount as string | undefined;
219
+ const quoteCurrencyId = item.quote_currency_id as string | undefined;
220
+ const isDynamicPrice = price?.pricing_type === 'dynamic';
221
+
222
+ const isQuoteValidForCurrency = isDynamicPrice && quoteAmount && quoteCurrencyId === currency.id;
223
+
224
+ if (isQuoteValidForCurrency) {
225
+ const totalAmount = new BN(quoteAmount || '0');
226
+ const unitAmount = quantity.gt(new BN(0)) ? totalAmount.add(quantity).sub(new BN(1)).div(quantity) : new BN(0);
227
+ return { unitAmount, totalAmount, isDynamicQuote: true };
228
+ }
229
+
230
+ if (isDynamicPrice && exchangeRate && price?.base_amount) {
231
+ const rate = Number(exchangeRate);
232
+ if (rate > 0 && Number.isFinite(rate)) {
233
+ const baseAmountUsd = Number(price.base_amount);
234
+ if (baseAmountUsd > 0 && Number.isFinite(baseAmountUsd)) {
235
+ const tokenAmount = baseAmountUsd / rate;
236
+ const unitAmount = fromTokenToUnit(tokenAmount.toFixed(currency.decimal || 8), currency.decimal || 8);
237
+ return {
238
+ unitAmount,
239
+ totalAmount: unitAmount.mul(quantity),
240
+ isDynamicQuote: false,
241
+ };
242
+ }
243
+ }
244
+ }
245
+
246
+ // For Stripe/USD mode: use base_amount directly when no exchange rate
247
+ // (applies to both dynamic and standard pricing — unit_amount may be in crypto denomination)
248
+ if (!exchangeRate && price?.base_amount != null) {
249
+ const baseAmountUsd = Number(price.base_amount);
250
+ if (baseAmountUsd >= 0 && Number.isFinite(baseAmountUsd)) {
251
+ const dec = currency.decimal || 2;
252
+ const unitAmountBN = fromTokenToUnit(baseAmountUsd.toFixed(dec), dec);
253
+ return {
254
+ unitAmount: unitAmountBN,
255
+ totalAmount: unitAmountBN.mul(quantity),
256
+ isDynamicQuote: false,
257
+ };
258
+ }
259
+ }
260
+
261
+ const unitAmount = new BN(getPriceUnitAmountByCurrency(price, currency));
262
+ return {
263
+ unitAmount,
264
+ totalAmount: unitAmount.mul(quantity),
265
+ isDynamicQuote: false,
266
+ };
267
+ }
268
+
269
+ export function getCheckoutAmount(
270
+ items: TLineItemExpanded[],
271
+ currency: TPaymentCurrency,
272
+ trialing = false,
273
+ upsell = true,
274
+ { exchangeRate = null }: { exchangeRate?: string | null } = {}
275
+ ): { subtotal: string; total: string; renew: string; discount: string; shipping: string; tax: string } {
276
+ let renew = new BN(0);
277
+ const total = items
278
+ .filter((x) => {
279
+ const price = upsell ? x.upsell_price || x.price : x.price;
280
+ return price != null;
281
+ })
282
+ .reduce((acc, x) => {
283
+ const quoteCurrencyId = x.quote_currency_id as string | undefined;
284
+
285
+ // Determine totalAmount: prefer custom_amount only when quote_currency_id confirms it matches
286
+ // (when quote_currency_id is absent, custom_amount denomination is unknown)
287
+ let totalAmount: BN;
288
+ if (x.custom_amount && quoteCurrencyId && quoteCurrencyId === currency.id) {
289
+ totalAmount = new BN(x.custom_amount);
290
+ } else {
291
+ ({ totalAmount } = getLineItemAmounts(x, currency, { useUpsell: upsell, exchangeRate }));
292
+ }
293
+
294
+ const price = upsell ? x.upsell_price || x.price : x.price;
295
+
296
+ if (price?.type === 'recurring') {
297
+ renew = renew.add(totalAmount);
298
+ if (trialing) {
299
+ return acc;
300
+ }
301
+ if (price.recurring?.usage_type === 'metered') {
302
+ return acc;
303
+ }
304
+ }
305
+
306
+ return acc.add(totalAmount);
307
+ }, new BN(0))
308
+ .toString();
309
+
310
+ return {
311
+ subtotal: total,
312
+ total,
313
+ renew: formatDynamicPrice(renew.toString(), !!exchangeRate),
314
+ discount: '0',
315
+ shipping: '0',
316
+ tax: '0',
317
+ };
318
+ }
@@ -0,0 +1,49 @@
1
+ import pWaitFor from 'p-wait-for';
2
+
3
+ import api, { API } from './api';
4
+ import type { CheckoutResult } from '../checkout/types';
5
+
6
+ /**
7
+ * Poll for checkout session completion.
8
+ * Extracted from react/src/payment/form/index.tsx:72-99
9
+ *
10
+ * Uses p-wait-for with 2s interval and 3 minute timeout.
11
+ * Completion condition: session.status === 'complete' && payment_status in ['paid', 'no_payment_required']
12
+ * Throws if paymentIntent has last_payment_error.
13
+ */
14
+ export async function waitForCheckoutComplete(sessionId: string): Promise<CheckoutResult> {
15
+ let result: CheckoutResult;
16
+
17
+ await pWaitFor(
18
+ async () => {
19
+ const { data } = await api.get(API.RETRIEVE_SESSION(sessionId));
20
+
21
+ if (
22
+ data.paymentIntent &&
23
+ data.paymentIntent.status === 'requires_action' &&
24
+ data.paymentIntent.last_payment_error
25
+ ) {
26
+ throw new Error(data.paymentIntent.last_payment_error.message);
27
+ }
28
+
29
+ result = data;
30
+
31
+ return (
32
+ data.checkoutSession?.status === 'complete' &&
33
+ ['paid', 'no_payment_required'].includes(data.checkoutSession?.payment_status)
34
+ );
35
+ },
36
+ { interval: 2000, timeout: 3 * 60 * 1000 }
37
+ );
38
+
39
+ // @ts-expect-error result is assigned inside the polling callback
40
+ return result;
41
+ }
42
+
43
+ /**
44
+ * Generate unique idempotency key for submit (Final Freeze Architecture).
45
+ * Format: ${sessionId}-${currencyId}-${timestamp}-${random}
46
+ */
47
+ export function generateIdempotencyKey(sessionId: string, currencyId: string): string {
48
+ return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
49
+ }
@@ -0,0 +1,13 @@
1
+ // Shared types used across all domains
2
+
3
+ export interface ApiConfig {
4
+ baseUrl?: string;
5
+ authToken?: string;
6
+ locale?: string;
7
+ livemode?: boolean;
8
+ }
9
+
10
+ export interface LoadingState {
11
+ isLoading: boolean;
12
+ error: string | null;
13
+ }
@@ -0,0 +1,254 @@
1
+ import isEmail from 'validator/lib/isEmail';
2
+
3
+ import type { CheckoutFormData } from '../checkout/types';
4
+
5
+ // ── Postal Code Patterns ──
6
+
7
+ const fourDigit = /^\d{4}$/;
8
+ const fiveDigit = /^\d{5}$/;
9
+ const sixDigit = /^\d{6}$/;
10
+
11
+ export const postalCodePatterns: Record<string, RegExp> = {
12
+ 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,
13
+ JE: /^JE\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$/i,
14
+ GG: /^GY\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$/i,
15
+ IM: /^IM\d[\dA-Z]?[ ]?\d[ABD-HJLN-UW-Z]{2}$/i,
16
+ US: /^\d{5}([ -]\d{4})?$/,
17
+ CA: /^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJ-NPRSTV-Z][ ]?\d[ABCEGHJ-NPRSTV-Z]\d$/i,
18
+ DE: fiveDigit,
19
+ JP: /^\d{3}-?\d{4}$/,
20
+ FR: /^\d{2}[ ]?\d{3}$/,
21
+ AU: fourDigit,
22
+ IT: fiveDigit,
23
+ CH: fourDigit,
24
+ AT: fourDigit,
25
+ ES: fiveDigit,
26
+ NL: /^\d{4}[ ]?[A-Z]{2}$/i,
27
+ BE: fourDigit,
28
+ DK: fourDigit,
29
+ SE: /^\d{3}[ ]?\d{2}$/,
30
+ NO: fourDigit,
31
+ BR: /^\d{5}-?\d{3}$/,
32
+ PT: /^\d{4}(-\d{3})?$/,
33
+ FI: fiveDigit,
34
+ KR: fiveDigit,
35
+ CN: sixDigit,
36
+ TW: /^\d{3}(\d{2})?$/,
37
+ SG: sixDigit,
38
+ IN: /^[1-9]\d{5}$/,
39
+ RU: sixDigit,
40
+ PL: /^\d{2}-\d{3}$/,
41
+ CZ: /^\d{3}[ ]?\d{2}$/,
42
+ HU: fourDigit,
43
+ RO: sixDigit,
44
+ BG: fourDigit,
45
+ HR: fiveDigit,
46
+ SK: /^\d{3}[ ]?\d{2}$/,
47
+ SI: fourDigit,
48
+ TR: fiveDigit,
49
+ IL: fiveDigit,
50
+ ZA: fourDigit,
51
+ MX: fiveDigit,
52
+ AR: /^([A-HJ-NP-Z])?\d{4}([A-Z]{3})?$/i,
53
+ CL: /^\d{7}$/,
54
+ CO: sixDigit,
55
+ NZ: fourDigit,
56
+ MY: fiveDigit,
57
+ TH: fiveDigit,
58
+ PH: fourDigit,
59
+ ID: fiveDigit,
60
+ VN: sixDigit,
61
+ };
62
+
63
+ export function validatePostalCode(postalCode: string, country?: string): boolean {
64
+ if (!postalCode) return true;
65
+ try {
66
+ const countryUpper = country?.toUpperCase();
67
+ if (!countryUpper) return false;
68
+ const pattern = postalCodePatterns[countryUpper];
69
+ return !pattern || pattern.test(postalCode);
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ // ── Field Validation (custom rules from metadata) ──
76
+
77
+ export function getFieldValidation(
78
+ fieldName: string,
79
+ validations?: Record<string, { pattern?: string; pattern_message?: Record<string, string> }>,
80
+ locale: string = 'en'
81
+ ): { pattern?: { value: RegExp; message: string } } {
82
+ if (!validations || !validations[fieldName]) return {};
83
+ const fieldValidation = validations[fieldName];
84
+ const rules: { pattern?: { value: RegExp; message: string } } = {};
85
+ if (fieldValidation.pattern) {
86
+ rules.pattern = {
87
+ value: new RegExp(fieldValidation.pattern),
88
+ message: fieldValidation.pattern_message?.[locale] || 'Invalid format',
89
+ };
90
+ }
91
+ return rules;
92
+ }
93
+
94
+ // ── Phone Validation (google-libphonenumber, matching payment-react) ──
95
+
96
+ let phoneUtil: any = null;
97
+
98
+ async function getPhoneUtil() {
99
+ if (!phoneUtil) {
100
+ const result = await import('google-libphonenumber');
101
+ const PhoneNumberUtil = (result.default || result)?.PhoneNumberUtil;
102
+ if (!PhoneNumberUtil) {
103
+ throw new Error('PhoneNumberUtil not found');
104
+ }
105
+ phoneUtil = PhoneNumberUtil.getInstance();
106
+ }
107
+ return phoneUtil;
108
+ }
109
+
110
+ const PHONE_REGEX_FALLBACK = /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/im;
111
+
112
+ export async function validatePhone(phone: string): Promise<boolean> {
113
+ if (!phone || phone.trim() === '') return false;
114
+ try {
115
+ const util = await getPhoneUtil();
116
+ const parsed = util.parseAndKeepRawInput(phone);
117
+ return util.isValidNumber(parsed);
118
+ } catch {
119
+ // Fallback to regex if google-libphonenumber fails to load
120
+ return PHONE_REGEX_FALLBACK.test(phone);
121
+ }
122
+ }
123
+
124
+ // ── Email Validation ──
125
+
126
+ export function validateEmail(email: string): boolean {
127
+ if (!email || email.trim() === '') return false;
128
+ return isEmail(email);
129
+ }
130
+
131
+ // ── Form Validation (pure function extracted from react/src/payment/form/index.tsx:617-713) ──
132
+
133
+ export interface ValidateFormOptions {
134
+ phoneEnabled: boolean;
135
+ addressMode: 'auto' | 'required' | null;
136
+ fieldValidation?: Record<string, { pattern?: string; pattern_message?: Record<string, string> }>;
137
+ locale?: string;
138
+ }
139
+
140
+ export interface FormErrors {
141
+ customer_name?: string;
142
+ customer_email?: string;
143
+ customer_phone?: string;
144
+ 'billing_address.country'?: string;
145
+ 'billing_address.state'?: string;
146
+ 'billing_address.line1'?: string;
147
+ 'billing_address.city'?: string;
148
+ 'billing_address.postal_code'?: string;
149
+ [key: string]: string | undefined;
150
+ }
151
+
152
+ export async function validateForm(
153
+ values: CheckoutFormData | null,
154
+ options: ValidateFormOptions
155
+ ): Promise<{ valid: boolean; errors: FormErrors }> {
156
+ const errors: FormErrors = {};
157
+ const { phoneEnabled, addressMode, fieldValidation, locale = 'en' } = options;
158
+
159
+ if (!values) {
160
+ return { valid: false, errors: { customer_name: 'Required' } };
161
+ }
162
+
163
+ // Name
164
+ const customerName = values.customer_name;
165
+ if (!customerName || customerName.trim() === '') {
166
+ errors.customer_name = 'Required';
167
+ } else {
168
+ const nameRule = getFieldValidation('customer_name', fieldValidation, locale);
169
+ if (nameRule.pattern && !nameRule.pattern.value.test(customerName)) {
170
+ errors.customer_name = nameRule.pattern.message;
171
+ }
172
+ }
173
+
174
+ // Email
175
+ const customerEmail = values.customer_email;
176
+ if (!customerEmail || !validateEmail(customerEmail)) {
177
+ errors.customer_email = !customerEmail ? 'Required' : 'Invalid email';
178
+ } else {
179
+ const emailRule = getFieldValidation('customer_email', fieldValidation, locale);
180
+ if (emailRule.pattern && !emailRule.pattern.value.test(customerEmail)) {
181
+ errors.customer_email = emailRule.pattern.message;
182
+ }
183
+ }
184
+
185
+ // Phone
186
+ if (phoneEnabled) {
187
+ const customerPhone = values.customer_phone;
188
+ if (!customerPhone || customerPhone.trim() === '') {
189
+ errors.customer_phone = 'Required';
190
+ } else if (!(await validatePhone(customerPhone))) {
191
+ errors.customer_phone = 'Invalid phone number';
192
+ } else {
193
+ const phoneRule = getFieldValidation('customer_phone', fieldValidation, locale);
194
+ if (phoneRule.pattern && !phoneRule.pattern.value.test(customerPhone)) {
195
+ errors.customer_phone = phoneRule.pattern.message;
196
+ }
197
+ }
198
+ }
199
+
200
+ // Billing address
201
+ const billingAddress = values.billing_address;
202
+ const postalCode = billingAddress?.postal_code;
203
+ const country = billingAddress?.country;
204
+ const state = billingAddress?.state;
205
+ const line1 = billingAddress?.line1;
206
+ const city = billingAddress?.city;
207
+
208
+ // Postal code & state are required in both auto and required modes (matching V1 behavior)
209
+ if (!postalCode) {
210
+ errors['billing_address.postal_code'] = 'Required';
211
+ } else if (!validatePostalCode(postalCode, country)) {
212
+ errors['billing_address.postal_code'] = 'Invalid postal code';
213
+ } else {
214
+ const postalRule = getFieldValidation('billing_address.postal_code', fieldValidation, locale);
215
+ if (postalRule.pattern && !postalRule.pattern.value.test(postalCode)) {
216
+ errors['billing_address.postal_code'] = postalRule.pattern.message;
217
+ }
218
+ }
219
+
220
+ if (!state) {
221
+ errors['billing_address.state'] = 'Required';
222
+ } else {
223
+ const stateRule = getFieldValidation('billing_address.state', fieldValidation, locale);
224
+ if (stateRule.pattern && !stateRule.pattern.value.test(state)) {
225
+ errors['billing_address.state'] = stateRule.pattern.message;
226
+ }
227
+ }
228
+
229
+ // Full address fields required only in 'required' mode
230
+ if (addressMode === 'required') {
231
+ if (!country) errors['billing_address.country'] = 'Required';
232
+ if (!line1) {
233
+ errors['billing_address.line1'] = 'Required';
234
+ } else {
235
+ const line1Rule = getFieldValidation('billing_address.line1', fieldValidation, locale);
236
+ if (line1Rule.pattern && !line1Rule.pattern.value.test(line1)) {
237
+ errors['billing_address.line1'] = line1Rule.pattern.message;
238
+ }
239
+ }
240
+ if (!city) {
241
+ errors['billing_address.city'] = 'Required';
242
+ } else {
243
+ const cityRule = getFieldValidation('billing_address.city', fieldValidation, locale);
244
+ if (cityRule.pattern && !cityRule.pattern.value.test(city)) {
245
+ errors['billing_address.city'] = cityRule.pattern.message;
246
+ }
247
+ }
248
+ }
249
+
250
+ return {
251
+ valid: Object.keys(errors).length === 0,
252
+ errors,
253
+ };
254
+ }