@easypayment/medusa-paypal-ui 1.1.0 → 1.2.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/CHANGELOG.md +97 -25
- package/README.md +781 -759
- package/dist/index.cjs +366 -170
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.mjs +352 -170
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +14 -3
- package/dist/order.cjs.map +1 -1
- package/dist/order.d.cts +13 -4
- package/dist/order.d.ts +13 -4
- package/dist/order.mjs +13 -4
- package/dist/order.mjs.map +1 -1
- package/package.json +76 -65
- package/src/adapters/MedusaNextPayPalAdapter.tsx +34 -5
- package/src/client/http.ts +63 -11
- package/src/client/paypal.ts +27 -1
- package/src/client/types.ts +10 -0
- package/src/components/PayPalAdvancedCard.tsx +138 -24
- package/src/components/PayPalPaymentSection.tsx +95 -202
- package/src/components/PayPalSmartButtons.tsx +136 -7
- package/src/constants.ts +22 -0
- package/src/hooks/usePayPalPaymentMethods.ts +15 -1
- package/src/index.ts +5 -0
- package/src/order.ts +7 -10
- package/src/utils/captured-state.ts +44 -0
- package/src/utils/next-errors.ts +5 -1
- package/src/utils/processing-overlay.ts +12 -0
package/dist/index.mjs
CHANGED
|
@@ -16,6 +16,18 @@ function isRetryableError(err) {
|
|
|
16
16
|
return err instanceof Error && !(err instanceof HttpError);
|
|
17
17
|
}
|
|
18
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
|
+
}
|
|
19
31
|
function toHeaderRecord(headers) {
|
|
20
32
|
if (!headers) {
|
|
21
33
|
return {};
|
|
@@ -40,39 +52,49 @@ function createHttpClient(opts) {
|
|
|
40
52
|
headers["x-publishable-api-key"] = opts.publishableApiKey;
|
|
41
53
|
}
|
|
42
54
|
const timeoutMs = 3e4;
|
|
43
|
-
let timeoutId;
|
|
44
55
|
const controller = new AbortController();
|
|
45
|
-
|
|
46
|
-
|
|
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);
|
|
47
62
|
}
|
|
48
|
-
const effectiveSignal = init?.signal || controller.signal;
|
|
49
63
|
let res;
|
|
64
|
+
let text;
|
|
50
65
|
try {
|
|
51
|
-
res = await fetch(url, {
|
|
66
|
+
res = await fetch(url, {
|
|
67
|
+
...init,
|
|
68
|
+
headers,
|
|
69
|
+
credentials: opts.credentials ?? "include",
|
|
70
|
+
signal: controller.signal
|
|
71
|
+
});
|
|
72
|
+
text = await res.text().catch(() => "");
|
|
52
73
|
} catch (err) {
|
|
53
|
-
if (err instanceof Error && err.name === "AbortError" && !
|
|
74
|
+
if (err instanceof Error && err.name === "AbortError" && !(callerSignal && callerSignal.aborted)) {
|
|
54
75
|
throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1e3}s`);
|
|
55
76
|
}
|
|
56
77
|
throw err;
|
|
57
78
|
} finally {
|
|
58
|
-
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
|
|
59
81
|
}
|
|
60
|
-
const text = await res.text().catch(() => "");
|
|
61
82
|
if (!res.ok) {
|
|
83
|
+
const parsedMessage = extractJsonMessage(text);
|
|
62
84
|
if (res.status === 401) {
|
|
63
85
|
throw new HttpError(
|
|
64
|
-
"[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
|
|
86
|
+
parsedMessage || "[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
|
|
65
87
|
401
|
|
66
88
|
);
|
|
67
89
|
}
|
|
68
90
|
if (res.status === 403) {
|
|
69
91
|
throw new HttpError(
|
|
70
|
-
"[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
|
|
92
|
+
parsedMessage || "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
|
|
71
93
|
403
|
|
72
94
|
);
|
|
73
95
|
}
|
|
74
96
|
throw new HttpError(
|
|
75
|
-
text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
|
|
97
|
+
parsedMessage || text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
|
|
76
98
|
res.status
|
|
77
99
|
);
|
|
78
100
|
}
|
|
@@ -113,6 +135,14 @@ function createHttpClient(opts) {
|
|
|
113
135
|
}
|
|
114
136
|
|
|
115
137
|
// src/client/paypal.ts
|
|
138
|
+
function generateIdempotencyKey() {
|
|
139
|
+
try {
|
|
140
|
+
const c = globalThis.crypto;
|
|
141
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
return `pp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
145
|
+
}
|
|
116
146
|
async function markPaymentComplete(baseUrl, cartId, publishableApiKey) {
|
|
117
147
|
const http = createHttpClient({ baseUrl, publishableApiKey });
|
|
118
148
|
return http.request(
|
|
@@ -138,7 +168,13 @@ function createPayPalStoreApi(opts) {
|
|
|
138
168
|
createOrder(cartId, isCardPayment = false) {
|
|
139
169
|
return http.request(`/store/paypal/create-order`, {
|
|
140
170
|
method: "POST",
|
|
141
|
-
headers: {
|
|
171
|
+
headers: {
|
|
172
|
+
"Content-Type": "application/json",
|
|
173
|
+
// One key per attempt — see generateIdempotencyKey. Deliberately NOT
|
|
174
|
+
// sent for capture-order: there the server's deterministic
|
|
175
|
+
// per-order-id fallback is the correct idempotency scope.
|
|
176
|
+
"Idempotency-Key": generateIdempotencyKey()
|
|
177
|
+
},
|
|
142
178
|
body: JSON.stringify({ cart_id: cartId, is_card_payment: isCardPayment })
|
|
143
179
|
});
|
|
144
180
|
},
|
|
@@ -268,14 +304,41 @@ function PayPalCurrencyNotice({ config }) {
|
|
|
268
304
|
}
|
|
269
305
|
|
|
270
306
|
// src/components/PayPalSmartButtons.tsx
|
|
271
|
-
import { useCallback, useMemo as useMemo3, useState as useState2 } from "react";
|
|
307
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
|
|
272
308
|
import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js";
|
|
273
309
|
|
|
310
|
+
// src/utils/captured-state.ts
|
|
311
|
+
var KEY_PREFIX = "__pp_captured::";
|
|
312
|
+
function markCartCaptured(cartId) {
|
|
313
|
+
if (!cartId) return;
|
|
314
|
+
try {
|
|
315
|
+
window.sessionStorage.setItem(`${KEY_PREFIX}${cartId}`, String(Date.now()));
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function wasCartCaptured(cartId) {
|
|
320
|
+
if (!cartId) return false;
|
|
321
|
+
try {
|
|
322
|
+
return window.sessionStorage.getItem(`${KEY_PREFIX}${cartId}`) !== null;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function clearCartCaptured(cartId) {
|
|
328
|
+
if (!cartId) return;
|
|
329
|
+
try {
|
|
330
|
+
window.sessionStorage.removeItem(`${KEY_PREFIX}${cartId}`);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
274
335
|
// src/utils/next-errors.ts
|
|
275
336
|
function isNextRouterError(e) {
|
|
276
337
|
if (typeof e !== "object" || e === null || !("digest" in e)) return false;
|
|
277
338
|
const digest = e.digest;
|
|
278
|
-
return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND"))
|
|
339
|
+
return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND") || // Next.js 15: notFound()/forbidden()/unauthorized() throw
|
|
340
|
+
// HTTPAccessFallbackError with digest "NEXT_HTTP_ERROR_FALLBACK;<status>".
|
|
341
|
+
digest.startsWith("NEXT_HTTP_ERROR_FALLBACK"));
|
|
279
342
|
}
|
|
280
343
|
|
|
281
344
|
// src/utils/processing-overlay.ts
|
|
@@ -339,8 +402,13 @@ function showProcessingOverlay() {
|
|
|
339
402
|
origReplace(...args);
|
|
340
403
|
onNav();
|
|
341
404
|
};
|
|
405
|
+
const onPageShow = (e) => {
|
|
406
|
+
if (e.persisted) hideProcessingOverlay();
|
|
407
|
+
};
|
|
408
|
+
window.addEventListener("pageshow", onPageShow);
|
|
342
409
|
cleanupNavListener = () => {
|
|
343
410
|
window.removeEventListener("popstate", onNav);
|
|
411
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
344
412
|
history.pushState = origPush;
|
|
345
413
|
history.replaceState = origReplace;
|
|
346
414
|
};
|
|
@@ -373,11 +441,56 @@ function PayPalSmartButtons(props) {
|
|
|
373
441
|
const [error, setError] = useState2(null);
|
|
374
442
|
const [processing, setProcessing] = useState2(false);
|
|
375
443
|
const [buttonsReady, setButtonsReady] = useState2(false);
|
|
444
|
+
const [loadTimedOut, setLoadTimedOut] = useState2(false);
|
|
445
|
+
const capturedRef = useRef2(null);
|
|
446
|
+
const [completionPending, setCompletionPending] = useState2(false);
|
|
376
447
|
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer();
|
|
377
448
|
const handleInit = useCallback(() => {
|
|
378
449
|
setButtonsReady(true);
|
|
379
450
|
}, []);
|
|
380
|
-
|
|
451
|
+
useEffect2(() => {
|
|
452
|
+
const onPageShow = (e) => {
|
|
453
|
+
if (e.persisted) setProcessing(false);
|
|
454
|
+
};
|
|
455
|
+
window.addEventListener("pageshow", onPageShow);
|
|
456
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
457
|
+
}, []);
|
|
458
|
+
useEffect2(() => {
|
|
459
|
+
if (capturedRef.current || !wasCartCaptured(cartId)) return;
|
|
460
|
+
capturedRef.current = {};
|
|
461
|
+
setCompletionPending(true);
|
|
462
|
+
const msg = "Your payment was already received for this cart. Please use the button below to finish placing your order \u2014 do not pay again.";
|
|
463
|
+
setError(msg);
|
|
464
|
+
}, [cartId]);
|
|
465
|
+
useEffect2(() => {
|
|
466
|
+
if (!isResolved || buttonsReady) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const t = setTimeout(() => setLoadTimedOut(true), 1e4);
|
|
470
|
+
return () => clearTimeout(t);
|
|
471
|
+
}, [isResolved, buttonsReady]);
|
|
472
|
+
const finalizeCapturedPayment = useCallback(async () => {
|
|
473
|
+
const captured = capturedRef.current;
|
|
474
|
+
if (!captured) return;
|
|
475
|
+
setProcessing(true);
|
|
476
|
+
showProcessingOverlay();
|
|
477
|
+
setError(null);
|
|
478
|
+
try {
|
|
479
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
|
|
480
|
+
clearCartCaptured(cartId);
|
|
481
|
+
await onPaid?.({ ...captured, ...completeResult });
|
|
482
|
+
setCompletionPending(false);
|
|
483
|
+
} catch (e) {
|
|
484
|
+
if (isNextRouterError(e)) return;
|
|
485
|
+
hideProcessingOverlay();
|
|
486
|
+
setProcessing(false);
|
|
487
|
+
setCompletionPending(true);
|
|
488
|
+
const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
|
|
489
|
+
setError(msg);
|
|
490
|
+
onError?.(msg);
|
|
491
|
+
}
|
|
492
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError]);
|
|
493
|
+
const showSpinner = (isPending || isResolved && !buttonsReady) && !loadTimedOut;
|
|
381
494
|
if (!config.currency_supported) return null;
|
|
382
495
|
const containerWidth = BUTTON_WIDTH_MAP[config.button_width ?? "responsive"] ?? "100%";
|
|
383
496
|
return /* @__PURE__ */ jsxs2("div", { style: { width: containerWidth, position: "relative" }, children: [
|
|
@@ -516,6 +629,28 @@ function PayPalSmartButtons(props) {
|
|
|
516
629
|
]
|
|
517
630
|
}
|
|
518
631
|
),
|
|
632
|
+
loadTimedOut && !buttonsReady && !isRejected && /* @__PURE__ */ jsxs2(
|
|
633
|
+
"div",
|
|
634
|
+
{
|
|
635
|
+
role: "alert",
|
|
636
|
+
style: {
|
|
637
|
+
display: "flex",
|
|
638
|
+
alignItems: "flex-start",
|
|
639
|
+
gap: 8,
|
|
640
|
+
padding: "10px 14px",
|
|
641
|
+
background: "#fef2f2",
|
|
642
|
+
border: "1px solid #fecaca",
|
|
643
|
+
borderRadius: 8,
|
|
644
|
+
fontSize: 13,
|
|
645
|
+
color: "#b91c1c",
|
|
646
|
+
lineHeight: 1.5
|
|
647
|
+
},
|
|
648
|
+
children: [
|
|
649
|
+
/* @__PURE__ */ jsx3("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
|
|
650
|
+
/* @__PURE__ */ jsx3("span", { children: "PayPal is not available for this purchase. Please try a different payment method." })
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
),
|
|
519
654
|
isRejected && /* @__PURE__ */ jsxs2(
|
|
520
655
|
"div",
|
|
521
656
|
{
|
|
@@ -576,7 +711,7 @@ function PayPalSmartButtons(props) {
|
|
|
576
711
|
]
|
|
577
712
|
}
|
|
578
713
|
),
|
|
579
|
-
isResolved && /* @__PURE__ */ jsx3("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ jsx3(
|
|
714
|
+
isResolved && !completionPending && /* @__PURE__ */ jsx3("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ jsx3(
|
|
580
715
|
PayPalButtons,
|
|
581
716
|
{
|
|
582
717
|
forceReRender: [config.currency, config.intent, cartId],
|
|
@@ -590,6 +725,10 @@ function PayPalSmartButtons(props) {
|
|
|
590
725
|
},
|
|
591
726
|
createOrder: async () => {
|
|
592
727
|
if (processing) throw new Error("Payment already processing");
|
|
728
|
+
if (capturedRef.current) {
|
|
729
|
+
void finalizeCapturedPayment();
|
|
730
|
+
throw new Error("Your payment was already received \u2014 finalizing your order.");
|
|
731
|
+
}
|
|
593
732
|
setError(null);
|
|
594
733
|
setProcessing(true);
|
|
595
734
|
try {
|
|
@@ -608,8 +747,8 @@ function PayPalSmartButtons(props) {
|
|
|
608
747
|
const orderId = String(data?.orderID || "");
|
|
609
748
|
if (!orderId) throw new Error("PayPal order ID is missing from approval response");
|
|
610
749
|
const result = await api.captureOrder(cartId, orderId);
|
|
611
|
-
|
|
612
|
-
|
|
750
|
+
capturedRef.current = result || {};
|
|
751
|
+
markCartCaptured(cartId);
|
|
613
752
|
} catch (e) {
|
|
614
753
|
if (isNextRouterError(e)) return;
|
|
615
754
|
hideProcessingOverlay();
|
|
@@ -617,7 +756,9 @@ function PayPalSmartButtons(props) {
|
|
|
617
756
|
const msg = e instanceof Error ? e.message : "Payment capture failed";
|
|
618
757
|
setError(msg);
|
|
619
758
|
onError?.(msg);
|
|
759
|
+
return;
|
|
620
760
|
}
|
|
761
|
+
await finalizeCapturedPayment();
|
|
621
762
|
},
|
|
622
763
|
onCancel: () => {
|
|
623
764
|
hideProcessingOverlay();
|
|
@@ -632,6 +773,29 @@ function PayPalSmartButtons(props) {
|
|
|
632
773
|
}
|
|
633
774
|
}
|
|
634
775
|
) }),
|
|
776
|
+
completionPending && /* @__PURE__ */ jsx3(
|
|
777
|
+
"button",
|
|
778
|
+
{
|
|
779
|
+
type: "button",
|
|
780
|
+
onClick: () => void finalizeCapturedPayment(),
|
|
781
|
+
disabled: processing,
|
|
782
|
+
"aria-busy": processing,
|
|
783
|
+
style: {
|
|
784
|
+
width: "100%",
|
|
785
|
+
padding: "12px 16px",
|
|
786
|
+
marginTop: 4,
|
|
787
|
+
background: "#0070ba",
|
|
788
|
+
color: "#ffffff",
|
|
789
|
+
border: "none",
|
|
790
|
+
borderRadius: 8,
|
|
791
|
+
fontSize: 14,
|
|
792
|
+
fontWeight: 600,
|
|
793
|
+
cursor: processing ? "not-allowed" : "pointer",
|
|
794
|
+
opacity: processing ? 0.7 : 1
|
|
795
|
+
},
|
|
796
|
+
children: "Retry \u2014 finish placing my order"
|
|
797
|
+
}
|
|
798
|
+
),
|
|
635
799
|
error ? /* @__PURE__ */ jsxs2(
|
|
636
800
|
"div",
|
|
637
801
|
{
|
|
@@ -659,7 +823,7 @@ function PayPalSmartButtons(props) {
|
|
|
659
823
|
}
|
|
660
824
|
|
|
661
825
|
// src/components/PayPalAdvancedCard.tsx
|
|
662
|
-
import { useMemo as useMemo4, useRef as
|
|
826
|
+
import React3, { useMemo as useMemo4, useRef as useRef3, useState as useState3 } from "react";
|
|
663
827
|
import {
|
|
664
828
|
PayPalCardFieldsProvider,
|
|
665
829
|
PayPalNumberField,
|
|
@@ -718,7 +882,8 @@ var labelStyle = {
|
|
|
718
882
|
function SubmitButton({
|
|
719
883
|
disabled,
|
|
720
884
|
label,
|
|
721
|
-
onSubmit
|
|
885
|
+
onSubmit,
|
|
886
|
+
onSubmitError
|
|
722
887
|
}) {
|
|
723
888
|
const { cardFieldsForm } = usePayPalCardFields();
|
|
724
889
|
const isDisabled = disabled || !cardFieldsForm;
|
|
@@ -730,10 +895,10 @@ function SubmitButton({
|
|
|
730
895
|
"aria-busy": disabled,
|
|
731
896
|
onClick: () => {
|
|
732
897
|
onSubmit();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
898
|
+
Promise.resolve(cardFieldsForm?.submit()).catch((e) => {
|
|
899
|
+
const msg = e instanceof Error && e.message ? e.message : "Card payment could not be submitted. Please check your card details.";
|
|
900
|
+
onSubmitError(msg);
|
|
901
|
+
});
|
|
737
902
|
},
|
|
738
903
|
style: {
|
|
739
904
|
width: "100%",
|
|
@@ -777,9 +942,56 @@ function PayPalAdvancedCard(props) {
|
|
|
777
942
|
);
|
|
778
943
|
const [error, setError] = useState3(null);
|
|
779
944
|
const [submitting, setSubmitting] = useState3(false);
|
|
780
|
-
const submittingRef =
|
|
781
|
-
const creatingOrderRef =
|
|
945
|
+
const submittingRef = useRef3(false);
|
|
946
|
+
const creatingOrderRef = useRef3(false);
|
|
947
|
+
const capturedRef = useRef3(null);
|
|
948
|
+
const [completionPending, setCompletionPending] = useState3(false);
|
|
782
949
|
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer2();
|
|
950
|
+
const resetSubmitState = React3.useCallback(() => {
|
|
951
|
+
hideProcessingOverlay();
|
|
952
|
+
creatingOrderRef.current = false;
|
|
953
|
+
submittingRef.current = false;
|
|
954
|
+
setSubmitting(false);
|
|
955
|
+
}, []);
|
|
956
|
+
React3.useEffect(() => {
|
|
957
|
+
const onPageShow = (e) => {
|
|
958
|
+
if (e.persisted) {
|
|
959
|
+
creatingOrderRef.current = false;
|
|
960
|
+
submittingRef.current = false;
|
|
961
|
+
setSubmitting(false);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
window.addEventListener("pageshow", onPageShow);
|
|
965
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
966
|
+
}, []);
|
|
967
|
+
React3.useEffect(() => {
|
|
968
|
+
if (capturedRef.current || !wasCartCaptured(cartId)) return;
|
|
969
|
+
capturedRef.current = {};
|
|
970
|
+
setCompletionPending(true);
|
|
971
|
+
setError(
|
|
972
|
+
"Your payment was already received for this cart. Please use the button below to finish placing your order \u2014 do not pay again."
|
|
973
|
+
);
|
|
974
|
+
}, [cartId]);
|
|
975
|
+
const finalizeCapturedPayment = React3.useCallback(async () => {
|
|
976
|
+
const captured = capturedRef.current;
|
|
977
|
+
if (!captured) return;
|
|
978
|
+
setSubmitting(true);
|
|
979
|
+
showProcessingOverlay();
|
|
980
|
+
setError(null);
|
|
981
|
+
try {
|
|
982
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
|
|
983
|
+
clearCartCaptured(cartId);
|
|
984
|
+
await onPaid?.({ ...captured, ...completeResult });
|
|
985
|
+
setCompletionPending(false);
|
|
986
|
+
} catch (e) {
|
|
987
|
+
if (isNextRouterError(e)) return;
|
|
988
|
+
resetSubmitState();
|
|
989
|
+
setCompletionPending(true);
|
|
990
|
+
const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
|
|
991
|
+
setError(msg);
|
|
992
|
+
onError?.(msg);
|
|
993
|
+
}
|
|
994
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError, resetSubmitState]);
|
|
783
995
|
if (!config.currency_supported) return null;
|
|
784
996
|
if (isRejected) {
|
|
785
997
|
return /* @__PURE__ */ jsxs3(
|
|
@@ -968,6 +1180,10 @@ function PayPalAdvancedCard(props) {
|
|
|
968
1180
|
style: cardStyle,
|
|
969
1181
|
createOrder: async () => {
|
|
970
1182
|
if (creatingOrderRef.current) throw new Error("Payment already processing");
|
|
1183
|
+
if (capturedRef.current) {
|
|
1184
|
+
void finalizeCapturedPayment();
|
|
1185
|
+
throw new Error("Your payment was already received \u2014 finalizing your order.");
|
|
1186
|
+
}
|
|
971
1187
|
creatingOrderRef.current = true;
|
|
972
1188
|
try {
|
|
973
1189
|
setError(null);
|
|
@@ -983,19 +1199,19 @@ function PayPalAdvancedCard(props) {
|
|
|
983
1199
|
setError(null);
|
|
984
1200
|
showProcessingOverlay();
|
|
985
1201
|
const orderId = String(data?.orderID || "");
|
|
1202
|
+
if (!orderId) throw new Error("PayPal order ID is missing from approval response");
|
|
986
1203
|
const result = await api.captureOrder(cartId, orderId);
|
|
987
|
-
|
|
988
|
-
|
|
1204
|
+
capturedRef.current = result || {};
|
|
1205
|
+
markCartCaptured(cartId);
|
|
989
1206
|
} catch (e) {
|
|
990
1207
|
if (isNextRouterError(e)) return;
|
|
991
|
-
|
|
992
|
-
creatingOrderRef.current = false;
|
|
993
|
-
submittingRef.current = false;
|
|
994
|
-
setSubmitting(false);
|
|
1208
|
+
resetSubmitState();
|
|
995
1209
|
const msg = e instanceof Error ? e.message : "Card payment failed";
|
|
996
1210
|
setError(msg);
|
|
997
1211
|
onError?.(msg);
|
|
1212
|
+
return;
|
|
998
1213
|
}
|
|
1214
|
+
await finalizeCapturedPayment();
|
|
999
1215
|
},
|
|
1000
1216
|
onCancel: () => {
|
|
1001
1217
|
hideProcessingOverlay();
|
|
@@ -1102,7 +1318,29 @@ function PayPalAdvancedCard(props) {
|
|
|
1102
1318
|
]
|
|
1103
1319
|
}
|
|
1104
1320
|
),
|
|
1105
|
-
/* @__PURE__ */ jsx4(
|
|
1321
|
+
completionPending ? /* @__PURE__ */ jsx4(
|
|
1322
|
+
"button",
|
|
1323
|
+
{
|
|
1324
|
+
type: "button",
|
|
1325
|
+
onClick: () => void finalizeCapturedPayment(),
|
|
1326
|
+
disabled: submitting,
|
|
1327
|
+
"aria-busy": submitting,
|
|
1328
|
+
style: {
|
|
1329
|
+
width: "100%",
|
|
1330
|
+
height: 48,
|
|
1331
|
+
padding: "0 20px",
|
|
1332
|
+
borderRadius: 8,
|
|
1333
|
+
border: "none",
|
|
1334
|
+
background: "#0070ba",
|
|
1335
|
+
color: "#ffffff",
|
|
1336
|
+
fontSize: 15,
|
|
1337
|
+
fontWeight: 600,
|
|
1338
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
1339
|
+
opacity: submitting ? 0.7 : 1
|
|
1340
|
+
},
|
|
1341
|
+
children: "Retry \u2014 finish placing my order"
|
|
1342
|
+
}
|
|
1343
|
+
) : /* @__PURE__ */ jsx4(
|
|
1106
1344
|
SubmitButton,
|
|
1107
1345
|
{
|
|
1108
1346
|
disabled: submitting,
|
|
@@ -1113,6 +1351,11 @@ function PayPalAdvancedCard(props) {
|
|
|
1113
1351
|
setError(null);
|
|
1114
1352
|
setSubmitting(true);
|
|
1115
1353
|
showProcessingOverlay();
|
|
1354
|
+
},
|
|
1355
|
+
onSubmitError: (msg) => {
|
|
1356
|
+
resetSubmitState();
|
|
1357
|
+
setError(msg);
|
|
1358
|
+
onError?.(msg);
|
|
1116
1359
|
}
|
|
1117
1360
|
}
|
|
1118
1361
|
),
|
|
@@ -1148,9 +1391,23 @@ function PayPalAdvancedCard(props) {
|
|
|
1148
1391
|
|
|
1149
1392
|
// src/adapters/MedusaNextPayPalAdapter.tsx
|
|
1150
1393
|
import { useCallback as useCallback2 } from "react";
|
|
1394
|
+
|
|
1395
|
+
// src/constants.ts
|
|
1396
|
+
var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
|
|
1397
|
+
var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
|
|
1398
|
+
var PAYPAL_PROVIDER_IDS = [
|
|
1399
|
+
PAYPAL_WALLET_PROVIDER_ID,
|
|
1400
|
+
PAYPAL_CARD_PROVIDER_ID
|
|
1401
|
+
];
|
|
1402
|
+
function isPayPalProviderId(providerId) {
|
|
1403
|
+
if (!providerId) return false;
|
|
1404
|
+
return PAYPAL_PROVIDER_IDS.includes(
|
|
1405
|
+
providerId
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
// src/adapters/MedusaNextPayPalAdapter.tsx
|
|
1151
1410
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1152
|
-
var DEFAULT_PAYPAL_PROVIDER_ID = "pp_paypal_paypal";
|
|
1153
|
-
var DEFAULT_PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
|
|
1154
1411
|
var SPIN_STYLE3 = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
|
|
1155
1412
|
function PayPalLoadingCard() {
|
|
1156
1413
|
return /* @__PURE__ */ jsxs4(
|
|
@@ -1208,6 +1465,26 @@ function PayPalErrorCard({ message }) {
|
|
|
1208
1465
|
}
|
|
1209
1466
|
);
|
|
1210
1467
|
}
|
|
1468
|
+
function PayPalUnavailableCard({ label }) {
|
|
1469
|
+
return /* @__PURE__ */ jsxs4(
|
|
1470
|
+
"div",
|
|
1471
|
+
{
|
|
1472
|
+
role: "status",
|
|
1473
|
+
style: {
|
|
1474
|
+
padding: "12px 16px",
|
|
1475
|
+
background: "#f9fafb",
|
|
1476
|
+
border: "1px solid #e5e7eb",
|
|
1477
|
+
borderRadius: 10,
|
|
1478
|
+
fontSize: 13,
|
|
1479
|
+
color: "#6b7280"
|
|
1480
|
+
},
|
|
1481
|
+
children: [
|
|
1482
|
+
label,
|
|
1483
|
+
" is currently unavailable. Please choose a different payment method."
|
|
1484
|
+
]
|
|
1485
|
+
}
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1211
1488
|
function MedusaNextPayPalAdapter(props) {
|
|
1212
1489
|
const {
|
|
1213
1490
|
cartId,
|
|
@@ -1219,8 +1496,8 @@ function MedusaNextPayPalAdapter(props) {
|
|
|
1219
1496
|
onError,
|
|
1220
1497
|
onPaid
|
|
1221
1498
|
} = props;
|
|
1222
|
-
const paypalProviderId = providerIds?.paypal ||
|
|
1223
|
-
const paypalCardProviderId = providerIds?.paypalCard ||
|
|
1499
|
+
const paypalProviderId = providerIds?.paypal || PAYPAL_WALLET_PROVIDER_ID;
|
|
1500
|
+
const paypalCardProviderId = providerIds?.paypalCard || PAYPAL_CARD_PROVIDER_ID;
|
|
1224
1501
|
const shouldRender = selectedProviderId === paypalProviderId || selectedProviderId === paypalCardProviderId;
|
|
1225
1502
|
const { config, loading, error } = usePayPalConfig({
|
|
1226
1503
|
baseUrl,
|
|
@@ -1240,8 +1517,12 @@ function MedusaNextPayPalAdapter(props) {
|
|
|
1240
1517
|
if (error) return /* @__PURE__ */ jsx5(PayPalErrorCard, { message: error });
|
|
1241
1518
|
if (!config) return null;
|
|
1242
1519
|
const isCardProvider = selectedProviderId === paypalCardProviderId;
|
|
1243
|
-
if (config.paypal_enabled === false && !isCardProvider)
|
|
1244
|
-
|
|
1520
|
+
if (config.paypal_enabled === false && !isCardProvider) {
|
|
1521
|
+
return /* @__PURE__ */ jsx5(PayPalUnavailableCard, { label: config.paypal_title || "PayPal" });
|
|
1522
|
+
}
|
|
1523
|
+
if (isCardProvider && config.card_enabled === false) {
|
|
1524
|
+
return /* @__PURE__ */ jsx5(PayPalUnavailableCard, { label: config.card_title || "Card payment" });
|
|
1525
|
+
}
|
|
1245
1526
|
const disableFunding = Array.isArray(config.disable_buttons) ? config.disable_buttons.join(",") : void 0;
|
|
1246
1527
|
return /* @__PURE__ */ jsxs4("div", { style: { display: "grid", gap: 12 }, children: [
|
|
1247
1528
|
/* @__PURE__ */ jsx5(PayPalCurrencyNotice, { config }),
|
|
@@ -1278,18 +1559,7 @@ function MedusaNextPayPalAdapter(props) {
|
|
|
1278
1559
|
}
|
|
1279
1560
|
|
|
1280
1561
|
// src/components/PayPalPaymentSection.tsx
|
|
1281
|
-
import { useCallback as useCallback3 } from "react";
|
|
1282
1562
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1283
|
-
var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
|
|
1284
|
-
var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
|
|
1285
|
-
var PAYPAL_PROVIDER_IDS = [
|
|
1286
|
-
PAYPAL_WALLET_PROVIDER_ID,
|
|
1287
|
-
PAYPAL_CARD_PROVIDER_ID
|
|
1288
|
-
];
|
|
1289
|
-
function isPayPalProviderId(id) {
|
|
1290
|
-
if (!id) return false;
|
|
1291
|
-
return PAYPAL_PROVIDER_IDS.includes(id);
|
|
1292
|
-
}
|
|
1293
1563
|
var SPIN_STYLE4 = `@keyframes _pp_section_spin { to { transform: rotate(360deg) } }`;
|
|
1294
1564
|
function SessionInitCard() {
|
|
1295
1565
|
return /* @__PURE__ */ jsxs5(
|
|
@@ -1328,62 +1598,6 @@ function SessionInitCard() {
|
|
|
1328
1598
|
}
|
|
1329
1599
|
);
|
|
1330
1600
|
}
|
|
1331
|
-
function ConfigLoadingCard() {
|
|
1332
|
-
return /* @__PURE__ */ jsxs5(
|
|
1333
|
-
"div",
|
|
1334
|
-
{
|
|
1335
|
-
role: "status",
|
|
1336
|
-
"aria-label": "Connecting to PayPal",
|
|
1337
|
-
style: {
|
|
1338
|
-
display: "flex",
|
|
1339
|
-
alignItems: "center",
|
|
1340
|
-
gap: 12,
|
|
1341
|
-
padding: "14px 16px",
|
|
1342
|
-
background: "#f9fafb",
|
|
1343
|
-
border: "1px solid #e5e7eb",
|
|
1344
|
-
borderRadius: 10
|
|
1345
|
-
},
|
|
1346
|
-
children: [
|
|
1347
|
-
/* @__PURE__ */ jsx6("style", { children: SPIN_STYLE4 }),
|
|
1348
|
-
/* @__PURE__ */ jsx6(
|
|
1349
|
-
"div",
|
|
1350
|
-
{
|
|
1351
|
-
style: {
|
|
1352
|
-
width: 22,
|
|
1353
|
-
height: 22,
|
|
1354
|
-
borderRadius: "50%",
|
|
1355
|
-
border: "2.5px solid #e5e7eb",
|
|
1356
|
-
borderTopColor: "#0070ba",
|
|
1357
|
-
animation: "_pp_section_spin .7s linear infinite",
|
|
1358
|
-
flexShrink: 0
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
),
|
|
1362
|
-
/* @__PURE__ */ jsxs5("div", { children: [
|
|
1363
|
-
/* @__PURE__ */ jsx6("div", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: "Connecting to PayPal\u2026" }),
|
|
1364
|
-
/* @__PURE__ */ jsx6("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 2 }, children: "Setting up secure payment" })
|
|
1365
|
-
] })
|
|
1366
|
-
]
|
|
1367
|
-
}
|
|
1368
|
-
);
|
|
1369
|
-
}
|
|
1370
|
-
function ErrorCard({ message }) {
|
|
1371
|
-
return /* @__PURE__ */ jsx6(
|
|
1372
|
-
"div",
|
|
1373
|
-
{
|
|
1374
|
-
role: "alert",
|
|
1375
|
-
style: {
|
|
1376
|
-
padding: "12px 16px",
|
|
1377
|
-
background: "#fef2f2",
|
|
1378
|
-
border: "1px solid #fecaca",
|
|
1379
|
-
borderRadius: 10,
|
|
1380
|
-
fontSize: 13,
|
|
1381
|
-
color: "#b91c1c"
|
|
1382
|
-
},
|
|
1383
|
-
children: message
|
|
1384
|
-
}
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
1601
|
function PayPalPaymentSection({
|
|
1388
1602
|
cartId,
|
|
1389
1603
|
selectedProviderId,
|
|
@@ -1394,67 +1608,24 @@ function PayPalPaymentSection({
|
|
|
1394
1608
|
onError,
|
|
1395
1609
|
onPaid
|
|
1396
1610
|
}) {
|
|
1397
|
-
|
|
1398
|
-
const { config, loading, error } = usePayPalConfig({
|
|
1399
|
-
baseUrl,
|
|
1400
|
-
publishableApiKey,
|
|
1401
|
-
cartId,
|
|
1402
|
-
enabled: shouldRender
|
|
1403
|
-
});
|
|
1404
|
-
const handlePaid = useCallback3(
|
|
1405
|
-
async (captureResult) => {
|
|
1406
|
-
onPaid?.(captureResult);
|
|
1407
|
-
await onSuccess?.(cartId);
|
|
1408
|
-
},
|
|
1409
|
-
[cartId, onPaid, onSuccess]
|
|
1410
|
-
);
|
|
1411
|
-
if (!shouldRender) return null;
|
|
1611
|
+
if (!isPayPalProviderId(selectedProviderId)) return null;
|
|
1412
1612
|
if (sessionLoading) return /* @__PURE__ */ jsx6(SessionInitCard, {});
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
PayPalProvider,
|
|
1426
|
-
{
|
|
1427
|
-
config,
|
|
1428
|
-
intent: config.intent === "authorize" ? "authorize" : "capture",
|
|
1429
|
-
disableFunding,
|
|
1430
|
-
children: isCardProvider ? /* @__PURE__ */ jsx6(
|
|
1431
|
-
PayPalAdvancedCard,
|
|
1432
|
-
{
|
|
1433
|
-
baseUrl,
|
|
1434
|
-
publishableApiKey,
|
|
1435
|
-
cartId,
|
|
1436
|
-
config,
|
|
1437
|
-
onPaid: handlePaid,
|
|
1438
|
-
onError
|
|
1439
|
-
}
|
|
1440
|
-
) : /* @__PURE__ */ jsx6(
|
|
1441
|
-
PayPalSmartButtons,
|
|
1442
|
-
{
|
|
1443
|
-
baseUrl,
|
|
1444
|
-
publishableApiKey,
|
|
1445
|
-
cartId,
|
|
1446
|
-
config,
|
|
1447
|
-
onPaid: handlePaid,
|
|
1448
|
-
onError
|
|
1449
|
-
}
|
|
1450
|
-
)
|
|
1451
|
-
}
|
|
1452
|
-
)
|
|
1453
|
-
] }, selectedProviderId);
|
|
1613
|
+
return /* @__PURE__ */ jsx6(
|
|
1614
|
+
MedusaNextPayPalAdapter,
|
|
1615
|
+
{
|
|
1616
|
+
cartId,
|
|
1617
|
+
selectedProviderId,
|
|
1618
|
+
baseUrl,
|
|
1619
|
+
publishableApiKey,
|
|
1620
|
+
onSuccess,
|
|
1621
|
+
onError,
|
|
1622
|
+
onPaid
|
|
1623
|
+
}
|
|
1624
|
+
);
|
|
1454
1625
|
}
|
|
1455
1626
|
|
|
1456
1627
|
// src/hooks/usePayPalPaymentMethods.ts
|
|
1457
|
-
import { useEffect as
|
|
1628
|
+
import { useEffect as useEffect3, useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
|
|
1458
1629
|
var MAX_CACHE_ENTRIES2 = 50;
|
|
1459
1630
|
var _cache2 = /* @__PURE__ */ new Map();
|
|
1460
1631
|
var CACHE_TTL2 = 5 * 60 * 1e3;
|
|
@@ -1473,7 +1644,8 @@ var DEFAULT_RESULT = {
|
|
|
1473
1644
|
paypalTitle: "PayPal",
|
|
1474
1645
|
cardEnabled: true,
|
|
1475
1646
|
cardTitle: "Credit or Debit Card",
|
|
1476
|
-
loading: false
|
|
1647
|
+
loading: false,
|
|
1648
|
+
error: null
|
|
1477
1649
|
};
|
|
1478
1650
|
function usePayPalPaymentMethods({
|
|
1479
1651
|
baseUrl,
|
|
@@ -1489,8 +1661,8 @@ function usePayPalPaymentMethods({
|
|
|
1489
1661
|
const hit = _cache2.get(key);
|
|
1490
1662
|
const seed = hit && Date.now() - hit.at < CACHE_TTL2 ? hit.result : null;
|
|
1491
1663
|
const [result, setResult] = useState4(seed ?? { ...DEFAULT_RESULT, loading: enabled });
|
|
1492
|
-
const fetchIdRef =
|
|
1493
|
-
|
|
1664
|
+
const fetchIdRef = useRef4(0);
|
|
1665
|
+
useEffect3(() => {
|
|
1494
1666
|
if (!enabled) {
|
|
1495
1667
|
setResult((prev) => ({ ...prev, loading: false }));
|
|
1496
1668
|
return;
|
|
@@ -1514,7 +1686,8 @@ function usePayPalPaymentMethods({
|
|
|
1514
1686
|
paypalTitle: typeof cfg.paypal_title === "string" && cfg.paypal_title ? cfg.paypal_title : "PayPal",
|
|
1515
1687
|
cardEnabled: cfg.card_enabled !== false,
|
|
1516
1688
|
cardTitle: typeof cfg.card_title === "string" && cfg.card_title ? cfg.card_title : "Credit or Debit Card",
|
|
1517
|
-
loading: false
|
|
1689
|
+
loading: false,
|
|
1690
|
+
error: null
|
|
1518
1691
|
};
|
|
1519
1692
|
cacheSet2(k, { result: next, at: Date.now() });
|
|
1520
1693
|
setResult(next);
|
|
@@ -1527,12 +1700,17 @@ function usePayPalPaymentMethods({
|
|
|
1527
1700
|
...DEFAULT_RESULT,
|
|
1528
1701
|
paypalEnabled: false,
|
|
1529
1702
|
cardEnabled: false,
|
|
1530
|
-
loading: false
|
|
1703
|
+
loading: false,
|
|
1704
|
+
error: msg || "PayPal is disabled"
|
|
1531
1705
|
};
|
|
1532
1706
|
setResult(disabled);
|
|
1533
1707
|
return;
|
|
1534
1708
|
}
|
|
1535
|
-
setResult({
|
|
1709
|
+
setResult({
|
|
1710
|
+
...DEFAULT_RESULT,
|
|
1711
|
+
loading: false,
|
|
1712
|
+
error: msg || "Failed to load PayPal payment methods"
|
|
1713
|
+
});
|
|
1536
1714
|
}
|
|
1537
1715
|
})();
|
|
1538
1716
|
return () => {
|
|
@@ -1551,12 +1729,16 @@ export {
|
|
|
1551
1729
|
PayPalPaymentSection,
|
|
1552
1730
|
PayPalProvider,
|
|
1553
1731
|
PayPalSmartButtons,
|
|
1732
|
+
clearCartCaptured,
|
|
1554
1733
|
createPayPalStoreApi,
|
|
1734
|
+
generateIdempotencyKey,
|
|
1555
1735
|
hideProcessingOverlay,
|
|
1556
1736
|
isPayPalProviderId,
|
|
1737
|
+
markCartCaptured,
|
|
1557
1738
|
markPaymentComplete,
|
|
1558
1739
|
showProcessingOverlay,
|
|
1559
1740
|
usePayPalConfig,
|
|
1560
|
-
usePayPalPaymentMethods
|
|
1741
|
+
usePayPalPaymentMethods,
|
|
1742
|
+
wasCartCaptured
|
|
1561
1743
|
};
|
|
1562
1744
|
//# sourceMappingURL=index.mjs.map
|