@easypayment/medusa-paypal-ui 1.0.50 → 1.0.52

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
@@ -30,9 +30,10 @@ __export(index_exports, {
30
30
  PayPalProvider: () => PayPalProvider,
31
31
  PayPalSmartButtons: () => PayPalSmartButtons,
32
32
  createPayPalStoreApi: () => createPayPalStoreApi,
33
- isNextRedirectError: () => isNextRedirectError,
33
+ hideProcessingOverlay: () => hideProcessingOverlay,
34
34
  isPayPalProviderId: () => isPayPalProviderId,
35
35
  markPaymentComplete: () => markPaymentComplete,
36
+ showProcessingOverlay: () => showProcessingOverlay,
36
37
  usePayPalConfig: () => usePayPalConfig,
37
38
  usePayPalPaymentMethods: () => usePayPalPaymentMethods
38
39
  });
@@ -51,7 +52,6 @@ function toHeaderRecord(headers) {
51
52
  }
52
53
  return { ...headers };
53
54
  }
54
- var DEFAULT_TIMEOUT_MS = 3e4;
55
55
  function createHttpClient(opts) {
56
56
  const base = opts.baseUrl.replace(/\/+$/, "");
57
57
  async function request(path, init) {
@@ -63,33 +63,25 @@ function createHttpClient(opts) {
63
63
  if (opts.publishableApiKey) {
64
64
  headers["x-publishable-api-key"] = opts.publishableApiKey;
65
65
  }
66
+ const timeoutMs = 3e4;
67
+ let timeoutId;
66
68
  const controller = new AbortController();
67
- let timedOut = false;
68
- const timer = setTimeout(() => {
69
- timedOut = true;
70
- controller.abort();
71
- }, DEFAULT_TIMEOUT_MS);
72
- const callerSignal = init?.signal;
73
- if (callerSignal) {
74
- if (callerSignal.aborted) {
75
- controller.abort();
76
- } else {
77
- callerSignal.addEventListener("abort", () => controller.abort(), { once: true });
78
- }
69
+ if (!init?.signal) {
70
+ timeoutId = setTimeout(() => controller.abort(), timeoutMs);
79
71
  }
72
+ const effectiveSignal = init?.signal || controller.signal;
80
73
  let res;
81
- let text;
82
74
  try {
83
- res = await fetch(url, { ...init, headers, credentials: "include", signal: controller.signal });
84
- text = await res.text().catch(() => "");
85
- } catch (e) {
86
- if (timedOut) {
87
- throw new Error("[PayPal] Request timed out. Please check your connection and try again.");
75
+ res = await fetch(url, { ...init, headers, credentials: "include", signal: effectiveSignal });
76
+ } catch (err) {
77
+ if (err instanceof Error && err.name === "AbortError" && !init?.signal) {
78
+ throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1e3}s`);
88
79
  }
89
- throw e;
80
+ throw err;
90
81
  } finally {
91
- clearTimeout(timer);
82
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
92
83
  }
84
+ const text = await res.text().catch(() => "");
93
85
  if (!res.ok) {
94
86
  if (res.status === 401) {
95
87
  throw new Error(
@@ -101,21 +93,9 @@ function createHttpClient(opts) {
101
93
  "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings."
102
94
  );
103
95
  }
104
- let parsed = null;
105
- try {
106
- parsed = text ? JSON.parse(text) : null;
107
- } catch {
108
- parsed = null;
109
- }
110
- const backendMessage = typeof parsed?.message === "string" ? parsed.message : "";
111
- const requestId = typeof parsed?.request_id === "string" ? parsed.request_id : "";
112
- if (res.status >= 500) {
113
- console.error(`[PayPal] ${path} failed (${res.status})`, text.slice(0, 1e3));
114
- throw new Error(
115
- `Payment service error${requestId ? ` (ref ${requestId})` : ""}. Please try again or contact support.`
116
- );
117
- }
118
- throw new Error(backendMessage || `Request failed (${res.status})`);
96
+ throw new Error(
97
+ text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`
98
+ );
119
99
  }
120
100
  if (!text) {
121
101
  throw new Error(`[PayPal] Empty response body from ${path} (${res.status})`);
@@ -136,34 +116,13 @@ function createHttpClient(opts) {
136
116
  }
137
117
 
138
118
  // src/client/paypal.ts
139
- function isNextRedirectError(e) {
140
- if (!e || typeof e !== "object") {
141
- return false;
142
- }
143
- const digest = e.digest;
144
- return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest === "NEXT_NOT_FOUND");
145
- }
146
119
  async function markPaymentComplete(baseUrl, cartId, publishableApiKey) {
147
- try {
148
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/store/paypal-complete`, {
149
- method: "POST",
150
- headers: {
151
- "Content-Type": "application/json",
152
- ...publishableApiKey ? { "x-publishable-api-key": publishableApiKey } : {}
153
- },
154
- body: JSON.stringify({ cart_id: cartId }),
155
- credentials: "include"
156
- });
157
- const data = await resp.json().catch(() => ({}));
158
- if (!resp.ok) {
159
- console.warn("[PayPal] paypal-complete returned", resp.status, data);
160
- return { ok: false, ...data || {} };
161
- }
162
- return { ok: true, ...data || {} };
163
- } catch (e) {
164
- console.warn("[PayPal] paypal-complete call failed:", e);
165
- return { ok: false };
166
- }
120
+ const http = createHttpClient({ baseUrl, publishableApiKey });
121
+ return http.request(`/store/paypal-complete`, {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({ cart_id: cartId })
125
+ });
167
126
  }
168
127
  function createPayPalStoreApi(opts) {
169
128
  const http = createHttpClient(opts);
@@ -194,8 +153,16 @@ function createPayPalStoreApi(opts) {
194
153
 
195
154
  // src/hooks/usePayPalConfig.ts
196
155
  var import_react = require("react");
156
+ var MAX_CACHE_ENTRIES = 50;
197
157
  var _cache = /* @__PURE__ */ new Map();
198
158
  var CACHE_TTL = 5 * 60 * 1e3;
159
+ function cacheSet(key, value) {
160
+ if (_cache.size >= MAX_CACHE_ENTRIES) {
161
+ const oldest = _cache.keys().next().value;
162
+ if (oldest !== void 0) _cache.delete(oldest);
163
+ }
164
+ _cache.set(key, value);
165
+ }
199
166
  function cacheKey(baseUrl, cartId) {
200
167
  return `${baseUrl}::${cartId ?? ""}`;
201
168
  }
@@ -239,12 +206,12 @@ function usePayPalConfig({
239
206
  try {
240
207
  const cfg = await api.getConfig(cartId, controller.signal);
241
208
  if (!mounted || id !== fetchIdRef.current) return;
242
- _cache.set(k, { config: cfg, at: Date.now() });
209
+ cacheSet(k, { config: cfg, at: Date.now() });
243
210
  setConfig(cfg);
244
211
  } catch (e) {
245
- if (e?.name === "AbortError") return;
212
+ if (e instanceof Error && e.name === "AbortError") return;
246
213
  if (!mounted || id !== fetchIdRef.current) return;
247
- setError(e?.message || "Failed to load PayPal config");
214
+ setError(e instanceof Error ? e.message : "Failed to load PayPal config");
248
215
  } finally {
249
216
  if (mounted && id === fetchIdRef.current) setLoading(false);
250
217
  }
@@ -274,7 +241,7 @@ function PayPalProvider(props) {
274
241
  "disable-funding": disableFunding,
275
242
  "data-partner-attribution-id": BN_CODE
276
243
  };
277
- }, [config, intent, disableFunding]);
244
+ }, [config.client_id, config.currency, config.client_token, intent, disableFunding]);
278
245
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
279
246
  import_react_paypal_js.PayPalScriptProvider,
280
247
  {
@@ -289,7 +256,7 @@ function PayPalProvider(props) {
289
256
  var import_jsx_runtime2 = require("react/jsx-runtime");
290
257
  function PayPalCurrencyNotice({ config }) {
291
258
  if (config.currency_supported) return null;
292
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: 12, border: "1px solid #ddd", borderRadius: 10 }, children: [
259
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { role: "alert", style: { padding: 12, border: "1px solid #ddd", borderRadius: 10 }, children: [
293
260
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "PayPal currency issue" }),
294
261
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: { margin: 0, paddingLeft: 18 }, children: (config.currency_errors || []).map((e) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { children: e }, e)) })
295
262
  ] });
@@ -298,8 +265,71 @@ function PayPalCurrencyNotice({ config }) {
298
265
  // src/components/PayPalSmartButtons.tsx
299
266
  var import_react3 = require("react");
300
267
  var import_react_paypal_js2 = require("@paypal/react-paypal-js");
268
+
269
+ // src/utils/next-errors.ts
270
+ function isNextRouterError(e) {
271
+ if (typeof e !== "object" || e === null || !("digest" in e)) return false;
272
+ const digest = e.digest;
273
+ return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND"));
274
+ }
275
+
276
+ // src/utils/processing-overlay.ts
277
+ var OVERLAY_ID = "__pp_processing_overlay";
278
+ function showProcessingOverlay() {
279
+ if (typeof document === "undefined") return;
280
+ if (document.getElementById(OVERLAY_ID)) return;
281
+ const overlay = document.createElement("div");
282
+ overlay.id = OVERLAY_ID;
283
+ overlay.setAttribute("role", "status");
284
+ overlay.setAttribute("aria-label", "Processing payment");
285
+ overlay.innerHTML = `
286
+ <style>
287
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
288
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
289
+ </style>
290
+ <div style="position:relative;width:56px;height:56px">
291
+ <div style="position:absolute;inset:0;border-radius:50%;border:3px solid #e5e7eb"></div>
292
+ <div style="position:absolute;inset:0;border-radius:50%;border:3px solid transparent;border-top-color:#0070ba;animation:_pp_spin .8s linear infinite"></div>
293
+ <svg viewBox="0 0 24 24" fill="none" stroke="#0070ba" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
294
+ style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px">
295
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
296
+ </svg>
297
+ </div>
298
+ <div style="text-align:center">
299
+ <div style="font-size:18px;font-weight:600;color:#111827;letter-spacing:-0.01em">Processing your payment</div>
300
+ <div style="font-size:14px;color:#6b7280;margin-top:6px;line-height:1.5">
301
+ This may take a few moments. Please do not close<br>or refresh this page.
302
+ </div>
303
+ </div>
304
+ <div style="width:200px;height:3px;background:#e5e7eb;border-radius:3px;overflow:hidden;margin-top:4px">
305
+ <div style="width:40%;height:100%;background:linear-gradient(90deg,#0070ba,#003087);border-radius:3px;animation:_pp_progress 1.5s ease-in-out infinite"></div>
306
+ </div>
307
+ `;
308
+ Object.assign(overlay.style, {
309
+ position: "fixed",
310
+ inset: "0",
311
+ zIndex: "99999",
312
+ background: "rgba(255,255,255,0.96)",
313
+ display: "flex",
314
+ flexDirection: "column",
315
+ alignItems: "center",
316
+ justifyContent: "center",
317
+ gap: "20px",
318
+ fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif"
319
+ });
320
+ document.body.appendChild(overlay);
321
+ }
322
+ function hideProcessingOverlay() {
323
+ if (typeof document === "undefined") return;
324
+ document.getElementById(OVERLAY_ID)?.remove();
325
+ }
326
+
327
+ // src/components/PayPalSmartButtons.tsx
301
328
  var import_jsx_runtime3 = require("react/jsx-runtime");
302
- var SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
329
+ var SPIN_STYLE = `
330
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
331
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
332
+ `;
303
333
  var BUTTON_WIDTH_MAP = {
304
334
  small: "300px",
305
335
  medium: "400px",
@@ -314,6 +344,12 @@ function PayPalSmartButtons(props) {
314
344
  );
315
345
  const [error, setError] = (0, import_react3.useState)(null);
316
346
  const [processing, setProcessing] = (0, import_react3.useState)(false);
347
+ const [buttonsReady, setButtonsReady] = (0, import_react3.useState)(false);
348
+ const [{ isPending, isResolved, isRejected }] = (0, import_react_paypal_js2.usePayPalScriptReducer)();
349
+ const handleInit = (0, import_react3.useCallback)(() => {
350
+ setButtonsReady(true);
351
+ }, []);
352
+ const showSpinner = isPending || isResolved && !buttonsReady;
317
353
  if (!config.currency_supported) return null;
318
354
  const containerWidth = BUTTON_WIDTH_MAP[config.button_width ?? "responsive"] ?? "100%";
319
355
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: containerWidth, position: "relative" }, children: [
@@ -321,41 +357,160 @@ function PayPalSmartButtons(props) {
321
357
  processing && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
322
358
  "div",
323
359
  {
360
+ role: "status",
361
+ "aria-label": "Processing payment",
324
362
  style: {
325
- position: "absolute",
363
+ position: "fixed",
326
364
  inset: 0,
327
- zIndex: 10,
328
- background: "rgba(255,255,255,0.90)",
329
- borderRadius: 8,
365
+ zIndex: 9999,
366
+ background: "rgba(255,255,255,0.96)",
330
367
  display: "flex",
331
368
  flexDirection: "column",
332
369
  alignItems: "center",
333
370
  justifyContent: "center",
371
+ gap: 20
372
+ },
373
+ children: [
374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { position: "relative", width: 56, height: 56 }, children: [
375
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
376
+ "div",
377
+ {
378
+ style: {
379
+ position: "absolute",
380
+ inset: 0,
381
+ borderRadius: "50%",
382
+ border: "3px solid #e5e7eb"
383
+ }
384
+ }
385
+ ),
386
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
387
+ "div",
388
+ {
389
+ style: {
390
+ position: "absolute",
391
+ inset: 0,
392
+ borderRadius: "50%",
393
+ border: "3px solid transparent",
394
+ borderTopColor: "#0070ba",
395
+ animation: "_pp_spin .8s linear infinite"
396
+ }
397
+ }
398
+ ),
399
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
400
+ "svg",
401
+ {
402
+ viewBox: "0 0 24 24",
403
+ fill: "none",
404
+ stroke: "#0070ba",
405
+ strokeWidth: "1.8",
406
+ strokeLinecap: "round",
407
+ strokeLinejoin: "round",
408
+ style: {
409
+ position: "absolute",
410
+ top: "50%",
411
+ left: "50%",
412
+ transform: "translate(-50%, -50%)",
413
+ width: 24,
414
+ height: 24
415
+ },
416
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" })
417
+ }
418
+ )
419
+ ] }),
420
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { textAlign: "center" }, children: [
421
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }, children: "Processing your payment" }),
422
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }, children: [
423
+ "This may take a few moments. Please do not close",
424
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("br", {}),
425
+ "or refresh this page."
426
+ ] })
427
+ ] }),
428
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
429
+ "div",
430
+ {
431
+ style: {
432
+ width: 200,
433
+ height: 3,
434
+ background: "#e5e7eb",
435
+ borderRadius: 3,
436
+ overflow: "hidden",
437
+ marginTop: 4
438
+ },
439
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
440
+ "div",
441
+ {
442
+ style: {
443
+ width: "40%",
444
+ height: "100%",
445
+ background: "linear-gradient(90deg, #0070ba, #003087)",
446
+ borderRadius: 3,
447
+ animation: "_pp_progress 1.5s ease-in-out infinite"
448
+ }
449
+ }
450
+ )
451
+ }
452
+ )
453
+ ]
454
+ }
455
+ ),
456
+ showSpinner && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
457
+ "div",
458
+ {
459
+ role: "status",
460
+ "aria-label": "Loading PayPal",
461
+ style: {
462
+ display: "flex",
463
+ alignItems: "center",
464
+ justifyContent: "center",
334
465
  gap: 10,
335
- minHeight: 60
466
+ padding: "18px 16px",
467
+ background: "#f9fafb",
468
+ border: "1px solid #e5e7eb",
469
+ borderRadius: 8,
470
+ minHeight: 55
336
471
  },
337
472
  children: [
338
473
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
339
474
  "div",
340
475
  {
341
476
  style: {
342
- width: 28,
343
- height: 28,
477
+ width: 22,
478
+ height: 22,
344
479
  borderRadius: "50%",
345
480
  border: "2.5px solid #e5e7eb",
346
481
  borderTopColor: "#0070ba",
347
- animation: "_pp_spin .7s linear infinite"
482
+ animation: "_pp_spin .7s linear infinite",
483
+ flexShrink: 0
348
484
  }
349
485
  }
350
486
  ),
351
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { textAlign: "center" }, children: [
352
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 13, color: "#374151", fontWeight: 500 }, children: "Processing your payment\u2026" }),
353
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 4 }, children: "Please do not close or refresh this page" })
354
- ] })
487
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 13, fontWeight: 500, color: "#6b7280" }, children: "Loading PayPal\u2026" })
488
+ ]
489
+ }
490
+ ),
491
+ isRejected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
492
+ "div",
493
+ {
494
+ role: "alert",
495
+ style: {
496
+ display: "flex",
497
+ alignItems: "flex-start",
498
+ gap: 8,
499
+ padding: "10px 14px",
500
+ background: "#fef2f2",
501
+ border: "1px solid #fecaca",
502
+ borderRadius: 8,
503
+ fontSize: 13,
504
+ color: "#b91c1c",
505
+ lineHeight: 1.5
506
+ },
507
+ children: [
508
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
509
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "PayPal failed to load. Please refresh the page or try a different payment method." })
355
510
  ]
356
511
  }
357
512
  ),
358
- config.environment === "sandbox" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
513
+ config.environment === "sandbox" && buttonsReady && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
359
514
  "div",
360
515
  {
361
516
  style: {
@@ -393,9 +548,11 @@ function PayPalSmartButtons(props) {
393
548
  ]
394
549
  }
395
550
  ),
396
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
551
+ 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)(
397
552
  import_react_paypal_js2.PayPalButtons,
398
553
  {
554
+ forceReRender: [config.currency, config.intent, cartId],
555
+ onInit: handleInit,
399
556
  style: {
400
557
  layout: "vertical",
401
558
  color: config.button_color,
@@ -404,40 +561,53 @@ function PayPalSmartButtons(props) {
404
561
  height: config.button_height
405
562
  },
406
563
  createOrder: async () => {
564
+ if (processing) throw new Error("Payment already processing");
407
565
  setError(null);
408
- const r = await api.createOrder(cartId);
409
- return r.id;
566
+ setProcessing(true);
567
+ try {
568
+ const r = await api.createOrder(cartId);
569
+ return r.id;
570
+ } catch (e) {
571
+ setProcessing(false);
572
+ throw e;
573
+ }
410
574
  },
411
575
  onApprove: async (data) => {
412
576
  try {
413
577
  setProcessing(true);
578
+ showProcessingOverlay();
414
579
  setError(null);
415
580
  const orderId = String(data?.orderID || "");
581
+ if (!orderId) throw new Error("PayPal order ID is missing from approval response");
416
582
  const result = await api.captureOrder(cartId, orderId);
417
583
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
418
- onPaid?.({ ...result, ...completeResult });
584
+ await onPaid?.({ ...result, ...completeResult });
419
585
  } catch (e) {
586
+ if (isNextRouterError(e)) throw e;
587
+ hideProcessingOverlay();
588
+ setProcessing(false);
420
589
  const msg = e instanceof Error ? e.message : "Payment capture failed";
421
590
  setError(msg);
422
591
  onError?.(msg);
423
- } finally {
424
- setProcessing(false);
425
592
  }
426
593
  },
427
594
  onCancel: () => {
595
+ hideProcessingOverlay();
428
596
  setProcessing(false);
429
597
  },
430
598
  onError: (err) => {
599
+ hideProcessingOverlay();
431
600
  setProcessing(false);
432
601
  const msg = err instanceof Error ? err.message : err?.message || "PayPal error";
433
602
  setError(msg);
434
603
  onError?.(msg);
435
604
  }
436
605
  }
437
- ),
606
+ ) }),
438
607
  error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
439
608
  "div",
440
609
  {
610
+ role: "alert",
441
611
  style: {
442
612
  marginTop: 10,
443
613
  display: "flex",
@@ -464,7 +634,10 @@ function PayPalSmartButtons(props) {
464
634
  var import_react4 = require("react");
465
635
  var import_react_paypal_js3 = require("@paypal/react-paypal-js");
466
636
  var import_jsx_runtime4 = require("react/jsx-runtime");
467
- var SPIN_STYLE2 = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
637
+ var SPIN_STYLE2 = `
638
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
639
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
640
+ `;
468
641
  var cardStyle = {
469
642
  input: {
470
643
  "font-size": "15px",
@@ -510,8 +683,7 @@ var labelStyle = {
510
683
  function SubmitButton({
511
684
  disabled,
512
685
  label,
513
- onSubmit,
514
- onSubmitError
686
+ onSubmit
515
687
  }) {
516
688
  const { cardFieldsForm } = (0, import_react_paypal_js3.usePayPalCardFields)();
517
689
  const isDisabled = disabled || !cardFieldsForm;
@@ -520,15 +692,12 @@ function SubmitButton({
520
692
  {
521
693
  type: "button",
522
694
  disabled: isDisabled,
695
+ "aria-busy": disabled,
523
696
  onClick: () => {
524
697
  onSubmit();
525
- const submitted = cardFieldsForm?.submit();
526
- if (submitted && typeof submitted.catch === "function") {
527
- submitted.catch((e) => {
528
- onSubmitError(
529
- e instanceof Error ? e.message : "Card payment failed. Please try again."
530
- );
531
- });
698
+ try {
699
+ cardFieldsForm?.submit();
700
+ } catch {
532
701
  }
533
702
  },
534
703
  style: {
@@ -573,7 +742,33 @@ function PayPalAdvancedCard(props) {
573
742
  );
574
743
  const [error, setError] = (0, import_react4.useState)(null);
575
744
  const [submitting, setSubmitting] = (0, import_react4.useState)(false);
745
+ const submittingRef = (0, import_react4.useRef)(false);
746
+ const [{ isPending, isResolved, isRejected }] = (0, import_react_paypal_js3.usePayPalScriptReducer)();
576
747
  if (!config.currency_supported) return null;
748
+ if (isRejected) {
749
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
750
+ "div",
751
+ {
752
+ role: "alert",
753
+ style: {
754
+ display: "flex",
755
+ alignItems: "flex-start",
756
+ gap: 8,
757
+ padding: "10px 14px",
758
+ background: "#fef2f2",
759
+ border: "1px solid #fecaca",
760
+ borderRadius: 8,
761
+ fontSize: 13,
762
+ color: "#b91c1c",
763
+ lineHeight: 1.5
764
+ },
765
+ children: [
766
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
767
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "PayPal failed to load. Please refresh the page or try a different payment method." })
768
+ ]
769
+ }
770
+ );
771
+ }
577
772
  if (!config.client_token) {
578
773
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
579
774
  "div",
@@ -591,42 +786,143 @@ function PayPalAdvancedCard(props) {
591
786
  );
592
787
  }
593
788
  const isSandbox = config.environment === "sandbox";
789
+ if (isPending) {
790
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
791
+ "div",
792
+ {
793
+ role: "status",
794
+ "aria-label": "Loading PayPal",
795
+ style: {
796
+ display: "flex",
797
+ alignItems: "center",
798
+ justifyContent: "center",
799
+ gap: 10,
800
+ padding: "18px 16px",
801
+ background: "#f9fafb",
802
+ border: "1px solid #e5e7eb",
803
+ borderRadius: 8,
804
+ minHeight: 55
805
+ },
806
+ children: [
807
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: SPIN_STYLE2 }),
808
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
809
+ "div",
810
+ {
811
+ style: {
812
+ width: 22,
813
+ height: 22,
814
+ borderRadius: "50%",
815
+ border: "2.5px solid #e5e7eb",
816
+ borderTopColor: "#0070ba",
817
+ animation: "_pp_spin .7s linear infinite",
818
+ flexShrink: 0
819
+ }
820
+ }
821
+ ),
822
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 13, fontWeight: 500, color: "#6b7280" }, children: "Loading card fields\u2026" })
823
+ ]
824
+ }
825
+ );
826
+ }
827
+ if (!isResolved) return null;
594
828
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { position: "relative" }, children: [
595
829
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: SPIN_STYLE2 }),
596
830
  submitting && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
597
831
  "div",
598
832
  {
833
+ role: "status",
834
+ "aria-label": "Processing payment",
599
835
  style: {
600
- position: "absolute",
836
+ position: "fixed",
601
837
  inset: 0,
602
- zIndex: 20,
603
- background: "rgba(255,255,255,0.92)",
604
- borderRadius: 12,
838
+ zIndex: 9999,
839
+ background: "rgba(255,255,255,0.96)",
605
840
  display: "flex",
606
841
  flexDirection: "column",
607
842
  alignItems: "center",
608
843
  justifyContent: "center",
609
- gap: 14,
610
- minHeight: 180
844
+ gap: 20
611
845
  },
612
846
  children: [
847
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { position: "relative", width: 56, height: 56 }, children: [
848
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
849
+ "div",
850
+ {
851
+ style: {
852
+ position: "absolute",
853
+ inset: 0,
854
+ borderRadius: "50%",
855
+ border: "3px solid #e5e7eb"
856
+ }
857
+ }
858
+ ),
859
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
860
+ "div",
861
+ {
862
+ style: {
863
+ position: "absolute",
864
+ inset: 0,
865
+ borderRadius: "50%",
866
+ border: "3px solid transparent",
867
+ borderTopColor: "#2563eb",
868
+ animation: "_pp_spin .8s linear infinite"
869
+ }
870
+ }
871
+ ),
872
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
873
+ "svg",
874
+ {
875
+ viewBox: "0 0 24 24",
876
+ fill: "none",
877
+ stroke: "#2563eb",
878
+ strokeWidth: "1.8",
879
+ strokeLinecap: "round",
880
+ strokeLinejoin: "round",
881
+ style: {
882
+ position: "absolute",
883
+ top: "50%",
884
+ left: "50%",
885
+ transform: "translate(-50%, -50%)",
886
+ width: 24,
887
+ height: 24
888
+ },
889
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" })
890
+ }
891
+ )
892
+ ] }),
893
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center" }, children: [
894
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }, children: "Processing your payment" }),
895
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }, children: [
896
+ "This may take a few moments. Please do not close",
897
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("br", {}),
898
+ "or refresh this page."
899
+ ] })
900
+ ] }),
613
901
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
614
902
  "div",
615
903
  {
616
904
  style: {
617
- width: 36,
618
- height: 36,
619
- borderRadius: "50%",
620
- border: "3px solid #dbeafe",
621
- borderTopColor: "#2563eb",
622
- animation: "_pp_spin .75s linear infinite"
623
- }
905
+ width: 200,
906
+ height: 3,
907
+ background: "#e5e7eb",
908
+ borderRadius: 3,
909
+ overflow: "hidden",
910
+ marginTop: 4
911
+ },
912
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
913
+ "div",
914
+ {
915
+ style: {
916
+ width: "40%",
917
+ height: "100%",
918
+ background: "linear-gradient(90deg, #2563eb, #1d4ed8)",
919
+ borderRadius: 3,
920
+ animation: "_pp_progress 1.5s ease-in-out infinite"
921
+ }
922
+ }
923
+ )
624
924
  }
625
- ),
626
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center" }, children: [
627
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 15, fontWeight: 600, color: "#111827" }, children: "Processing your payment\u2026" }),
628
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 13, color: "#6b7280", marginTop: 4 }, children: "Please do not close or refresh this page" })
629
- ] })
925
+ )
630
926
  ]
631
927
  }
632
928
  ),
@@ -635,6 +931,7 @@ function PayPalAdvancedCard(props) {
635
931
  {
636
932
  style: cardStyle,
637
933
  createOrder: async () => {
934
+ if (submittingRef.current) throw new Error("Payment already processing");
638
935
  setError(null);
639
936
  const r = await api.createOrder(cartId, true);
640
937
  return r.id;
@@ -642,25 +939,32 @@ function PayPalAdvancedCard(props) {
642
939
  onApprove: async (data) => {
643
940
  try {
644
941
  setError(null);
942
+ showProcessingOverlay();
645
943
  const orderId = String(data?.orderID || "");
646
944
  const result = await api.captureOrder(cartId, orderId);
647
945
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
648
- onPaid?.({ ...result, ...completeResult });
946
+ await onPaid?.({ ...result, ...completeResult });
649
947
  } catch (e) {
948
+ if (isNextRouterError(e)) throw e;
949
+ hideProcessingOverlay();
950
+ submittingRef.current = false;
951
+ setSubmitting(false);
650
952
  const msg = e instanceof Error ? e.message : "Card payment failed";
651
953
  setError(msg);
652
954
  onError?.(msg);
653
- } finally {
654
- setSubmitting(false);
655
955
  }
656
956
  },
657
957
  onCancel: () => {
958
+ hideProcessingOverlay();
959
+ submittingRef.current = false;
658
960
  setSubmitting(false);
659
961
  },
660
962
  onError: (e) => {
661
963
  const msg = e instanceof Error ? e.message : e?.message || "CardFields error";
662
964
  setError(msg);
663
965
  onError?.(msg);
966
+ hideProcessingOverlay();
967
+ submittingRef.current = false;
664
968
  setSubmitting(false);
665
969
  },
666
970
  children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -759,19 +1063,18 @@ function PayPalAdvancedCard(props) {
759
1063
  disabled: submitting,
760
1064
  label: submitting ? "Processing\u2026" : "Pay by Card",
761
1065
  onSubmit: () => {
1066
+ if (submittingRef.current) return;
1067
+ submittingRef.current = true;
762
1068
  setError(null);
763
1069
  setSubmitting(true);
764
- },
765
- onSubmitError: (msg) => {
766
- setError(msg);
767
- onError?.(msg);
768
- setSubmitting(false);
1070
+ showProcessingOverlay();
769
1071
  }
770
1072
  }
771
1073
  ),
772
1074
  error && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
773
1075
  "div",
774
1076
  {
1077
+ role: "alert",
775
1078
  style: {
776
1079
  display: "flex",
777
1080
  alignItems: "flex-start",
@@ -808,6 +1111,8 @@ function PayPalLoadingCard() {
808
1111
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
809
1112
  "div",
810
1113
  {
1114
+ role: "status",
1115
+ "aria-label": "Connecting to PayPal",
811
1116
  style: {
812
1117
  display: "flex",
813
1118
  alignItems: "center",
@@ -845,6 +1150,7 @@ function PayPalErrorCard({ message }) {
845
1150
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
846
1151
  "div",
847
1152
  {
1153
+ role: "alert",
848
1154
  style: {
849
1155
  padding: "12px 16px",
850
1156
  background: "#fef2f2",
@@ -878,16 +1184,11 @@ function MedusaNextPayPalAdapter(props) {
878
1184
  enabled: shouldRender
879
1185
  });
880
1186
  const handlePaid = (0, import_react5.useCallback)(
881
- (captureResult) => {
1187
+ async (captureResult) => {
882
1188
  onPaid?.(captureResult);
883
- Promise.resolve(onSuccess?.(cartId)).catch((e) => {
884
- if (isNextRedirectError(e)) return;
885
- onError?.(
886
- e instanceof Error ? e.message : "Your payment was taken but the order could not be finalized. Please contact support before paying again."
887
- );
888
- });
1189
+ await onSuccess?.(cartId);
889
1190
  },
890
- [cartId, onPaid, onSuccess, onError]
1191
+ [cartId, onPaid, onSuccess]
891
1192
  );
892
1193
  if (!shouldRender) return null;
893
1194
  if (loading) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalLoadingCard, {});
@@ -949,6 +1250,8 @@ function SessionInitCard() {
949
1250
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
950
1251
  "div",
951
1252
  {
1253
+ role: "status",
1254
+ "aria-label": "Setting up payment",
952
1255
  style: {
953
1256
  display: "flex",
954
1257
  alignItems: "center",
@@ -984,6 +1287,8 @@ function ConfigLoadingCard() {
984
1287
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
985
1288
  "div",
986
1289
  {
1290
+ role: "status",
1291
+ "aria-label": "Connecting to PayPal",
987
1292
  style: {
988
1293
  display: "flex",
989
1294
  alignItems: "center",
@@ -1021,6 +1326,7 @@ function ErrorCard({ message }) {
1021
1326
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1022
1327
  "div",
1023
1328
  {
1329
+ role: "alert",
1024
1330
  style: {
1025
1331
  padding: "12px 16px",
1026
1332
  background: "#fef2f2",
@@ -1051,16 +1357,11 @@ function PayPalPaymentSection({
1051
1357
  enabled: shouldRender
1052
1358
  });
1053
1359
  const handlePaid = (0, import_react6.useCallback)(
1054
- (captureResult) => {
1360
+ async (captureResult) => {
1055
1361
  onPaid?.(captureResult);
1056
- Promise.resolve(onSuccess?.(cartId)).catch((e) => {
1057
- if (isNextRedirectError(e)) return;
1058
- onError?.(
1059
- e instanceof Error ? e.message : "Your payment was taken but the order could not be finalized. Please contact support before paying again."
1060
- );
1061
- });
1362
+ await onSuccess?.(cartId);
1062
1363
  },
1063
- [cartId, onPaid, onSuccess, onError]
1364
+ [cartId, onPaid, onSuccess]
1064
1365
  );
1065
1366
  if (!shouldRender) return null;
1066
1367
  if (sessionLoading) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SessionInitCard, {});
@@ -1109,8 +1410,16 @@ function PayPalPaymentSection({
1109
1410
 
1110
1411
  // src/hooks/usePayPalPaymentMethods.ts
1111
1412
  var import_react7 = require("react");
1413
+ var MAX_CACHE_ENTRIES2 = 50;
1112
1414
  var _cache2 = /* @__PURE__ */ new Map();
1113
1415
  var CACHE_TTL2 = 5 * 60 * 1e3;
1416
+ function cacheSet2(key, value) {
1417
+ if (_cache2.size >= MAX_CACHE_ENTRIES2) {
1418
+ const oldest = _cache2.keys().next().value;
1419
+ if (oldest !== void 0) _cache2.delete(oldest);
1420
+ }
1421
+ _cache2.set(key, value);
1422
+ }
1114
1423
  function cacheKey2(baseUrl, cartId) {
1115
1424
  return `ppm::${baseUrl}::${cartId ?? ""}`;
1116
1425
  }
@@ -1162,12 +1471,13 @@ function usePayPalPaymentMethods({
1162
1471
  cardTitle: typeof cfg.card_title === "string" && cfg.card_title ? cfg.card_title : "Credit or Debit Card",
1163
1472
  loading: false
1164
1473
  };
1165
- _cache2.set(k, { result: next, at: Date.now() });
1474
+ cacheSet2(k, { result: next, at: Date.now() });
1166
1475
  setResult(next);
1167
1476
  } catch (e) {
1168
- if (e?.name === "AbortError") return;
1477
+ const msg = e instanceof Error ? e.message : "";
1478
+ if (e instanceof Error && e.name === "AbortError") return;
1169
1479
  if (!mounted || id !== fetchIdRef.current) return;
1170
- if (e?.message?.includes("403") || e?.message?.includes("Forbidden")) {
1480
+ if (msg.includes("403") || msg.includes("Forbidden")) {
1171
1481
  const disabled = {
1172
1482
  ...DEFAULT_RESULT,
1173
1483
  paypalEnabled: false,
@@ -1198,9 +1508,10 @@ function usePayPalPaymentMethods({
1198
1508
  PayPalProvider,
1199
1509
  PayPalSmartButtons,
1200
1510
  createPayPalStoreApi,
1201
- isNextRedirectError,
1511
+ hideProcessingOverlay,
1202
1512
  isPayPalProviderId,
1203
1513
  markPaymentComplete,
1514
+ showProcessingOverlay,
1204
1515
  usePayPalConfig,
1205
1516
  usePayPalPaymentMethods
1206
1517
  });