@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.mjs CHANGED
@@ -13,7 +13,6 @@ function toHeaderRecord(headers) {
13
13
  }
14
14
  return { ...headers };
15
15
  }
16
- var DEFAULT_TIMEOUT_MS = 3e4;
17
16
  function createHttpClient(opts) {
18
17
  const base = opts.baseUrl.replace(/\/+$/, "");
19
18
  async function request(path, init) {
@@ -25,33 +24,25 @@ function createHttpClient(opts) {
25
24
  if (opts.publishableApiKey) {
26
25
  headers["x-publishable-api-key"] = opts.publishableApiKey;
27
26
  }
27
+ const timeoutMs = 3e4;
28
+ let timeoutId;
28
29
  const controller = new AbortController();
29
- let timedOut = false;
30
- const timer = setTimeout(() => {
31
- timedOut = true;
32
- controller.abort();
33
- }, DEFAULT_TIMEOUT_MS);
34
- const callerSignal = init?.signal;
35
- if (callerSignal) {
36
- if (callerSignal.aborted) {
37
- controller.abort();
38
- } else {
39
- callerSignal.addEventListener("abort", () => controller.abort(), { once: true });
40
- }
30
+ if (!init?.signal) {
31
+ timeoutId = setTimeout(() => controller.abort(), timeoutMs);
41
32
  }
33
+ const effectiveSignal = init?.signal || controller.signal;
42
34
  let res;
43
- let text;
44
35
  try {
45
- res = await fetch(url, { ...init, headers, credentials: "include", signal: controller.signal });
46
- text = await res.text().catch(() => "");
47
- } catch (e) {
48
- if (timedOut) {
49
- throw new Error("[PayPal] Request timed out. Please check your connection and try again.");
36
+ res = await fetch(url, { ...init, headers, credentials: "include", signal: effectiveSignal });
37
+ } catch (err) {
38
+ if (err instanceof Error && err.name === "AbortError" && !init?.signal) {
39
+ throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1e3}s`);
50
40
  }
51
- throw e;
41
+ throw err;
52
42
  } finally {
53
- clearTimeout(timer);
43
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
54
44
  }
45
+ const text = await res.text().catch(() => "");
55
46
  if (!res.ok) {
56
47
  if (res.status === 401) {
57
48
  throw new Error(
@@ -63,21 +54,9 @@ function createHttpClient(opts) {
63
54
  "[PayPal] Forbidden (403) \u2014 this request is not allowed. Check your CORS and API key settings."
64
55
  );
65
56
  }
66
- let parsed = null;
67
- try {
68
- parsed = text ? JSON.parse(text) : null;
69
- } catch {
70
- parsed = null;
71
- }
72
- const backendMessage = typeof parsed?.message === "string" ? parsed.message : "";
73
- const requestId = typeof parsed?.request_id === "string" ? parsed.request_id : "";
74
- if (res.status >= 500) {
75
- console.error(`[PayPal] ${path} failed (${res.status})`, text.slice(0, 1e3));
76
- throw new Error(
77
- `Payment service error${requestId ? ` (ref ${requestId})` : ""}. Please try again or contact support.`
78
- );
79
- }
80
- throw new Error(backendMessage || `Request failed (${res.status})`);
57
+ throw new Error(
58
+ text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`
59
+ );
81
60
  }
82
61
  if (!text) {
83
62
  throw new Error(`[PayPal] Empty response body from ${path} (${res.status})`);
@@ -98,34 +77,13 @@ function createHttpClient(opts) {
98
77
  }
99
78
 
100
79
  // src/client/paypal.ts
101
- function isNextRedirectError(e) {
102
- if (!e || typeof e !== "object") {
103
- return false;
104
- }
105
- const digest = e.digest;
106
- return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest === "NEXT_NOT_FOUND");
107
- }
108
80
  async function markPaymentComplete(baseUrl, cartId, publishableApiKey) {
109
- try {
110
- const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/store/paypal-complete`, {
111
- method: "POST",
112
- headers: {
113
- "Content-Type": "application/json",
114
- ...publishableApiKey ? { "x-publishable-api-key": publishableApiKey } : {}
115
- },
116
- body: JSON.stringify({ cart_id: cartId }),
117
- credentials: "include"
118
- });
119
- const data = await resp.json().catch(() => ({}));
120
- if (!resp.ok) {
121
- console.warn("[PayPal] paypal-complete returned", resp.status, data);
122
- return { ok: false, ...data || {} };
123
- }
124
- return { ok: true, ...data || {} };
125
- } catch (e) {
126
- console.warn("[PayPal] paypal-complete call failed:", e);
127
- return { ok: false };
128
- }
81
+ const http = createHttpClient({ baseUrl, publishableApiKey });
82
+ return http.request(`/store/paypal-complete`, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json" },
85
+ body: JSON.stringify({ cart_id: cartId })
86
+ });
129
87
  }
130
88
  function createPayPalStoreApi(opts) {
131
89
  const http = createHttpClient(opts);
@@ -156,8 +114,16 @@ function createPayPalStoreApi(opts) {
156
114
 
157
115
  // src/hooks/usePayPalConfig.ts
158
116
  import { useEffect, useMemo, useRef, useState } from "react";
117
+ var MAX_CACHE_ENTRIES = 50;
159
118
  var _cache = /* @__PURE__ */ new Map();
160
119
  var CACHE_TTL = 5 * 60 * 1e3;
120
+ function cacheSet(key, value) {
121
+ if (_cache.size >= MAX_CACHE_ENTRIES) {
122
+ const oldest = _cache.keys().next().value;
123
+ if (oldest !== void 0) _cache.delete(oldest);
124
+ }
125
+ _cache.set(key, value);
126
+ }
161
127
  function cacheKey(baseUrl, cartId) {
162
128
  return `${baseUrl}::${cartId ?? ""}`;
163
129
  }
@@ -201,12 +167,12 @@ function usePayPalConfig({
201
167
  try {
202
168
  const cfg = await api.getConfig(cartId, controller.signal);
203
169
  if (!mounted || id !== fetchIdRef.current) return;
204
- _cache.set(k, { config: cfg, at: Date.now() });
170
+ cacheSet(k, { config: cfg, at: Date.now() });
205
171
  setConfig(cfg);
206
172
  } catch (e) {
207
- if (e?.name === "AbortError") return;
173
+ if (e instanceof Error && e.name === "AbortError") return;
208
174
  if (!mounted || id !== fetchIdRef.current) return;
209
- setError(e?.message || "Failed to load PayPal config");
175
+ setError(e instanceof Error ? e.message : "Failed to load PayPal config");
210
176
  } finally {
211
177
  if (mounted && id === fetchIdRef.current) setLoading(false);
212
178
  }
@@ -236,7 +202,7 @@ function PayPalProvider(props) {
236
202
  "disable-funding": disableFunding,
237
203
  "data-partner-attribution-id": BN_CODE
238
204
  };
239
- }, [config, intent, disableFunding]);
205
+ }, [config.client_id, config.currency, config.client_token, intent, disableFunding]);
240
206
  return /* @__PURE__ */ jsx(
241
207
  PayPalScriptProvider,
242
208
  {
@@ -251,17 +217,80 @@ function PayPalProvider(props) {
251
217
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
252
218
  function PayPalCurrencyNotice({ config }) {
253
219
  if (config.currency_supported) return null;
254
- return /* @__PURE__ */ jsxs("div", { style: { padding: 12, border: "1px solid #ddd", borderRadius: 10 }, children: [
220
+ return /* @__PURE__ */ jsxs("div", { role: "alert", style: { padding: 12, border: "1px solid #ddd", borderRadius: 10 }, children: [
255
221
  /* @__PURE__ */ jsx2("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "PayPal currency issue" }),
256
222
  /* @__PURE__ */ jsx2("ul", { style: { margin: 0, paddingLeft: 18 }, children: (config.currency_errors || []).map((e) => /* @__PURE__ */ jsx2("li", { children: e }, e)) })
257
223
  ] });
258
224
  }
259
225
 
260
226
  // src/components/PayPalSmartButtons.tsx
261
- import { useMemo as useMemo3, useState as useState2 } from "react";
262
- import { PayPalButtons } from "@paypal/react-paypal-js";
227
+ import { useCallback, useMemo as useMemo3, useState as useState2 } from "react";
228
+ import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js";
229
+
230
+ // src/utils/next-errors.ts
231
+ function isNextRouterError(e) {
232
+ if (typeof e !== "object" || e === null || !("digest" in e)) return false;
233
+ const digest = e.digest;
234
+ return typeof digest === "string" && (digest.startsWith("NEXT_REDIRECT") || digest.startsWith("NEXT_NOT_FOUND"));
235
+ }
236
+
237
+ // src/utils/processing-overlay.ts
238
+ var OVERLAY_ID = "__pp_processing_overlay";
239
+ function showProcessingOverlay() {
240
+ if (typeof document === "undefined") return;
241
+ if (document.getElementById(OVERLAY_ID)) return;
242
+ const overlay = document.createElement("div");
243
+ overlay.id = OVERLAY_ID;
244
+ overlay.setAttribute("role", "status");
245
+ overlay.setAttribute("aria-label", "Processing payment");
246
+ overlay.innerHTML = `
247
+ <style>
248
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
249
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
250
+ </style>
251
+ <div style="position:relative;width:56px;height:56px">
252
+ <div style="position:absolute;inset:0;border-radius:50%;border:3px solid #e5e7eb"></div>
253
+ <div style="position:absolute;inset:0;border-radius:50%;border:3px solid transparent;border-top-color:#0070ba;animation:_pp_spin .8s linear infinite"></div>
254
+ <svg viewBox="0 0 24 24" fill="none" stroke="#0070ba" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
255
+ style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px">
256
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
257
+ </svg>
258
+ </div>
259
+ <div style="text-align:center">
260
+ <div style="font-size:18px;font-weight:600;color:#111827;letter-spacing:-0.01em">Processing your payment</div>
261
+ <div style="font-size:14px;color:#6b7280;margin-top:6px;line-height:1.5">
262
+ This may take a few moments. Please do not close<br>or refresh this page.
263
+ </div>
264
+ </div>
265
+ <div style="width:200px;height:3px;background:#e5e7eb;border-radius:3px;overflow:hidden;margin-top:4px">
266
+ <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>
267
+ </div>
268
+ `;
269
+ Object.assign(overlay.style, {
270
+ position: "fixed",
271
+ inset: "0",
272
+ zIndex: "99999",
273
+ background: "rgba(255,255,255,0.96)",
274
+ display: "flex",
275
+ flexDirection: "column",
276
+ alignItems: "center",
277
+ justifyContent: "center",
278
+ gap: "20px",
279
+ fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif"
280
+ });
281
+ document.body.appendChild(overlay);
282
+ }
283
+ function hideProcessingOverlay() {
284
+ if (typeof document === "undefined") return;
285
+ document.getElementById(OVERLAY_ID)?.remove();
286
+ }
287
+
288
+ // src/components/PayPalSmartButtons.tsx
263
289
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
264
- var SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
290
+ var SPIN_STYLE = `
291
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
292
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
293
+ `;
265
294
  var BUTTON_WIDTH_MAP = {
266
295
  small: "300px",
267
296
  medium: "400px",
@@ -276,6 +305,12 @@ function PayPalSmartButtons(props) {
276
305
  );
277
306
  const [error, setError] = useState2(null);
278
307
  const [processing, setProcessing] = useState2(false);
308
+ const [buttonsReady, setButtonsReady] = useState2(false);
309
+ const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer();
310
+ const handleInit = useCallback(() => {
311
+ setButtonsReady(true);
312
+ }, []);
313
+ const showSpinner = isPending || isResolved && !buttonsReady;
279
314
  if (!config.currency_supported) return null;
280
315
  const containerWidth = BUTTON_WIDTH_MAP[config.button_width ?? "responsive"] ?? "100%";
281
316
  return /* @__PURE__ */ jsxs2("div", { style: { width: containerWidth, position: "relative" }, children: [
@@ -283,41 +318,160 @@ function PayPalSmartButtons(props) {
283
318
  processing && /* @__PURE__ */ jsxs2(
284
319
  "div",
285
320
  {
321
+ role: "status",
322
+ "aria-label": "Processing payment",
286
323
  style: {
287
- position: "absolute",
324
+ position: "fixed",
288
325
  inset: 0,
289
- zIndex: 10,
290
- background: "rgba(255,255,255,0.90)",
291
- borderRadius: 8,
326
+ zIndex: 9999,
327
+ background: "rgba(255,255,255,0.96)",
292
328
  display: "flex",
293
329
  flexDirection: "column",
294
330
  alignItems: "center",
295
331
  justifyContent: "center",
332
+ gap: 20
333
+ },
334
+ children: [
335
+ /* @__PURE__ */ jsxs2("div", { style: { position: "relative", width: 56, height: 56 }, children: [
336
+ /* @__PURE__ */ jsx3(
337
+ "div",
338
+ {
339
+ style: {
340
+ position: "absolute",
341
+ inset: 0,
342
+ borderRadius: "50%",
343
+ border: "3px solid #e5e7eb"
344
+ }
345
+ }
346
+ ),
347
+ /* @__PURE__ */ jsx3(
348
+ "div",
349
+ {
350
+ style: {
351
+ position: "absolute",
352
+ inset: 0,
353
+ borderRadius: "50%",
354
+ border: "3px solid transparent",
355
+ borderTopColor: "#0070ba",
356
+ animation: "_pp_spin .8s linear infinite"
357
+ }
358
+ }
359
+ ),
360
+ /* @__PURE__ */ jsx3(
361
+ "svg",
362
+ {
363
+ viewBox: "0 0 24 24",
364
+ fill: "none",
365
+ stroke: "#0070ba",
366
+ strokeWidth: "1.8",
367
+ strokeLinecap: "round",
368
+ strokeLinejoin: "round",
369
+ style: {
370
+ position: "absolute",
371
+ top: "50%",
372
+ left: "50%",
373
+ transform: "translate(-50%, -50%)",
374
+ width: 24,
375
+ height: 24
376
+ },
377
+ children: /* @__PURE__ */ jsx3("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" })
378
+ }
379
+ )
380
+ ] }),
381
+ /* @__PURE__ */ jsxs2("div", { style: { textAlign: "center" }, children: [
382
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }, children: "Processing your payment" }),
383
+ /* @__PURE__ */ jsxs2("div", { style: { fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }, children: [
384
+ "This may take a few moments. Please do not close",
385
+ /* @__PURE__ */ jsx3("br", {}),
386
+ "or refresh this page."
387
+ ] })
388
+ ] }),
389
+ /* @__PURE__ */ jsx3(
390
+ "div",
391
+ {
392
+ style: {
393
+ width: 200,
394
+ height: 3,
395
+ background: "#e5e7eb",
396
+ borderRadius: 3,
397
+ overflow: "hidden",
398
+ marginTop: 4
399
+ },
400
+ children: /* @__PURE__ */ jsx3(
401
+ "div",
402
+ {
403
+ style: {
404
+ width: "40%",
405
+ height: "100%",
406
+ background: "linear-gradient(90deg, #0070ba, #003087)",
407
+ borderRadius: 3,
408
+ animation: "_pp_progress 1.5s ease-in-out infinite"
409
+ }
410
+ }
411
+ )
412
+ }
413
+ )
414
+ ]
415
+ }
416
+ ),
417
+ showSpinner && /* @__PURE__ */ jsxs2(
418
+ "div",
419
+ {
420
+ role: "status",
421
+ "aria-label": "Loading PayPal",
422
+ style: {
423
+ display: "flex",
424
+ alignItems: "center",
425
+ justifyContent: "center",
296
426
  gap: 10,
297
- minHeight: 60
427
+ padding: "18px 16px",
428
+ background: "#f9fafb",
429
+ border: "1px solid #e5e7eb",
430
+ borderRadius: 8,
431
+ minHeight: 55
298
432
  },
299
433
  children: [
300
434
  /* @__PURE__ */ jsx3(
301
435
  "div",
302
436
  {
303
437
  style: {
304
- width: 28,
305
- height: 28,
438
+ width: 22,
439
+ height: 22,
306
440
  borderRadius: "50%",
307
441
  border: "2.5px solid #e5e7eb",
308
442
  borderTopColor: "#0070ba",
309
- animation: "_pp_spin .7s linear infinite"
443
+ animation: "_pp_spin .7s linear infinite",
444
+ flexShrink: 0
310
445
  }
311
446
  }
312
447
  ),
313
- /* @__PURE__ */ jsxs2("div", { style: { textAlign: "center" }, children: [
314
- /* @__PURE__ */ jsx3("div", { style: { fontSize: 13, color: "#374151", fontWeight: 500 }, children: "Processing your payment\u2026" }),
315
- /* @__PURE__ */ jsx3("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 4 }, children: "Please do not close or refresh this page" })
316
- ] })
448
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: 13, fontWeight: 500, color: "#6b7280" }, children: "Loading PayPal\u2026" })
317
449
  ]
318
450
  }
319
451
  ),
320
- config.environment === "sandbox" && /* @__PURE__ */ jsxs2(
452
+ isRejected && /* @__PURE__ */ jsxs2(
453
+ "div",
454
+ {
455
+ role: "alert",
456
+ style: {
457
+ display: "flex",
458
+ alignItems: "flex-start",
459
+ gap: 8,
460
+ padding: "10px 14px",
461
+ background: "#fef2f2",
462
+ border: "1px solid #fecaca",
463
+ borderRadius: 8,
464
+ fontSize: 13,
465
+ color: "#b91c1c",
466
+ lineHeight: 1.5
467
+ },
468
+ children: [
469
+ /* @__PURE__ */ jsx3("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
470
+ /* @__PURE__ */ jsx3("span", { children: "PayPal failed to load. Please refresh the page or try a different payment method." })
471
+ ]
472
+ }
473
+ ),
474
+ config.environment === "sandbox" && buttonsReady && /* @__PURE__ */ jsxs2(
321
475
  "div",
322
476
  {
323
477
  style: {
@@ -355,9 +509,11 @@ function PayPalSmartButtons(props) {
355
509
  ]
356
510
  }
357
511
  ),
358
- /* @__PURE__ */ jsx3(
512
+ isResolved && /* @__PURE__ */ jsx3("div", { style: buttonsReady ? void 0 : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }, children: /* @__PURE__ */ jsx3(
359
513
  PayPalButtons,
360
514
  {
515
+ forceReRender: [config.currency, config.intent, cartId],
516
+ onInit: handleInit,
361
517
  style: {
362
518
  layout: "vertical",
363
519
  color: config.button_color,
@@ -366,40 +522,53 @@ function PayPalSmartButtons(props) {
366
522
  height: config.button_height
367
523
  },
368
524
  createOrder: async () => {
525
+ if (processing) throw new Error("Payment already processing");
369
526
  setError(null);
370
- const r = await api.createOrder(cartId);
371
- return r.id;
527
+ setProcessing(true);
528
+ try {
529
+ const r = await api.createOrder(cartId);
530
+ return r.id;
531
+ } catch (e) {
532
+ setProcessing(false);
533
+ throw e;
534
+ }
372
535
  },
373
536
  onApprove: async (data) => {
374
537
  try {
375
538
  setProcessing(true);
539
+ showProcessingOverlay();
376
540
  setError(null);
377
541
  const orderId = String(data?.orderID || "");
542
+ if (!orderId) throw new Error("PayPal order ID is missing from approval response");
378
543
  const result = await api.captureOrder(cartId, orderId);
379
544
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
380
- onPaid?.({ ...result, ...completeResult });
545
+ await onPaid?.({ ...result, ...completeResult });
381
546
  } catch (e) {
547
+ if (isNextRouterError(e)) throw e;
548
+ hideProcessingOverlay();
549
+ setProcessing(false);
382
550
  const msg = e instanceof Error ? e.message : "Payment capture failed";
383
551
  setError(msg);
384
552
  onError?.(msg);
385
- } finally {
386
- setProcessing(false);
387
553
  }
388
554
  },
389
555
  onCancel: () => {
556
+ hideProcessingOverlay();
390
557
  setProcessing(false);
391
558
  },
392
559
  onError: (err) => {
560
+ hideProcessingOverlay();
393
561
  setProcessing(false);
394
562
  const msg = err instanceof Error ? err.message : err?.message || "PayPal error";
395
563
  setError(msg);
396
564
  onError?.(msg);
397
565
  }
398
566
  }
399
- ),
567
+ ) }),
400
568
  error ? /* @__PURE__ */ jsxs2(
401
569
  "div",
402
570
  {
571
+ role: "alert",
403
572
  style: {
404
573
  marginTop: 10,
405
574
  display: "flex",
@@ -423,16 +592,20 @@ function PayPalSmartButtons(props) {
423
592
  }
424
593
 
425
594
  // src/components/PayPalAdvancedCard.tsx
426
- import { useMemo as useMemo4, useState as useState3 } from "react";
595
+ import { useMemo as useMemo4, useRef as useRef2, useState as useState3 } from "react";
427
596
  import {
428
597
  PayPalCardFieldsProvider,
429
598
  PayPalNumberField,
430
599
  PayPalExpiryField,
431
600
  PayPalCVVField,
432
- usePayPalCardFields
601
+ usePayPalCardFields,
602
+ usePayPalScriptReducer as usePayPalScriptReducer2
433
603
  } from "@paypal/react-paypal-js";
434
604
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
435
- var SPIN_STYLE2 = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
605
+ var SPIN_STYLE2 = `
606
+ @keyframes _pp_spin { to { transform: rotate(360deg) } }
607
+ @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
608
+ `;
436
609
  var cardStyle = {
437
610
  input: {
438
611
  "font-size": "15px",
@@ -478,8 +651,7 @@ var labelStyle = {
478
651
  function SubmitButton({
479
652
  disabled,
480
653
  label,
481
- onSubmit,
482
- onSubmitError
654
+ onSubmit
483
655
  }) {
484
656
  const { cardFieldsForm } = usePayPalCardFields();
485
657
  const isDisabled = disabled || !cardFieldsForm;
@@ -488,15 +660,12 @@ function SubmitButton({
488
660
  {
489
661
  type: "button",
490
662
  disabled: isDisabled,
663
+ "aria-busy": disabled,
491
664
  onClick: () => {
492
665
  onSubmit();
493
- const submitted = cardFieldsForm?.submit();
494
- if (submitted && typeof submitted.catch === "function") {
495
- submitted.catch((e) => {
496
- onSubmitError(
497
- e instanceof Error ? e.message : "Card payment failed. Please try again."
498
- );
499
- });
666
+ try {
667
+ cardFieldsForm?.submit();
668
+ } catch {
500
669
  }
501
670
  },
502
671
  style: {
@@ -541,7 +710,33 @@ function PayPalAdvancedCard(props) {
541
710
  );
542
711
  const [error, setError] = useState3(null);
543
712
  const [submitting, setSubmitting] = useState3(false);
713
+ const submittingRef = useRef2(false);
714
+ const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer2();
544
715
  if (!config.currency_supported) return null;
716
+ if (isRejected) {
717
+ return /* @__PURE__ */ jsxs3(
718
+ "div",
719
+ {
720
+ role: "alert",
721
+ style: {
722
+ display: "flex",
723
+ alignItems: "flex-start",
724
+ gap: 8,
725
+ padding: "10px 14px",
726
+ background: "#fef2f2",
727
+ border: "1px solid #fecaca",
728
+ borderRadius: 8,
729
+ fontSize: 13,
730
+ color: "#b91c1c",
731
+ lineHeight: 1.5
732
+ },
733
+ children: [
734
+ /* @__PURE__ */ jsx4("span", { style: { flexShrink: 0, fontSize: 15 }, children: "\u26A0\uFE0F" }),
735
+ /* @__PURE__ */ jsx4("span", { children: "PayPal failed to load. Please refresh the page or try a different payment method." })
736
+ ]
737
+ }
738
+ );
739
+ }
545
740
  if (!config.client_token) {
546
741
  return /* @__PURE__ */ jsx4(
547
742
  "div",
@@ -559,42 +754,143 @@ function PayPalAdvancedCard(props) {
559
754
  );
560
755
  }
561
756
  const isSandbox = config.environment === "sandbox";
757
+ if (isPending) {
758
+ return /* @__PURE__ */ jsxs3(
759
+ "div",
760
+ {
761
+ role: "status",
762
+ "aria-label": "Loading PayPal",
763
+ style: {
764
+ display: "flex",
765
+ alignItems: "center",
766
+ justifyContent: "center",
767
+ gap: 10,
768
+ padding: "18px 16px",
769
+ background: "#f9fafb",
770
+ border: "1px solid #e5e7eb",
771
+ borderRadius: 8,
772
+ minHeight: 55
773
+ },
774
+ children: [
775
+ /* @__PURE__ */ jsx4("style", { children: SPIN_STYLE2 }),
776
+ /* @__PURE__ */ jsx4(
777
+ "div",
778
+ {
779
+ style: {
780
+ width: 22,
781
+ height: 22,
782
+ borderRadius: "50%",
783
+ border: "2.5px solid #e5e7eb",
784
+ borderTopColor: "#0070ba",
785
+ animation: "_pp_spin .7s linear infinite",
786
+ flexShrink: 0
787
+ }
788
+ }
789
+ ),
790
+ /* @__PURE__ */ jsx4("div", { style: { fontSize: 13, fontWeight: 500, color: "#6b7280" }, children: "Loading card fields\u2026" })
791
+ ]
792
+ }
793
+ );
794
+ }
795
+ if (!isResolved) return null;
562
796
  return /* @__PURE__ */ jsxs3("div", { style: { position: "relative" }, children: [
563
797
  /* @__PURE__ */ jsx4("style", { children: SPIN_STYLE2 }),
564
798
  submitting && /* @__PURE__ */ jsxs3(
565
799
  "div",
566
800
  {
801
+ role: "status",
802
+ "aria-label": "Processing payment",
567
803
  style: {
568
- position: "absolute",
804
+ position: "fixed",
569
805
  inset: 0,
570
- zIndex: 20,
571
- background: "rgba(255,255,255,0.92)",
572
- borderRadius: 12,
806
+ zIndex: 9999,
807
+ background: "rgba(255,255,255,0.96)",
573
808
  display: "flex",
574
809
  flexDirection: "column",
575
810
  alignItems: "center",
576
811
  justifyContent: "center",
577
- gap: 14,
578
- minHeight: 180
812
+ gap: 20
579
813
  },
580
814
  children: [
815
+ /* @__PURE__ */ jsxs3("div", { style: { position: "relative", width: 56, height: 56 }, children: [
816
+ /* @__PURE__ */ jsx4(
817
+ "div",
818
+ {
819
+ style: {
820
+ position: "absolute",
821
+ inset: 0,
822
+ borderRadius: "50%",
823
+ border: "3px solid #e5e7eb"
824
+ }
825
+ }
826
+ ),
827
+ /* @__PURE__ */ jsx4(
828
+ "div",
829
+ {
830
+ style: {
831
+ position: "absolute",
832
+ inset: 0,
833
+ borderRadius: "50%",
834
+ border: "3px solid transparent",
835
+ borderTopColor: "#2563eb",
836
+ animation: "_pp_spin .8s linear infinite"
837
+ }
838
+ }
839
+ ),
840
+ /* @__PURE__ */ jsx4(
841
+ "svg",
842
+ {
843
+ viewBox: "0 0 24 24",
844
+ fill: "none",
845
+ stroke: "#2563eb",
846
+ strokeWidth: "1.8",
847
+ strokeLinecap: "round",
848
+ strokeLinejoin: "round",
849
+ style: {
850
+ position: "absolute",
851
+ top: "50%",
852
+ left: "50%",
853
+ transform: "translate(-50%, -50%)",
854
+ width: 24,
855
+ height: 24
856
+ },
857
+ children: /* @__PURE__ */ jsx4("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" })
858
+ }
859
+ )
860
+ ] }),
861
+ /* @__PURE__ */ jsxs3("div", { style: { textAlign: "center" }, children: [
862
+ /* @__PURE__ */ jsx4("div", { style: { fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }, children: "Processing your payment" }),
863
+ /* @__PURE__ */ jsxs3("div", { style: { fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }, children: [
864
+ "This may take a few moments. Please do not close",
865
+ /* @__PURE__ */ jsx4("br", {}),
866
+ "or refresh this page."
867
+ ] })
868
+ ] }),
581
869
  /* @__PURE__ */ jsx4(
582
870
  "div",
583
871
  {
584
872
  style: {
585
- width: 36,
586
- height: 36,
587
- borderRadius: "50%",
588
- border: "3px solid #dbeafe",
589
- borderTopColor: "#2563eb",
590
- animation: "_pp_spin .75s linear infinite"
591
- }
873
+ width: 200,
874
+ height: 3,
875
+ background: "#e5e7eb",
876
+ borderRadius: 3,
877
+ overflow: "hidden",
878
+ marginTop: 4
879
+ },
880
+ children: /* @__PURE__ */ jsx4(
881
+ "div",
882
+ {
883
+ style: {
884
+ width: "40%",
885
+ height: "100%",
886
+ background: "linear-gradient(90deg, #2563eb, #1d4ed8)",
887
+ borderRadius: 3,
888
+ animation: "_pp_progress 1.5s ease-in-out infinite"
889
+ }
890
+ }
891
+ )
592
892
  }
593
- ),
594
- /* @__PURE__ */ jsxs3("div", { style: { textAlign: "center" }, children: [
595
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 15, fontWeight: 600, color: "#111827" }, children: "Processing your payment\u2026" }),
596
- /* @__PURE__ */ jsx4("div", { style: { fontSize: 13, color: "#6b7280", marginTop: 4 }, children: "Please do not close or refresh this page" })
597
- ] })
893
+ )
598
894
  ]
599
895
  }
600
896
  ),
@@ -603,6 +899,7 @@ function PayPalAdvancedCard(props) {
603
899
  {
604
900
  style: cardStyle,
605
901
  createOrder: async () => {
902
+ if (submittingRef.current) throw new Error("Payment already processing");
606
903
  setError(null);
607
904
  const r = await api.createOrder(cartId, true);
608
905
  return r.id;
@@ -610,25 +907,32 @@ function PayPalAdvancedCard(props) {
610
907
  onApprove: async (data) => {
611
908
  try {
612
909
  setError(null);
910
+ showProcessingOverlay();
613
911
  const orderId = String(data?.orderID || "");
614
912
  const result = await api.captureOrder(cartId, orderId);
615
913
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
616
- onPaid?.({ ...result, ...completeResult });
914
+ await onPaid?.({ ...result, ...completeResult });
617
915
  } catch (e) {
916
+ if (isNextRouterError(e)) throw e;
917
+ hideProcessingOverlay();
918
+ submittingRef.current = false;
919
+ setSubmitting(false);
618
920
  const msg = e instanceof Error ? e.message : "Card payment failed";
619
921
  setError(msg);
620
922
  onError?.(msg);
621
- } finally {
622
- setSubmitting(false);
623
923
  }
624
924
  },
625
925
  onCancel: () => {
926
+ hideProcessingOverlay();
927
+ submittingRef.current = false;
626
928
  setSubmitting(false);
627
929
  },
628
930
  onError: (e) => {
629
931
  const msg = e instanceof Error ? e.message : e?.message || "CardFields error";
630
932
  setError(msg);
631
933
  onError?.(msg);
934
+ hideProcessingOverlay();
935
+ submittingRef.current = false;
632
936
  setSubmitting(false);
633
937
  },
634
938
  children: /* @__PURE__ */ jsxs3(
@@ -727,19 +1031,18 @@ function PayPalAdvancedCard(props) {
727
1031
  disabled: submitting,
728
1032
  label: submitting ? "Processing\u2026" : "Pay by Card",
729
1033
  onSubmit: () => {
1034
+ if (submittingRef.current) return;
1035
+ submittingRef.current = true;
730
1036
  setError(null);
731
1037
  setSubmitting(true);
732
- },
733
- onSubmitError: (msg) => {
734
- setError(msg);
735
- onError?.(msg);
736
- setSubmitting(false);
1038
+ showProcessingOverlay();
737
1039
  }
738
1040
  }
739
1041
  ),
740
1042
  error && /* @__PURE__ */ jsxs3(
741
1043
  "div",
742
1044
  {
1045
+ role: "alert",
743
1046
  style: {
744
1047
  display: "flex",
745
1048
  alignItems: "flex-start",
@@ -767,7 +1070,7 @@ function PayPalAdvancedCard(props) {
767
1070
  }
768
1071
 
769
1072
  // src/adapters/MedusaNextPayPalAdapter.tsx
770
- import { useCallback } from "react";
1073
+ import { useCallback as useCallback2 } from "react";
771
1074
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
772
1075
  var DEFAULT_PAYPAL_PROVIDER_ID = "pp_paypal_paypal";
773
1076
  var DEFAULT_PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
@@ -776,6 +1079,8 @@ function PayPalLoadingCard() {
776
1079
  return /* @__PURE__ */ jsxs4(
777
1080
  "div",
778
1081
  {
1082
+ role: "status",
1083
+ "aria-label": "Connecting to PayPal",
779
1084
  style: {
780
1085
  display: "flex",
781
1086
  alignItems: "center",
@@ -813,6 +1118,7 @@ function PayPalErrorCard({ message }) {
813
1118
  return /* @__PURE__ */ jsx5(
814
1119
  "div",
815
1120
  {
1121
+ role: "alert",
816
1122
  style: {
817
1123
  padding: "12px 16px",
818
1124
  background: "#fef2f2",
@@ -845,17 +1151,12 @@ function MedusaNextPayPalAdapter(props) {
845
1151
  cartId,
846
1152
  enabled: shouldRender
847
1153
  });
848
- const handlePaid = useCallback(
849
- (captureResult) => {
1154
+ const handlePaid = useCallback2(
1155
+ async (captureResult) => {
850
1156
  onPaid?.(captureResult);
851
- Promise.resolve(onSuccess?.(cartId)).catch((e) => {
852
- if (isNextRedirectError(e)) return;
853
- onError?.(
854
- e instanceof Error ? e.message : "Your payment was taken but the order could not be finalized. Please contact support before paying again."
855
- );
856
- });
1157
+ await onSuccess?.(cartId);
857
1158
  },
858
- [cartId, onPaid, onSuccess, onError]
1159
+ [cartId, onPaid, onSuccess]
859
1160
  );
860
1161
  if (!shouldRender) return null;
861
1162
  if (loading) return /* @__PURE__ */ jsx5(PayPalLoadingCard, {});
@@ -900,7 +1201,7 @@ function MedusaNextPayPalAdapter(props) {
900
1201
  }
901
1202
 
902
1203
  // src/components/PayPalPaymentSection.tsx
903
- import { useCallback as useCallback2 } from "react";
1204
+ import { useCallback as useCallback3 } from "react";
904
1205
  import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
905
1206
  var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
906
1207
  var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
@@ -917,6 +1218,8 @@ function SessionInitCard() {
917
1218
  return /* @__PURE__ */ jsxs5(
918
1219
  "div",
919
1220
  {
1221
+ role: "status",
1222
+ "aria-label": "Setting up payment",
920
1223
  style: {
921
1224
  display: "flex",
922
1225
  alignItems: "center",
@@ -952,6 +1255,8 @@ function ConfigLoadingCard() {
952
1255
  return /* @__PURE__ */ jsxs5(
953
1256
  "div",
954
1257
  {
1258
+ role: "status",
1259
+ "aria-label": "Connecting to PayPal",
955
1260
  style: {
956
1261
  display: "flex",
957
1262
  alignItems: "center",
@@ -989,6 +1294,7 @@ function ErrorCard({ message }) {
989
1294
  return /* @__PURE__ */ jsx6(
990
1295
  "div",
991
1296
  {
1297
+ role: "alert",
992
1298
  style: {
993
1299
  padding: "12px 16px",
994
1300
  background: "#fef2f2",
@@ -1018,17 +1324,12 @@ function PayPalPaymentSection({
1018
1324
  cartId,
1019
1325
  enabled: shouldRender
1020
1326
  });
1021
- const handlePaid = useCallback2(
1022
- (captureResult) => {
1327
+ const handlePaid = useCallback3(
1328
+ async (captureResult) => {
1023
1329
  onPaid?.(captureResult);
1024
- Promise.resolve(onSuccess?.(cartId)).catch((e) => {
1025
- if (isNextRedirectError(e)) return;
1026
- onError?.(
1027
- e instanceof Error ? e.message : "Your payment was taken but the order could not be finalized. Please contact support before paying again."
1028
- );
1029
- });
1330
+ await onSuccess?.(cartId);
1030
1331
  },
1031
- [cartId, onPaid, onSuccess, onError]
1332
+ [cartId, onPaid, onSuccess]
1032
1333
  );
1033
1334
  if (!shouldRender) return null;
1034
1335
  if (sessionLoading) return /* @__PURE__ */ jsx6(SessionInitCard, {});
@@ -1076,9 +1377,17 @@ function PayPalPaymentSection({
1076
1377
  }
1077
1378
 
1078
1379
  // src/hooks/usePayPalPaymentMethods.ts
1079
- import { useEffect as useEffect2, useMemo as useMemo5, useRef as useRef2, useState as useState4 } from "react";
1380
+ import { useEffect as useEffect2, useMemo as useMemo5, useRef as useRef3, useState as useState4 } from "react";
1381
+ var MAX_CACHE_ENTRIES2 = 50;
1080
1382
  var _cache2 = /* @__PURE__ */ new Map();
1081
1383
  var CACHE_TTL2 = 5 * 60 * 1e3;
1384
+ function cacheSet2(key, value) {
1385
+ if (_cache2.size >= MAX_CACHE_ENTRIES2) {
1386
+ const oldest = _cache2.keys().next().value;
1387
+ if (oldest !== void 0) _cache2.delete(oldest);
1388
+ }
1389
+ _cache2.set(key, value);
1390
+ }
1082
1391
  function cacheKey2(baseUrl, cartId) {
1083
1392
  return `ppm::${baseUrl}::${cartId ?? ""}`;
1084
1393
  }
@@ -1103,7 +1412,7 @@ function usePayPalPaymentMethods({
1103
1412
  const hit = _cache2.get(key);
1104
1413
  const seed = hit && Date.now() - hit.at < CACHE_TTL2 ? hit.result : null;
1105
1414
  const [result, setResult] = useState4(seed ?? { ...DEFAULT_RESULT, loading: enabled });
1106
- const fetchIdRef = useRef2(0);
1415
+ const fetchIdRef = useRef3(0);
1107
1416
  useEffect2(() => {
1108
1417
  if (!enabled) {
1109
1418
  setResult((prev) => ({ ...prev, loading: false }));
@@ -1130,12 +1439,13 @@ function usePayPalPaymentMethods({
1130
1439
  cardTitle: typeof cfg.card_title === "string" && cfg.card_title ? cfg.card_title : "Credit or Debit Card",
1131
1440
  loading: false
1132
1441
  };
1133
- _cache2.set(k, { result: next, at: Date.now() });
1442
+ cacheSet2(k, { result: next, at: Date.now() });
1134
1443
  setResult(next);
1135
1444
  } catch (e) {
1136
- if (e?.name === "AbortError") return;
1445
+ const msg = e instanceof Error ? e.message : "";
1446
+ if (e instanceof Error && e.name === "AbortError") return;
1137
1447
  if (!mounted || id !== fetchIdRef.current) return;
1138
- if (e?.message?.includes("403") || e?.message?.includes("Forbidden")) {
1448
+ if (msg.includes("403") || msg.includes("Forbidden")) {
1139
1449
  const disabled = {
1140
1450
  ...DEFAULT_RESULT,
1141
1451
  paypalEnabled: false,
@@ -1165,9 +1475,10 @@ export {
1165
1475
  PayPalProvider,
1166
1476
  PayPalSmartButtons,
1167
1477
  createPayPalStoreApi,
1168
- isNextRedirectError,
1478
+ hideProcessingOverlay,
1169
1479
  isPayPalProviderId,
1170
1480
  markPaymentComplete,
1481
+ showProcessingOverlay,
1171
1482
  usePayPalConfig,
1172
1483
  usePayPalPaymentMethods
1173
1484
  };