@easypayment/medusa-paypal-ui 1.1.0 → 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 +36 -0
- package/dist/index.cjs +224 -29
- 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 +220 -35
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -7
- package/src/client/http.ts +55 -11
- 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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ All notable changes to `@easypayment/medusa-paypal-ui` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## 1.1.0
|
|
6
6
|
|
|
7
|
+
### Fixed (re-audit)
|
|
8
|
+
- **Card submit rejections no longer strand the buyer under the processing
|
|
9
|
+
overlay.** `cardFieldsForm.submit()` returns a promise whose rejection is not
|
|
10
|
+
routed to the provider's `onError`; it is now caught, the overlay/state reset,
|
|
11
|
+
and the validation message shown.
|
|
12
|
+
- **A payment can no longer be taken twice after a finalization failure.** Once
|
|
13
|
+
a capture succeeds, any later failure (order completion, consumer callback)
|
|
14
|
+
now shows a dedicated "Retry — finish placing my order" action that retries
|
|
15
|
+
only the finalization; the PayPal button/card form no longer re-appears to
|
|
16
|
+
invite a second, distinct charge for the same cart.
|
|
17
|
+
- **Back-button (bfcache) restores no longer leave a stuck overlay.** The DOM
|
|
18
|
+
processing overlay and both components' React overlays reset on `pageshow`
|
|
19
|
+
with `persisted: true`.
|
|
20
|
+
- **Requests can no longer hang forever.** The 30s HTTP timeout now also applies
|
|
21
|
+
when a caller passes an `AbortSignal` (hook config fetches), and it covers
|
|
22
|
+
reading the response body, not just the headers.
|
|
23
|
+
- **Buyers now see the backend's error message.** Error bodies are parsed as
|
|
24
|
+
JSON and the backend's buyer-facing `message` (unsupported currency,
|
|
25
|
+
rate-limit, "payment processed but order not finalized", "PayPal is currently
|
|
26
|
+
disabled") is shown instead of a raw JSON blob or a misleading CORS hint.
|
|
27
|
+
- **Ineligible PayPal buttons no longer spin forever.** If the SDK resolves but
|
|
28
|
+
never initializes the buttons (ineligible country/currency/funding), a clear
|
|
29
|
+
"PayPal is not available" message replaces the infinite "Loading PayPal…"
|
|
30
|
+
spinner after 10s.
|
|
31
|
+
- **Missing `orderID` guard added to the card `onApprove`** (matching the
|
|
32
|
+
buttons flow) so an empty id fails fast with a clear message.
|
|
33
|
+
- **Next.js 15 `notFound()` is no longer treated as a payment failure**
|
|
34
|
+
(`NEXT_HTTP_ERROR_FALLBACK` digests are now recognized as router errors).
|
|
35
|
+
- **CJS TypeScript consumers get correct types.** The exports map now serves
|
|
36
|
+
`dist/*.d.cts` under the `require` condition (fixes TS1479 under
|
|
37
|
+
`node16`/`nodenext` resolution).
|
|
38
|
+
- **`PayPalSettingsResponse` now matches the actual endpoint response**
|
|
39
|
+
(`paymentAction`, `advancedCardEnabled`); the old `data` envelope shape is
|
|
40
|
+
kept as deprecated so existing code compiles.
|
|
41
|
+
|
|
42
|
+
|
|
7
43
|
### Added
|
|
8
44
|
- **Transient-failure retry** in the HTTP client (opt-in `retries`, jittered
|
|
9
45
|
backoff on network errors / timeouts / 429 / 5xx). Wired into the idempotent
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __export = (target, all) => {
|
|
8
10
|
for (var name in all)
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/index.ts
|
|
@@ -55,6 +65,18 @@ function isRetryableError(err) {
|
|
|
55
65
|
return err instanceof Error && !(err instanceof HttpError);
|
|
56
66
|
}
|
|
57
67
|
var wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
68
|
+
function extractJsonMessage(text) {
|
|
69
|
+
if (!text) return null;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(text);
|
|
72
|
+
const message = parsed?.message;
|
|
73
|
+
if (typeof message === "string" && message.trim()) {
|
|
74
|
+
return message.trim().slice(0, 500);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
58
80
|
function toHeaderRecord(headers) {
|
|
59
81
|
if (!headers) {
|
|
60
82
|
return {};
|
|
@@ -79,39 +101,49 @@ function createHttpClient(opts) {
|
|
|
79
101
|
headers["x-publishable-api-key"] = opts.publishableApiKey;
|
|
80
102
|
}
|
|
81
103
|
const timeoutMs = 3e4;
|
|
82
|
-
let timeoutId;
|
|
83
104
|
const controller = new AbortController();
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
const callerSignal = init?.signal ?? null;
|
|
107
|
+
const onCallerAbort = () => controller.abort();
|
|
108
|
+
if (callerSignal) {
|
|
109
|
+
if (callerSignal.aborted) controller.abort();
|
|
110
|
+
else callerSignal.addEventListener("abort", onCallerAbort);
|
|
86
111
|
}
|
|
87
|
-
const effectiveSignal = init?.signal || controller.signal;
|
|
88
112
|
let res;
|
|
113
|
+
let text;
|
|
89
114
|
try {
|
|
90
|
-
res = await fetch(url, {
|
|
115
|
+
res = await fetch(url, {
|
|
116
|
+
...init,
|
|
117
|
+
headers,
|
|
118
|
+
credentials: "include",
|
|
119
|
+
signal: controller.signal
|
|
120
|
+
});
|
|
121
|
+
text = await res.text().catch(() => "");
|
|
91
122
|
} catch (err) {
|
|
92
|
-
if (err instanceof Error && err.name === "AbortError" && !
|
|
123
|
+
if (err instanceof Error && err.name === "AbortError" && !(callerSignal && callerSignal.aborted)) {
|
|
93
124
|
throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1e3}s`);
|
|
94
125
|
}
|
|
95
126
|
throw err;
|
|
96
127
|
} finally {
|
|
97
|
-
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
|
98
130
|
}
|
|
99
|
-
const text = await res.text().catch(() => "");
|
|
100
131
|
if (!res.ok) {
|
|
132
|
+
const parsedMessage = extractJsonMessage(text);
|
|
101
133
|
if (res.status === 401) {
|
|
102
134
|
throw new HttpError(
|
|
103
|
-
"[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
|
|
135
|
+
parsedMessage || "[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
|
|
104
136
|
401
|
|
105
137
|
);
|
|
106
138
|
}
|
|
107
139
|
if (res.status === 403) {
|
|
108
140
|
throw new HttpError(
|
|
109
|
-
"[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
|
|
141
|
+
parsedMessage || "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
|
|
110
142
|
403
|
|
111
143
|
);
|
|
112
144
|
}
|
|
113
145
|
throw new HttpError(
|
|
114
|
-
text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
|
|
146
|
+
parsedMessage || text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
|
|
115
147
|
res.status
|
|
116
148
|
);
|
|
117
149
|
}
|
|
@@ -314,7 +346,9 @@ var import_react_paypal_js2 = require("@paypal/react-paypal-js");
|
|
|
314
346
|
function isNextRouterError(e) {
|
|
315
347
|
if (typeof e !== "object" || e === null || !("digest" in e)) return false;
|
|
316
348
|
const digest = e.digest;
|
|
317
|
-
return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND"))
|
|
349
|
+
return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND") || // Next.js 15: notFound()/forbidden()/unauthorized() throw
|
|
350
|
+
// HTTPAccessFallbackError with digest "NEXT_HTTP_ERROR_FALLBACK;<status>".
|
|
351
|
+
digest.startsWith("NEXT_HTTP_ERROR_FALLBACK"));
|
|
318
352
|
}
|
|
319
353
|
|
|
320
354
|
// src/utils/processing-overlay.ts
|
|
@@ -378,8 +412,13 @@ function showProcessingOverlay() {
|
|
|
378
412
|
origReplace(...args);
|
|
379
413
|
onNav();
|
|
380
414
|
};
|
|
415
|
+
const onPageShow = (e) => {
|
|
416
|
+
if (e.persisted) hideProcessingOverlay();
|
|
417
|
+
};
|
|
418
|
+
window.addEventListener("pageshow", onPageShow);
|
|
381
419
|
cleanupNavListener = () => {
|
|
382
420
|
window.removeEventListener("popstate", onNav);
|
|
421
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
383
422
|
history.pushState = origPush;
|
|
384
423
|
history.replaceState = origReplace;
|
|
385
424
|
};
|
|
@@ -412,11 +451,48 @@ function PayPalSmartButtons(props) {
|
|
|
412
451
|
const [error, setError] = (0, import_react3.useState)(null);
|
|
413
452
|
const [processing, setProcessing] = (0, import_react3.useState)(false);
|
|
414
453
|
const [buttonsReady, setButtonsReady] = (0, import_react3.useState)(false);
|
|
454
|
+
const [loadTimedOut, setLoadTimedOut] = (0, import_react3.useState)(false);
|
|
455
|
+
const capturedRef = (0, import_react3.useRef)(null);
|
|
456
|
+
const [completionPending, setCompletionPending] = (0, import_react3.useState)(false);
|
|
415
457
|
const [{ isPending, isResolved, isRejected }] = (0, import_react_paypal_js2.usePayPalScriptReducer)();
|
|
416
458
|
const handleInit = (0, import_react3.useCallback)(() => {
|
|
417
459
|
setButtonsReady(true);
|
|
418
460
|
}, []);
|
|
419
|
-
|
|
461
|
+
(0, import_react3.useEffect)(() => {
|
|
462
|
+
const onPageShow = (e) => {
|
|
463
|
+
if (e.persisted) setProcessing(false);
|
|
464
|
+
};
|
|
465
|
+
window.addEventListener("pageshow", onPageShow);
|
|
466
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
467
|
+
}, []);
|
|
468
|
+
(0, import_react3.useEffect)(() => {
|
|
469
|
+
if (!isResolved || buttonsReady) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const t = setTimeout(() => setLoadTimedOut(true), 1e4);
|
|
473
|
+
return () => clearTimeout(t);
|
|
474
|
+
}, [isResolved, buttonsReady]);
|
|
475
|
+
const finalizeCapturedPayment = (0, import_react3.useCallback)(async () => {
|
|
476
|
+
const captured = capturedRef.current;
|
|
477
|
+
if (!captured) return;
|
|
478
|
+
setProcessing(true);
|
|
479
|
+
showProcessingOverlay();
|
|
480
|
+
setError(null);
|
|
481
|
+
try {
|
|
482
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
|
|
483
|
+
await onPaid?.({ ...captured, ...completeResult });
|
|
484
|
+
setCompletionPending(false);
|
|
485
|
+
} catch (e) {
|
|
486
|
+
if (isNextRouterError(e)) return;
|
|
487
|
+
hideProcessingOverlay();
|
|
488
|
+
setProcessing(false);
|
|
489
|
+
setCompletionPending(true);
|
|
490
|
+
const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
|
|
491
|
+
setError(msg);
|
|
492
|
+
onError?.(msg);
|
|
493
|
+
}
|
|
494
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError]);
|
|
495
|
+
const showSpinner = (isPending || isResolved && !buttonsReady) && !loadTimedOut;
|
|
420
496
|
if (!config.currency_supported) return null;
|
|
421
497
|
const containerWidth = BUTTON_WIDTH_MAP[config.button_width ?? "responsive"] ?? "100%";
|
|
422
498
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: containerWidth, position: "relative" }, children: [
|
|
@@ -555,6 +631,28 @@ function PayPalSmartButtons(props) {
|
|
|
555
631
|
]
|
|
556
632
|
}
|
|
557
633
|
),
|
|
634
|
+
loadTimedOut && !buttonsReady && !isRejected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
635
|
+
"div",
|
|
636
|
+
{
|
|
637
|
+
role: "alert",
|
|
638
|
+
style: {
|
|
639
|
+
display: "flex",
|
|
640
|
+
alignItems: "flex-start",
|
|
641
|
+
gap: 8,
|
|
642
|
+
padding: "10px 14px",
|
|
643
|
+
background: "#fef2f2",
|
|
644
|
+
border: "1px solid #fecaca",
|
|
645
|
+
borderRadius: 8,
|
|
646
|
+
fontSize: 13,
|
|
647
|
+
color: "#b91c1c",
|
|
648
|
+
lineHeight: 1.5
|
|
649
|
+
},
|
|
650
|
+
children: [
|
|
651
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
|
|
652
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "PayPal is not available for this purchase. Please try a different payment method." })
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
),
|
|
558
656
|
isRejected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
559
657
|
"div",
|
|
560
658
|
{
|
|
@@ -615,7 +713,7 @@ function PayPalSmartButtons(props) {
|
|
|
615
713
|
]
|
|
616
714
|
}
|
|
617
715
|
),
|
|
618
|
-
isResolved && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
716
|
+
isResolved && !completionPending && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
619
717
|
import_react_paypal_js2.PayPalButtons,
|
|
620
718
|
{
|
|
621
719
|
forceReRender: [config.currency, config.intent, cartId],
|
|
@@ -629,6 +727,10 @@ function PayPalSmartButtons(props) {
|
|
|
629
727
|
},
|
|
630
728
|
createOrder: async () => {
|
|
631
729
|
if (processing) throw new Error("Payment already processing");
|
|
730
|
+
if (capturedRef.current) {
|
|
731
|
+
void finalizeCapturedPayment();
|
|
732
|
+
throw new Error("Your payment was already received \u2014 finalizing your order.");
|
|
733
|
+
}
|
|
632
734
|
setError(null);
|
|
633
735
|
setProcessing(true);
|
|
634
736
|
try {
|
|
@@ -647,8 +749,7 @@ function PayPalSmartButtons(props) {
|
|
|
647
749
|
const orderId = String(data?.orderID || "");
|
|
648
750
|
if (!orderId) throw new Error("PayPal order ID is missing from approval response");
|
|
649
751
|
const result = await api.captureOrder(cartId, orderId);
|
|
650
|
-
|
|
651
|
-
await onPaid?.({ ...result, ...completeResult });
|
|
752
|
+
capturedRef.current = result || {};
|
|
652
753
|
} catch (e) {
|
|
653
754
|
if (isNextRouterError(e)) return;
|
|
654
755
|
hideProcessingOverlay();
|
|
@@ -656,7 +757,9 @@ function PayPalSmartButtons(props) {
|
|
|
656
757
|
const msg = e instanceof Error ? e.message : "Payment capture failed";
|
|
657
758
|
setError(msg);
|
|
658
759
|
onError?.(msg);
|
|
760
|
+
return;
|
|
659
761
|
}
|
|
762
|
+
await finalizeCapturedPayment();
|
|
660
763
|
},
|
|
661
764
|
onCancel: () => {
|
|
662
765
|
hideProcessingOverlay();
|
|
@@ -671,6 +774,29 @@ function PayPalSmartButtons(props) {
|
|
|
671
774
|
}
|
|
672
775
|
}
|
|
673
776
|
) }),
|
|
777
|
+
completionPending && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
778
|
+
"button",
|
|
779
|
+
{
|
|
780
|
+
type: "button",
|
|
781
|
+
onClick: () => void finalizeCapturedPayment(),
|
|
782
|
+
disabled: processing,
|
|
783
|
+
"aria-busy": processing,
|
|
784
|
+
style: {
|
|
785
|
+
width: "100%",
|
|
786
|
+
padding: "12px 16px",
|
|
787
|
+
marginTop: 4,
|
|
788
|
+
background: "#0070ba",
|
|
789
|
+
color: "#ffffff",
|
|
790
|
+
border: "none",
|
|
791
|
+
borderRadius: 8,
|
|
792
|
+
fontSize: 14,
|
|
793
|
+
fontWeight: 600,
|
|
794
|
+
cursor: processing ? "not-allowed" : "pointer",
|
|
795
|
+
opacity: processing ? 0.7 : 1
|
|
796
|
+
},
|
|
797
|
+
children: "Retry \u2014 finish placing my order"
|
|
798
|
+
}
|
|
799
|
+
),
|
|
674
800
|
error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
675
801
|
"div",
|
|
676
802
|
{
|
|
@@ -698,7 +824,7 @@ function PayPalSmartButtons(props) {
|
|
|
698
824
|
}
|
|
699
825
|
|
|
700
826
|
// src/components/PayPalAdvancedCard.tsx
|
|
701
|
-
var import_react4 = require("react");
|
|
827
|
+
var import_react4 = __toESM(require("react"), 1);
|
|
702
828
|
var import_react_paypal_js3 = require("@paypal/react-paypal-js");
|
|
703
829
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
704
830
|
var SPIN_STYLE2 = `
|
|
@@ -750,7 +876,8 @@ var labelStyle = {
|
|
|
750
876
|
function SubmitButton({
|
|
751
877
|
disabled,
|
|
752
878
|
label,
|
|
753
|
-
onSubmit
|
|
879
|
+
onSubmit,
|
|
880
|
+
onSubmitError
|
|
754
881
|
}) {
|
|
755
882
|
const { cardFieldsForm } = (0, import_react_paypal_js3.usePayPalCardFields)();
|
|
756
883
|
const isDisabled = disabled || !cardFieldsForm;
|
|
@@ -762,10 +889,10 @@ function SubmitButton({
|
|
|
762
889
|
"aria-busy": disabled,
|
|
763
890
|
onClick: () => {
|
|
764
891
|
onSubmit();
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
892
|
+
Promise.resolve(cardFieldsForm?.submit()).catch((e) => {
|
|
893
|
+
const msg = e instanceof Error && e.message ? e.message : "Card payment could not be submitted. Please check your card details.";
|
|
894
|
+
onSubmitError(msg);
|
|
895
|
+
});
|
|
769
896
|
},
|
|
770
897
|
style: {
|
|
771
898
|
width: "100%",
|
|
@@ -811,7 +938,45 @@ function PayPalAdvancedCard(props) {
|
|
|
811
938
|
const [submitting, setSubmitting] = (0, import_react4.useState)(false);
|
|
812
939
|
const submittingRef = (0, import_react4.useRef)(false);
|
|
813
940
|
const creatingOrderRef = (0, import_react4.useRef)(false);
|
|
941
|
+
const capturedRef = (0, import_react4.useRef)(null);
|
|
942
|
+
const [completionPending, setCompletionPending] = (0, import_react4.useState)(false);
|
|
814
943
|
const [{ isPending, isResolved, isRejected }] = (0, import_react_paypal_js3.usePayPalScriptReducer)();
|
|
944
|
+
const resetSubmitState = import_react4.default.useCallback(() => {
|
|
945
|
+
hideProcessingOverlay();
|
|
946
|
+
creatingOrderRef.current = false;
|
|
947
|
+
submittingRef.current = false;
|
|
948
|
+
setSubmitting(false);
|
|
949
|
+
}, []);
|
|
950
|
+
import_react4.default.useEffect(() => {
|
|
951
|
+
const onPageShow = (e) => {
|
|
952
|
+
if (e.persisted) {
|
|
953
|
+
creatingOrderRef.current = false;
|
|
954
|
+
submittingRef.current = false;
|
|
955
|
+
setSubmitting(false);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
window.addEventListener("pageshow", onPageShow);
|
|
959
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
960
|
+
}, []);
|
|
961
|
+
const finalizeCapturedPayment = import_react4.default.useCallback(async () => {
|
|
962
|
+
const captured = capturedRef.current;
|
|
963
|
+
if (!captured) return;
|
|
964
|
+
setSubmitting(true);
|
|
965
|
+
showProcessingOverlay();
|
|
966
|
+
setError(null);
|
|
967
|
+
try {
|
|
968
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
|
|
969
|
+
await onPaid?.({ ...captured, ...completeResult });
|
|
970
|
+
setCompletionPending(false);
|
|
971
|
+
} catch (e) {
|
|
972
|
+
if (isNextRouterError(e)) return;
|
|
973
|
+
resetSubmitState();
|
|
974
|
+
setCompletionPending(true);
|
|
975
|
+
const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
|
|
976
|
+
setError(msg);
|
|
977
|
+
onError?.(msg);
|
|
978
|
+
}
|
|
979
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError, resetSubmitState]);
|
|
815
980
|
if (!config.currency_supported) return null;
|
|
816
981
|
if (isRejected) {
|
|
817
982
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
@@ -1000,6 +1165,10 @@ function PayPalAdvancedCard(props) {
|
|
|
1000
1165
|
style: cardStyle,
|
|
1001
1166
|
createOrder: async () => {
|
|
1002
1167
|
if (creatingOrderRef.current) throw new Error("Payment already processing");
|
|
1168
|
+
if (capturedRef.current) {
|
|
1169
|
+
void finalizeCapturedPayment();
|
|
1170
|
+
throw new Error("Your payment was already received \u2014 finalizing your order.");
|
|
1171
|
+
}
|
|
1003
1172
|
creatingOrderRef.current = true;
|
|
1004
1173
|
try {
|
|
1005
1174
|
setError(null);
|
|
@@ -1015,19 +1184,18 @@ function PayPalAdvancedCard(props) {
|
|
|
1015
1184
|
setError(null);
|
|
1016
1185
|
showProcessingOverlay();
|
|
1017
1186
|
const orderId = String(data?.orderID || "");
|
|
1187
|
+
if (!orderId) throw new Error("PayPal order ID is missing from approval response");
|
|
1018
1188
|
const result = await api.captureOrder(cartId, orderId);
|
|
1019
|
-
|
|
1020
|
-
await onPaid?.({ ...result, ...completeResult });
|
|
1189
|
+
capturedRef.current = result || {};
|
|
1021
1190
|
} catch (e) {
|
|
1022
1191
|
if (isNextRouterError(e)) return;
|
|
1023
|
-
|
|
1024
|
-
creatingOrderRef.current = false;
|
|
1025
|
-
submittingRef.current = false;
|
|
1026
|
-
setSubmitting(false);
|
|
1192
|
+
resetSubmitState();
|
|
1027
1193
|
const msg = e instanceof Error ? e.message : "Card payment failed";
|
|
1028
1194
|
setError(msg);
|
|
1029
1195
|
onError?.(msg);
|
|
1196
|
+
return;
|
|
1030
1197
|
}
|
|
1198
|
+
await finalizeCapturedPayment();
|
|
1031
1199
|
},
|
|
1032
1200
|
onCancel: () => {
|
|
1033
1201
|
hideProcessingOverlay();
|
|
@@ -1134,7 +1302,29 @@ function PayPalAdvancedCard(props) {
|
|
|
1134
1302
|
]
|
|
1135
1303
|
}
|
|
1136
1304
|
),
|
|
1137
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1305
|
+
completionPending ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1306
|
+
"button",
|
|
1307
|
+
{
|
|
1308
|
+
type: "button",
|
|
1309
|
+
onClick: () => void finalizeCapturedPayment(),
|
|
1310
|
+
disabled: submitting,
|
|
1311
|
+
"aria-busy": submitting,
|
|
1312
|
+
style: {
|
|
1313
|
+
width: "100%",
|
|
1314
|
+
height: 48,
|
|
1315
|
+
padding: "0 20px",
|
|
1316
|
+
borderRadius: 8,
|
|
1317
|
+
border: "none",
|
|
1318
|
+
background: "#0070ba",
|
|
1319
|
+
color: "#ffffff",
|
|
1320
|
+
fontSize: 15,
|
|
1321
|
+
fontWeight: 600,
|
|
1322
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
1323
|
+
opacity: submitting ? 0.7 : 1
|
|
1324
|
+
},
|
|
1325
|
+
children: "Retry \u2014 finish placing my order"
|
|
1326
|
+
}
|
|
1327
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1138
1328
|
SubmitButton,
|
|
1139
1329
|
{
|
|
1140
1330
|
disabled: submitting,
|
|
@@ -1145,6 +1335,11 @@ function PayPalAdvancedCard(props) {
|
|
|
1145
1335
|
setError(null);
|
|
1146
1336
|
setSubmitting(true);
|
|
1147
1337
|
showProcessingOverlay();
|
|
1338
|
+
},
|
|
1339
|
+
onSubmitError: (msg) => {
|
|
1340
|
+
resetSubmitState();
|
|
1341
|
+
setError(msg);
|
|
1342
|
+
onError?.(msg);
|
|
1148
1343
|
}
|
|
1149
1344
|
}
|
|
1150
1345
|
),
|