@easypayment/medusa-paypal-ui 1.0.56 → 1.1.1
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/CHANGELOG.md +61 -0
- package/dist/index.cjs +283 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +279 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -7
- package/src/client/http.ts +122 -16
- package/src/client/paypal.ts +26 -10
- package/src/client/types.ts +10 -0
- package/src/components/PayPalAdvancedCard.tsx +114 -24
- package/src/components/PayPalSmartButtons.tsx +112 -7
- package/src/utils/next-errors.ts +5 -1
- package/src/utils/processing-overlay.ts +12 -0
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/client/http.ts
|
|
4
|
+
var HttpError = class extends Error {
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "HttpError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var RETRY_BASE_DELAY_MS = 400;
|
|
12
|
+
function isRetryableStatus(status) {
|
|
13
|
+
return status === 429 || status >= 500;
|
|
14
|
+
}
|
|
15
|
+
function isRetryableError(err) {
|
|
16
|
+
return err instanceof Error && !(err instanceof HttpError);
|
|
17
|
+
}
|
|
18
|
+
var wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
function extractJsonMessage(text) {
|
|
20
|
+
if (!text) return null;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(text);
|
|
23
|
+
const message = parsed?.message;
|
|
24
|
+
if (typeof message === "string" && message.trim()) {
|
|
25
|
+
return message.trim().slice(0, 500);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
4
31
|
function toHeaderRecord(headers) {
|
|
5
32
|
if (!headers) {
|
|
6
33
|
return {};
|
|
@@ -15,7 +42,7 @@ function toHeaderRecord(headers) {
|
|
|
15
42
|
}
|
|
16
43
|
function createHttpClient(opts) {
|
|
17
44
|
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
18
|
-
async function
|
|
45
|
+
async function attempt(path, init) {
|
|
19
46
|
const url = `${base}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
20
47
|
const headers = {
|
|
21
48
|
Accept: "application/json",
|
|
@@ -25,41 +52,54 @@ function createHttpClient(opts) {
|
|
|
25
52
|
headers["x-publishable-api-key"] = opts.publishableApiKey;
|
|
26
53
|
}
|
|
27
54
|
const timeoutMs = 3e4;
|
|
28
|
-
let timeoutId;
|
|
29
55
|
const controller = new AbortController();
|
|
30
|
-
|
|
31
|
-
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
57
|
+
const callerSignal = init?.signal ?? null;
|
|
58
|
+
const onCallerAbort = () => controller.abort();
|
|
59
|
+
if (callerSignal) {
|
|
60
|
+
if (callerSignal.aborted) controller.abort();
|
|
61
|
+
else callerSignal.addEventListener("abort", onCallerAbort);
|
|
32
62
|
}
|
|
33
|
-
const effectiveSignal = init?.signal || controller.signal;
|
|
34
63
|
let res;
|
|
64
|
+
let text;
|
|
35
65
|
try {
|
|
36
|
-
res = await fetch(url, {
|
|
66
|
+
res = await fetch(url, {
|
|
67
|
+
...init,
|
|
68
|
+
headers,
|
|
69
|
+
credentials: "include",
|
|
70
|
+
signal: controller.signal
|
|
71
|
+
});
|
|
72
|
+
text = await res.text().catch(() => "");
|
|
37
73
|
} catch (err) {
|
|
38
|
-
if (err instanceof Error && err.name === "AbortError" && !
|
|
74
|
+
if (err instanceof Error && err.name === "AbortError" && !(callerSignal && callerSignal.aborted)) {
|
|
39
75
|
throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1e3}s`);
|
|
40
76
|
}
|
|
41
77
|
throw err;
|
|
42
78
|
} finally {
|
|
43
|
-
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
|
44
81
|
}
|
|
45
|
-
const text = await res.text().catch(() => "");
|
|
46
82
|
if (!res.ok) {
|
|
83
|
+
const parsedMessage = extractJsonMessage(text);
|
|
47
84
|
if (res.status === 401) {
|
|
48
|
-
throw new
|
|
49
|
-
"[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY"
|
|
85
|
+
throw new HttpError(
|
|
86
|
+
parsedMessage || "[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
|
|
87
|
+
401
|
|
50
88
|
);
|
|
51
89
|
}
|
|
52
90
|
if (res.status === 403) {
|
|
53
|
-
throw new
|
|
54
|
-
"[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings."
|
|
91
|
+
throw new HttpError(
|
|
92
|
+
parsedMessage || "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
|
|
93
|
+
403
|
|
55
94
|
);
|
|
56
95
|
}
|
|
57
|
-
throw new
|
|
58
|
-
text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})
|
|
96
|
+
throw new HttpError(
|
|
97
|
+
parsedMessage || text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
|
|
98
|
+
res.status
|
|
59
99
|
);
|
|
60
100
|
}
|
|
61
101
|
if (!text) {
|
|
62
|
-
throw new
|
|
102
|
+
throw new HttpError(`[PayPal] Empty response body from ${path} (${res.status})`, res.status);
|
|
63
103
|
}
|
|
64
104
|
const contentType = res.headers.get("content-type") || "";
|
|
65
105
|
if (!contentType.includes("application/json")) {
|
|
@@ -73,17 +113,39 @@ function createHttpClient(opts) {
|
|
|
73
113
|
throw new Error(`[PayPal] Failed to parse JSON response from ${path}`);
|
|
74
114
|
}
|
|
75
115
|
}
|
|
116
|
+
async function request(path, init, reqOpts) {
|
|
117
|
+
const retries = Math.max(0, reqOpts?.retries ?? 0);
|
|
118
|
+
let lastError;
|
|
119
|
+
for (let i = 0; i <= retries; i++) {
|
|
120
|
+
try {
|
|
121
|
+
return await attempt(path, init);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
lastError = err;
|
|
124
|
+
const transient = err instanceof HttpError && isRetryableStatus(err.status) || isRetryableError(err);
|
|
125
|
+
const aborted = err instanceof Error && err.name === "AbortError" && !!init?.signal;
|
|
126
|
+
if (i >= retries || !transient || aborted) {
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
await wait(RETRY_BASE_DELAY_MS * Math.pow(2, i) + Math.random() * RETRY_BASE_DELAY_MS);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw lastError;
|
|
133
|
+
}
|
|
76
134
|
return { request };
|
|
77
135
|
}
|
|
78
136
|
|
|
79
137
|
// src/client/paypal.ts
|
|
80
138
|
async function markPaymentComplete(baseUrl, cartId, publishableApiKey) {
|
|
81
139
|
const http = createHttpClient({ baseUrl, publishableApiKey });
|
|
82
|
-
return http.request(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
140
|
+
return http.request(
|
|
141
|
+
`/store/paypal-complete`,
|
|
142
|
+
{
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ cart_id: cartId })
|
|
146
|
+
},
|
|
147
|
+
{ retries: 3 }
|
|
148
|
+
);
|
|
87
149
|
}
|
|
88
150
|
function createPayPalStoreApi(opts) {
|
|
89
151
|
const http = createHttpClient(opts);
|
|
@@ -103,11 +165,15 @@ function createPayPalStoreApi(opts) {
|
|
|
103
165
|
});
|
|
104
166
|
},
|
|
105
167
|
captureOrder(cartId, orderId) {
|
|
106
|
-
return http.request(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
168
|
+
return http.request(
|
|
169
|
+
`/store/paypal/capture-order`,
|
|
170
|
+
{
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "Content-Type": "application/json" },
|
|
173
|
+
body: JSON.stringify({ cart_id: cartId, order_id: orderId })
|
|
174
|
+
},
|
|
175
|
+
{ retries: 2 }
|
|
176
|
+
);
|
|
111
177
|
}
|
|
112
178
|
};
|
|
113
179
|
}
|
|
@@ -224,14 +290,16 @@ function PayPalCurrencyNotice({ config }) {
|
|
|
224
290
|
}
|
|
225
291
|
|
|
226
292
|
// src/components/PayPalSmartButtons.tsx
|
|
227
|
-
import { useCallback, useMemo as useMemo3, useState as useState2 } from "react";
|
|
293
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
|
|
228
294
|
import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js";
|
|
229
295
|
|
|
230
296
|
// src/utils/next-errors.ts
|
|
231
297
|
function isNextRouterError(e) {
|
|
232
298
|
if (typeof e !== "object" || e === null || !("digest" in e)) return false;
|
|
233
299
|
const digest = e.digest;
|
|
234
|
-
return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND"))
|
|
300
|
+
return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND") || // Next.js 15: notFound()/forbidden()/unauthorized() throw
|
|
301
|
+
// HTTPAccessFallbackError with digest "NEXT_HTTP_ERROR_FALLBACK;<status>".
|
|
302
|
+
digest.startsWith("NEXT_HTTP_ERROR_FALLBACK"));
|
|
235
303
|
}
|
|
236
304
|
|
|
237
305
|
// src/utils/processing-overlay.ts
|
|
@@ -295,8 +363,13 @@ function showProcessingOverlay() {
|
|
|
295
363
|
origReplace(...args);
|
|
296
364
|
onNav();
|
|
297
365
|
};
|
|
366
|
+
const onPageShow = (e) => {
|
|
367
|
+
if (e.persisted) hideProcessingOverlay();
|
|
368
|
+
};
|
|
369
|
+
window.addEventListener("pageshow", onPageShow);
|
|
298
370
|
cleanupNavListener = () => {
|
|
299
371
|
window.removeEventListener("popstate", onNav);
|
|
372
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
300
373
|
history.pushState = origPush;
|
|
301
374
|
history.replaceState = origReplace;
|
|
302
375
|
};
|
|
@@ -329,11 +402,48 @@ function PayPalSmartButtons(props) {
|
|
|
329
402
|
const [error, setError] = useState2(null);
|
|
330
403
|
const [processing, setProcessing] = useState2(false);
|
|
331
404
|
const [buttonsReady, setButtonsReady] = useState2(false);
|
|
405
|
+
const [loadTimedOut, setLoadTimedOut] = useState2(false);
|
|
406
|
+
const capturedRef = useRef2(null);
|
|
407
|
+
const [completionPending, setCompletionPending] = useState2(false);
|
|
332
408
|
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer();
|
|
333
409
|
const handleInit = useCallback(() => {
|
|
334
410
|
setButtonsReady(true);
|
|
335
411
|
}, []);
|
|
336
|
-
|
|
412
|
+
useEffect2(() => {
|
|
413
|
+
const onPageShow = (e) => {
|
|
414
|
+
if (e.persisted) setProcessing(false);
|
|
415
|
+
};
|
|
416
|
+
window.addEventListener("pageshow", onPageShow);
|
|
417
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
418
|
+
}, []);
|
|
419
|
+
useEffect2(() => {
|
|
420
|
+
if (!isResolved || buttonsReady) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const t = setTimeout(() => setLoadTimedOut(true), 1e4);
|
|
424
|
+
return () => clearTimeout(t);
|
|
425
|
+
}, [isResolved, buttonsReady]);
|
|
426
|
+
const finalizeCapturedPayment = useCallback(async () => {
|
|
427
|
+
const captured = capturedRef.current;
|
|
428
|
+
if (!captured) return;
|
|
429
|
+
setProcessing(true);
|
|
430
|
+
showProcessingOverlay();
|
|
431
|
+
setError(null);
|
|
432
|
+
try {
|
|
433
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
|
|
434
|
+
await onPaid?.({ ...captured, ...completeResult });
|
|
435
|
+
setCompletionPending(false);
|
|
436
|
+
} catch (e) {
|
|
437
|
+
if (isNextRouterError(e)) return;
|
|
438
|
+
hideProcessingOverlay();
|
|
439
|
+
setProcessing(false);
|
|
440
|
+
setCompletionPending(true);
|
|
441
|
+
const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
|
|
442
|
+
setError(msg);
|
|
443
|
+
onError?.(msg);
|
|
444
|
+
}
|
|
445
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError]);
|
|
446
|
+
const showSpinner = (isPending || isResolved && !buttonsReady) && !loadTimedOut;
|
|
337
447
|
if (!config.currency_supported) return null;
|
|
338
448
|
const containerWidth = BUTTON_WIDTH_MAP[config.button_width ?? "responsive"] ?? "100%";
|
|
339
449
|
return /* @__PURE__ */ jsxs2("div", { style: { width: containerWidth, position: "relative" }, children: [
|
|
@@ -472,6 +582,28 @@ function PayPalSmartButtons(props) {
|
|
|
472
582
|
]
|
|
473
583
|
}
|
|
474
584
|
),
|
|
585
|
+
loadTimedOut && !buttonsReady && !isRejected && /* @__PURE__ */ jsxs2(
|
|
586
|
+
"div",
|
|
587
|
+
{
|
|
588
|
+
role: "alert",
|
|
589
|
+
style: {
|
|
590
|
+
display: "flex",
|
|
591
|
+
alignItems: "flex-start",
|
|
592
|
+
gap: 8,
|
|
593
|
+
padding: "10px 14px",
|
|
594
|
+
background: "#fef2f2",
|
|
595
|
+
border: "1px solid #fecaca",
|
|
596
|
+
borderRadius: 8,
|
|
597
|
+
fontSize: 13,
|
|
598
|
+
color: "#b91c1c",
|
|
599
|
+
lineHeight: 1.5
|
|
600
|
+
},
|
|
601
|
+
children: [
|
|
602
|
+
/* @__PURE__ */ jsx3("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
|
|
603
|
+
/* @__PURE__ */ jsx3("span", { children: "PayPal is not available for this purchase. Please try a different payment method." })
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
),
|
|
475
607
|
isRejected && /* @__PURE__ */ jsxs2(
|
|
476
608
|
"div",
|
|
477
609
|
{
|
|
@@ -532,7 +664,7 @@ function PayPalSmartButtons(props) {
|
|
|
532
664
|
]
|
|
533
665
|
}
|
|
534
666
|
),
|
|
535
|
-
isResolved && /* @__PURE__ */ jsx3("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ jsx3(
|
|
667
|
+
isResolved && !completionPending && /* @__PURE__ */ jsx3("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ jsx3(
|
|
536
668
|
PayPalButtons,
|
|
537
669
|
{
|
|
538
670
|
forceReRender: [config.currency, config.intent, cartId],
|
|
@@ -546,6 +678,10 @@ function PayPalSmartButtons(props) {
|
|
|
546
678
|
},
|
|
547
679
|
createOrder: async () => {
|
|
548
680
|
if (processing) throw new Error("Payment already processing");
|
|
681
|
+
if (capturedRef.current) {
|
|
682
|
+
void finalizeCapturedPayment();
|
|
683
|
+
throw new Error("Your payment was already received \u2014 finalizing your order.");
|
|
684
|
+
}
|
|
549
685
|
setError(null);
|
|
550
686
|
setProcessing(true);
|
|
551
687
|
try {
|
|
@@ -564,8 +700,7 @@ function PayPalSmartButtons(props) {
|
|
|
564
700
|
const orderId = String(data?.orderID || "");
|
|
565
701
|
if (!orderId) throw new Error("PayPal order ID is missing from approval response");
|
|
566
702
|
const result = await api.captureOrder(cartId, orderId);
|
|
567
|
-
|
|
568
|
-
await onPaid?.({ ...result, ...completeResult });
|
|
703
|
+
capturedRef.current = result || {};
|
|
569
704
|
} catch (e) {
|
|
570
705
|
if (isNextRouterError(e)) return;
|
|
571
706
|
hideProcessingOverlay();
|
|
@@ -573,7 +708,9 @@ function PayPalSmartButtons(props) {
|
|
|
573
708
|
const msg = e instanceof Error ? e.message : "Payment capture failed";
|
|
574
709
|
setError(msg);
|
|
575
710
|
onError?.(msg);
|
|
711
|
+
return;
|
|
576
712
|
}
|
|
713
|
+
await finalizeCapturedPayment();
|
|
577
714
|
},
|
|
578
715
|
onCancel: () => {
|
|
579
716
|
hideProcessingOverlay();
|
|
@@ -588,6 +725,29 @@ function PayPalSmartButtons(props) {
|
|
|
588
725
|
}
|
|
589
726
|
}
|
|
590
727
|
) }),
|
|
728
|
+
completionPending && /* @__PURE__ */ jsx3(
|
|
729
|
+
"button",
|
|
730
|
+
{
|
|
731
|
+
type: "button",
|
|
732
|
+
onClick: () => void finalizeCapturedPayment(),
|
|
733
|
+
disabled: processing,
|
|
734
|
+
"aria-busy": processing,
|
|
735
|
+
style: {
|
|
736
|
+
width: "100%",
|
|
737
|
+
padding: "12px 16px",
|
|
738
|
+
marginTop: 4,
|
|
739
|
+
background: "#0070ba",
|
|
740
|
+
color: "#ffffff",
|
|
741
|
+
border: "none",
|
|
742
|
+
borderRadius: 8,
|
|
743
|
+
fontSize: 14,
|
|
744
|
+
fontWeight: 600,
|
|
745
|
+
cursor: processing ? "not-allowed" : "pointer",
|
|
746
|
+
opacity: processing ? 0.7 : 1
|
|
747
|
+
},
|
|
748
|
+
children: "Retry \u2014 finish placing my order"
|
|
749
|
+
}
|
|
750
|
+
),
|
|
591
751
|
error ? /* @__PURE__ */ jsxs2(
|
|
592
752
|
"div",
|
|
593
753
|
{
|
|
@@ -615,7 +775,7 @@ function PayPalSmartButtons(props) {
|
|
|
615
775
|
}
|
|
616
776
|
|
|
617
777
|
// src/components/PayPalAdvancedCard.tsx
|
|
618
|
-
import { useMemo as useMemo4, useRef as
|
|
778
|
+
import React3, { useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
|
|
619
779
|
import {
|
|
620
780
|
PayPalCardFieldsProvider,
|
|
621
781
|
PayPalNumberField,
|
|
@@ -674,7 +834,8 @@ var labelStyle = {
|
|
|
674
834
|
function SubmitButton({
|
|
675
835
|
disabled,
|
|
676
836
|
label,
|
|
677
|
-
onSubmit
|
|
837
|
+
onSubmit,
|
|
838
|
+
onSubmitError
|
|
678
839
|
}) {
|
|
679
840
|
const { cardFieldsForm } = usePayPalCardFields();
|
|
680
841
|
const isDisabled = disabled || !cardFieldsForm;
|
|
@@ -686,10 +847,10 @@ function SubmitButton({
|
|
|
686
847
|
"aria-busy": disabled,
|
|
687
848
|
onClick: () => {
|
|
688
849
|
onSubmit();
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
850
|
+
Promise.resolve(cardFieldsForm?.submit()).catch((e) => {
|
|
851
|
+
const msg = e instanceof Error && e.message ? e.message : "Card payment could not be submitted. Please check your card details.";
|
|
852
|
+
onSubmitError(msg);
|
|
853
|
+
});
|
|
693
854
|
},
|
|
694
855
|
style: {
|
|
695
856
|
width: "100%",
|
|
@@ -733,9 +894,47 @@ function PayPalAdvancedCard(props) {
|
|
|
733
894
|
);
|
|
734
895
|
const [error, setError] = useState3(null);
|
|
735
896
|
const [submitting, setSubmitting] = useState3(false);
|
|
736
|
-
const submittingRef =
|
|
737
|
-
const creatingOrderRef =
|
|
897
|
+
const submittingRef = useRef3(false);
|
|
898
|
+
const creatingOrderRef = useRef3(false);
|
|
899
|
+
const capturedRef = useRef3(null);
|
|
900
|
+
const [completionPending, setCompletionPending] = useState3(false);
|
|
738
901
|
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer2();
|
|
902
|
+
const resetSubmitState = React3.useCallback(() => {
|
|
903
|
+
hideProcessingOverlay();
|
|
904
|
+
creatingOrderRef.current = false;
|
|
905
|
+
submittingRef.current = false;
|
|
906
|
+
setSubmitting(false);
|
|
907
|
+
}, []);
|
|
908
|
+
React3.useEffect(() => {
|
|
909
|
+
const onPageShow = (e) => {
|
|
910
|
+
if (e.persisted) {
|
|
911
|
+
creatingOrderRef.current = false;
|
|
912
|
+
submittingRef.current = false;
|
|
913
|
+
setSubmitting(false);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
window.addEventListener("pageshow", onPageShow);
|
|
917
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
918
|
+
}, []);
|
|
919
|
+
const finalizeCapturedPayment = React3.useCallback(async () => {
|
|
920
|
+
const captured = capturedRef.current;
|
|
921
|
+
if (!captured) return;
|
|
922
|
+
setSubmitting(true);
|
|
923
|
+
showProcessingOverlay();
|
|
924
|
+
setError(null);
|
|
925
|
+
try {
|
|
926
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
|
|
927
|
+
await onPaid?.({ ...captured, ...completeResult });
|
|
928
|
+
setCompletionPending(false);
|
|
929
|
+
} catch (e) {
|
|
930
|
+
if (isNextRouterError(e)) return;
|
|
931
|
+
resetSubmitState();
|
|
932
|
+
setCompletionPending(true);
|
|
933
|
+
const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
|
|
934
|
+
setError(msg);
|
|
935
|
+
onError?.(msg);
|
|
936
|
+
}
|
|
937
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError, resetSubmitState]);
|
|
739
938
|
if (!config.currency_supported) return null;
|
|
740
939
|
if (isRejected) {
|
|
741
940
|
return /* @__PURE__ */ jsxs3(
|
|
@@ -924,6 +1123,10 @@ function PayPalAdvancedCard(props) {
|
|
|
924
1123
|
style: cardStyle,
|
|
925
1124
|
createOrder: async () => {
|
|
926
1125
|
if (creatingOrderRef.current) throw new Error("Payment already processing");
|
|
1126
|
+
if (capturedRef.current) {
|
|
1127
|
+
void finalizeCapturedPayment();
|
|
1128
|
+
throw new Error("Your payment was already received \u2014 finalizing your order.");
|
|
1129
|
+
}
|
|
927
1130
|
creatingOrderRef.current = true;
|
|
928
1131
|
try {
|
|
929
1132
|
setError(null);
|
|
@@ -939,19 +1142,18 @@ function PayPalAdvancedCard(props) {
|
|
|
939
1142
|
setError(null);
|
|
940
1143
|
showProcessingOverlay();
|
|
941
1144
|
const orderId = String(data?.orderID || "");
|
|
1145
|
+
if (!orderId) throw new Error("PayPal order ID is missing from approval response");
|
|
942
1146
|
const result = await api.captureOrder(cartId, orderId);
|
|
943
|
-
|
|
944
|
-
await onPaid?.({ ...result, ...completeResult });
|
|
1147
|
+
capturedRef.current = result || {};
|
|
945
1148
|
} catch (e) {
|
|
946
1149
|
if (isNextRouterError(e)) return;
|
|
947
|
-
|
|
948
|
-
creatingOrderRef.current = false;
|
|
949
|
-
submittingRef.current = false;
|
|
950
|
-
setSubmitting(false);
|
|
1150
|
+
resetSubmitState();
|
|
951
1151
|
const msg = e instanceof Error ? e.message : "Card payment failed";
|
|
952
1152
|
setError(msg);
|
|
953
1153
|
onError?.(msg);
|
|
1154
|
+
return;
|
|
954
1155
|
}
|
|
1156
|
+
await finalizeCapturedPayment();
|
|
955
1157
|
},
|
|
956
1158
|
onCancel: () => {
|
|
957
1159
|
hideProcessingOverlay();
|
|
@@ -1058,7 +1260,29 @@ function PayPalAdvancedCard(props) {
|
|
|
1058
1260
|
]
|
|
1059
1261
|
}
|
|
1060
1262
|
),
|
|
1061
|
-
/* @__PURE__ */ jsx4(
|
|
1263
|
+
completionPending ? /* @__PURE__ */ jsx4(
|
|
1264
|
+
"button",
|
|
1265
|
+
{
|
|
1266
|
+
type: "button",
|
|
1267
|
+
onClick: () => void finalizeCapturedPayment(),
|
|
1268
|
+
disabled: submitting,
|
|
1269
|
+
"aria-busy": submitting,
|
|
1270
|
+
style: {
|
|
1271
|
+
width: "100%",
|
|
1272
|
+
height: 48,
|
|
1273
|
+
padding: "0 20px",
|
|
1274
|
+
borderRadius: 8,
|
|
1275
|
+
border: "none",
|
|
1276
|
+
background: "#0070ba",
|
|
1277
|
+
color: "#ffffff",
|
|
1278
|
+
fontSize: 15,
|
|
1279
|
+
fontWeight: 600,
|
|
1280
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
1281
|
+
opacity: submitting ? 0.7 : 1
|
|
1282
|
+
},
|
|
1283
|
+
children: "Retry \u2014 finish placing my order"
|
|
1284
|
+
}
|
|
1285
|
+
) : /* @__PURE__ */ jsx4(
|
|
1062
1286
|
SubmitButton,
|
|
1063
1287
|
{
|
|
1064
1288
|
disabled: submitting,
|
|
@@ -1069,6 +1293,11 @@ function PayPalAdvancedCard(props) {
|
|
|
1069
1293
|
setError(null);
|
|
1070
1294
|
setSubmitting(true);
|
|
1071
1295
|
showProcessingOverlay();
|
|
1296
|
+
},
|
|
1297
|
+
onSubmitError: (msg) => {
|
|
1298
|
+
resetSubmitState();
|
|
1299
|
+
setError(msg);
|
|
1300
|
+
onError?.(msg);
|
|
1072
1301
|
}
|
|
1073
1302
|
}
|
|
1074
1303
|
),
|
|
@@ -1410,7 +1639,7 @@ function PayPalPaymentSection({
|
|
|
1410
1639
|
}
|
|
1411
1640
|
|
|
1412
1641
|
// src/hooks/usePayPalPaymentMethods.ts
|
|
1413
|
-
import { useEffect as
|
|
1642
|
+
import { useEffect as useEffect3, useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
|
|
1414
1643
|
var MAX_CACHE_ENTRIES2 = 50;
|
|
1415
1644
|
var _cache2 = /* @__PURE__ */ new Map();
|
|
1416
1645
|
var CACHE_TTL2 = 5 * 60 * 1e3;
|
|
@@ -1445,8 +1674,8 @@ function usePayPalPaymentMethods({
|
|
|
1445
1674
|
const hit = _cache2.get(key);
|
|
1446
1675
|
const seed = hit && Date.now() - hit.at < CACHE_TTL2 ? hit.result : null;
|
|
1447
1676
|
const [result, setResult] = useState4(seed ?? { ...DEFAULT_RESULT, loading: enabled });
|
|
1448
|
-
const fetchIdRef =
|
|
1449
|
-
|
|
1677
|
+
const fetchIdRef = useRef4(0);
|
|
1678
|
+
useEffect3(() => {
|
|
1450
1679
|
if (!enabled) {
|
|
1451
1680
|
setResult((prev) => ({ ...prev, loading: false }));
|
|
1452
1681
|
return;
|