@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/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
@@ -29,13 +39,17 @@ __export(index_exports, {
29
39
  PayPalPaymentSection: () => PayPalPaymentSection,
30
40
  PayPalProvider: () => PayPalProvider,
31
41
  PayPalSmartButtons: () => PayPalSmartButtons,
42
+ clearCartCaptured: () => clearCartCaptured,
32
43
  createPayPalStoreApi: () => createPayPalStoreApi,
44
+ generateIdempotencyKey: () => generateIdempotencyKey,
33
45
  hideProcessingOverlay: () => hideProcessingOverlay,
34
46
  isPayPalProviderId: () => isPayPalProviderId,
47
+ markCartCaptured: () => markCartCaptured,
35
48
  markPaymentComplete: () => markPaymentComplete,
36
49
  showProcessingOverlay: () => showProcessingOverlay,
37
50
  usePayPalConfig: () => usePayPalConfig,
38
- usePayPalPaymentMethods: () => usePayPalPaymentMethods
51
+ usePayPalPaymentMethods: () => usePayPalPaymentMethods,
52
+ wasCartCaptured: () => wasCartCaptured
39
53
  });
40
54
  module.exports = __toCommonJS(index_exports);
41
55
 
@@ -55,6 +69,18 @@ function isRetryableError(err) {
55
69
  return err instanceof Error && !(err instanceof HttpError);
56
70
  }
57
71
  var wait = (ms) => new Promise((r) => setTimeout(r, ms));
72
+ function extractJsonMessage(text) {
73
+ if (!text) return null;
74
+ try {
75
+ const parsed = JSON.parse(text);
76
+ const message = parsed?.message;
77
+ if (typeof message === "string" && message.trim()) {
78
+ return message.trim().slice(0, 500);
79
+ }
80
+ } catch {
81
+ }
82
+ return null;
83
+ }
58
84
  function toHeaderRecord(headers) {
59
85
  if (!headers) {
60
86
  return {};
@@ -79,39 +105,49 @@ function createHttpClient(opts) {
79
105
  headers["x-publishable-api-key"] = opts.publishableApiKey;
80
106
  }
81
107
  const timeoutMs = 3e4;
82
- let timeoutId;
83
108
  const controller = new AbortController();
84
- if (!init?.signal) {
85
- timeoutId = setTimeout(() => controller.abort(), timeoutMs);
109
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
110
+ const callerSignal = init?.signal ?? null;
111
+ const onCallerAbort = () => controller.abort();
112
+ if (callerSignal) {
113
+ if (callerSignal.aborted) controller.abort();
114
+ else callerSignal.addEventListener("abort", onCallerAbort);
86
115
  }
87
- const effectiveSignal = init?.signal || controller.signal;
88
116
  let res;
117
+ let text;
89
118
  try {
90
- res = await fetch(url, { ...init, headers, credentials: "include", signal: effectiveSignal });
119
+ res = await fetch(url, {
120
+ ...init,
121
+ headers,
122
+ credentials: opts.credentials ?? "include",
123
+ signal: controller.signal
124
+ });
125
+ text = await res.text().catch(() => "");
91
126
  } catch (err) {
92
- if (err instanceof Error && err.name === "AbortError" && !init?.signal) {
127
+ if (err instanceof Error && err.name === "AbortError" && !(callerSignal && callerSignal.aborted)) {
93
128
  throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1e3}s`);
94
129
  }
95
130
  throw err;
96
131
  } finally {
97
- if (timeoutId !== void 0) clearTimeout(timeoutId);
132
+ clearTimeout(timeoutId);
133
+ if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort);
98
134
  }
99
- const text = await res.text().catch(() => "");
100
135
  if (!res.ok) {
136
+ const parsedMessage = extractJsonMessage(text);
101
137
  if (res.status === 401) {
102
138
  throw new HttpError(
103
- "[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
139
+ parsedMessage || "[PayPal] Unauthorized (401) \u2014 check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
104
140
  401
105
141
  );
106
142
  }
107
143
  if (res.status === 403) {
108
144
  throw new HttpError(
109
- "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
145
+ parsedMessage || "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings.",
110
146
  403
111
147
  );
112
148
  }
113
149
  throw new HttpError(
114
- text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
150
+ parsedMessage || text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
115
151
  res.status
116
152
  );
117
153
  }
@@ -152,6 +188,14 @@ function createHttpClient(opts) {
152
188
  }
153
189
 
154
190
  // src/client/paypal.ts
191
+ function generateIdempotencyKey() {
192
+ try {
193
+ const c = globalThis.crypto;
194
+ if (c?.randomUUID) return c.randomUUID();
195
+ } catch {
196
+ }
197
+ return `pp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
198
+ }
155
199
  async function markPaymentComplete(baseUrl, cartId, publishableApiKey) {
156
200
  const http = createHttpClient({ baseUrl, publishableApiKey });
157
201
  return http.request(
@@ -177,7 +221,13 @@ function createPayPalStoreApi(opts) {
177
221
  createOrder(cartId, isCardPayment = false) {
178
222
  return http.request(`/store/paypal/create-order`, {
179
223
  method: "POST",
180
- headers: { "Content-Type": "application/json" },
224
+ headers: {
225
+ "Content-Type": "application/json",
226
+ // One key per attempt — see generateIdempotencyKey. Deliberately NOT
227
+ // sent for capture-order: there the server's deterministic
228
+ // per-order-id fallback is the correct idempotency scope.
229
+ "Idempotency-Key": generateIdempotencyKey()
230
+ },
181
231
  body: JSON.stringify({ cart_id: cartId, is_card_payment: isCardPayment })
182
232
  });
183
233
  },
@@ -310,11 +360,38 @@ function PayPalCurrencyNotice({ config }) {
310
360
  var import_react3 = require("react");
311
361
  var import_react_paypal_js2 = require("@paypal/react-paypal-js");
312
362
 
363
+ // src/utils/captured-state.ts
364
+ var KEY_PREFIX = "__pp_captured::";
365
+ function markCartCaptured(cartId) {
366
+ if (!cartId) return;
367
+ try {
368
+ window.sessionStorage.setItem(`${KEY_PREFIX}${cartId}`, String(Date.now()));
369
+ } catch {
370
+ }
371
+ }
372
+ function wasCartCaptured(cartId) {
373
+ if (!cartId) return false;
374
+ try {
375
+ return window.sessionStorage.getItem(`${KEY_PREFIX}${cartId}`) !== null;
376
+ } catch {
377
+ return false;
378
+ }
379
+ }
380
+ function clearCartCaptured(cartId) {
381
+ if (!cartId) return;
382
+ try {
383
+ window.sessionStorage.removeItem(`${KEY_PREFIX}${cartId}`);
384
+ } catch {
385
+ }
386
+ }
387
+
313
388
  // src/utils/next-errors.ts
314
389
  function isNextRouterError(e) {
315
390
  if (typeof e !== "object" || e === null || !("digest" in e)) return false;
316
391
  const digest = e.digest;
317
- return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND"));
392
+ return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND") || // Next.js 15: notFound()/forbidden()/unauthorized() throw
393
+ // HTTPAccessFallbackError with digest "NEXT_HTTP_ERROR_FALLBACK;<status>".
394
+ digest.startsWith("NEXT_HTTP_ERROR_FALLBACK"));
318
395
  }
319
396
 
320
397
  // src/utils/processing-overlay.ts
@@ -378,8 +455,13 @@ function showProcessingOverlay() {
378
455
  origReplace(...args);
379
456
  onNav();
380
457
  };
458
+ const onPageShow = (e) => {
459
+ if (e.persisted) hideProcessingOverlay();
460
+ };
461
+ window.addEventListener("pageshow", onPageShow);
381
462
  cleanupNavListener = () => {
382
463
  window.removeEventListener("popstate", onNav);
464
+ window.removeEventListener("pageshow", onPageShow);
383
465
  history.pushState = origPush;
384
466
  history.replaceState = origReplace;
385
467
  };
@@ -412,11 +494,56 @@ function PayPalSmartButtons(props) {
412
494
  const [error, setError] = (0, import_react3.useState)(null);
413
495
  const [processing, setProcessing] = (0, import_react3.useState)(false);
414
496
  const [buttonsReady, setButtonsReady] = (0, import_react3.useState)(false);
497
+ const [loadTimedOut, setLoadTimedOut] = (0, import_react3.useState)(false);
498
+ const capturedRef = (0, import_react3.useRef)(null);
499
+ const [completionPending, setCompletionPending] = (0, import_react3.useState)(false);
415
500
  const [{ isPending, isResolved, isRejected }] = (0, import_react_paypal_js2.usePayPalScriptReducer)();
416
501
  const handleInit = (0, import_react3.useCallback)(() => {
417
502
  setButtonsReady(true);
418
503
  }, []);
419
- const showSpinner = isPending || isResolved && !buttonsReady;
504
+ (0, import_react3.useEffect)(() => {
505
+ const onPageShow = (e) => {
506
+ if (e.persisted) setProcessing(false);
507
+ };
508
+ window.addEventListener("pageshow", onPageShow);
509
+ return () => window.removeEventListener("pageshow", onPageShow);
510
+ }, []);
511
+ (0, import_react3.useEffect)(() => {
512
+ if (capturedRef.current || !wasCartCaptured(cartId)) return;
513
+ capturedRef.current = {};
514
+ setCompletionPending(true);
515
+ 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.";
516
+ setError(msg);
517
+ }, [cartId]);
518
+ (0, import_react3.useEffect)(() => {
519
+ if (!isResolved || buttonsReady) {
520
+ return;
521
+ }
522
+ const t = setTimeout(() => setLoadTimedOut(true), 1e4);
523
+ return () => clearTimeout(t);
524
+ }, [isResolved, buttonsReady]);
525
+ const finalizeCapturedPayment = (0, import_react3.useCallback)(async () => {
526
+ const captured = capturedRef.current;
527
+ if (!captured) return;
528
+ setProcessing(true);
529
+ showProcessingOverlay();
530
+ setError(null);
531
+ try {
532
+ const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
533
+ clearCartCaptured(cartId);
534
+ await onPaid?.({ ...captured, ...completeResult });
535
+ setCompletionPending(false);
536
+ } catch (e) {
537
+ if (isNextRouterError(e)) return;
538
+ hideProcessingOverlay();
539
+ setProcessing(false);
540
+ setCompletionPending(true);
541
+ const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
542
+ setError(msg);
543
+ onError?.(msg);
544
+ }
545
+ }, [baseUrl, cartId, publishableApiKey, onPaid, onError]);
546
+ const showSpinner = (isPending || isResolved && !buttonsReady) && !loadTimedOut;
420
547
  if (!config.currency_supported) return null;
421
548
  const containerWidth = BUTTON_WIDTH_MAP[config.button_width ?? "responsive"] ?? "100%";
422
549
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: containerWidth, position: "relative" }, children: [
@@ -555,6 +682,28 @@ function PayPalSmartButtons(props) {
555
682
  ]
556
683
  }
557
684
  ),
685
+ loadTimedOut && !buttonsReady && !isRejected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
686
+ "div",
687
+ {
688
+ role: "alert",
689
+ style: {
690
+ display: "flex",
691
+ alignItems: "flex-start",
692
+ gap: 8,
693
+ padding: "10px 14px",
694
+ background: "#fef2f2",
695
+ border: "1px solid #fecaca",
696
+ borderRadius: 8,
697
+ fontSize: 13,
698
+ color: "#b91c1c",
699
+ lineHeight: 1.5
700
+ },
701
+ children: [
702
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
703
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "PayPal is not available for this purchase. Please try a different payment method." })
704
+ ]
705
+ }
706
+ ),
558
707
  isRejected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
559
708
  "div",
560
709
  {
@@ -615,7 +764,7 @@ function PayPalSmartButtons(props) {
615
764
  ]
616
765
  }
617
766
  ),
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)(
767
+ 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
768
  import_react_paypal_js2.PayPalButtons,
620
769
  {
621
770
  forceReRender: [config.currency, config.intent, cartId],
@@ -629,6 +778,10 @@ function PayPalSmartButtons(props) {
629
778
  },
630
779
  createOrder: async () => {
631
780
  if (processing) throw new Error("Payment already processing");
781
+ if (capturedRef.current) {
782
+ void finalizeCapturedPayment();
783
+ throw new Error("Your payment was already received \u2014 finalizing your order.");
784
+ }
632
785
  setError(null);
633
786
  setProcessing(true);
634
787
  try {
@@ -647,8 +800,8 @@ function PayPalSmartButtons(props) {
647
800
  const orderId = String(data?.orderID || "");
648
801
  if (!orderId) throw new Error("PayPal order ID is missing from approval response");
649
802
  const result = await api.captureOrder(cartId, orderId);
650
- const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
651
- await onPaid?.({ ...result, ...completeResult });
803
+ capturedRef.current = result || {};
804
+ markCartCaptured(cartId);
652
805
  } catch (e) {
653
806
  if (isNextRouterError(e)) return;
654
807
  hideProcessingOverlay();
@@ -656,7 +809,9 @@ function PayPalSmartButtons(props) {
656
809
  const msg = e instanceof Error ? e.message : "Payment capture failed";
657
810
  setError(msg);
658
811
  onError?.(msg);
812
+ return;
659
813
  }
814
+ await finalizeCapturedPayment();
660
815
  },
661
816
  onCancel: () => {
662
817
  hideProcessingOverlay();
@@ -671,6 +826,29 @@ function PayPalSmartButtons(props) {
671
826
  }
672
827
  }
673
828
  ) }),
829
+ completionPending && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
830
+ "button",
831
+ {
832
+ type: "button",
833
+ onClick: () => void finalizeCapturedPayment(),
834
+ disabled: processing,
835
+ "aria-busy": processing,
836
+ style: {
837
+ width: "100%",
838
+ padding: "12px 16px",
839
+ marginTop: 4,
840
+ background: "#0070ba",
841
+ color: "#ffffff",
842
+ border: "none",
843
+ borderRadius: 8,
844
+ fontSize: 14,
845
+ fontWeight: 600,
846
+ cursor: processing ? "not-allowed" : "pointer",
847
+ opacity: processing ? 0.7 : 1
848
+ },
849
+ children: "Retry \u2014 finish placing my order"
850
+ }
851
+ ),
674
852
  error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
675
853
  "div",
676
854
  {
@@ -698,7 +876,7 @@ function PayPalSmartButtons(props) {
698
876
  }
699
877
 
700
878
  // src/components/PayPalAdvancedCard.tsx
701
- var import_react4 = require("react");
879
+ var import_react4 = __toESM(require("react"), 1);
702
880
  var import_react_paypal_js3 = require("@paypal/react-paypal-js");
703
881
  var import_jsx_runtime4 = require("react/jsx-runtime");
704
882
  var SPIN_STYLE2 = `
@@ -750,7 +928,8 @@ var labelStyle = {
750
928
  function SubmitButton({
751
929
  disabled,
752
930
  label,
753
- onSubmit
931
+ onSubmit,
932
+ onSubmitError
754
933
  }) {
755
934
  const { cardFieldsForm } = (0, import_react_paypal_js3.usePayPalCardFields)();
756
935
  const isDisabled = disabled || !cardFieldsForm;
@@ -762,10 +941,10 @@ function SubmitButton({
762
941
  "aria-busy": disabled,
763
942
  onClick: () => {
764
943
  onSubmit();
765
- try {
766
- cardFieldsForm?.submit();
767
- } catch {
768
- }
944
+ Promise.resolve(cardFieldsForm?.submit()).catch((e) => {
945
+ const msg = e instanceof Error && e.message ? e.message : "Card payment could not be submitted. Please check your card details.";
946
+ onSubmitError(msg);
947
+ });
769
948
  },
770
949
  style: {
771
950
  width: "100%",
@@ -811,7 +990,54 @@ function PayPalAdvancedCard(props) {
811
990
  const [submitting, setSubmitting] = (0, import_react4.useState)(false);
812
991
  const submittingRef = (0, import_react4.useRef)(false);
813
992
  const creatingOrderRef = (0, import_react4.useRef)(false);
993
+ const capturedRef = (0, import_react4.useRef)(null);
994
+ const [completionPending, setCompletionPending] = (0, import_react4.useState)(false);
814
995
  const [{ isPending, isResolved, isRejected }] = (0, import_react_paypal_js3.usePayPalScriptReducer)();
996
+ const resetSubmitState = import_react4.default.useCallback(() => {
997
+ hideProcessingOverlay();
998
+ creatingOrderRef.current = false;
999
+ submittingRef.current = false;
1000
+ setSubmitting(false);
1001
+ }, []);
1002
+ import_react4.default.useEffect(() => {
1003
+ const onPageShow = (e) => {
1004
+ if (e.persisted) {
1005
+ creatingOrderRef.current = false;
1006
+ submittingRef.current = false;
1007
+ setSubmitting(false);
1008
+ }
1009
+ };
1010
+ window.addEventListener("pageshow", onPageShow);
1011
+ return () => window.removeEventListener("pageshow", onPageShow);
1012
+ }, []);
1013
+ import_react4.default.useEffect(() => {
1014
+ if (capturedRef.current || !wasCartCaptured(cartId)) return;
1015
+ capturedRef.current = {};
1016
+ setCompletionPending(true);
1017
+ setError(
1018
+ "Your payment was already received for this cart. Please use the button below to finish placing your order \u2014 do not pay again."
1019
+ );
1020
+ }, [cartId]);
1021
+ const finalizeCapturedPayment = import_react4.default.useCallback(async () => {
1022
+ const captured = capturedRef.current;
1023
+ if (!captured) return;
1024
+ setSubmitting(true);
1025
+ showProcessingOverlay();
1026
+ setError(null);
1027
+ try {
1028
+ const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
1029
+ clearCartCaptured(cartId);
1030
+ await onPaid?.({ ...captured, ...completeResult });
1031
+ setCompletionPending(false);
1032
+ } catch (e) {
1033
+ if (isNextRouterError(e)) return;
1034
+ resetSubmitState();
1035
+ setCompletionPending(true);
1036
+ const msg = "Your payment was received, but finalizing your order failed. Please use the Retry button below \u2014 do not pay again.";
1037
+ setError(msg);
1038
+ onError?.(msg);
1039
+ }
1040
+ }, [baseUrl, cartId, publishableApiKey, onPaid, onError, resetSubmitState]);
815
1041
  if (!config.currency_supported) return null;
816
1042
  if (isRejected) {
817
1043
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -1000,6 +1226,10 @@ function PayPalAdvancedCard(props) {
1000
1226
  style: cardStyle,
1001
1227
  createOrder: async () => {
1002
1228
  if (creatingOrderRef.current) throw new Error("Payment already processing");
1229
+ if (capturedRef.current) {
1230
+ void finalizeCapturedPayment();
1231
+ throw new Error("Your payment was already received \u2014 finalizing your order.");
1232
+ }
1003
1233
  creatingOrderRef.current = true;
1004
1234
  try {
1005
1235
  setError(null);
@@ -1015,19 +1245,19 @@ function PayPalAdvancedCard(props) {
1015
1245
  setError(null);
1016
1246
  showProcessingOverlay();
1017
1247
  const orderId = String(data?.orderID || "");
1248
+ if (!orderId) throw new Error("PayPal order ID is missing from approval response");
1018
1249
  const result = await api.captureOrder(cartId, orderId);
1019
- const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
1020
- await onPaid?.({ ...result, ...completeResult });
1250
+ capturedRef.current = result || {};
1251
+ markCartCaptured(cartId);
1021
1252
  } catch (e) {
1022
1253
  if (isNextRouterError(e)) return;
1023
- hideProcessingOverlay();
1024
- creatingOrderRef.current = false;
1025
- submittingRef.current = false;
1026
- setSubmitting(false);
1254
+ resetSubmitState();
1027
1255
  const msg = e instanceof Error ? e.message : "Card payment failed";
1028
1256
  setError(msg);
1029
1257
  onError?.(msg);
1258
+ return;
1030
1259
  }
1260
+ await finalizeCapturedPayment();
1031
1261
  },
1032
1262
  onCancel: () => {
1033
1263
  hideProcessingOverlay();
@@ -1134,7 +1364,29 @@ function PayPalAdvancedCard(props) {
1134
1364
  ]
1135
1365
  }
1136
1366
  ),
1137
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1367
+ completionPending ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1368
+ "button",
1369
+ {
1370
+ type: "button",
1371
+ onClick: () => void finalizeCapturedPayment(),
1372
+ disabled: submitting,
1373
+ "aria-busy": submitting,
1374
+ style: {
1375
+ width: "100%",
1376
+ height: 48,
1377
+ padding: "0 20px",
1378
+ borderRadius: 8,
1379
+ border: "none",
1380
+ background: "#0070ba",
1381
+ color: "#ffffff",
1382
+ fontSize: 15,
1383
+ fontWeight: 600,
1384
+ cursor: submitting ? "not-allowed" : "pointer",
1385
+ opacity: submitting ? 0.7 : 1
1386
+ },
1387
+ children: "Retry \u2014 finish placing my order"
1388
+ }
1389
+ ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1138
1390
  SubmitButton,
1139
1391
  {
1140
1392
  disabled: submitting,
@@ -1145,6 +1397,11 @@ function PayPalAdvancedCard(props) {
1145
1397
  setError(null);
1146
1398
  setSubmitting(true);
1147
1399
  showProcessingOverlay();
1400
+ },
1401
+ onSubmitError: (msg) => {
1402
+ resetSubmitState();
1403
+ setError(msg);
1404
+ onError?.(msg);
1148
1405
  }
1149
1406
  }
1150
1407
  ),
@@ -1180,9 +1437,23 @@ function PayPalAdvancedCard(props) {
1180
1437
 
1181
1438
  // src/adapters/MedusaNextPayPalAdapter.tsx
1182
1439
  var import_react5 = require("react");
1440
+
1441
+ // src/constants.ts
1442
+ var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
1443
+ var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
1444
+ var PAYPAL_PROVIDER_IDS = [
1445
+ PAYPAL_WALLET_PROVIDER_ID,
1446
+ PAYPAL_CARD_PROVIDER_ID
1447
+ ];
1448
+ function isPayPalProviderId(providerId) {
1449
+ if (!providerId) return false;
1450
+ return PAYPAL_PROVIDER_IDS.includes(
1451
+ providerId
1452
+ );
1453
+ }
1454
+
1455
+ // src/adapters/MedusaNextPayPalAdapter.tsx
1183
1456
  var import_jsx_runtime5 = require("react/jsx-runtime");
1184
- var DEFAULT_PAYPAL_PROVIDER_ID = "pp_paypal_paypal";
1185
- var DEFAULT_PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
1186
1457
  var SPIN_STYLE3 = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
1187
1458
  function PayPalLoadingCard() {
1188
1459
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -1240,6 +1511,26 @@ function PayPalErrorCard({ message }) {
1240
1511
  }
1241
1512
  );
1242
1513
  }
1514
+ function PayPalUnavailableCard({ label }) {
1515
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1516
+ "div",
1517
+ {
1518
+ role: "status",
1519
+ style: {
1520
+ padding: "12px 16px",
1521
+ background: "#f9fafb",
1522
+ border: "1px solid #e5e7eb",
1523
+ borderRadius: 10,
1524
+ fontSize: 13,
1525
+ color: "#6b7280"
1526
+ },
1527
+ children: [
1528
+ label,
1529
+ " is currently unavailable. Please choose a different payment method."
1530
+ ]
1531
+ }
1532
+ );
1533
+ }
1243
1534
  function MedusaNextPayPalAdapter(props) {
1244
1535
  const {
1245
1536
  cartId,
@@ -1251,8 +1542,8 @@ function MedusaNextPayPalAdapter(props) {
1251
1542
  onError,
1252
1543
  onPaid
1253
1544
  } = props;
1254
- const paypalProviderId = providerIds?.paypal || DEFAULT_PAYPAL_PROVIDER_ID;
1255
- const paypalCardProviderId = providerIds?.paypalCard || DEFAULT_PAYPAL_CARD_PROVIDER_ID;
1545
+ const paypalProviderId = providerIds?.paypal || PAYPAL_WALLET_PROVIDER_ID;
1546
+ const paypalCardProviderId = providerIds?.paypalCard || PAYPAL_CARD_PROVIDER_ID;
1256
1547
  const shouldRender = selectedProviderId === paypalProviderId || selectedProviderId === paypalCardProviderId;
1257
1548
  const { config, loading, error } = usePayPalConfig({
1258
1549
  baseUrl,
@@ -1272,8 +1563,12 @@ function MedusaNextPayPalAdapter(props) {
1272
1563
  if (error) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalErrorCard, { message: error });
1273
1564
  if (!config) return null;
1274
1565
  const isCardProvider = selectedProviderId === paypalCardProviderId;
1275
- if (config.paypal_enabled === false && !isCardProvider) return null;
1276
- if (isCardProvider && config.card_enabled === false) return null;
1566
+ if (config.paypal_enabled === false && !isCardProvider) {
1567
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalUnavailableCard, { label: config.paypal_title || "PayPal" });
1568
+ }
1569
+ if (isCardProvider && config.card_enabled === false) {
1570
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalUnavailableCard, { label: config.card_title || "Card payment" });
1571
+ }
1277
1572
  const disableFunding = Array.isArray(config.disable_buttons) ? config.disable_buttons.join(",") : void 0;
1278
1573
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "grid", gap: 12 }, children: [
1279
1574
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalCurrencyNotice, { config }),
@@ -1310,18 +1605,7 @@ function MedusaNextPayPalAdapter(props) {
1310
1605
  }
1311
1606
 
1312
1607
  // src/components/PayPalPaymentSection.tsx
1313
- var import_react6 = require("react");
1314
1608
  var import_jsx_runtime6 = require("react/jsx-runtime");
1315
- var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
1316
- var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
1317
- var PAYPAL_PROVIDER_IDS = [
1318
- PAYPAL_WALLET_PROVIDER_ID,
1319
- PAYPAL_CARD_PROVIDER_ID
1320
- ];
1321
- function isPayPalProviderId(id) {
1322
- if (!id) return false;
1323
- return PAYPAL_PROVIDER_IDS.includes(id);
1324
- }
1325
1609
  var SPIN_STYLE4 = `@keyframes _pp_section_spin { to { transform: rotate(360deg) } }`;
1326
1610
  function SessionInitCard() {
1327
1611
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -1360,62 +1644,6 @@ function SessionInitCard() {
1360
1644
  }
1361
1645
  );
1362
1646
  }
1363
- function ConfigLoadingCard() {
1364
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1365
- "div",
1366
- {
1367
- role: "status",
1368
- "aria-label": "Connecting to PayPal",
1369
- style: {
1370
- display: "flex",
1371
- alignItems: "center",
1372
- gap: 12,
1373
- padding: "14px 16px",
1374
- background: "#f9fafb",
1375
- border: "1px solid #e5e7eb",
1376
- borderRadius: 10
1377
- },
1378
- children: [
1379
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: SPIN_STYLE4 }),
1380
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1381
- "div",
1382
- {
1383
- style: {
1384
- width: 22,
1385
- height: 22,
1386
- borderRadius: "50%",
1387
- border: "2.5px solid #e5e7eb",
1388
- borderTopColor: "#0070ba",
1389
- animation: "_pp_section_spin .7s linear infinite",
1390
- flexShrink: 0
1391
- }
1392
- }
1393
- ),
1394
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
1395
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: "Connecting to PayPal\u2026" }),
1396
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 2 }, children: "Setting up secure payment" })
1397
- ] })
1398
- ]
1399
- }
1400
- );
1401
- }
1402
- function ErrorCard({ message }) {
1403
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1404
- "div",
1405
- {
1406
- role: "alert",
1407
- style: {
1408
- padding: "12px 16px",
1409
- background: "#fef2f2",
1410
- border: "1px solid #fecaca",
1411
- borderRadius: 10,
1412
- fontSize: 13,
1413
- color: "#b91c1c"
1414
- },
1415
- children: message
1416
- }
1417
- );
1418
- }
1419
1647
  function PayPalPaymentSection({
1420
1648
  cartId,
1421
1649
  selectedProviderId,
@@ -1426,67 +1654,24 @@ function PayPalPaymentSection({
1426
1654
  onError,
1427
1655
  onPaid
1428
1656
  }) {
1429
- const shouldRender = isPayPalProviderId(selectedProviderId);
1430
- const { config, loading, error } = usePayPalConfig({
1431
- baseUrl,
1432
- publishableApiKey,
1433
- cartId,
1434
- enabled: shouldRender
1435
- });
1436
- const handlePaid = (0, import_react6.useCallback)(
1437
- async (captureResult) => {
1438
- onPaid?.(captureResult);
1439
- await onSuccess?.(cartId);
1440
- },
1441
- [cartId, onPaid, onSuccess]
1442
- );
1443
- if (!shouldRender) return null;
1657
+ if (!isPayPalProviderId(selectedProviderId)) return null;
1444
1658
  if (sessionLoading) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SessionInitCard, {});
1445
- if (loading) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ConfigLoadingCard, {});
1446
- if (error) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorCard, { message: error });
1447
- if (!config) return null;
1448
- if (config.paypal_enabled === false && selectedProviderId === PAYPAL_WALLET_PROVIDER_ID) {
1449
- return null;
1450
- }
1451
- const isCardProvider = selectedProviderId === PAYPAL_CARD_PROVIDER_ID;
1452
- if (isCardProvider && config.card_enabled === false) return null;
1453
- const disableFunding = Array.isArray(config.disable_buttons) ? config.disable_buttons.join(",") : void 0;
1454
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "grid", gap: 12 }, children: [
1455
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PayPalCurrencyNotice, { config }),
1456
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1457
- PayPalProvider,
1458
- {
1459
- config,
1460
- intent: config.intent === "authorize" ? "authorize" : "capture",
1461
- disableFunding,
1462
- children: isCardProvider ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1463
- PayPalAdvancedCard,
1464
- {
1465
- baseUrl,
1466
- publishableApiKey,
1467
- cartId,
1468
- config,
1469
- onPaid: handlePaid,
1470
- onError
1471
- }
1472
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1473
- PayPalSmartButtons,
1474
- {
1475
- baseUrl,
1476
- publishableApiKey,
1477
- cartId,
1478
- config,
1479
- onPaid: handlePaid,
1480
- onError
1481
- }
1482
- )
1483
- }
1484
- )
1485
- ] }, selectedProviderId);
1659
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1660
+ MedusaNextPayPalAdapter,
1661
+ {
1662
+ cartId,
1663
+ selectedProviderId,
1664
+ baseUrl,
1665
+ publishableApiKey,
1666
+ onSuccess,
1667
+ onError,
1668
+ onPaid
1669
+ }
1670
+ );
1486
1671
  }
1487
1672
 
1488
1673
  // src/hooks/usePayPalPaymentMethods.ts
1489
- var import_react7 = require("react");
1674
+ var import_react6 = require("react");
1490
1675
  var MAX_CACHE_ENTRIES2 = 50;
1491
1676
  var _cache2 = /* @__PURE__ */ new Map();
1492
1677
  var CACHE_TTL2 = 5 * 60 * 1e3;
@@ -1505,7 +1690,8 @@ var DEFAULT_RESULT = {
1505
1690
  paypalTitle: "PayPal",
1506
1691
  cardEnabled: true,
1507
1692
  cardTitle: "Credit or Debit Card",
1508
- loading: false
1693
+ loading: false,
1694
+ error: null
1509
1695
  };
1510
1696
  function usePayPalPaymentMethods({
1511
1697
  baseUrl,
@@ -1513,16 +1699,16 @@ function usePayPalPaymentMethods({
1513
1699
  cartId,
1514
1700
  enabled = true
1515
1701
  }) {
1516
- const api = (0, import_react7.useMemo)(
1702
+ const api = (0, import_react6.useMemo)(
1517
1703
  () => createPayPalStoreApi({ baseUrl, publishableApiKey }),
1518
1704
  [baseUrl, publishableApiKey]
1519
1705
  );
1520
1706
  const key = cacheKey2(baseUrl, cartId);
1521
1707
  const hit = _cache2.get(key);
1522
1708
  const seed = hit && Date.now() - hit.at < CACHE_TTL2 ? hit.result : null;
1523
- const [result, setResult] = (0, import_react7.useState)(seed ?? { ...DEFAULT_RESULT, loading: enabled });
1524
- const fetchIdRef = (0, import_react7.useRef)(0);
1525
- (0, import_react7.useEffect)(() => {
1709
+ const [result, setResult] = (0, import_react6.useState)(seed ?? { ...DEFAULT_RESULT, loading: enabled });
1710
+ const fetchIdRef = (0, import_react6.useRef)(0);
1711
+ (0, import_react6.useEffect)(() => {
1526
1712
  if (!enabled) {
1527
1713
  setResult((prev) => ({ ...prev, loading: false }));
1528
1714
  return;
@@ -1546,7 +1732,8 @@ function usePayPalPaymentMethods({
1546
1732
  paypalTitle: typeof cfg.paypal_title === "string" && cfg.paypal_title ? cfg.paypal_title : "PayPal",
1547
1733
  cardEnabled: cfg.card_enabled !== false,
1548
1734
  cardTitle: typeof cfg.card_title === "string" && cfg.card_title ? cfg.card_title : "Credit or Debit Card",
1549
- loading: false
1735
+ loading: false,
1736
+ error: null
1550
1737
  };
1551
1738
  cacheSet2(k, { result: next, at: Date.now() });
1552
1739
  setResult(next);
@@ -1559,12 +1746,17 @@ function usePayPalPaymentMethods({
1559
1746
  ...DEFAULT_RESULT,
1560
1747
  paypalEnabled: false,
1561
1748
  cardEnabled: false,
1562
- loading: false
1749
+ loading: false,
1750
+ error: msg || "PayPal is disabled"
1563
1751
  };
1564
1752
  setResult(disabled);
1565
1753
  return;
1566
1754
  }
1567
- setResult({ ...DEFAULT_RESULT, loading: false });
1755
+ setResult({
1756
+ ...DEFAULT_RESULT,
1757
+ loading: false,
1758
+ error: msg || "Failed to load PayPal payment methods"
1759
+ });
1568
1760
  }
1569
1761
  })();
1570
1762
  return () => {
@@ -1584,12 +1776,16 @@ function usePayPalPaymentMethods({
1584
1776
  PayPalPaymentSection,
1585
1777
  PayPalProvider,
1586
1778
  PayPalSmartButtons,
1779
+ clearCartCaptured,
1587
1780
  createPayPalStoreApi,
1781
+ generateIdempotencyKey,
1588
1782
  hideProcessingOverlay,
1589
1783
  isPayPalProviderId,
1784
+ markCartCaptured,
1590
1785
  markPaymentComplete,
1591
1786
  showProcessingOverlay,
1592
1787
  usePayPalConfig,
1593
- usePayPalPaymentMethods
1788
+ usePayPalPaymentMethods,
1789
+ wasCartCaptured
1594
1790
  });
1595
1791
  //# sourceMappingURL=index.cjs.map