@easypayment/medusa-paypal-ui 1.1.1 → 1.2.2

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
@@ -39,13 +39,17 @@ __export(index_exports, {
39
39
  PayPalPaymentSection: () => PayPalPaymentSection,
40
40
  PayPalProvider: () => PayPalProvider,
41
41
  PayPalSmartButtons: () => PayPalSmartButtons,
42
+ clearCartCaptured: () => clearCartCaptured,
42
43
  createPayPalStoreApi: () => createPayPalStoreApi,
44
+ generateIdempotencyKey: () => generateIdempotencyKey,
43
45
  hideProcessingOverlay: () => hideProcessingOverlay,
44
46
  isPayPalProviderId: () => isPayPalProviderId,
47
+ markCartCaptured: () => markCartCaptured,
45
48
  markPaymentComplete: () => markPaymentComplete,
46
49
  showProcessingOverlay: () => showProcessingOverlay,
47
50
  usePayPalConfig: () => usePayPalConfig,
48
- usePayPalPaymentMethods: () => usePayPalPaymentMethods
51
+ usePayPalPaymentMethods: () => usePayPalPaymentMethods,
52
+ wasCartCaptured: () => wasCartCaptured
49
53
  });
50
54
  module.exports = __toCommonJS(index_exports);
51
55
 
@@ -115,7 +119,7 @@ function createHttpClient(opts) {
115
119
  res = await fetch(url, {
116
120
  ...init,
117
121
  headers,
118
- credentials: "include",
122
+ credentials: opts.credentials ?? "include",
119
123
  signal: controller.signal
120
124
  });
121
125
  text = await res.text().catch(() => "");
@@ -184,6 +188,14 @@ function createHttpClient(opts) {
184
188
  }
185
189
 
186
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
+ }
187
199
  async function markPaymentComplete(baseUrl, cartId, publishableApiKey) {
188
200
  const http = createHttpClient({ baseUrl, publishableApiKey });
189
201
  return http.request(
@@ -209,7 +221,13 @@ function createPayPalStoreApi(opts) {
209
221
  createOrder(cartId, isCardPayment = false) {
210
222
  return http.request(`/store/paypal/create-order`, {
211
223
  method: "POST",
212
- 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
+ },
213
231
  body: JSON.stringify({ cart_id: cartId, is_card_payment: isCardPayment })
214
232
  });
215
233
  },
@@ -342,6 +360,31 @@ function PayPalCurrencyNotice({ config }) {
342
360
  var import_react3 = require("react");
343
361
  var import_react_paypal_js2 = require("@paypal/react-paypal-js");
344
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
+
345
388
  // src/utils/next-errors.ts
346
389
  function isNextRouterError(e) {
347
390
  if (typeof e !== "object" || e === null || !("digest" in e)) return false;
@@ -465,6 +508,13 @@ function PayPalSmartButtons(props) {
465
508
  window.addEventListener("pageshow", onPageShow);
466
509
  return () => window.removeEventListener("pageshow", onPageShow);
467
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]);
468
518
  (0, import_react3.useEffect)(() => {
469
519
  if (!isResolved || buttonsReady) {
470
520
  return;
@@ -480,6 +530,7 @@ function PayPalSmartButtons(props) {
480
530
  setError(null);
481
531
  try {
482
532
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
533
+ clearCartCaptured(cartId);
483
534
  await onPaid?.({ ...captured, ...completeResult });
484
535
  setCompletionPending(false);
485
536
  } catch (e) {
@@ -750,6 +801,7 @@ function PayPalSmartButtons(props) {
750
801
  if (!orderId) throw new Error("PayPal order ID is missing from approval response");
751
802
  const result = await api.captureOrder(cartId, orderId);
752
803
  capturedRef.current = result || {};
804
+ markCartCaptured(cartId);
753
805
  } catch (e) {
754
806
  if (isNextRouterError(e)) return;
755
807
  hideProcessingOverlay();
@@ -958,6 +1010,14 @@ function PayPalAdvancedCard(props) {
958
1010
  window.addEventListener("pageshow", onPageShow);
959
1011
  return () => window.removeEventListener("pageshow", onPageShow);
960
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]);
961
1021
  const finalizeCapturedPayment = import_react4.default.useCallback(async () => {
962
1022
  const captured = capturedRef.current;
963
1023
  if (!captured) return;
@@ -966,6 +1026,7 @@ function PayPalAdvancedCard(props) {
966
1026
  setError(null);
967
1027
  try {
968
1028
  const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey);
1029
+ clearCartCaptured(cartId);
969
1030
  await onPaid?.({ ...captured, ...completeResult });
970
1031
  setCompletionPending(false);
971
1032
  } catch (e) {
@@ -1187,6 +1248,7 @@ function PayPalAdvancedCard(props) {
1187
1248
  if (!orderId) throw new Error("PayPal order ID is missing from approval response");
1188
1249
  const result = await api.captureOrder(cartId, orderId);
1189
1250
  capturedRef.current = result || {};
1251
+ markCartCaptured(cartId);
1190
1252
  } catch (e) {
1191
1253
  if (isNextRouterError(e)) return;
1192
1254
  resetSubmitState();
@@ -1375,9 +1437,23 @@ function PayPalAdvancedCard(props) {
1375
1437
 
1376
1438
  // src/adapters/MedusaNextPayPalAdapter.tsx
1377
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
1378
1456
  var import_jsx_runtime5 = require("react/jsx-runtime");
1379
- var DEFAULT_PAYPAL_PROVIDER_ID = "pp_paypal_paypal";
1380
- var DEFAULT_PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
1381
1457
  var SPIN_STYLE3 = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`;
1382
1458
  function PayPalLoadingCard() {
1383
1459
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -1435,6 +1511,26 @@ function PayPalErrorCard({ message }) {
1435
1511
  }
1436
1512
  );
1437
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
+ }
1438
1534
  function MedusaNextPayPalAdapter(props) {
1439
1535
  const {
1440
1536
  cartId,
@@ -1446,8 +1542,8 @@ function MedusaNextPayPalAdapter(props) {
1446
1542
  onError,
1447
1543
  onPaid
1448
1544
  } = props;
1449
- const paypalProviderId = providerIds?.paypal || DEFAULT_PAYPAL_PROVIDER_ID;
1450
- 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;
1451
1547
  const shouldRender = selectedProviderId === paypalProviderId || selectedProviderId === paypalCardProviderId;
1452
1548
  const { config, loading, error } = usePayPalConfig({
1453
1549
  baseUrl,
@@ -1467,8 +1563,12 @@ function MedusaNextPayPalAdapter(props) {
1467
1563
  if (error) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalErrorCard, { message: error });
1468
1564
  if (!config) return null;
1469
1565
  const isCardProvider = selectedProviderId === paypalCardProviderId;
1470
- if (config.paypal_enabled === false && !isCardProvider) return null;
1471
- 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
+ }
1472
1572
  const disableFunding = Array.isArray(config.disable_buttons) ? config.disable_buttons.join(",") : void 0;
1473
1573
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "grid", gap: 12 }, children: [
1474
1574
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PayPalCurrencyNotice, { config }),
@@ -1505,18 +1605,7 @@ function MedusaNextPayPalAdapter(props) {
1505
1605
  }
1506
1606
 
1507
1607
  // src/components/PayPalPaymentSection.tsx
1508
- var import_react6 = require("react");
1509
1608
  var import_jsx_runtime6 = require("react/jsx-runtime");
1510
- var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
1511
- var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
1512
- var PAYPAL_PROVIDER_IDS = [
1513
- PAYPAL_WALLET_PROVIDER_ID,
1514
- PAYPAL_CARD_PROVIDER_ID
1515
- ];
1516
- function isPayPalProviderId(id) {
1517
- if (!id) return false;
1518
- return PAYPAL_PROVIDER_IDS.includes(id);
1519
- }
1520
1609
  var SPIN_STYLE4 = `@keyframes _pp_section_spin { to { transform: rotate(360deg) } }`;
1521
1610
  function SessionInitCard() {
1522
1611
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
@@ -1555,62 +1644,6 @@ function SessionInitCard() {
1555
1644
  }
1556
1645
  );
1557
1646
  }
1558
- function ConfigLoadingCard() {
1559
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1560
- "div",
1561
- {
1562
- role: "status",
1563
- "aria-label": "Connecting to PayPal",
1564
- style: {
1565
- display: "flex",
1566
- alignItems: "center",
1567
- gap: 12,
1568
- padding: "14px 16px",
1569
- background: "#f9fafb",
1570
- border: "1px solid #e5e7eb",
1571
- borderRadius: 10
1572
- },
1573
- children: [
1574
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("style", { children: SPIN_STYLE4 }),
1575
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1576
- "div",
1577
- {
1578
- style: {
1579
- width: 22,
1580
- height: 22,
1581
- borderRadius: "50%",
1582
- border: "2.5px solid #e5e7eb",
1583
- borderTopColor: "#0070ba",
1584
- animation: "_pp_section_spin .7s linear infinite",
1585
- flexShrink: 0
1586
- }
1587
- }
1588
- ),
1589
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
1590
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: "Connecting to PayPal\u2026" }),
1591
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { fontSize: 12, color: "#6b7280", marginTop: 2 }, children: "Setting up secure payment" })
1592
- ] })
1593
- ]
1594
- }
1595
- );
1596
- }
1597
- function ErrorCard({ message }) {
1598
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1599
- "div",
1600
- {
1601
- role: "alert",
1602
- style: {
1603
- padding: "12px 16px",
1604
- background: "#fef2f2",
1605
- border: "1px solid #fecaca",
1606
- borderRadius: 10,
1607
- fontSize: 13,
1608
- color: "#b91c1c"
1609
- },
1610
- children: message
1611
- }
1612
- );
1613
- }
1614
1647
  function PayPalPaymentSection({
1615
1648
  cartId,
1616
1649
  selectedProviderId,
@@ -1621,67 +1654,24 @@ function PayPalPaymentSection({
1621
1654
  onError,
1622
1655
  onPaid
1623
1656
  }) {
1624
- const shouldRender = isPayPalProviderId(selectedProviderId);
1625
- const { config, loading, error } = usePayPalConfig({
1626
- baseUrl,
1627
- publishableApiKey,
1628
- cartId,
1629
- enabled: shouldRender
1630
- });
1631
- const handlePaid = (0, import_react6.useCallback)(
1632
- async (captureResult) => {
1633
- onPaid?.(captureResult);
1634
- await onSuccess?.(cartId);
1635
- },
1636
- [cartId, onPaid, onSuccess]
1637
- );
1638
- if (!shouldRender) return null;
1657
+ if (!isPayPalProviderId(selectedProviderId)) return null;
1639
1658
  if (sessionLoading) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SessionInitCard, {});
1640
- if (loading) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ConfigLoadingCard, {});
1641
- if (error) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorCard, { message: error });
1642
- if (!config) return null;
1643
- if (config.paypal_enabled === false && selectedProviderId === PAYPAL_WALLET_PROVIDER_ID) {
1644
- return null;
1645
- }
1646
- const isCardProvider = selectedProviderId === PAYPAL_CARD_PROVIDER_ID;
1647
- if (isCardProvider && config.card_enabled === false) return null;
1648
- const disableFunding = Array.isArray(config.disable_buttons) ? config.disable_buttons.join(",") : void 0;
1649
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "grid", gap: 12 }, children: [
1650
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PayPalCurrencyNotice, { config }),
1651
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1652
- PayPalProvider,
1653
- {
1654
- config,
1655
- intent: config.intent === "authorize" ? "authorize" : "capture",
1656
- disableFunding,
1657
- children: isCardProvider ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1658
- PayPalAdvancedCard,
1659
- {
1660
- baseUrl,
1661
- publishableApiKey,
1662
- cartId,
1663
- config,
1664
- onPaid: handlePaid,
1665
- onError
1666
- }
1667
- ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1668
- PayPalSmartButtons,
1669
- {
1670
- baseUrl,
1671
- publishableApiKey,
1672
- cartId,
1673
- config,
1674
- onPaid: handlePaid,
1675
- onError
1676
- }
1677
- )
1678
- }
1679
- )
1680
- ] }, 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
+ );
1681
1671
  }
1682
1672
 
1683
1673
  // src/hooks/usePayPalPaymentMethods.ts
1684
- var import_react7 = require("react");
1674
+ var import_react6 = require("react");
1685
1675
  var MAX_CACHE_ENTRIES2 = 50;
1686
1676
  var _cache2 = /* @__PURE__ */ new Map();
1687
1677
  var CACHE_TTL2 = 5 * 60 * 1e3;
@@ -1700,7 +1690,8 @@ var DEFAULT_RESULT = {
1700
1690
  paypalTitle: "PayPal",
1701
1691
  cardEnabled: true,
1702
1692
  cardTitle: "Credit or Debit Card",
1703
- loading: false
1693
+ loading: false,
1694
+ error: null
1704
1695
  };
1705
1696
  function usePayPalPaymentMethods({
1706
1697
  baseUrl,
@@ -1708,16 +1699,16 @@ function usePayPalPaymentMethods({
1708
1699
  cartId,
1709
1700
  enabled = true
1710
1701
  }) {
1711
- const api = (0, import_react7.useMemo)(
1702
+ const api = (0, import_react6.useMemo)(
1712
1703
  () => createPayPalStoreApi({ baseUrl, publishableApiKey }),
1713
1704
  [baseUrl, publishableApiKey]
1714
1705
  );
1715
1706
  const key = cacheKey2(baseUrl, cartId);
1716
1707
  const hit = _cache2.get(key);
1717
1708
  const seed = hit && Date.now() - hit.at < CACHE_TTL2 ? hit.result : null;
1718
- const [result, setResult] = (0, import_react7.useState)(seed ?? { ...DEFAULT_RESULT, loading: enabled });
1719
- const fetchIdRef = (0, import_react7.useRef)(0);
1720
- (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)(() => {
1721
1712
  if (!enabled) {
1722
1713
  setResult((prev) => ({ ...prev, loading: false }));
1723
1714
  return;
@@ -1741,7 +1732,8 @@ function usePayPalPaymentMethods({
1741
1732
  paypalTitle: typeof cfg.paypal_title === "string" && cfg.paypal_title ? cfg.paypal_title : "PayPal",
1742
1733
  cardEnabled: cfg.card_enabled !== false,
1743
1734
  cardTitle: typeof cfg.card_title === "string" && cfg.card_title ? cfg.card_title : "Credit or Debit Card",
1744
- loading: false
1735
+ loading: false,
1736
+ error: null
1745
1737
  };
1746
1738
  cacheSet2(k, { result: next, at: Date.now() });
1747
1739
  setResult(next);
@@ -1754,12 +1746,17 @@ function usePayPalPaymentMethods({
1754
1746
  ...DEFAULT_RESULT,
1755
1747
  paypalEnabled: false,
1756
1748
  cardEnabled: false,
1757
- loading: false
1749
+ loading: false,
1750
+ error: msg || "PayPal is disabled"
1758
1751
  };
1759
1752
  setResult(disabled);
1760
1753
  return;
1761
1754
  }
1762
- setResult({ ...DEFAULT_RESULT, loading: false });
1755
+ setResult({
1756
+ ...DEFAULT_RESULT,
1757
+ loading: false,
1758
+ error: msg || "Failed to load PayPal payment methods"
1759
+ });
1763
1760
  }
1764
1761
  })();
1765
1762
  return () => {
@@ -1779,12 +1776,16 @@ function usePayPalPaymentMethods({
1779
1776
  PayPalPaymentSection,
1780
1777
  PayPalProvider,
1781
1778
  PayPalSmartButtons,
1779
+ clearCartCaptured,
1782
1780
  createPayPalStoreApi,
1781
+ generateIdempotencyKey,
1783
1782
  hideProcessingOverlay,
1784
1783
  isPayPalProviderId,
1784
+ markCartCaptured,
1785
1785
  markPaymentComplete,
1786
1786
  showProcessingOverlay,
1787
1787
  usePayPalConfig,
1788
- usePayPalPaymentMethods
1788
+ usePayPalPaymentMethods,
1789
+ wasCartCaptured
1789
1790
  });
1790
1791
  //# sourceMappingURL=index.cjs.map