@blocklet/payment-react-headless 1.27.1 → 1.28.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.
- package/es/checkout/hooks/useSubmit.d.ts +3 -0
- package/es/checkout/hooks/useSubmit.js +12 -2
- package/es/shared/api.d.ts +1 -0
- package/es/shared/api.js +1 -0
- package/es/shared/polling.d.ts +5 -4
- package/es/shared/polling.js +23 -14
- package/lib/checkout/hooks/useSubmit.d.ts +3 -0
- package/lib/checkout/hooks/useSubmit.js +12 -2
- package/lib/shared/api.d.ts +1 -0
- package/lib/shared/api.js +1 -0
- package/lib/shared/polling.d.ts +5 -4
- package/lib/shared/polling.js +25 -15
- package/package.json +5 -5
- package/src/checkout/hooks/useSubmit.ts +25 -2
- package/src/shared/api.ts +1 -0
- package/src/shared/polling.ts +52 -32
- package/tsconfig.json +2 -1
|
@@ -30,6 +30,9 @@ export interface UseSubmitReturn {
|
|
|
30
30
|
stripeConfirm: () => Promise<void>;
|
|
31
31
|
stripeCancel: () => Promise<void>;
|
|
32
32
|
stripeSkip: () => Promise<void>;
|
|
33
|
+
/** Signal that DID Connect completed successfully — triggers completion polling.
|
|
34
|
+
* Use this in CF Workers (no WebSocket) to poll session status after connect.close(). */
|
|
35
|
+
didConnectComplete: () => Promise<void>;
|
|
33
36
|
vendorStatus: VendorOrderStatus | null;
|
|
34
37
|
/** Whether the checkout config is locked (user clicked "Connect and Pay", pending login/submit) */
|
|
35
38
|
locked: boolean;
|
|
@@ -116,13 +116,15 @@ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit
|
|
|
116
116
|
try {
|
|
117
117
|
const { data } = await api.get(API.VENDOR_ORDER_STATUS(sessionId));
|
|
118
118
|
if (!mountedRef.current) return;
|
|
119
|
+
const vendors = Array.isArray(data?.vendors) ? data.vendors : [];
|
|
119
120
|
const needCheckError = Date.now() - startTime > 6e3;
|
|
120
|
-
const allCompleted =
|
|
121
|
-
const hasFailed =
|
|
121
|
+
const allCompleted = vendors.every((v) => v.progress >= 100);
|
|
122
|
+
const hasFailed = vendors.some(
|
|
122
123
|
(v) => v.status === "failed" || needCheckError && !!v.error && !!v.error_message
|
|
123
124
|
);
|
|
124
125
|
setVendorStatus({
|
|
125
126
|
...data,
|
|
127
|
+
vendors,
|
|
126
128
|
isAllCompleted: !hasFailed && allCompleted,
|
|
127
129
|
hasFailed
|
|
128
130
|
});
|
|
@@ -503,6 +505,13 @@ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit
|
|
|
503
505
|
setContext({ type: "error", message: err.message || "Failed to skip payment method", code: "SKIP_FAILED" });
|
|
504
506
|
}
|
|
505
507
|
});
|
|
508
|
+
const didConnectComplete = useMemoizedFn(async () => {
|
|
509
|
+
if (status === "submitting" || status === "completed") return;
|
|
510
|
+
pollingAbortRef.current = false;
|
|
511
|
+
setStatus("submitting");
|
|
512
|
+
setContext(null);
|
|
513
|
+
await handleCompletion();
|
|
514
|
+
});
|
|
506
515
|
return {
|
|
507
516
|
status,
|
|
508
517
|
context,
|
|
@@ -515,6 +524,7 @@ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit
|
|
|
515
524
|
stripeConfirm,
|
|
516
525
|
stripeCancel,
|
|
517
526
|
stripeSkip,
|
|
527
|
+
didConnectComplete,
|
|
518
528
|
vendorStatus,
|
|
519
529
|
locked,
|
|
520
530
|
lock
|
package/es/shared/api.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export default api;
|
|
|
21
21
|
export declare const API: {
|
|
22
22
|
readonly START_PAYMENT_LINK: (id: string) => string;
|
|
23
23
|
readonly RETRIEVE_SESSION: (id: string) => string;
|
|
24
|
+
readonly STATUS_SESSION: (id: string) => string;
|
|
24
25
|
readonly SWITCH_CURRENCY: (id: string) => string;
|
|
25
26
|
readonly SUBMIT: (id: string) => string;
|
|
26
27
|
readonly DONATE_SUBMIT: (id: string) => string;
|
package/es/shared/api.js
CHANGED
|
@@ -61,6 +61,7 @@ export default api;
|
|
|
61
61
|
export const API = {
|
|
62
62
|
START_PAYMENT_LINK: (id) => `/api/checkout-sessions/start/${id}`,
|
|
63
63
|
RETRIEVE_SESSION: (id) => `/api/checkout-sessions/retrieve/${id}`,
|
|
64
|
+
STATUS_SESSION: (id) => `/api/checkout-sessions/status/${id}`,
|
|
64
65
|
SWITCH_CURRENCY: (id) => `/api/checkout-sessions/${id}/switch-currency`,
|
|
65
66
|
SUBMIT: (id) => `/api/checkout-sessions/${id}/submit`,
|
|
66
67
|
DONATE_SUBMIT: (id) => `/api/checkout-sessions/${id}/donate-submit`,
|
package/es/shared/polling.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { CheckoutResult } from '../checkout/types';
|
|
2
2
|
/**
|
|
3
3
|
* Poll for checkout session completion.
|
|
4
|
-
* Extracted from react/src/payment/form/index.tsx:72-99
|
|
5
4
|
*
|
|
6
|
-
* Uses
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Uses lightweight /status endpoint during polling (single D1 query, ~50ms)
|
|
6
|
+
* instead of full /retrieve (multiple includes, ~850ms). Only fetches full
|
|
7
|
+
* session data on the final successful poll for the success page.
|
|
8
|
+
*
|
|
9
|
+
* This reduces D1 load during payment processing and speeds up detection.
|
|
9
10
|
*/
|
|
10
11
|
export declare function waitForCheckoutComplete(sessionId: string): Promise<CheckoutResult>;
|
|
11
12
|
/**
|
package/es/shared/polling.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
import pWaitFor from "p-wait-for";
|
|
2
1
|
import api, { API } from "./api.js";
|
|
2
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3
|
+
function nextPollInterval(elapsedMs) {
|
|
4
|
+
if (elapsedMs < 5e3) return 500;
|
|
5
|
+
if (elapsedMs < 3e4) return 1500;
|
|
6
|
+
return 3e3;
|
|
7
|
+
}
|
|
8
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
9
|
export async function waitForCheckoutComplete(sessionId) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const startedAt = Date.now();
|
|
11
|
+
while (true) {
|
|
12
|
+
const { data: data2 } = await api.get(API.STATUS_SESSION(sessionId));
|
|
13
|
+
if (data2.paymentIntent && data2.paymentIntent.status === "requires_action" && data2.paymentIntent.last_payment_error) {
|
|
14
|
+
throw new Error(data2.paymentIntent.last_payment_error.message);
|
|
15
|
+
}
|
|
16
|
+
const done = data2.checkoutSession?.status === "complete" && ["paid", "no_payment_required"].includes(data2.checkoutSession?.payment_status);
|
|
17
|
+
if (done) break;
|
|
18
|
+
const elapsed = Date.now() - startedAt;
|
|
19
|
+
if (elapsed >= POLL_TIMEOUT_MS) {
|
|
20
|
+
throw new Error("waitForCheckoutComplete: timed out");
|
|
21
|
+
}
|
|
22
|
+
await sleep(nextPollInterval(elapsed));
|
|
23
|
+
}
|
|
24
|
+
const { data } = await api.get(API.RETRIEVE_SESSION(sessionId));
|
|
25
|
+
return data;
|
|
17
26
|
}
|
|
18
27
|
export function generateIdempotencyKey(sessionId, currencyId) {
|
|
19
28
|
return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
|
|
@@ -30,6 +30,9 @@ export interface UseSubmitReturn {
|
|
|
30
30
|
stripeConfirm: () => Promise<void>;
|
|
31
31
|
stripeCancel: () => Promise<void>;
|
|
32
32
|
stripeSkip: () => Promise<void>;
|
|
33
|
+
/** Signal that DID Connect completed successfully — triggers completion polling.
|
|
34
|
+
* Use this in CF Workers (no WebSocket) to poll session status after connect.close(). */
|
|
35
|
+
didConnectComplete: () => Promise<void>;
|
|
33
36
|
vendorStatus: VendorOrderStatus | null;
|
|
34
37
|
/** Whether the checkout config is locked (user clicked "Connect and Pay", pending login/submit) */
|
|
35
38
|
locked: boolean;
|
|
@@ -115,11 +115,13 @@ function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit, isDon
|
|
|
115
115
|
data
|
|
116
116
|
} = await _api.default.get(_api.API.VENDOR_ORDER_STATUS(sessionId));
|
|
117
117
|
if (!mountedRef.current) return;
|
|
118
|
+
const vendors = Array.isArray(data?.vendors) ? data.vendors : [];
|
|
118
119
|
const needCheckError = Date.now() - startTime > 6e3;
|
|
119
|
-
const allCompleted =
|
|
120
|
-
const hasFailed =
|
|
120
|
+
const allCompleted = vendors.every(v => v.progress >= 100);
|
|
121
|
+
const hasFailed = vendors.some(v => v.status === "failed" || needCheckError && !!v.error && !!v.error_message);
|
|
121
122
|
setVendorStatus({
|
|
122
123
|
...data,
|
|
124
|
+
vendors,
|
|
123
125
|
isAllCompleted: !hasFailed && allCompleted,
|
|
124
126
|
hasFailed
|
|
125
127
|
});
|
|
@@ -528,6 +530,13 @@ function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit, isDon
|
|
|
528
530
|
});
|
|
529
531
|
}
|
|
530
532
|
});
|
|
533
|
+
const didConnectComplete = (0, _ahooks.useMemoizedFn)(async () => {
|
|
534
|
+
if (status === "submitting" || status === "completed") return;
|
|
535
|
+
pollingAbortRef.current = false;
|
|
536
|
+
setStatus("submitting");
|
|
537
|
+
setContext(null);
|
|
538
|
+
await handleCompletion();
|
|
539
|
+
});
|
|
531
540
|
return {
|
|
532
541
|
status,
|
|
533
542
|
context,
|
|
@@ -540,6 +549,7 @@ function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit, isDon
|
|
|
540
549
|
stripeConfirm,
|
|
541
550
|
stripeCancel,
|
|
542
551
|
stripeSkip,
|
|
552
|
+
didConnectComplete,
|
|
543
553
|
vendorStatus,
|
|
544
554
|
locked,
|
|
545
555
|
lock
|
package/lib/shared/api.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export default api;
|
|
|
21
21
|
export declare const API: {
|
|
22
22
|
readonly START_PAYMENT_LINK: (id: string) => string;
|
|
23
23
|
readonly RETRIEVE_SESSION: (id: string) => string;
|
|
24
|
+
readonly STATUS_SESSION: (id: string) => string;
|
|
24
25
|
readonly SWITCH_CURRENCY: (id: string) => string;
|
|
25
26
|
readonly SUBMIT: (id: string) => string;
|
|
26
27
|
readonly DONATE_SUBMIT: (id: string) => string;
|
package/lib/shared/api.js
CHANGED
|
@@ -68,6 +68,7 @@ module.exports = api;
|
|
|
68
68
|
const API = exports.API = {
|
|
69
69
|
START_PAYMENT_LINK: id => `/api/checkout-sessions/start/${id}`,
|
|
70
70
|
RETRIEVE_SESSION: id => `/api/checkout-sessions/retrieve/${id}`,
|
|
71
|
+
STATUS_SESSION: id => `/api/checkout-sessions/status/${id}`,
|
|
71
72
|
SWITCH_CURRENCY: id => `/api/checkout-sessions/${id}/switch-currency`,
|
|
72
73
|
SUBMIT: id => `/api/checkout-sessions/${id}/submit`,
|
|
73
74
|
DONATE_SUBMIT: id => `/api/checkout-sessions/${id}/donate-submit`,
|
package/lib/shared/polling.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { CheckoutResult } from '../checkout/types';
|
|
2
2
|
/**
|
|
3
3
|
* Poll for checkout session completion.
|
|
4
|
-
* Extracted from react/src/payment/form/index.tsx:72-99
|
|
5
4
|
*
|
|
6
|
-
* Uses
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Uses lightweight /status endpoint during polling (single D1 query, ~50ms)
|
|
6
|
+
* instead of full /retrieve (multiple includes, ~850ms). Only fetches full
|
|
7
|
+
* session data on the final successful poll for the success page.
|
|
8
|
+
*
|
|
9
|
+
* This reduces D1 load during payment processing and speeds up detection.
|
|
9
10
|
*/
|
|
10
11
|
export declare function waitForCheckoutComplete(sessionId: string): Promise<CheckoutResult>;
|
|
11
12
|
/**
|
package/lib/shared/polling.js
CHANGED
|
@@ -5,27 +5,37 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.generateIdempotencyKey = generateIdempotencyKey;
|
|
7
7
|
exports.waitForCheckoutComplete = waitForCheckoutComplete;
|
|
8
|
-
var _pWaitFor = _interopRequireDefault(require("p-wait-for"));
|
|
9
8
|
var _api = _interopRequireWildcard(require("./api"));
|
|
10
9
|
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
|
11
10
|
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
|
12
|
-
|
|
11
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
12
|
+
function nextPollInterval(elapsedMs) {
|
|
13
|
+
if (elapsedMs < 5e3) return 500;
|
|
14
|
+
if (elapsedMs < 3e4) return 1500;
|
|
15
|
+
return 3e3;
|
|
16
|
+
}
|
|
17
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
13
18
|
async function waitForCheckoutComplete(sessionId) {
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
const startedAt = Date.now();
|
|
20
|
+
while (true) {
|
|
16
21
|
const {
|
|
17
|
-
data
|
|
18
|
-
} = await _api.default.get(_api.API.
|
|
19
|
-
if (
|
|
20
|
-
throw new Error(
|
|
22
|
+
data: data2
|
|
23
|
+
} = await _api.default.get(_api.API.STATUS_SESSION(sessionId));
|
|
24
|
+
if (data2.paymentIntent && data2.paymentIntent.status === "requires_action" && data2.paymentIntent.last_payment_error) {
|
|
25
|
+
throw new Error(data2.paymentIntent.last_payment_error.message);
|
|
26
|
+
}
|
|
27
|
+
const done = data2.checkoutSession?.status === "complete" && ["paid", "no_payment_required"].includes(data2.checkoutSession?.payment_status);
|
|
28
|
+
if (done) break;
|
|
29
|
+
const elapsed = Date.now() - startedAt;
|
|
30
|
+
if (elapsed >= POLL_TIMEOUT_MS) {
|
|
31
|
+
throw new Error("waitForCheckoutComplete: timed out");
|
|
21
32
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return result;
|
|
33
|
+
await sleep(nextPollInterval(elapsed));
|
|
34
|
+
}
|
|
35
|
+
const {
|
|
36
|
+
data
|
|
37
|
+
} = await _api.default.get(_api.API.RETRIEVE_SESSION(sessionId));
|
|
38
|
+
return data;
|
|
29
39
|
}
|
|
30
40
|
function generateIdempotencyKey(sessionId, currencyId) {
|
|
31
41
|
return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react-headless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "Headless React hooks for payment-kit checkout",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@arcblock/ws": "^1.
|
|
36
|
+
"@arcblock/ws": "^1.30.9",
|
|
37
37
|
"@blocklet/js-sdk": "workspace:*",
|
|
38
|
-
"@blocklet/payment-types": "1.
|
|
39
|
-
"@ocap/util": "^1.
|
|
38
|
+
"@blocklet/payment-types": "1.28.0",
|
|
39
|
+
"@ocap/util": "^1.30.9",
|
|
40
40
|
"ahooks": "^3.8.5",
|
|
41
41
|
"google-libphonenumber": "^3.2.42",
|
|
42
42
|
"lodash": "^4.18.1",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"publishConfig": {
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "1486b54f913b83fb42323a89cce7503814d0685a"
|
|
64
64
|
}
|
|
@@ -57,6 +57,9 @@ export interface UseSubmitReturn {
|
|
|
57
57
|
stripeConfirm: () => Promise<void>;
|
|
58
58
|
stripeCancel: () => Promise<void>;
|
|
59
59
|
stripeSkip: () => Promise<void>;
|
|
60
|
+
/** Signal that DID Connect completed successfully — triggers completion polling.
|
|
61
|
+
* Use this in CF Workers (no WebSocket) to poll session status after connect.close(). */
|
|
62
|
+
didConnectComplete: () => Promise<void>;
|
|
60
63
|
vendorStatus: VendorOrderStatus | null;
|
|
61
64
|
/** Whether the checkout config is locked (user clicked "Connect and Pay", pending login/submit) */
|
|
62
65
|
locked: boolean;
|
|
@@ -210,15 +213,17 @@ export function useSubmit(
|
|
|
210
213
|
const { data } = await api.get(API.VENDOR_ORDER_STATUS(sessionId));
|
|
211
214
|
if (!mountedRef.current) return;
|
|
212
215
|
|
|
216
|
+
const vendors = Array.isArray(data?.vendors) ? data.vendors : [];
|
|
213
217
|
const needCheckError = Date.now() - startTime > 6000;
|
|
214
|
-
const allCompleted =
|
|
215
|
-
const hasFailed =
|
|
218
|
+
const allCompleted = vendors.every((v: VendorStatus) => v.progress >= 100);
|
|
219
|
+
const hasFailed = vendors.some(
|
|
216
220
|
(v: VendorStatus & { error?: string; error_message?: string }) =>
|
|
217
221
|
v.status === 'failed' || (needCheckError && !!v.error && !!v.error_message)
|
|
218
222
|
);
|
|
219
223
|
|
|
220
224
|
setVendorStatus({
|
|
221
225
|
...data,
|
|
226
|
+
vendors,
|
|
222
227
|
isAllCompleted: !hasFailed && allCompleted,
|
|
223
228
|
hasFailed,
|
|
224
229
|
});
|
|
@@ -704,6 +709,23 @@ export function useSubmit(
|
|
|
704
709
|
}
|
|
705
710
|
});
|
|
706
711
|
|
|
712
|
+
// DID Connect complete — mirrors V1 handleConnected() / stripeConfirm() pattern.
|
|
713
|
+
// In CF Workers (no WebSocket relay), the frontend has no real-time notification
|
|
714
|
+
// when DID Connect payment succeeds. Call this from onSuccess to poll session status.
|
|
715
|
+
//
|
|
716
|
+
// Recovers from in-modal retry: if the first sign attempt failed (onError fired,
|
|
717
|
+
// status was reset to 'idle' / 'failed' and pollingAbortRef was set), the user can
|
|
718
|
+
// still retry inside the same DID Connect modal. When that retry finally fires
|
|
719
|
+
// onSuccess we must resume polling from any non-terminal status — only short-circuit
|
|
720
|
+
// when completion is already in flight or finalized.
|
|
721
|
+
const didConnectComplete = useMemoizedFn(async () => {
|
|
722
|
+
if (status === 'submitting' || status === 'completed') return;
|
|
723
|
+
pollingAbortRef.current = false;
|
|
724
|
+
setStatus('submitting');
|
|
725
|
+
setContext(null);
|
|
726
|
+
await handleCompletion();
|
|
727
|
+
});
|
|
728
|
+
|
|
707
729
|
return {
|
|
708
730
|
status,
|
|
709
731
|
context,
|
|
@@ -716,6 +738,7 @@ export function useSubmit(
|
|
|
716
738
|
stripeConfirm,
|
|
717
739
|
stripeCancel,
|
|
718
740
|
stripeSkip,
|
|
741
|
+
didConnectComplete,
|
|
719
742
|
vendorStatus,
|
|
720
743
|
locked,
|
|
721
744
|
lock,
|
package/src/shared/api.ts
CHANGED
|
@@ -98,6 +98,7 @@ export default api;
|
|
|
98
98
|
export const API = {
|
|
99
99
|
START_PAYMENT_LINK: (id: string) => `/api/checkout-sessions/start/${id}`,
|
|
100
100
|
RETRIEVE_SESSION: (id: string) => `/api/checkout-sessions/retrieve/${id}`,
|
|
101
|
+
STATUS_SESSION: (id: string) => `/api/checkout-sessions/status/${id}`,
|
|
101
102
|
SWITCH_CURRENCY: (id: string) => `/api/checkout-sessions/${id}/switch-currency`,
|
|
102
103
|
SUBMIT: (id: string) => `/api/checkout-sessions/${id}/submit`,
|
|
103
104
|
DONATE_SUBMIT: (id: string) => `/api/checkout-sessions/${id}/donate-submit`,
|
package/src/shared/polling.ts
CHANGED
|
@@ -1,43 +1,63 @@
|
|
|
1
|
-
import pWaitFor from 'p-wait-for';
|
|
2
|
-
|
|
3
1
|
import api, { API } from './api';
|
|
4
2
|
import type { CheckoutResult } from '../checkout/types';
|
|
5
3
|
|
|
4
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Progressive polling interval — typical DID Connect + on-chain confirm + DB
|
|
8
|
+
* write completes in 1–3s, so a short cadence in that window matters most for
|
|
9
|
+
* perceived latency. Backoff after that to keep D1 load bounded for slow paths
|
|
10
|
+
* (chain congestion, async vendor processing, etc.).
|
|
11
|
+
*/
|
|
12
|
+
function nextPollInterval(elapsedMs: number): number {
|
|
13
|
+
if (elapsedMs < 5_000) return 500;
|
|
14
|
+
if (elapsedMs < 30_000) return 1_500;
|
|
15
|
+
return 3_000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* Poll for checkout session completion.
|
|
8
|
-
* Extracted from react/src/payment/form/index.tsx:72-99
|
|
9
22
|
*
|
|
10
|
-
* Uses
|
|
11
|
-
*
|
|
12
|
-
*
|
|
23
|
+
* Uses lightweight /status endpoint during polling (single D1 query, ~50ms)
|
|
24
|
+
* instead of full /retrieve (multiple includes, ~850ms). Only fetches full
|
|
25
|
+
* session data on the final successful poll for the success page.
|
|
26
|
+
*
|
|
27
|
+
* This reduces D1 load during payment processing and speeds up detection.
|
|
13
28
|
*/
|
|
14
29
|
export async function waitForCheckoutComplete(sessionId: string): Promise<CheckoutResult> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
const startedAt = Date.now();
|
|
31
|
+
|
|
32
|
+
// Phase 1: Lightweight status polling with progressive backoff.
|
|
33
|
+
// First check fires immediately — happy-path DID flows where the backend
|
|
34
|
+
// already finalized before the polling kicks in must not wait an interval.
|
|
35
|
+
while (true) {
|
|
36
|
+
const { data } = await api.get(API.STATUS_SESSION(sessionId));
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
data.paymentIntent &&
|
|
40
|
+
data.paymentIntent.status === 'requires_action' &&
|
|
41
|
+
data.paymentIntent.last_payment_error
|
|
42
|
+
) {
|
|
43
|
+
throw new Error(data.paymentIntent.last_payment_error.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const done =
|
|
47
|
+
data.checkoutSession?.status === 'complete' &&
|
|
48
|
+
['paid', 'no_payment_required'].includes(data.checkoutSession?.payment_status);
|
|
49
|
+
if (done) break;
|
|
50
|
+
|
|
51
|
+
const elapsed = Date.now() - startedAt;
|
|
52
|
+
if (elapsed >= POLL_TIMEOUT_MS) {
|
|
53
|
+
throw new Error('waitForCheckoutComplete: timed out');
|
|
54
|
+
}
|
|
55
|
+
await sleep(nextPollInterval(elapsed));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Phase 2: Full retrieve for success page data
|
|
59
|
+
const { data } = await api.get(API.RETRIEVE_SESSION(sessionId));
|
|
60
|
+
return data;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
/**
|