@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/CHANGELOG.md +97 -61
- package/README.md +781 -759
- package/dist/index.cjs +143 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.mjs +133 -136
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +14 -3
- package/dist/order.cjs.map +1 -1
- package/dist/order.d.cts +13 -4
- package/dist/order.d.ts +13 -4
- package/dist/order.mjs +13 -4
- package/dist/order.mjs.map +1 -1
- package/package.json +76 -75
- package/src/adapters/MedusaNextPayPalAdapter.tsx +34 -5
- package/src/client/http.ts +9 -1
- package/src/client/paypal.ts +27 -1
- package/src/components/PayPalAdvancedCard.tsx +24 -0
- package/src/components/PayPalPaymentSection.tsx +95 -202
- package/src/components/PayPalSmartButtons.tsx +24 -0
- package/src/constants.ts +22 -0
- package/src/hooks/usePayPalPaymentMethods.ts +15 -1
- package/src/index.ts +5 -0
- package/src/order.ts +7 -10
- package/src/utils/captured-state.ts +44 -0
package/dist/order.d.cts
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
|
-
|
|
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.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
|
-
|
|
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/
|
|
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 ?? "";
|
package/dist/order.mjs.map
CHANGED
|
@@ -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\
|
|
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,75 +1,76 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@easypayment/medusa-paypal-ui",
|
|
3
|
-
"version": "1.
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@easypayment/medusa-paypal-ui",
|
|
3
|
+
"version": "1.2.2",
|
|
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)
|
|
130
|
-
|
|
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(",")
|
package/src/client/http.ts
CHANGED
|
@@ -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 = {
|
|
@@ -101,7 +109,7 @@ export function createHttpClient(opts: HttpOptions) {
|
|
|
101
109
|
res = await fetch(url, {
|
|
102
110
|
...init,
|
|
103
111
|
headers,
|
|
104
|
-
credentials: "include",
|
|
112
|
+
credentials: opts.credentials ?? "include",
|
|
105
113
|
signal: controller.signal,
|
|
106
114
|
})
|
|
107
115
|
text = await res.text().catch(() => "")
|
package/src/client/paypal.ts
CHANGED
|
@@ -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: {
|
|
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
|
},
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
|
|
14
14
|
import type { PayPalConfig } from "../client/types"
|
|
15
|
+
import {
|
|
16
|
+
clearCartCaptured,
|
|
17
|
+
markCartCaptured,
|
|
18
|
+
wasCartCaptured,
|
|
19
|
+
} from "../utils/captured-state"
|
|
15
20
|
import { isNextRouterError } from "../utils/next-errors"
|
|
16
21
|
import { showProcessingOverlay, hideProcessingOverlay } from "../utils/processing-overlay"
|
|
17
22
|
|
|
@@ -187,6 +192,19 @@ export function PayPalAdvancedCard(props: {
|
|
|
187
192
|
return () => window.removeEventListener("pageshow", onPageShow)
|
|
188
193
|
}, [])
|
|
189
194
|
|
|
195
|
+
// A reload/crash between capture and finalization loses the in-memory
|
|
196
|
+
// capturedRef — restore it from the persisted per-cart flag so the buyer
|
|
197
|
+
// gets the finalize-only Retry button instead of card fields that would
|
|
198
|
+
// charge (or confusingly fail) again.
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
if (capturedRef.current || !wasCartCaptured(cartId)) return
|
|
201
|
+
capturedRef.current = {}
|
|
202
|
+
setCompletionPending(true)
|
|
203
|
+
setError(
|
|
204
|
+
"Your payment was already received for this cart. Please use the button below to finish placing your order — do not pay again."
|
|
205
|
+
)
|
|
206
|
+
}, [cartId])
|
|
207
|
+
|
|
190
208
|
const finalizeCapturedPayment = React.useCallback(async () => {
|
|
191
209
|
const captured = capturedRef.current
|
|
192
210
|
if (!captured) return
|
|
@@ -195,6 +213,9 @@ export function PayPalAdvancedCard(props: {
|
|
|
195
213
|
setError(null)
|
|
196
214
|
try {
|
|
197
215
|
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
|
|
216
|
+
// The order is finalized — the persisted "captured" flag has served its
|
|
217
|
+
// purpose. Clear it before the consumer callback (which may redirect).
|
|
218
|
+
clearCartCaptured(cartId)
|
|
198
219
|
await onPaid?.({ ...captured, ...completeResult })
|
|
199
220
|
setCompletionPending(false)
|
|
200
221
|
} catch (e: unknown) {
|
|
@@ -411,6 +432,9 @@ export function PayPalAdvancedCard(props: {
|
|
|
411
432
|
if (!orderId) throw new Error("PayPal order ID is missing from approval response")
|
|
412
433
|
const result = await api.captureOrder(cartId, orderId)
|
|
413
434
|
capturedRef.current = (result || {}) as Record<string, unknown>
|
|
435
|
+
// Persist across reloads: from here on, only finalization may
|
|
436
|
+
// ever be retried for this cart — never a fresh payment.
|
|
437
|
+
markCartCaptured(cartId)
|
|
414
438
|
} catch (e: unknown) {
|
|
415
439
|
if (isNextRouterError(e)) return
|
|
416
440
|
resetSubmitState()
|