@easypayment/medusa-paypal-ui 1.0.49 → 1.0.50
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/README.md +60 -0
- package/dist/index.cjs +88 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.mjs +88 -13
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +86 -0
- package/dist/order.cjs.map +1 -0
- package/dist/order.d.cts +66 -0
- package/dist/order.d.ts +66 -0
- package/dist/order.mjs +56 -0
- package/dist/order.mjs.map +1 -0
- package/package.json +6 -1
- package/src/adapters/MedusaNextPayPalAdapter.tsx +13 -2
- package/src/client/http.ts +51 -5
- package/src/client/paypal.ts +23 -3
- package/src/components/PayPalAdvancedCard.tsx +18 -1
- package/src/components/PayPalPaymentSection.tsx +13 -2
- package/src/order.ts +124 -0
package/dist/order.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-safe helpers for displaying PayPal payments on order / confirmation
|
|
3
|
+
* pages.
|
|
4
|
+
*
|
|
5
|
+
* This module is intentionally free of React and any client-only code, so it
|
|
6
|
+
* can be imported from Next.js **server components** (e.g. the order
|
|
7
|
+
* confirmation page) without pulling in the checkout UI.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { getPaymentLabel, fetchPayPalConfig } from "@easypayment/medusa-paypal-ui/order"
|
|
11
|
+
*
|
|
12
|
+
* // In the (async) server component that renders the payment method:
|
|
13
|
+
* const titles = await fetchPayPalConfig({
|
|
14
|
+
* baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
|
|
15
|
+
* publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* <Text>
|
|
19
|
+
* {getPaymentLabel(
|
|
20
|
+
* payment.provider_id,
|
|
21
|
+
* paymentInfoMap[payment.provider_id]?.title,
|
|
22
|
+
* titles
|
|
23
|
+
* )}
|
|
24
|
+
* </Text>
|
|
25
|
+
*/
|
|
26
|
+
declare const PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
|
|
27
|
+
declare const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
|
|
28
|
+
type PayPalPaymentInfo = {
|
|
29
|
+
title: string;
|
|
30
|
+
};
|
|
31
|
+
/** Built-in fallback titles, used only when admin titles aren't supplied. */
|
|
32
|
+
declare const paypalPaymentInfoMap: Record<string, PayPalPaymentInfo>;
|
|
33
|
+
/** Admin-configured titles, as returned by `GET /store/paypal/config`. */
|
|
34
|
+
type PayPalConfigTitles = {
|
|
35
|
+
paypal_title?: string | null;
|
|
36
|
+
card_title?: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** True when the provider id belongs to this PayPal plugin. */
|
|
39
|
+
declare function isPayPalProviderId(providerId?: string | null): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Null-safe payment label resolver for order / confirmation pages.
|
|
42
|
+
*
|
|
43
|
+
* Precedence for the PayPal providers: **admin-configured title** (`titles`,
|
|
44
|
+
* from {@link fetchPayPalConfig}) → built-in default. For any other provider:
|
|
45
|
+
* `fallback` → raw id. Never throws on an unknown / missing provider, which is
|
|
46
|
+
* what causes the common `Cannot read properties of undefined (reading 'title')`
|
|
47
|
+
* crash on the order confirmation page.
|
|
48
|
+
*
|
|
49
|
+
* @param titles Admin titles so the order page shows the same labels configured
|
|
50
|
+
* in Medusa Admin → Settings → PayPal (e.g. a custom "Credit or Debit Card").
|
|
51
|
+
*/
|
|
52
|
+
declare function getPaymentLabel(providerId: string | undefined | null, fallback?: string, titles?: PayPalConfigTitles): string;
|
|
53
|
+
/**
|
|
54
|
+
* Fetch the admin-configured PayPal titles from the storefront API
|
|
55
|
+
* (`GET /store/paypal/config`). Server-safe (uses the global `fetch`).
|
|
56
|
+
*
|
|
57
|
+
* Returns `{}` on any error/network failure so callers can fall back to the
|
|
58
|
+
* built-in defaults without their own try/catch.
|
|
59
|
+
*/
|
|
60
|
+
declare function fetchPayPalConfig(opts: {
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
publishableApiKey?: string;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}): Promise<PayPalConfigTitles>;
|
|
65
|
+
|
|
66
|
+
export { PAYPAL_CARD_PROVIDER_ID, PAYPAL_WALLET_PROVIDER_ID, type PayPalConfigTitles, type PayPalPaymentInfo, fetchPayPalConfig, getPaymentLabel, isPayPalProviderId, paypalPaymentInfoMap };
|
package/dist/order.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-safe helpers for displaying PayPal payments on order / confirmation
|
|
3
|
+
* pages.
|
|
4
|
+
*
|
|
5
|
+
* This module is intentionally free of React and any client-only code, so it
|
|
6
|
+
* can be imported from Next.js **server components** (e.g. the order
|
|
7
|
+
* confirmation page) without pulling in the checkout UI.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { getPaymentLabel, fetchPayPalConfig } from "@easypayment/medusa-paypal-ui/order"
|
|
11
|
+
*
|
|
12
|
+
* // In the (async) server component that renders the payment method:
|
|
13
|
+
* const titles = await fetchPayPalConfig({
|
|
14
|
+
* baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
|
|
15
|
+
* publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* <Text>
|
|
19
|
+
* {getPaymentLabel(
|
|
20
|
+
* payment.provider_id,
|
|
21
|
+
* paymentInfoMap[payment.provider_id]?.title,
|
|
22
|
+
* titles
|
|
23
|
+
* )}
|
|
24
|
+
* </Text>
|
|
25
|
+
*/
|
|
26
|
+
declare const PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
|
|
27
|
+
declare const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
|
|
28
|
+
type PayPalPaymentInfo = {
|
|
29
|
+
title: string;
|
|
30
|
+
};
|
|
31
|
+
/** Built-in fallback titles, used only when admin titles aren't supplied. */
|
|
32
|
+
declare const paypalPaymentInfoMap: Record<string, PayPalPaymentInfo>;
|
|
33
|
+
/** Admin-configured titles, as returned by `GET /store/paypal/config`. */
|
|
34
|
+
type PayPalConfigTitles = {
|
|
35
|
+
paypal_title?: string | null;
|
|
36
|
+
card_title?: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** True when the provider id belongs to this PayPal plugin. */
|
|
39
|
+
declare function isPayPalProviderId(providerId?: string | null): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Null-safe payment label resolver for order / confirmation pages.
|
|
42
|
+
*
|
|
43
|
+
* Precedence for the PayPal providers: **admin-configured title** (`titles`,
|
|
44
|
+
* from {@link fetchPayPalConfig}) → built-in default. For any other provider:
|
|
45
|
+
* `fallback` → raw id. Never throws on an unknown / missing provider, which is
|
|
46
|
+
* what causes the common `Cannot read properties of undefined (reading 'title')`
|
|
47
|
+
* crash on the order confirmation page.
|
|
48
|
+
*
|
|
49
|
+
* @param titles Admin titles so the order page shows the same labels configured
|
|
50
|
+
* in Medusa Admin → Settings → PayPal (e.g. a custom "Credit or Debit Card").
|
|
51
|
+
*/
|
|
52
|
+
declare function getPaymentLabel(providerId: string | undefined | null, fallback?: string, titles?: PayPalConfigTitles): string;
|
|
53
|
+
/**
|
|
54
|
+
* Fetch the admin-configured PayPal titles from the storefront API
|
|
55
|
+
* (`GET /store/paypal/config`). Server-safe (uses the global `fetch`).
|
|
56
|
+
*
|
|
57
|
+
* Returns `{}` on any error/network failure so callers can fall back to the
|
|
58
|
+
* built-in defaults without their own try/catch.
|
|
59
|
+
*/
|
|
60
|
+
declare function fetchPayPalConfig(opts: {
|
|
61
|
+
baseUrl: string;
|
|
62
|
+
publishableApiKey?: string;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}): Promise<PayPalConfigTitles>;
|
|
65
|
+
|
|
66
|
+
export { PAYPAL_CARD_PROVIDER_ID, PAYPAL_WALLET_PROVIDER_ID, type PayPalConfigTitles, type PayPalPaymentInfo, fetchPayPalConfig, getPaymentLabel, isPayPalProviderId, paypalPaymentInfoMap };
|
package/dist/order.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/order.ts
|
|
2
|
+
var PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal";
|
|
3
|
+
var PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card";
|
|
4
|
+
var paypalPaymentInfoMap = {
|
|
5
|
+
[PAYPAL_WALLET_PROVIDER_ID]: { title: "PayPal" },
|
|
6
|
+
[PAYPAL_CARD_PROVIDER_ID]: { title: "Credit or Debit Card" }
|
|
7
|
+
};
|
|
8
|
+
function isPayPalProviderId(providerId) {
|
|
9
|
+
return providerId === PAYPAL_WALLET_PROVIDER_ID || providerId === PAYPAL_CARD_PROVIDER_ID;
|
|
10
|
+
}
|
|
11
|
+
function getPaymentLabel(providerId, fallback, titles) {
|
|
12
|
+
if (!providerId) {
|
|
13
|
+
return fallback ?? "";
|
|
14
|
+
}
|
|
15
|
+
if (providerId === PAYPAL_WALLET_PROVIDER_ID) {
|
|
16
|
+
return titles?.paypal_title?.trim() || paypalPaymentInfoMap[PAYPAL_WALLET_PROVIDER_ID].title;
|
|
17
|
+
}
|
|
18
|
+
if (providerId === PAYPAL_CARD_PROVIDER_ID) {
|
|
19
|
+
return titles?.card_title?.trim() || paypalPaymentInfoMap[PAYPAL_CARD_PROVIDER_ID].title;
|
|
20
|
+
}
|
|
21
|
+
return fallback ?? providerId;
|
|
22
|
+
}
|
|
23
|
+
async function fetchPayPalConfig(opts) {
|
|
24
|
+
const { baseUrl, publishableApiKey, signal } = opts;
|
|
25
|
+
if (!baseUrl) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/store/paypal/config`, {
|
|
30
|
+
headers: {
|
|
31
|
+
accept: "application/json",
|
|
32
|
+
...publishableApiKey ? { "x-publishable-api-key": publishableApiKey } : {}
|
|
33
|
+
},
|
|
34
|
+
signal
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const cfg = await res.json();
|
|
40
|
+
return {
|
|
41
|
+
paypal_title: typeof cfg.paypal_title === "string" ? cfg.paypal_title : void 0,
|
|
42
|
+
card_title: typeof cfg.card_title === "string" ? cfg.card_title : void 0
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
PAYPAL_CARD_PROVIDER_ID,
|
|
50
|
+
PAYPAL_WALLET_PROVIDER_ID,
|
|
51
|
+
fetchPayPalConfig,
|
|
52
|
+
getPaymentLabel,
|
|
53
|
+
isPayPalProviderId,
|
|
54
|
+
paypalPaymentInfoMap
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=order.mjs.map
|
|
@@ -0,0 +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 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,\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,MAAM,MAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC,wBAAwB;AAAA,MAC3E,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,GAAI,oBAAoB,EAAE,yBAAyB,kBAAkB,IAAI,CAAC;AAAA,MAC5E;AAAA,MACA;AAAA,IACF,CAAC;AACD,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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easypayment/medusa-paypal-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.50",
|
|
4
4
|
"description": "Enterprise Gold PayPal UI module for Medusa v2 storefront (Next.js)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"import": "./dist/index.mjs",
|
|
21
21
|
"require": "./dist/index.cjs"
|
|
22
22
|
},
|
|
23
|
+
"./order": {
|
|
24
|
+
"types": "./dist/order.d.ts",
|
|
25
|
+
"import": "./dist/order.mjs",
|
|
26
|
+
"require": "./dist/order.cjs"
|
|
27
|
+
},
|
|
23
28
|
"./package.json": "./package.json"
|
|
24
29
|
},
|
|
25
30
|
"peerDependencies": {
|
|
@@ -6,6 +6,7 @@ import { PayPalAdvancedCard } from "../components/PayPalAdvancedCard"
|
|
|
6
6
|
import { PayPalProvider } from "../components/PayPalProvider"
|
|
7
7
|
import { PayPalSmartButtons } from "../components/PayPalSmartButtons"
|
|
8
8
|
import { usePayPalConfig } from "../hooks/usePayPalConfig"
|
|
9
|
+
import { isNextRedirectError } from "../client/paypal"
|
|
9
10
|
|
|
10
11
|
const DEFAULT_PAYPAL_PROVIDER_ID = "pp_paypal_paypal"
|
|
11
12
|
const DEFAULT_PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card"
|
|
@@ -109,9 +110,19 @@ export function MedusaNextPayPalAdapter(props: MedusaNextPayPalAdapterProps) {
|
|
|
109
110
|
const handlePaid = useCallback(
|
|
110
111
|
(captureResult: unknown) => {
|
|
111
112
|
onPaid?.(captureResult)
|
|
112
|
-
onSuccess
|
|
113
|
+
// onSuccess usually calls placeOrder(), which redirects on success. Run it
|
|
114
|
+
// without blocking the spinner, but surface real failures so a captured
|
|
115
|
+
// payment never ends silently with no order and no error shown.
|
|
116
|
+
Promise.resolve(onSuccess?.(cartId)).catch((e: unknown) => {
|
|
117
|
+
if (isNextRedirectError(e)) return
|
|
118
|
+
onError?.(
|
|
119
|
+
e instanceof Error
|
|
120
|
+
? e.message
|
|
121
|
+
: "Your payment was taken but the order could not be finalized. Please contact support before paying again."
|
|
122
|
+
)
|
|
123
|
+
})
|
|
113
124
|
},
|
|
114
|
-
[cartId, onPaid, onSuccess]
|
|
125
|
+
[cartId, onPaid, onSuccess, onError]
|
|
115
126
|
)
|
|
116
127
|
|
|
117
128
|
if (!shouldRender) return null
|
package/src/client/http.ts
CHANGED
|
@@ -19,6 +19,8 @@ function toHeaderRecord(headers?: RequestInit["headers"]): Record<string, string
|
|
|
19
19
|
return { ...headers }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 30000
|
|
23
|
+
|
|
22
24
|
export function createHttpClient(opts: HttpOptions) {
|
|
23
25
|
const base = opts.baseUrl.replace(/\/+$/, "")
|
|
24
26
|
|
|
@@ -33,8 +35,36 @@ export function createHttpClient(opts: HttpOptions) {
|
|
|
33
35
|
headers["x-publishable-api-key"] = opts.publishableApiKey
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// Always enforce a timeout so a hung backend can't leave the checkout
|
|
39
|
+
// spinner up forever, while still honouring a caller-provided abort signal.
|
|
40
|
+
const controller = new AbortController()
|
|
41
|
+
let timedOut = false
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
timedOut = true
|
|
44
|
+
controller.abort()
|
|
45
|
+
}, DEFAULT_TIMEOUT_MS)
|
|
46
|
+
const callerSignal = init?.signal
|
|
47
|
+
if (callerSignal) {
|
|
48
|
+
if (callerSignal.aborted) {
|
|
49
|
+
controller.abort()
|
|
50
|
+
} else {
|
|
51
|
+
callerSignal.addEventListener("abort", () => controller.abort(), { once: true })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let res: Response
|
|
56
|
+
let text: string
|
|
57
|
+
try {
|
|
58
|
+
res = await fetch(url, { ...init, headers, credentials: "include", signal: controller.signal })
|
|
59
|
+
text = await res.text().catch(() => "")
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (timedOut) {
|
|
62
|
+
throw new Error("[PayPal] Request timed out. Please check your connection and try again.")
|
|
63
|
+
}
|
|
64
|
+
throw e
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timer)
|
|
67
|
+
}
|
|
38
68
|
|
|
39
69
|
if (!res.ok) {
|
|
40
70
|
if (res.status === 401) {
|
|
@@ -47,9 +77,25 @@ export function createHttpClient(opts: HttpOptions) {
|
|
|
47
77
|
"[PayPal] Forbidden (403) — this request is not allowed. Check your CORS and API key settings."
|
|
48
78
|
)
|
|
49
79
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
80
|
+
// Prefer the backend's clean { message, request_id } JSON. For 5xx, do not
|
|
81
|
+
// surface raw server internals to end users — log them and show a generic
|
|
82
|
+
// message (with a correlation id when available).
|
|
83
|
+
type ErrBody = { message?: unknown; request_id?: unknown }
|
|
84
|
+
let parsed: ErrBody | null = null
|
|
85
|
+
try {
|
|
86
|
+
parsed = text ? (JSON.parse(text) as ErrBody) : null
|
|
87
|
+
} catch {
|
|
88
|
+
parsed = null
|
|
89
|
+
}
|
|
90
|
+
const backendMessage = typeof parsed?.message === "string" ? parsed.message : ""
|
|
91
|
+
const requestId = typeof parsed?.request_id === "string" ? parsed.request_id : ""
|
|
92
|
+
if (res.status >= 500) {
|
|
93
|
+
console.error(`[PayPal] ${path} failed (${res.status})`, text.slice(0, 1000))
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Payment service error${requestId ? ` (ref ${requestId})` : ""}. Please try again or contact support.`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
throw new Error(backendMessage || `Request failed (${res.status})`)
|
|
53
99
|
}
|
|
54
100
|
if (!text) {
|
|
55
101
|
throw new Error(`[PayPal] Empty response body from ${path} (${res.status})`)
|
package/src/client/paypal.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import type { PayPalConfig, PayPalSettingsResponse } from "./types"
|
|
2
2
|
import { createHttpClient, type HttpOptions } from "./http"
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* True when an error is a Next.js navigation signal (`redirect()` / `notFound()`
|
|
6
|
+
* from a server action). These MUST be re-thrown / ignored, never shown as an
|
|
7
|
+
* error — a successful `placeOrder()` redirect surfaces as one of these.
|
|
8
|
+
*/
|
|
9
|
+
export function isNextRedirectError(e: unknown): boolean {
|
|
10
|
+
if (!e || typeof e !== "object") {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
const digest = (e as { digest?: unknown }).digest
|
|
14
|
+
return (
|
|
15
|
+
typeof digest === "string" &&
|
|
16
|
+
(digest.startsWith("NEXT_REDIRECT") || digest === "NEXT_NOT_FOUND")
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
4
20
|
export async function markPaymentComplete(
|
|
5
21
|
baseUrl: string,
|
|
6
22
|
cartId: string,
|
|
7
23
|
publishableApiKey?: string
|
|
8
|
-
): Promise<Record<string, any>> {
|
|
24
|
+
): Promise<{ ok: boolean } & Record<string, any>> {
|
|
9
25
|
try {
|
|
10
26
|
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/store/paypal-complete`, {
|
|
11
27
|
method: "POST",
|
|
@@ -17,10 +33,14 @@ export async function markPaymentComplete(
|
|
|
17
33
|
credentials: "include",
|
|
18
34
|
})
|
|
19
35
|
const data = await resp.json().catch(() => ({}))
|
|
20
|
-
|
|
36
|
+
if (!resp.ok) {
|
|
37
|
+
console.warn("[PayPal] paypal-complete returned", resp.status, data)
|
|
38
|
+
return { ok: false, ...(data || {}) }
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, ...(data || {}) }
|
|
21
41
|
} catch (e) {
|
|
22
42
|
console.warn("[PayPal] paypal-complete call failed:", e)
|
|
23
|
-
return {}
|
|
43
|
+
return { ok: false }
|
|
24
44
|
}
|
|
25
45
|
}
|
|
26
46
|
|
|
@@ -62,10 +62,12 @@ function SubmitButton({
|
|
|
62
62
|
disabled,
|
|
63
63
|
label,
|
|
64
64
|
onSubmit,
|
|
65
|
+
onSubmitError,
|
|
65
66
|
}: {
|
|
66
67
|
disabled: boolean
|
|
67
68
|
label: string
|
|
68
69
|
onSubmit: () => void
|
|
70
|
+
onSubmitError: (message: string) => void
|
|
69
71
|
}) {
|
|
70
72
|
const { cardFieldsForm } = usePayPalCardFields()
|
|
71
73
|
const isDisabled = disabled || !cardFieldsForm
|
|
@@ -76,7 +78,17 @@ function SubmitButton({
|
|
|
76
78
|
disabled={isDisabled}
|
|
77
79
|
onClick={() => {
|
|
78
80
|
onSubmit()
|
|
79
|
-
|
|
81
|
+
// submit() can reject (network / tokenization / 3DS) WITHOUT firing the
|
|
82
|
+
// provider onError, which would otherwise leave the "Processing…"
|
|
83
|
+
// overlay stuck forever. Always catch it and surface the error.
|
|
84
|
+
const submitted = cardFieldsForm?.submit()
|
|
85
|
+
if (submitted && typeof submitted.catch === "function") {
|
|
86
|
+
submitted.catch((e: unknown) => {
|
|
87
|
+
onSubmitError(
|
|
88
|
+
e instanceof Error ? e.message : "Card payment failed. Please try again."
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
80
92
|
}}
|
|
81
93
|
style={{
|
|
82
94
|
width: "100%",
|
|
@@ -321,6 +333,11 @@ export function PayPalAdvancedCard(props: {
|
|
|
321
333
|
setError(null)
|
|
322
334
|
setSubmitting(true)
|
|
323
335
|
}}
|
|
336
|
+
onSubmitError={(msg) => {
|
|
337
|
+
setError(msg)
|
|
338
|
+
onError?.(msg)
|
|
339
|
+
setSubmitting(false)
|
|
340
|
+
}}
|
|
324
341
|
/>
|
|
325
342
|
|
|
326
343
|
{error && (
|
|
@@ -6,6 +6,7 @@ import { PayPalAdvancedCard } from "./PayPalAdvancedCard"
|
|
|
6
6
|
import { PayPalProvider } from "./PayPalProvider"
|
|
7
7
|
import { PayPalSmartButtons } from "./PayPalSmartButtons"
|
|
8
8
|
import { usePayPalConfig } from "../hooks/usePayPalConfig"
|
|
9
|
+
import { isNextRedirectError } from "../client/paypal"
|
|
9
10
|
|
|
10
11
|
export const PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal" as const
|
|
11
12
|
export const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card" as const
|
|
@@ -142,9 +143,19 @@ export function PayPalPaymentSection({
|
|
|
142
143
|
const handlePaid = useCallback(
|
|
143
144
|
(captureResult: unknown) => {
|
|
144
145
|
onPaid?.(captureResult)
|
|
145
|
-
onSuccess
|
|
146
|
+
// onSuccess usually calls placeOrder(), which redirects on success. Run it
|
|
147
|
+
// without blocking the spinner, but surface real failures so a captured
|
|
148
|
+
// payment never ends silently with no order and no error shown.
|
|
149
|
+
Promise.resolve(onSuccess?.(cartId)).catch((e: unknown) => {
|
|
150
|
+
if (isNextRedirectError(e)) return
|
|
151
|
+
onError?.(
|
|
152
|
+
e instanceof Error
|
|
153
|
+
? e.message
|
|
154
|
+
: "Your payment was taken but the order could not be finalized. Please contact support before paying again."
|
|
155
|
+
)
|
|
156
|
+
})
|
|
146
157
|
},
|
|
147
|
-
[cartId, onPaid, onSuccess]
|
|
158
|
+
[cartId, onPaid, onSuccess, onError]
|
|
148
159
|
)
|
|
149
160
|
|
|
150
161
|
if (!shouldRender) return null
|
package/src/order.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-safe helpers for displaying PayPal payments on order / confirmation
|
|
3
|
+
* pages.
|
|
4
|
+
*
|
|
5
|
+
* This module is intentionally free of React and any client-only code, so it
|
|
6
|
+
* can be imported from Next.js **server components** (e.g. the order
|
|
7
|
+
* confirmation page) without pulling in the checkout UI.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { getPaymentLabel, fetchPayPalConfig } from "@easypayment/medusa-paypal-ui/order"
|
|
11
|
+
*
|
|
12
|
+
* // In the (async) server component that renders the payment method:
|
|
13
|
+
* const titles = await fetchPayPalConfig({
|
|
14
|
+
* baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,
|
|
15
|
+
* publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* <Text>
|
|
19
|
+
* {getPaymentLabel(
|
|
20
|
+
* payment.provider_id,
|
|
21
|
+
* paymentInfoMap[payment.provider_id]?.title,
|
|
22
|
+
* titles
|
|
23
|
+
* )}
|
|
24
|
+
* </Text>
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const PAYPAL_WALLET_PROVIDER_ID = "pp_paypal_paypal"
|
|
28
|
+
export const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card"
|
|
29
|
+
|
|
30
|
+
export type PayPalPaymentInfo = { title: string }
|
|
31
|
+
|
|
32
|
+
/** Built-in fallback titles, used only when admin titles aren't supplied. */
|
|
33
|
+
export const paypalPaymentInfoMap: Record<string, PayPalPaymentInfo> = {
|
|
34
|
+
[PAYPAL_WALLET_PROVIDER_ID]: { title: "PayPal" },
|
|
35
|
+
[PAYPAL_CARD_PROVIDER_ID]: { title: "Credit or Debit Card" },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Admin-configured titles, as returned by `GET /store/paypal/config`. */
|
|
39
|
+
export type PayPalConfigTitles = {
|
|
40
|
+
paypal_title?: string | null
|
|
41
|
+
card_title?: string | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when the provider id belongs to this PayPal plugin. */
|
|
45
|
+
export function isPayPalProviderId(providerId?: string | null): boolean {
|
|
46
|
+
return (
|
|
47
|
+
providerId === PAYPAL_WALLET_PROVIDER_ID ||
|
|
48
|
+
providerId === PAYPAL_CARD_PROVIDER_ID
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Null-safe payment label resolver for order / confirmation pages.
|
|
54
|
+
*
|
|
55
|
+
* Precedence for the PayPal providers: **admin-configured title** (`titles`,
|
|
56
|
+
* from {@link fetchPayPalConfig}) → built-in default. For any other provider:
|
|
57
|
+
* `fallback` → raw id. Never throws on an unknown / missing provider, which is
|
|
58
|
+
* what causes the common `Cannot read properties of undefined (reading 'title')`
|
|
59
|
+
* crash on the order confirmation page.
|
|
60
|
+
*
|
|
61
|
+
* @param titles Admin titles so the order page shows the same labels configured
|
|
62
|
+
* in Medusa Admin → Settings → PayPal (e.g. a custom "Credit or Debit Card").
|
|
63
|
+
*/
|
|
64
|
+
export function getPaymentLabel(
|
|
65
|
+
providerId: string | undefined | null,
|
|
66
|
+
fallback?: string,
|
|
67
|
+
titles?: PayPalConfigTitles
|
|
68
|
+
): string {
|
|
69
|
+
if (!providerId) {
|
|
70
|
+
return fallback ?? ""
|
|
71
|
+
}
|
|
72
|
+
if (providerId === PAYPAL_WALLET_PROVIDER_ID) {
|
|
73
|
+
return (
|
|
74
|
+
titles?.paypal_title?.trim() ||
|
|
75
|
+
paypalPaymentInfoMap[PAYPAL_WALLET_PROVIDER_ID].title
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
if (providerId === PAYPAL_CARD_PROVIDER_ID) {
|
|
79
|
+
return (
|
|
80
|
+
titles?.card_title?.trim() ||
|
|
81
|
+
paypalPaymentInfoMap[PAYPAL_CARD_PROVIDER_ID].title
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
return fallback ?? providerId
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fetch the admin-configured PayPal titles from the storefront API
|
|
89
|
+
* (`GET /store/paypal/config`). Server-safe (uses the global `fetch`).
|
|
90
|
+
*
|
|
91
|
+
* Returns `{}` on any error/network failure so callers can fall back to the
|
|
92
|
+
* built-in defaults without their own try/catch.
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchPayPalConfig(opts: {
|
|
95
|
+
baseUrl: string
|
|
96
|
+
publishableApiKey?: string
|
|
97
|
+
signal?: AbortSignal
|
|
98
|
+
}): Promise<PayPalConfigTitles> {
|
|
99
|
+
const { baseUrl, publishableApiKey, signal } = opts
|
|
100
|
+
if (!baseUrl) {
|
|
101
|
+
return {}
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/store/paypal/config`, {
|
|
105
|
+
headers: {
|
|
106
|
+
accept: "application/json",
|
|
107
|
+
...(publishableApiKey ? { "x-publishable-api-key": publishableApiKey } : {}),
|
|
108
|
+
},
|
|
109
|
+
signal,
|
|
110
|
+
})
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
return {}
|
|
113
|
+
}
|
|
114
|
+
const cfg = (await res.json()) as Record<string, unknown>
|
|
115
|
+
return {
|
|
116
|
+
paypal_title:
|
|
117
|
+
typeof cfg.paypal_title === "string" ? cfg.paypal_title : undefined,
|
|
118
|
+
card_title:
|
|
119
|
+
typeof cfg.card_title === "string" ? cfg.card_title : undefined,
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
return {}
|
|
123
|
+
}
|
|
124
|
+
}
|