@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,493 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { useMemoizedFn } from "ahooks";
3
+ import { WsClient } from "@arcblock/ws";
4
+ import api, { API } from "../../shared/api.js";
5
+ import { waitForCheckoutComplete, generateIdempotencyKey } from "../../shared/polling.js";
6
+ import { getErrorMessage, getAxiosErrorDetails } from "../../types/checkout-augmented.js";
7
+ import {
8
+ RELAY_SOCKET_PREFIX,
9
+ getAppId,
10
+ getRelayChannel,
11
+ getRelayProtocol,
12
+ getSocketHost,
13
+ getSessionFingerprint,
14
+ buildSubmitPayload,
15
+ isQuoteError,
16
+ abortStripePayment,
17
+ submitCheckout,
18
+ confirmFastCheckout
19
+ } from "../core/submit.js";
20
+ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit, isDonation, formValues, validateForm, refreshSession, updateSessionData) {
21
+ const session = sessionData?.checkoutSession;
22
+ const [status, setStatus] = useState("idle");
23
+ const [context, setContext] = useState(null);
24
+ const [result, setResult] = useState(null);
25
+ const [locked, setLocked] = useState(false);
26
+ const lock = useMemoizedFn(() => setLocked(true));
27
+ const unlock = useMemoizedFn(() => setLocked(false));
28
+ const mountedRef = useRef(true);
29
+ const pollingAbortRef = useRef(false);
30
+ const socketRef = useRef(null);
31
+ const subscriptionRef = useRef(null);
32
+ const lastPayloadRef = useRef(null);
33
+ const [vendorStatus, setVendorStatus] = useState(null);
34
+ const vendorTimerRef = useRef(null);
35
+ const quoteRetryCountRef = useRef(0);
36
+ const MAX_QUOTE_RETRIES = 2;
37
+ const idempotencyKeyRef = useRef("");
38
+ const sessionFingerprintRef = useRef("");
39
+ const completedDetectedRef = useRef(false);
40
+ useEffect(() => {
41
+ if (completedDetectedRef.current) return;
42
+ if (!session || !sessionData) return;
43
+ const isSessionComplete = session.status === "complete" && ["paid", "no_payment_required"].includes(session.payment_status);
44
+ if (isSessionComplete && status === "idle") {
45
+ completedDetectedRef.current = true;
46
+ setResult(sessionData);
47
+ setStatus("completed");
48
+ }
49
+ }, [session, sessionData]);
50
+ useEffect(() => {
51
+ mountedRef.current = true;
52
+ if (!sessionId || !getAppId()) return void 0;
53
+ const socket = new WsClient(`${getRelayProtocol()}//${getSocketHost()}${RELAY_SOCKET_PREFIX}`, {
54
+ longpollerTimeout: 5e3,
55
+ heartbeatIntervalMs: 30 * 1e3
56
+ });
57
+ socket.connect();
58
+ socketRef.current = socket;
59
+ const sub = socket.subscribe(getRelayChannel("events"));
60
+ sub.channel = "events";
61
+ subscriptionRef.current = sub;
62
+ return () => {
63
+ mountedRef.current = false;
64
+ pollingAbortRef.current = true;
65
+ if (subscriptionRef.current) {
66
+ socket.unsubscribe(getRelayChannel(subscriptionRef.current.channel));
67
+ subscriptionRef.current = null;
68
+ }
69
+ socket.disconnect();
70
+ socketRef.current = null;
71
+ };
72
+ }, [sessionId]);
73
+ const handleWsComplete = useMemoizedFn(async ({ response }) => {
74
+ if (response.id === sessionId && status !== "completed") {
75
+ pollingAbortRef.current = true;
76
+ await handleCompletion();
77
+ }
78
+ });
79
+ useEffect(() => {
80
+ const sub = subscriptionRef.current;
81
+ if (sub) {
82
+ sub.on("checkout.session.completed", handleWsComplete);
83
+ }
84
+ return () => {
85
+ if (sub) {
86
+ try {
87
+ sub.off?.("checkout.session.completed", handleWsComplete);
88
+ } catch {
89
+ }
90
+ }
91
+ };
92
+ }, [subscriptionRef.current, status]);
93
+ const handleCompletion = useMemoizedFn(async () => {
94
+ try {
95
+ const completionResult = await waitForCheckoutComplete(sessionId);
96
+ if (!mountedRef.current) return;
97
+ setResult(completionResult);
98
+ updateSessionData?.(completionResult);
99
+ setStatus("completed");
100
+ setContext(null);
101
+ } catch (err) {
102
+ if (!mountedRef.current) return;
103
+ setStatus("failed");
104
+ setContext({ type: "error", message: getErrorMessage(err) || "Payment verification failed" });
105
+ }
106
+ });
107
+ useEffect(() => {
108
+ if (status !== "completed" || !sessionId) return void 0;
109
+ const hasVendors = session?.line_items?.some(
110
+ (item) => !!item?.price?.product?.vendor_config?.length
111
+ );
112
+ if (!hasVendors) return void 0;
113
+ const startTime = Date.now();
114
+ const fetchVendorStatus = async () => {
115
+ try {
116
+ const { data } = await api.get(API.VENDOR_ORDER_STATUS(sessionId));
117
+ if (!mountedRef.current) return;
118
+ const needCheckError = Date.now() - startTime > 6e3;
119
+ const allCompleted = data?.vendors?.every((v) => v.progress >= 100);
120
+ const hasFailed = data?.vendors?.some(
121
+ (v) => v.status === "failed" || needCheckError && !!v.error && !!v.error_message
122
+ );
123
+ setVendorStatus({
124
+ ...data,
125
+ isAllCompleted: !hasFailed && allCompleted,
126
+ hasFailed
127
+ });
128
+ if (hasFailed || allCompleted) return;
129
+ vendorTimerRef.current = setTimeout(fetchVendorStatus, 5e3);
130
+ } catch {
131
+ }
132
+ };
133
+ fetchVendorStatus();
134
+ return () => {
135
+ if (vendorTimerRef.current) clearTimeout(vendorTimerRef.current);
136
+ };
137
+ }, [status, sessionId]);
138
+ const handleQuoteError = useMemoizedFn(async (errorCode, errorData) => {
139
+ try {
140
+ await refreshSession(true);
141
+ if (!mountedRef.current) return;
142
+ } catch {
143
+ }
144
+ if (errorCode === "QUOTE_UPDATED" && errorData?.checkoutSession) {
145
+ quoteRetryCountRef.current = 0;
146
+ setStatus("failed");
147
+ setContext({ type: "error", message: "Price updated, please resubmit", code: errorCode });
148
+ return;
149
+ }
150
+ const canAutoRetry = ["QUOTE_LOCK_EXPIRED", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED"].includes(
151
+ errorCode
152
+ );
153
+ if (canAutoRetry && quoteRetryCountRef.current < MAX_QUOTE_RETRIES) {
154
+ quoteRetryCountRef.current += 1;
155
+ setStatus("idle");
156
+ setContext(null);
157
+ await execute(true);
158
+ return;
159
+ }
160
+ quoteRetryCountRef.current = 0;
161
+ setStatus("failed");
162
+ setContext({
163
+ type: "error",
164
+ message: "Price updated, please resubmit",
165
+ code: errorCode
166
+ });
167
+ });
168
+ const routeSubmitResponse = useMemoizedFn(async (data) => {
169
+ if (isStripe) {
170
+ const stripeCtx = data.stripeContext;
171
+ if (stripeCtx?.status === "succeeded") {
172
+ await handleCompletion();
173
+ return;
174
+ }
175
+ const piCtx = data.paymentIntent;
176
+ const clientSecret = stripeCtx?.client_secret || piCtx?.client_secret;
177
+ if (clientSecret) {
178
+ const intentType = stripeCtx?.intent_type || "payment_intent";
179
+ setStatus("waiting_stripe");
180
+ setContext({ type: "stripe", clientSecret, intentType });
181
+ return;
182
+ }
183
+ if (data.noPaymentRequired) {
184
+ try {
185
+ const confirmResult = await confirmFastCheckout(sessionId);
186
+ const confirmSession = confirmResult.checkoutSession;
187
+ if (confirmResult.fastPaid || confirmSession?.status === "complete") {
188
+ await handleCompletion();
189
+ return;
190
+ }
191
+ } catch {
192
+ }
193
+ }
194
+ await handleCompletion();
195
+ return;
196
+ }
197
+ if (data.noPaymentRequired) {
198
+ try {
199
+ const confirmResult = await confirmFastCheckout(sessionId);
200
+ const confirmSession = confirmResult.checkoutSession;
201
+ if (confirmResult.fastPaid || confirmSession?.status === "complete") {
202
+ await handleCompletion();
203
+ } else {
204
+ setStatus("waiting_did");
205
+ setContext({
206
+ type: "did_connect",
207
+ action: session?.mode || "payment",
208
+ checkpointId: sessionId,
209
+ extraParams: {
210
+ checkoutSessionId: sessionId,
211
+ sessionUserDid: session?.user?.did
212
+ }
213
+ });
214
+ }
215
+ } catch {
216
+ if (!mountedRef.current) return;
217
+ setStatus("waiting_did");
218
+ setContext({
219
+ type: "did_connect",
220
+ action: session?.mode || "payment",
221
+ checkpointId: sessionId,
222
+ extraParams: {
223
+ checkoutSessionId: sessionId,
224
+ sessionUserDid: session?.user?.did
225
+ }
226
+ });
227
+ }
228
+ return;
229
+ }
230
+ const fastPayInfo = data.fastPayInfo;
231
+ const balance = data.balance;
232
+ const delegation = data.delegation;
233
+ if (isCredit) {
234
+ if (data.creditSufficient === true && fastPayInfo) {
235
+ setStatus("confirming_fast_pay");
236
+ setContext({
237
+ type: "fast_pay",
238
+ payType: "credit",
239
+ amount: fastPayInfo.amount || "0",
240
+ payer: fastPayInfo.payer || ""
241
+ });
242
+ return;
243
+ }
244
+ setStatus("credit_insufficient");
245
+ setContext({
246
+ type: "credit_insufficient",
247
+ available: "0",
248
+ required: "0"
249
+ });
250
+ return;
251
+ }
252
+ if ((balance?.sufficient || delegation?.sufficient) && !isDonation && fastPayInfo) {
253
+ setStatus("confirming_fast_pay");
254
+ setContext({
255
+ type: "fast_pay",
256
+ payType: fastPayInfo.type || "balance",
257
+ amount: fastPayInfo.amount || "",
258
+ payer: fastPayInfo.payer || ""
259
+ });
260
+ return;
261
+ }
262
+ setStatus("waiting_did");
263
+ setContext({
264
+ type: "did_connect",
265
+ action: session?.mode || "payment",
266
+ checkpointId: sessionId,
267
+ extraParams: {
268
+ checkoutSessionId: sessionId,
269
+ sessionUserDid: session?.user?.did
270
+ }
271
+ });
272
+ });
273
+ const handleSubmitError = useMemoizedFn(async (err) => {
274
+ const { code: errorCode, data: errorData, message: errMessage } = getAxiosErrorDetails(err);
275
+ if (errorCode && isQuoteError(errorCode)) {
276
+ await handleQuoteError(errorCode, errorData);
277
+ return;
278
+ }
279
+ if (errorCode === "RATE_BELOW_SLIPPAGE_LIMIT") {
280
+ try {
281
+ await refreshSession(true);
282
+ } catch {
283
+ }
284
+ if (!mountedRef.current) return;
285
+ idempotencyKeyRef.current = "";
286
+ sessionFingerprintRef.current = "";
287
+ setStatus("failed");
288
+ setContext({
289
+ type: "error",
290
+ message: errorData?.error || "Exchange rate below acceptable limit, please retry",
291
+ code: errorCode
292
+ });
293
+ return;
294
+ }
295
+ if (errorCode === "RATE_UNAVAILABLE") {
296
+ try {
297
+ await refreshSession(true);
298
+ } catch {
299
+ }
300
+ if (!mountedRef.current) return;
301
+ setStatus("failed");
302
+ setContext({
303
+ type: "error",
304
+ message: errorData?.rateError || "Exchange rate unavailable",
305
+ code: errorCode
306
+ });
307
+ return;
308
+ }
309
+ if (errorCode === "PRICE_CHANGED") {
310
+ setStatus("confirming_price");
311
+ setContext({
312
+ type: "price_change",
313
+ changePercent: errorData?.change_percent || 0
314
+ });
315
+ return;
316
+ }
317
+ if (errorCode === "PRICE_UNAVAILABLE" || errorCode === "PRICE_UNSTABLE") {
318
+ try {
319
+ await refreshSession(true);
320
+ } catch {
321
+ }
322
+ if (!mountedRef.current) return;
323
+ setStatus("failed");
324
+ setContext({
325
+ type: "error",
326
+ message: errorData?.error || "Price unavailable, please retry",
327
+ code: errorCode
328
+ });
329
+ return;
330
+ }
331
+ if (errorCode === "UNIFIED_APP_REQUIRED" || errorCode === "CUSTOMER_LIMITED") {
332
+ setStatus("failed");
333
+ setContext({
334
+ type: "error",
335
+ message: errorData?.error || errMessage || "Cannot complete payment",
336
+ code: errorCode
337
+ });
338
+ return;
339
+ }
340
+ const message = errorData?.error || errMessage || "Payment failed";
341
+ setStatus("failed");
342
+ setContext({ type: "error", message, code: errorCode });
343
+ });
344
+ const execute = useMemoizedFn(async (force = false) => {
345
+ if (!force && status !== "idle") return;
346
+ if (!force) quoteRetryCountRef.current = 0;
347
+ if (!await validateForm()) return;
348
+ const user = session?.user;
349
+ if (!user?.sourceAppPid) {
350
+ const hasVendorConfig = session?.line_items?.some(
351
+ (item) => !!item?.price?.product?.vendor_config?.length
352
+ );
353
+ if (hasVendorConfig) {
354
+ setStatus("failed");
355
+ setContext({ type: "error", message: "Vendor account required", code: "VENDOR_ACCOUNT_REQUIRED" });
356
+ return;
357
+ }
358
+ }
359
+ setStatus("submitting");
360
+ setContext(null);
361
+ setResult(null);
362
+ pollingAbortRef.current = false;
363
+ const fingerprint = getSessionFingerprint(session, currencyId);
364
+ if (fingerprint !== sessionFingerprintRef.current || !idempotencyKeyRef.current) {
365
+ sessionFingerprintRef.current = fingerprint;
366
+ idempotencyKeyRef.current = generateIdempotencyKey(sessionId, currencyId || "");
367
+ }
368
+ const payload = buildSubmitPayload(sessionId, currencyId, formValues, session, false, idempotencyKeyRef.current);
369
+ lastPayloadRef.current = payload;
370
+ try {
371
+ const data = await submitCheckout(sessionId, isDonation, payload);
372
+ if (!mountedRef.current) return;
373
+ await routeSubmitResponse(data);
374
+ } catch (err) {
375
+ if (!mountedRef.current) return;
376
+ await handleSubmitError(err);
377
+ }
378
+ });
379
+ const confirm = useMemoizedFn(async () => {
380
+ if (status === "confirming_fast_pay") {
381
+ setStatus("submitting");
382
+ try {
383
+ const data = await confirmFastCheckout(sessionId);
384
+ if (!mountedRef.current) return;
385
+ if (data.fastPaid) {
386
+ await handleCompletion();
387
+ } else {
388
+ setStatus("waiting_did");
389
+ setContext({
390
+ type: "did_connect",
391
+ action: session?.mode || "payment",
392
+ checkpointId: sessionId,
393
+ extraParams: {
394
+ checkoutSessionId: sessionId,
395
+ sessionUserDid: session?.user?.did
396
+ }
397
+ });
398
+ }
399
+ } catch (err) {
400
+ if (!mountedRef.current) return;
401
+ const { code: errorCode, data: errorData, message: errMsg } = getAxiosErrorDetails(err);
402
+ if (errorCode && isQuoteError(errorCode)) {
403
+ await handleQuoteError(errorCode, errorData);
404
+ return;
405
+ }
406
+ if (errorCode === "RATE_UNAVAILABLE") {
407
+ try {
408
+ await refreshSession(true);
409
+ } catch {
410
+ }
411
+ if (!mountedRef.current) return;
412
+ setStatus("failed");
413
+ setContext({
414
+ type: "error",
415
+ message: errorData?.rateError || "Exchange rate unavailable",
416
+ code: errorCode
417
+ });
418
+ return;
419
+ }
420
+ setStatus("failed");
421
+ setContext({
422
+ type: "error",
423
+ message: errorData?.error || errMsg || "Fast pay confirmation failed",
424
+ code: errorCode
425
+ });
426
+ }
427
+ return;
428
+ }
429
+ if (status === "confirming_price") {
430
+ setStatus("submitting");
431
+ setContext(null);
432
+ pollingAbortRef.current = false;
433
+ const payload = buildSubmitPayload(sessionId, currencyId, formValues, session, true, idempotencyKeyRef.current);
434
+ lastPayloadRef.current = payload;
435
+ try {
436
+ const data = await submitCheckout(sessionId, isDonation, payload);
437
+ if (!mountedRef.current) return;
438
+ await routeSubmitResponse(data);
439
+ } catch (err) {
440
+ if (!mountedRef.current) return;
441
+ await handleSubmitError(err);
442
+ }
443
+ }
444
+ });
445
+ const cancel = useMemoizedFn(() => {
446
+ if (status === "confirming_price" || status === "confirming_fast_pay" || status === "credit_insufficient") {
447
+ setStatus("idle");
448
+ setContext(null);
449
+ unlock();
450
+ }
451
+ });
452
+ const retry = useMemoizedFn(async () => {
453
+ if (status !== "failed") return;
454
+ setStatus("idle");
455
+ setContext(null);
456
+ setResult(null);
457
+ await execute(true);
458
+ });
459
+ const reset = useMemoizedFn(() => {
460
+ pollingAbortRef.current = true;
461
+ setStatus("idle");
462
+ setContext(null);
463
+ setResult(null);
464
+ unlock();
465
+ });
466
+ const stripeConfirm = useMemoizedFn(async () => {
467
+ if (status !== "waiting_stripe") return;
468
+ setStatus("submitting");
469
+ setContext(null);
470
+ await handleCompletion();
471
+ });
472
+ const stripeCancel = useMemoizedFn(async () => {
473
+ if (status !== "waiting_stripe") return;
474
+ await abortStripePayment(sessionId);
475
+ setStatus("idle");
476
+ setContext(null);
477
+ });
478
+ return {
479
+ status,
480
+ context,
481
+ execute,
482
+ confirm,
483
+ cancel,
484
+ result,
485
+ retry,
486
+ reset,
487
+ stripeConfirm,
488
+ stripeCancel,
489
+ vendorStatus,
490
+ locked,
491
+ lock
492
+ };
493
+ }
@@ -0,0 +1,2 @@
1
+ import type { UseSubmitReturn } from './useSubmit';
2
+ export declare function useSubmitFeature(): UseSubmitReturn;
@@ -0,0 +1,4 @@
1
+ import { useSubmitContext } from "../context/SubmitContext.js";
2
+ export function useSubmitFeature() {
3
+ return useSubmitContext();
4
+ }
@@ -0,0 +1,5 @@
1
+ export interface UseUpsellReturn {
2
+ upsell: (fromId: string, toId: string) => Promise<void>;
3
+ downsell: (priceId: string) => Promise<void>;
4
+ }
5
+ export declare function useUpsell(): UseUpsellReturn;
@@ -0,0 +1,25 @@
1
+ import { useMemoizedFn } from "ahooks";
2
+ import { getErrorMessage } from "../../types/checkout-augmented.js";
3
+ import { useSessionContext } from "../context/SessionContext.js";
4
+ import { usePaymentMethodContext } from "../context/PaymentMethodContext.js";
5
+ import { performUpsell, performDownsell } from "../core/lineItems.js";
6
+ export function useUpsell() {
7
+ const { session, effectiveSessionId, refresh } = useSessionContext();
8
+ const { currency } = usePaymentMethodContext();
9
+ const currencyId = currency?.id || null;
10
+ const upsell = useMemoizedFn(async (fromId, toId) => {
11
+ try {
12
+ await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
13
+ } catch (err) {
14
+ console.error("Failed to upsell:", getErrorMessage(err));
15
+ }
16
+ });
17
+ const downsell = useMemoizedFn(async (priceId) => {
18
+ try {
19
+ await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
20
+ } catch (err) {
21
+ console.error("Failed to downsell:", getErrorMessage(err));
22
+ }
23
+ });
24
+ return { upsell, downsell };
25
+ }
@@ -0,0 +1,37 @@
1
+ export type { UseCheckoutReturn, SubmitStatus, SubmitContext, FieldConfig, CheckoutFormData, CheckoutResult, } from './types';
2
+ export { CheckoutProvider, type CheckoutProviderProps, useSessionContext, usePaymentMethodContext, useExchangeRateContext, useCustomerFormContext, useSubmitContext, } from './context';
3
+ export { useCheckout } from './hooks/useCheckout';
4
+ export { useCheckoutSession } from './hooks/useCheckoutSession';
5
+ export type { UseCheckoutSessionReturn, SessionData } from './hooks/useCheckoutSession';
6
+ export { usePaymentMethod } from './hooks/usePaymentMethod';
7
+ export type { UsePaymentMethodReturn } from './hooks/usePaymentMethod';
8
+ export { usePricing } from './hooks/usePricing';
9
+ export type { UsePricingReturn } from './hooks/usePricing';
10
+ export { useCustomerForm } from './hooks/useCustomerForm';
11
+ export type { UseCustomerFormReturn } from './hooks/useCustomerForm';
12
+ export { useSubmit } from './hooks/useSubmit';
13
+ export type { UseSubmitReturn } from './hooks/useSubmit';
14
+ export { useProduct } from './hooks/useProduct';
15
+ export type { UseProductReturn } from './hooks/useProduct';
16
+ export { useLineItems } from './hooks/useLineItems';
17
+ export type { UseLineItemsReturn } from './hooks/useLineItems';
18
+ export { useBillingInterval } from './hooks/useBillingInterval';
19
+ export type { UseBillingIntervalReturn, BillingIntervalData } from './hooks/useBillingInterval';
20
+ export { useUpsell } from './hooks/useUpsell';
21
+ export type { UseUpsellReturn } from './hooks/useUpsell';
22
+ export { useCrossSell } from './hooks/useCrossSell';
23
+ export type { UseCrossSellReturn } from './hooks/useCrossSell';
24
+ export { usePromotion } from './hooks/usePromotion';
25
+ export type { UsePromotionReturn } from './hooks/usePromotion';
26
+ export { useExchangeRate } from './hooks/useExchangeRate';
27
+ export type { UseExchangeRateReturn } from './hooks/useExchangeRate';
28
+ export { useSlippage } from './hooks/useSlippage';
29
+ export type { UseSlippageReturn } from './hooks/useSlippage';
30
+ export { useCheckoutStatus } from './hooks/useCheckoutStatus';
31
+ export type { UseCheckoutStatusReturn } from './hooks/useCheckoutStatus';
32
+ export { usePricingFeature } from './hooks/usePricingFeature';
33
+ export type { UsePricingFeatureReturn } from './hooks/usePricingFeature';
34
+ export { usePaymentMethodFeature } from './hooks/usePaymentMethodFeature';
35
+ export { useCustomerFormFeature } from './hooks/useCustomerFormFeature';
36
+ export { useSubmitFeature } from './hooks/useSubmitFeature';
37
+ export { getPriceUnitAmountByCurrency } from '../shared/format';
@@ -0,0 +1,28 @@
1
+ export {
2
+ CheckoutProvider,
3
+ useSessionContext,
4
+ usePaymentMethodContext,
5
+ useExchangeRateContext,
6
+ useCustomerFormContext,
7
+ useSubmitContext
8
+ } from "./context/index.js";
9
+ export { useCheckout } from "./hooks/useCheckout.js";
10
+ export { useCheckoutSession } from "./hooks/useCheckoutSession.js";
11
+ export { usePaymentMethod } from "./hooks/usePaymentMethod.js";
12
+ export { usePricing } from "./hooks/usePricing.js";
13
+ export { useCustomerForm } from "./hooks/useCustomerForm.js";
14
+ export { useSubmit } from "./hooks/useSubmit.js";
15
+ export { useProduct } from "./hooks/useProduct.js";
16
+ export { useLineItems } from "./hooks/useLineItems.js";
17
+ export { useBillingInterval } from "./hooks/useBillingInterval.js";
18
+ export { useUpsell } from "./hooks/useUpsell.js";
19
+ export { useCrossSell } from "./hooks/useCrossSell.js";
20
+ export { usePromotion } from "./hooks/usePromotion.js";
21
+ export { useExchangeRate } from "./hooks/useExchangeRate.js";
22
+ export { useSlippage } from "./hooks/useSlippage.js";
23
+ export { useCheckoutStatus } from "./hooks/useCheckoutStatus.js";
24
+ export { usePricingFeature } from "./hooks/usePricingFeature.js";
25
+ export { usePaymentMethodFeature } from "./hooks/usePaymentMethodFeature.js";
26
+ export { useCustomerFormFeature } from "./hooks/useCustomerFormFeature.js";
27
+ export { useSubmitFeature } from "./hooks/useSubmitFeature.js";
28
+ export { getPriceUnitAmountByCurrency } from "../shared/format.js";