@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/order.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Provider ids registered by `@easypayment/medusa-payment-paypal`.
3
+ *
4
+ * Single source of truth — `PayPalPaymentSection`, `MedusaNextPayPalAdapter`,
5
+ * and the server-safe `order` entrypoint all re-export from here so the ids
6
+ * can never drift between entrypoints.
7
+ */
8
+ declare const PAYPAL_WALLET_PROVIDER_ID: "pp_paypal_paypal";
9
+ declare const PAYPAL_CARD_PROVIDER_ID: "pp_paypal_card_paypal_card";
10
+ /** True when the provider id belongs to this PayPal plugin. */
11
+ declare function isPayPalProviderId(providerId?: string | null): boolean;
12
+
1
13
  /**
2
14
  * Server-safe helpers for displaying PayPal payments on order / confirmation
3
15
  * pages.
@@ -23,8 +35,7 @@
23
35
  * )}
24
36
  * </Text>
25
37
  */
26
- declare const PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
27
- declare const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
38
+
28
39
  type PayPalPaymentInfo = {
29
40
  title: string;
30
41
  };
@@ -35,8 +46,6 @@ type PayPalConfigTitles = {
35
46
  paypal_title?: string | null;
36
47
  card_title?: string | null;
37
48
  };
38
- /** True when the provider id belongs to this PayPal plugin. */
39
- declare function isPayPalProviderId(providerId?: string | null): boolean;
40
49
  /**
41
50
  * Null-safe payment label resolver for order / confirmation pages.
42
51
  *
package/dist/order.mjs CHANGED
@@ -1,13 +1,22 @@
1
- // src/order.ts
1
+ // src/constants.ts
2
2
  var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
3
3
  var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
4
+ var PAYPAL_PROVIDER_IDS = [
5
+ PAYPAL_WALLET_PROVIDER_ID,
6
+ PAYPAL_CARD_PROVIDER_ID
7
+ ];
8
+ function isPayPalProviderId(providerId) {
9
+ if (!providerId) return false;
10
+ return PAYPAL_PROVIDER_IDS.includes(
11
+ providerId
12
+ );
13
+ }
14
+
15
+ // src/order.ts
4
16
  var paypalPaymentInfoMap = {
5
17
  [PAYPAL_WALLET_PROVIDER_ID]: { title: "PayPal" },
6
18
  [PAYPAL_CARD_PROVIDER_ID]: { title: "Credit or Debit Card" }
7
19
  };
8
- function isPayPalProviderId(providerId) {
9
- return providerId === PAYPAL_WALLET_PROVIDER_ID || providerId === PAYPAL_CARD_PROVIDER_ID;
10
- }
11
20
  function getPaymentLabel(providerId, fallback, titles) {
12
21
  if (!providerId) {
13
22
  return fallback ?? "";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/order.ts"],"sourcesContent":["/**\r\n * Server-safe helpers for displaying PayPal payments on order / confirmation\r\n * pages.\r\n *\r\n * This module is intentionally free of React and any client-only code, so it\r\n * can be imported from Next.js **server components** (e.g. the order\r\n * confirmation page) without pulling in the checkout UI.\r\n *\r\n * @example\r\n * import { getPaymentLabel, fetchPayPalConfig } from \"@easypayment/medusa-paypal-ui/order\"\r\n *\r\n * // In the (async) server component that renders the payment method:\r\n * const titles = await fetchPayPalConfig({\r\n * baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,\r\n * publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,\r\n * })\r\n *\r\n * <Text>\r\n * {getPaymentLabel(\r\n * payment.provider_id,\r\n * paymentInfoMap[payment.provider_id]?.title,\r\n * titles\r\n * )}\r\n * </Text>\r\n */\r\n\r\nexport const PAYPAL_WALLET_PROVIDER_ID = \"pp_paypal_paypal\"\r\nexport const PAYPAL_CARD_PROVIDER_ID = \"pp_paypal_card_paypal_card\"\r\n\r\nexport type PayPalPaymentInfo = { title: string }\r\n\r\n/** Built-in fallback titles, used only when admin titles aren't supplied. */\r\nexport const paypalPaymentInfoMap: Record<string, PayPalPaymentInfo> = {\r\n [PAYPAL_WALLET_PROVIDER_ID]: { title: \"PayPal\" },\r\n [PAYPAL_CARD_PROVIDER_ID]: { title: \"Credit or Debit Card\" },\r\n}\r\n\r\n/** Admin-configured titles, as returned by `GET /store/paypal/config`. */\r\nexport type PayPalConfigTitles = {\r\n paypal_title?: string | null\r\n card_title?: string | null\r\n}\r\n\r\n/** True when the provider id belongs to this PayPal plugin. */\r\nexport function isPayPalProviderId(providerId?: string | null): boolean {\r\n return (\r\n providerId === PAYPAL_WALLET_PROVIDER_ID ||\r\n providerId === PAYPAL_CARD_PROVIDER_ID\r\n )\r\n}\r\n\r\n/**\r\n * Null-safe payment label resolver for order / confirmation pages.\r\n *\r\n * Precedence for the PayPal providers: **admin-configured title** (`titles`,\r\n * from {@link fetchPayPalConfig}) → built-in default. For any other provider:\r\n * `fallback` → raw id. Never throws on an unknown / missing provider, which is\r\n * what causes the common `Cannot read properties of undefined (reading 'title')`\r\n * crash on the order confirmation page.\r\n *\r\n * @param titles Admin titles so the order page shows the same labels configured\r\n * in Medusa Admin → Settings → PayPal (e.g. a custom \"Credit or Debit Card\").\r\n */\r\nexport function getPaymentLabel(\r\n providerId: string | undefined | null,\r\n fallback?: string,\r\n titles?: PayPalConfigTitles\r\n): string {\r\n if (!providerId) {\r\n return fallback ?? \"\"\r\n }\r\n if (providerId === PAYPAL_WALLET_PROVIDER_ID) {\r\n return (\r\n titles?.paypal_title?.trim() ||\r\n paypalPaymentInfoMap[PAYPAL_WALLET_PROVIDER_ID].title\r\n )\r\n }\r\n if (providerId === PAYPAL_CARD_PROVIDER_ID) {\r\n return (\r\n titles?.card_title?.trim() ||\r\n paypalPaymentInfoMap[PAYPAL_CARD_PROVIDER_ID].title\r\n )\r\n }\r\n return fallback ?? providerId\r\n}\r\n\r\n/**\r\n * Fetch the admin-configured PayPal titles from the storefront API\r\n * (`GET /store/paypal/config`). Server-safe (uses the global `fetch`).\r\n *\r\n * Returns `{}` on any error/network failure so callers can fall back to the\r\n * built-in defaults without their own try/catch.\r\n */\r\nexport async function fetchPayPalConfig(opts: {\r\n baseUrl: string\r\n publishableApiKey?: string\r\n signal?: AbortSignal\r\n}): Promise<PayPalConfigTitles> {\r\n const { baseUrl, publishableApiKey, signal } = opts\r\n if (!baseUrl) {\r\n return {}\r\n }\r\n try {\r\n const timeoutMs = 30_000\r\n let effectiveSignal = signal\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined\r\n if (!signal) {\r\n const controller = new AbortController()\r\n timeoutId = setTimeout(() => controller.abort(), timeoutMs)\r\n effectiveSignal = controller.signal\r\n }\r\n let res: Response\r\n try {\r\n res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/store/paypal/config`, {\r\n headers: {\r\n accept: \"application/json\",\r\n ...(publishableApiKey ? { \"x-publishable-api-key\": publishableApiKey } : {}),\r\n },\r\n signal: effectiveSignal,\r\n })\r\n } finally {\r\n if (timeoutId !== undefined) clearTimeout(timeoutId)\r\n }\r\n if (!res.ok) {\r\n return {}\r\n }\r\n const cfg = (await res.json()) as Record<string, unknown>\r\n return {\r\n paypal_title:\r\n typeof cfg.paypal_title === \"string\" ? cfg.paypal_title : undefined,\r\n card_title:\r\n typeof cfg.card_title === \"string\" ? cfg.card_title : undefined,\r\n }\r\n } catch {\r\n return {}\r\n }\r\n}\r\n"],"mappings":";AA0BO,IAAM,4BAA4B;AAClC,IAAM,0BAA0B;AAKhC,IAAM,uBAA0D;AAAA,EACrE,CAAC,yBAAyB,GAAG,EAAE,OAAO,SAAS;AAAA,EAC/C,CAAC,uBAAuB,GAAG,EAAE,OAAO,uBAAuB;AAC7D;AASO,SAAS,mBAAmB,YAAqC;AACtE,SACE,eAAe,6BACf,eAAe;AAEnB;AAcO,SAAS,gBACd,YACA,UACA,QACQ;AACR,MAAI,CAAC,YAAY;AACf,WAAO,YAAY;AAAA,EACrB;AACA,MAAI,eAAe,2BAA2B;AAC5C,WACE,QAAQ,cAAc,KAAK,KAC3B,qBAAqB,yBAAyB,EAAE;AAAA,EAEpD;AACA,MAAI,eAAe,yBAAyB;AAC1C,WACE,QAAQ,YAAY,KAAK,KACzB,qBAAqB,uBAAuB,EAAE;AAAA,EAElD;AACA,SAAO,YAAY;AACrB;AASA,eAAsB,kBAAkB,MAIR;AAC9B,QAAM,EAAE,SAAS,mBAAmB,OAAO,IAAI;AAC/C,MAAI,CAAC,SAAS;AACZ,WAAO,CAAC;AAAA,EACV;AACA,MAAI;AACF,UAAM,YAAY;AAClB,QAAI,kBAAkB;AACtB,QAAI;AACJ,QAAI,CAAC,QAAQ;AACX,YAAM,aAAa,IAAI,gBAAgB;AACvC,kBAAY,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC1D,wBAAkB,WAAW;AAAA,IAC/B;AACA,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC,wBAAwB;AAAA,QACrE,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,GAAI,oBAAoB,EAAE,yBAAyB,kBAAkB,IAAI,CAAC;AAAA,QAC5E;AAAA,QACA,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,UAAE;AACA,UAAI,cAAc,OAAW,cAAa,SAAS;AAAA,IACrD;AACA,QAAI,CAAC,IAAI,IAAI;AACX,aAAO,CAAC;AAAA,IACV;AACA,UAAM,MAAO,MAAM,IAAI,KAAK;AAC5B,WAAO;AAAA,MACL,cACE,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAAA,MAC5D,YACE,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,IAC1D;AAAA,EACF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/constants.ts","../src/order.ts"],"sourcesContent":["/**\n * Provider ids registered by `@easypayment/medusa-payment-paypal`.\n *\n * Single source of truth — `PayPalPaymentSection`, `MedusaNextPayPalAdapter`,\n * and the server-safe `order` entrypoint all re-export from here so the ids\n * can never drift between entrypoints.\n */\nexport const PAYPAL_WALLET_PROVIDER_ID = \"pp_paypal_paypal\" as const\nexport const PAYPAL_CARD_PROVIDER_ID = \"pp_paypal_card_paypal_card\" as const\n\nexport const PAYPAL_PROVIDER_IDS = [\n PAYPAL_WALLET_PROVIDER_ID,\n PAYPAL_CARD_PROVIDER_ID,\n] as const\n\n/** True when the provider id belongs to this PayPal plugin. */\nexport function isPayPalProviderId(providerId?: string | null): boolean {\n if (!providerId) return false\n return PAYPAL_PROVIDER_IDS.includes(\n providerId as (typeof PAYPAL_PROVIDER_IDS)[number]\n )\n}\n","/**\r\n * Server-safe helpers for displaying PayPal payments on order / confirmation\r\n * pages.\r\n *\r\n * This module is intentionally free of React and any client-only code, so it\r\n * can be imported from Next.js **server components** (e.g. the order\r\n * confirmation page) without pulling in the checkout UI.\r\n *\r\n * @example\r\n * import { getPaymentLabel, fetchPayPalConfig } from \"@easypayment/medusa-paypal-ui/order\"\r\n *\r\n * // In the (async) server component that renders the payment method:\r\n * const titles = await fetchPayPalConfig({\r\n * baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,\r\n * publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,\r\n * })\r\n *\r\n * <Text>\r\n * {getPaymentLabel(\r\n * payment.provider_id,\r\n * paymentInfoMap[payment.provider_id]?.title,\r\n * titles\r\n * )}\r\n * </Text>\r\n */\r\n\r\nimport {\r\n PAYPAL_WALLET_PROVIDER_ID,\r\n PAYPAL_CARD_PROVIDER_ID,\r\n isPayPalProviderId,\r\n} from \"./constants\"\r\n\r\nexport { PAYPAL_WALLET_PROVIDER_ID, PAYPAL_CARD_PROVIDER_ID, isPayPalProviderId }\r\n\r\nexport type PayPalPaymentInfo = { title: string }\r\n\r\n/** Built-in fallback titles, used only when admin titles aren't supplied. */\r\nexport const paypalPaymentInfoMap: Record<string, PayPalPaymentInfo> = {\r\n [PAYPAL_WALLET_PROVIDER_ID]: { title: \"PayPal\" },\r\n [PAYPAL_CARD_PROVIDER_ID]: { title: \"Credit or Debit Card\" },\r\n}\r\n\r\n/** Admin-configured titles, as returned by `GET /store/paypal/config`. */\r\nexport type PayPalConfigTitles = {\r\n paypal_title?: string | null\r\n card_title?: string | null\r\n}\r\n\r\n/**\r\n * Null-safe payment label resolver for order / confirmation pages.\r\n *\r\n * Precedence for the PayPal providers: **admin-configured title** (`titles`,\r\n * from {@link fetchPayPalConfig}) → built-in default. For any other provider:\r\n * `fallback` → raw id. Never throws on an unknown / missing provider, which is\r\n * what causes the common `Cannot read properties of undefined (reading 'title')`\r\n * crash on the order confirmation page.\r\n *\r\n * @param titles Admin titles so the order page shows the same labels configured\r\n * in Medusa Admin → Settings → PayPal (e.g. a custom \"Credit or Debit Card\").\r\n */\r\nexport function getPaymentLabel(\r\n providerId: string | undefined | null,\r\n fallback?: string,\r\n titles?: PayPalConfigTitles\r\n): string {\r\n if (!providerId) {\r\n return fallback ?? \"\"\r\n }\r\n if (providerId === PAYPAL_WALLET_PROVIDER_ID) {\r\n return (\r\n titles?.paypal_title?.trim() ||\r\n paypalPaymentInfoMap[PAYPAL_WALLET_PROVIDER_ID].title\r\n )\r\n }\r\n if (providerId === PAYPAL_CARD_PROVIDER_ID) {\r\n return (\r\n titles?.card_title?.trim() ||\r\n paypalPaymentInfoMap[PAYPAL_CARD_PROVIDER_ID].title\r\n )\r\n }\r\n return fallback ?? providerId\r\n}\r\n\r\n/**\r\n * Fetch the admin-configured PayPal titles from the storefront API\r\n * (`GET /store/paypal/config`). Server-safe (uses the global `fetch`).\r\n *\r\n * Returns `{}` on any error/network failure so callers can fall back to the\r\n * built-in defaults without their own try/catch.\r\n */\r\nexport async function fetchPayPalConfig(opts: {\r\n baseUrl: string\r\n publishableApiKey?: string\r\n signal?: AbortSignal\r\n}): Promise<PayPalConfigTitles> {\r\n const { baseUrl, publishableApiKey, signal } = opts\r\n if (!baseUrl) {\r\n return {}\r\n }\r\n try {\r\n const timeoutMs = 30_000\r\n let effectiveSignal = signal\r\n let timeoutId: ReturnType<typeof setTimeout> | undefined\r\n if (!signal) {\r\n const controller = new AbortController()\r\n timeoutId = setTimeout(() => controller.abort(), timeoutMs)\r\n effectiveSignal = controller.signal\r\n }\r\n let res: Response\r\n try {\r\n res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/store/paypal/config`, {\r\n headers: {\r\n accept: \"application/json\",\r\n ...(publishableApiKey ? { \"x-publishable-api-key\": publishableApiKey } : {}),\r\n },\r\n signal: effectiveSignal,\r\n })\r\n } finally {\r\n if (timeoutId !== undefined) clearTimeout(timeoutId)\r\n }\r\n if (!res.ok) {\r\n return {}\r\n }\r\n const cfg = (await res.json()) as Record<string, unknown>\r\n return {\r\n paypal_title:\r\n typeof cfg.paypal_title === \"string\" ? cfg.paypal_title : undefined,\r\n card_title:\r\n typeof cfg.card_title === \"string\" ? cfg.card_title : undefined,\r\n }\r\n } catch {\r\n return {}\r\n }\r\n}\r\n"],"mappings":";AAOO,IAAM,4BAA4B;AAClC,IAAM,0BAA0B;AAEhC,IAAM,sBAAsB;AAAA,EACjC;AAAA,EACA;AACF;AAGO,SAAS,mBAAmB,YAAqC;AACtE,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,oBAAoB;AAAA,IACzB;AAAA,EACF;AACF;;;ACgBO,IAAM,uBAA0D;AAAA,EACrE,CAAC,yBAAyB,GAAG,EAAE,OAAO,SAAS;AAAA,EAC/C,CAAC,uBAAuB,GAAG,EAAE,OAAO,uBAAuB;AAC7D;AAoBO,SAAS,gBACd,YACA,UACA,QACQ;AACR,MAAI,CAAC,YAAY;AACf,WAAO,YAAY;AAAA,EACrB;AACA,MAAI,eAAe,2BAA2B;AAC5C,WACE,QAAQ,cAAc,KAAK,KAC3B,qBAAqB,yBAAyB,EAAE;AAAA,EAEpD;AACA,MAAI,eAAe,yBAAyB;AAC1C,WACE,QAAQ,YAAY,KAAK,KACzB,qBAAqB,uBAAuB,EAAE;AAAA,EAElD;AACA,SAAO,YAAY;AACrB;AASA,eAAsB,kBAAkB,MAIR;AAC9B,QAAM,EAAE,SAAS,mBAAmB,OAAO,IAAI;AAC/C,MAAI,CAAC,SAAS;AACZ,WAAO,CAAC;AAAA,EACV;AACA,MAAI;AACF,UAAM,YAAY;AAClB,QAAI,kBAAkB;AACtB,QAAI;AACJ,QAAI,CAAC,QAAQ;AACX,YAAM,aAAa,IAAI,gBAAgB;AACvC,kBAAY,WAAW,MAAM,WAAW,MAAM,GAAG,SAAS;AAC1D,wBAAkB,WAAW;AAAA,IAC/B;AACA,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC,wBAAwB;AAAA,QACrE,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,GAAI,oBAAoB,EAAE,yBAAyB,kBAAkB,IAAI,CAAC;AAAA,QAC5E;AAAA,QACA,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,UAAE;AACA,UAAI,cAAc,OAAW,cAAa,SAAS;AAAA,IACrD;AACA,QAAI,CAAC,IAAI,IAAI;AACX,aAAO,CAAC;AAAA,IACV;AACA,UAAM,MAAO,MAAM,IAAI,KAAK;AAC5B,WAAO;AAAA,MACL,cACE,OAAO,IAAI,iBAAiB,WAAW,IAAI,eAAe;AAAA,MAC5D,YACE,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,IAC1D;AAAA,EACF,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
package/package.json CHANGED
@@ -1,65 +1,76 @@
1
- {
2
- "name": "@easypayment/medusa-paypal-ui",
3
- "version": "1.1.0",
4
- "description": "Enterprise Gold PayPal UI module for Medusa v2 storefront (Next.js)",
5
- "license": "MIT",
6
- "type": "module",
7
- "sideEffects": false,
8
- "main": "./dist/index.cjs",
9
- "module": "./dist/index.mjs",
10
- "types": "./dist/index.d.ts",
11
- "files": [
12
- "dist",
13
- "src",
14
- "README.md",
15
- "CHANGELOG.md",
16
- "LICENSE"
17
- ],
18
- "exports": {
19
- ".": {
20
- "types": "./dist/index.d.ts",
21
- "import": "./dist/index.mjs",
22
- "require": "./dist/index.cjs"
23
- },
24
- "./order": {
25
- "types": "./dist/order.d.ts",
26
- "import": "./dist/order.mjs",
27
- "require": "./dist/order.cjs"
28
- },
29
- "./package.json": "./package.json"
30
- },
31
- "peerDependencies": {
32
- "next": ">=14",
33
- "react": "^18.0.0 || ^19.0.0",
34
- "react-dom": "^18.0.0 || ^19.0.0"
35
- },
36
- "peerDependenciesMeta": {
37
- "next": {
38
- "optional": true
39
- }
40
- },
41
- "dependencies": {
42
- "@paypal/react-paypal-js": "^9.2.0"
43
- },
44
- "devDependencies": {
45
- "@testing-library/jest-dom": "^6.5.0",
46
- "@testing-library/react": "^16.0.0",
47
- "@types/react": "^19.2.14",
48
- "@types/react-dom": "^19.2.3",
49
- "jsdom": "^24.0.0",
50
- "tsup": "^8.0.0",
51
- "typescript": "^5.0.0",
52
- "vitest": "^2.0.5"
53
- },
54
- "scripts": {
55
- "build": "tsup",
56
- "dev": "tsup --watch",
57
- "test": "vitest run",
58
- "test:watch": "vitest",
59
- "prepublishOnly": "npm run build"
60
- },
61
- "repository": {
62
- "type": "git",
63
- "url": "https://github.com/easypaymentplugins/medusa-paypal-ui"
64
- }
65
- }
1
+ {
2
+ "name": "@easypayment/medusa-paypal-ui",
3
+ "version": "1.2.0",
4
+ "description": "Enterprise Gold PayPal UI module for Medusa v2 storefront (Next.js)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "README.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.mjs"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "./order": {
30
+ "import": {
31
+ "types": "./dist/order.d.ts",
32
+ "default": "./dist/order.mjs"
33
+ },
34
+ "require": {
35
+ "types": "./dist/order.d.cts",
36
+ "default": "./dist/order.cjs"
37
+ }
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "peerDependencies": {
42
+ "next": ">=14",
43
+ "react": "^18.0.0 || ^19.0.0",
44
+ "react-dom": "^18.0.0 || ^19.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "next": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "dependencies": {
52
+ "@paypal/react-paypal-js": "^9.2.0"
53
+ },
54
+ "devDependencies": {
55
+ "@testing-library/jest-dom": "^6.5.0",
56
+ "@testing-library/react": "^16.0.0",
57
+ "@types/react": "^19.2.14",
58
+ "@types/react-dom": "^19.2.3",
59
+ "jsdom": "^24.0.0",
60
+ "tsup": "^8.0.0",
61
+ "typescript": "^5.0.0",
62
+ "vitest": "^2.0.5"
63
+ },
64
+ "scripts": {
65
+ "build": "tsup",
66
+ "dev": "tsup --watch",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
69
+ "prepublishOnly": "npm run build",
70
+ "typecheck": "tsc --noEmit"
71
+ },
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "https://github.com/easypaymentplugins/medusa-paypal-ui"
75
+ }
76
+ }
@@ -5,11 +5,12 @@ import { PayPalCurrencyNotice } from "../components/PayPalCurrencyNotice"
5
5
  import { PayPalAdvancedCard } from "../components/PayPalAdvancedCard"
6
6
  import { PayPalProvider } from "../components/PayPalProvider"
7
7
  import { PayPalSmartButtons } from "../components/PayPalSmartButtons"
8
+ import {
9
+ PAYPAL_WALLET_PROVIDER_ID as DEFAULT_PAYPAL_PROVIDER_ID,
10
+ PAYPAL_CARD_PROVIDER_ID as DEFAULT_PAYPAL_CARD_PROVIDER_ID,
11
+ } from "../constants"
8
12
  import { usePayPalConfig } from "../hooks/usePayPalConfig"
9
13
 
10
- const DEFAULT_PAYPAL_PROVIDER_ID = "pp_paypal_paypal"
11
- const DEFAULT_PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card"
12
-
13
14
  const SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`
14
15
 
15
16
  function PayPalLoadingCard() {
@@ -69,6 +70,30 @@ function PayPalErrorCard({ message }: { message: string }) {
69
70
  )
70
71
  }
71
72
 
73
+ /**
74
+ * Shown when the merchant has disabled the selected PayPal method in admin
75
+ * settings. Rendering nothing here (the old behavior) left the buyer staring
76
+ * at blank space under a selected payment option.
77
+ */
78
+ function PayPalUnavailableCard({ label }: { label: string }) {
79
+ return (
80
+ <div
81
+ role="status"
82
+ style={{
83
+ padding: "12px 16px",
84
+ background: "#f9fafb",
85
+ border: "1px solid #e5e7eb",
86
+ borderRadius: 10,
87
+ fontSize: 13,
88
+ color: "#6b7280",
89
+ }}
90
+ >
91
+ {label} is currently unavailable. Please choose a different payment
92
+ method.
93
+ </div>
94
+ )
95
+ }
96
+
72
97
  export type MedusaNextPayPalAdapterProps = {
73
98
  cartId: string
74
99
  selectedProviderId: string | null | undefined
@@ -126,8 +151,12 @@ export function MedusaNextPayPalAdapter(props: MedusaNextPayPalAdapterProps) {
126
151
  if (!config) return null
127
152
 
128
153
  const isCardProvider = selectedProviderId === paypalCardProviderId
129
- if (config.paypal_enabled === false && !isCardProvider) return null
130
- if (isCardProvider && config.card_enabled === false) return null
154
+ if (config.paypal_enabled === false && !isCardProvider) {
155
+ return <PayPalUnavailableCard label={config.paypal_title || "PayPal"} />
156
+ }
157
+ if (isCardProvider && config.card_enabled === false) {
158
+ return <PayPalUnavailableCard label={config.card_title || "Card payment"} />
159
+ }
131
160
 
132
161
  const disableFunding = Array.isArray(config.disable_buttons)
133
162
  ? config.disable_buttons.join(",")
@@ -1,6 +1,14 @@
1
1
  export type HttpOptions = {
2
2
  baseUrl: string
3
3
  publishableApiKey?: string
4
+ /**
5
+ * `credentials` mode for every request. Defaults to "include" (the
6
+ * historical behavior, required when the backend sits behind cookie-based
7
+ * gateways like Cloudflare Access). Set to "omit" or "same-origin" when the
8
+ * backend answers with `Access-Control-Allow-Origin: *` — browsers reject
9
+ * credentialed requests against a wildcard CORS origin outright.
10
+ */
11
+ credentials?: RequestCredentials
4
12
  }
5
13
 
6
14
  export type RequestOptions = {
@@ -35,6 +43,21 @@ function isRetryableError(err: unknown): boolean {
35
43
 
36
44
  const wait = (ms: number) => new Promise((r) => setTimeout(r, ms))
37
45
 
46
+ /** Best-effort extraction of a `{ message: string }` payload from a response body. */
47
+ function extractJsonMessage(text: string): string | null {
48
+ if (!text) return null
49
+ try {
50
+ const parsed = JSON.parse(text)
51
+ const message = parsed?.message
52
+ if (typeof message === "string" && message.trim()) {
53
+ return message.trim().slice(0, 500)
54
+ }
55
+ } catch {
56
+ // not JSON — caller falls back to the raw text
57
+ }
58
+ return null
59
+ }
60
+
38
61
  function toHeaderRecord(headers?: RequestInit["headers"]): Record<string, string> {
39
62
  if (!headers) {
40
63
  return {}
@@ -65,41 +88,70 @@ export function createHttpClient(opts: HttpOptions) {
65
88
  headers["x-publishable-api-key"] = opts.publishableApiKey
66
89
  }
67
90
 
91
+ // Always arm the timeout — a caller-supplied signal composes with it
92
+ // rather than replacing it. Without this, hook-issued requests (which
93
+ // always pass an abort signal) could hang forever on a stalled
94
+ // connection. The timer also covers reading the body: a server that sends
95
+ // headers then stalls the response stream would otherwise hang the
96
+ // request indefinitely with the old header-only timeout.
68
97
  const timeoutMs = 30_000
69
- let timeoutId: ReturnType<typeof setTimeout> | undefined
70
98
  const controller = new AbortController()
71
- if (!init?.signal) {
72
- timeoutId = setTimeout(() => controller.abort(), timeoutMs)
99
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
100
+ const callerSignal = init?.signal ?? null
101
+ const onCallerAbort = () => controller.abort()
102
+ if (callerSignal) {
103
+ if (callerSignal.aborted) controller.abort()
104
+ else callerSignal.addEventListener("abort", onCallerAbort)
73
105
  }
74
- const effectiveSignal = init?.signal || controller.signal
75
106
  let res: Response
107
+ let text: string
76
108
  try {
77
- res = await fetch(url, { ...init, headers, credentials: "include", signal: effectiveSignal })
109
+ res = await fetch(url, {
110
+ ...init,
111
+ headers,
112
+ credentials: opts.credentials ?? "include",
113
+ signal: controller.signal,
114
+ })
115
+ text = await res.text().catch(() => "")
78
116
  } catch (err) {
79
- if (err instanceof Error && err.name === "AbortError" && !init?.signal) {
117
+ if (
118
+ err instanceof Error &&
119
+ err.name === "AbortError" &&
120
+ !(callerSignal && callerSignal.aborted)
121
+ ) {
80
122
  throw new Error(`[PayPal] Request to ${path} timed out after ${timeoutMs / 1000}s`)
81
123
  }
82
124
  throw err
83
125
  } finally {
84
- if (timeoutId !== undefined) clearTimeout(timeoutId)
126
+ clearTimeout(timeoutId)
127
+ if (callerSignal) callerSignal.removeEventListener("abort", onCallerAbort)
85
128
  }
86
- const text = await res.text().catch(() => "")
87
129
 
88
130
  if (!res.ok) {
131
+ // Backend routes return structured JSON errors ({ message }) written for
132
+ // the buyer (e.g. unsupported currency, rate-limit, "payment processed
133
+ // but order not finalized"). Prefer that message over raw response text
134
+ // or a generic hint so the user sees something actionable instead of a
135
+ // JSON blob.
136
+ const parsedMessage = extractJsonMessage(text)
89
137
  if (res.status === 401) {
90
138
  throw new HttpError(
91
- "[PayPal] Unauthorized (401) — check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
139
+ parsedMessage ||
140
+ "[PayPal] Unauthorized (401) — check that your publishable API key is correct and set in NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
92
141
  401
93
142
  )
94
143
  }
95
144
  if (res.status === 403) {
96
145
  throw new HttpError(
97
- "[PayPal] Forbidden (403) — this request is not allowed. Check your CORS and API key settings.",
146
+ parsedMessage ||
147
+ "[PayPal] Forbidden (403) — this request is not allowed. Check your CORS and API key settings.",
98
148
  403
99
149
  )
100
150
  }
101
151
  throw new HttpError(
102
- text.slice(0, 500).replace(/<[^>]*>/g, "") || `Request failed (${res.status})`,
152
+ parsedMessage ||
153
+ text.slice(0, 500).replace(/<[^>]*>/g, "") ||
154
+ `Request failed (${res.status})`,
103
155
  res.status
104
156
  )
105
157
  }
@@ -1,6 +1,26 @@
1
1
  import type { PayPalConfig, PayPalSettingsResponse } from "./types"
2
2
  import { createHttpClient, type HttpOptions } from "./http"
3
3
 
4
+ /**
5
+ * Fresh idempotency key per create-order ATTEMPT (i.e. per buyer click).
6
+ *
7
+ * The backend derives the PayPal-Request-Id from this header. It must be
8
+ * unique per attempt: PayPal replays the cached response for a reused request
9
+ * id, so a key stable across cart changes would hand back the ORIGINAL order —
10
+ * at the original total — after the buyer edits the cart, charging a stale
11
+ * amount. Reuse/dedup of an unchanged order is handled server-side by the
12
+ * stored-order staleness check, not by this key.
13
+ */
14
+ export function generateIdempotencyKey(): string {
15
+ try {
16
+ const c = (globalThis as { crypto?: Crypto }).crypto
17
+ if (c?.randomUUID) return c.randomUUID()
18
+ } catch {
19
+ // fall through
20
+ }
21
+ return `pp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`
22
+ }
23
+
4
24
  export async function markPaymentComplete(
5
25
  baseUrl: string,
6
26
  cartId: string,
@@ -38,7 +58,13 @@ export function createPayPalStoreApi(opts: HttpOptions) {
38
58
  createOrder(cartId: string, isCardPayment = false) {
39
59
  return http.request<{ id: string }>(`/store/paypal/create-order`, {
40
60
  method: "POST",
41
- headers: { "Content-Type": "application/json" },
61
+ headers: {
62
+ "Content-Type": "application/json",
63
+ // One key per attempt — see generateIdempotencyKey. Deliberately NOT
64
+ // sent for capture-order: there the server's deterministic
65
+ // per-order-id fallback is the correct idempotency scope.
66
+ "Idempotency-Key": generateIdempotencyKey(),
67
+ },
42
68
  body: JSON.stringify({ cart_id: cartId, is_card_payment: isCardPayment }),
43
69
  })
44
70
  },
@@ -21,6 +21,16 @@ export type PayPalConfig = {
21
21
  }
22
22
 
23
23
  export type PayPalSettingsResponse = {
24
+ /**
25
+ * What `GET /store/paypal/settings` actually returns: the configured
26
+ * payment action and whether advanced card fields are enabled.
27
+ */
28
+ paymentAction?: "capture" | "authorize"
29
+ advancedCardEnabled?: boolean
30
+ /**
31
+ * @deprecated The settings endpoint has never returned a `data` envelope —
32
+ * kept only so existing (already-null-safe) consumer code keeps compiling.
33
+ */
24
34
  data?: {
25
35
  api_details?: { currency_code?: string }
26
36
  paypal_settings?: Record<string, unknown>