@easypayment/medusa-paypal-ui 1.2.0 → 1.2.3

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.
@@ -1 +1 @@
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":[]}
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","/**\n * Server-safe helpers for displaying PayPal payments on order / confirmation\n * pages.\n *\n * This module is intentionally free of React and any client-only code, so it\n * can be imported from Next.js **server components** (e.g. the order\n * confirmation page) without pulling in the checkout UI.\n *\n * @example\n * import { getPaymentLabel, fetchPayPalConfig } from \"@easypayment/medusa-paypal-ui/order\"\n *\n * // In the (async) server component that renders the payment method:\n * const titles = await fetchPayPalConfig({\n * baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,\n * publishableApiKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,\n * })\n *\n * <Text>\n * {getPaymentLabel(\n * payment.provider_id,\n * paymentInfoMap[payment.provider_id]?.title,\n * titles\n * )}\n * </Text>\n */\n\nimport {\n PAYPAL_WALLET_PROVIDER_ID,\n PAYPAL_CARD_PROVIDER_ID,\n isPayPalProviderId,\n} from \"./constants\"\n\nexport {\n PAYPAL_WALLET_PROVIDER_ID,\n PAYPAL_CARD_PROVIDER_ID,\n isPayPalProviderId,\n}\n\nexport type PayPalPaymentInfo = { title: string }\n\n/** Built-in fallback titles, used only when admin titles aren't supplied. */\nexport const paypalPaymentInfoMap: Record<string, PayPalPaymentInfo> = {\n [PAYPAL_WALLET_PROVIDER_ID]: { title: \"PayPal\" },\n [PAYPAL_CARD_PROVIDER_ID]: { title: \"Credit or Debit Card\" },\n}\n\n/** Admin-configured titles, as returned by `GET /store/paypal/config`. */\nexport type PayPalConfigTitles = {\n paypal_title?: string | null\n card_title?: string | null\n}\n\n/**\n * Null-safe payment label resolver for order / confirmation pages.\n *\n * Precedence for the PayPal providers: **admin-configured title** (`titles`,\n * from {@link fetchPayPalConfig}) → built-in default. For any other provider:\n * `fallback` → raw id. Never throws on an unknown / missing provider, which is\n * what causes the common `Cannot read properties of undefined (reading 'title')`\n * crash on the order confirmation page.\n *\n * @param titles Admin titles so the order page shows the same labels configured\n * in Medusa Admin → Settings → PayPal (e.g. a custom \"Credit or Debit Card\").\n */\nexport function getPaymentLabel(\n providerId: string | undefined | null,\n fallback?: string,\n titles?: PayPalConfigTitles\n): string {\n if (!providerId) {\n return fallback ?? \"\"\n }\n if (providerId === PAYPAL_WALLET_PROVIDER_ID) {\n return (\n titles?.paypal_title?.trim() ||\n paypalPaymentInfoMap[PAYPAL_WALLET_PROVIDER_ID].title\n )\n }\n if (providerId === PAYPAL_CARD_PROVIDER_ID) {\n return (\n titles?.card_title?.trim() ||\n paypalPaymentInfoMap[PAYPAL_CARD_PROVIDER_ID].title\n )\n }\n return fallback ?? providerId\n}\n\n/**\n * Fetch the admin-configured PayPal titles from the storefront API\n * (`GET /store/paypal/config`). Server-safe (uses the global `fetch`).\n *\n * Returns `{}` on any error/network failure so callers can fall back to the\n * built-in defaults without their own try/catch.\n */\nexport async function fetchPayPalConfig(opts: {\n baseUrl: string\n publishableApiKey?: string\n signal?: AbortSignal\n}): Promise<PayPalConfigTitles> {\n const { baseUrl, publishableApiKey, signal } = opts\n if (!baseUrl) {\n return {}\n }\n try {\n const timeoutMs = 30_000\n let effectiveSignal = signal\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n if (!signal) {\n const controller = new AbortController()\n timeoutId = setTimeout(() => controller.abort(), timeoutMs)\n effectiveSignal = controller.signal\n }\n let res: Response\n try {\n res = await fetch(`${baseUrl.replace(/\\/$/, \"\")}/store/paypal/config`, {\n headers: {\n accept: \"application/json\",\n ...(publishableApiKey\n ? { \"x-publishable-api-key\": publishableApiKey }\n : {}),\n },\n signal: effectiveSignal,\n })\n } finally {\n if (timeoutId !== undefined) clearTimeout(timeoutId)\n }\n if (!res.ok) {\n return {}\n }\n const cfg = (await res.json()) as Record<string, unknown>\n return {\n paypal_title:\n typeof cfg.paypal_title === \"string\" ? cfg.paypal_title : undefined,\n card_title:\n typeof cfg.card_title === \"string\" ? cfg.card_title : undefined,\n }\n } catch {\n return {}\n }\n}\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;;;ACoBO,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,oBACA,EAAE,yBAAyB,kBAAkB,IAC7C,CAAC;AAAA,QACP;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,6 +1,6 @@
1
1
  {
2
2
  "name": "@easypayment/medusa-paypal-ui",
3
- "version": "1.2.0",
3
+ "version": "1.2.3",
4
4
  "description": "Enterprise Gold PayPal UI module for Medusa v2 storefront (Next.js)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,195 +1,196 @@
1
- "use client"
2
-
3
- import React, { useCallback } from "react"
4
- import { PayPalCurrencyNotice } from "../components/PayPalCurrencyNotice"
5
- import { PayPalAdvancedCard } from "../components/PayPalAdvancedCard"
6
- import { PayPalProvider } from "../components/PayPalProvider"
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"
12
- import { usePayPalConfig } from "../hooks/usePayPalConfig"
13
-
14
- const SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`
15
-
16
- function PayPalLoadingCard() {
17
- return (
18
- <div
19
- role="status"
20
- aria-label="Connecting to PayPal"
21
- style={{
22
- display: "flex",
23
- alignItems: "center",
24
- gap: 12,
25
- padding: "14px 16px",
26
- background: "#f9fafb",
27
- border: "1px solid #e5e7eb",
28
- borderRadius: 10,
29
- }}
30
- >
31
- <style>{SPIN_STYLE}</style>
32
- <div
33
- style={{
34
- width: 22,
35
- height: 22,
36
- borderRadius: "50%",
37
- border: "2.5px solid #e5e7eb",
38
- borderTopColor: "#0070ba",
39
- animation: "_pp_spin .7s linear infinite",
40
- flexShrink: 0,
41
- }}
42
- />
43
- <div>
44
- <div style={{ fontSize: 13, fontWeight: 500, color: "#111827" }}>
45
- Connecting to PayPal…
46
- </div>
47
- <div style={{ fontSize: 12, color: "#6b7280", marginTop: 2 }}>
48
- Setting up secure payment
49
- </div>
50
- </div>
51
- </div>
52
- )
53
- }
54
-
55
- function PayPalErrorCard({ message }: { message: string }) {
56
- return (
57
- <div
58
- role="alert"
59
- style={{
60
- padding: "12px 16px",
61
- background: "#fef2f2",
62
- border: "1px solid #fecaca",
63
- borderRadius: 10,
64
- fontSize: 13,
65
- color: "#b91c1c",
66
- }}
67
- >
68
- {message}
69
- </div>
70
- )
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
-
97
- export type MedusaNextPayPalAdapterProps = {
98
- cartId: string
99
- selectedProviderId: string | null | undefined
100
- baseUrl: string
101
- publishableApiKey?: string
102
- providerIds?: {
103
- paypal?: string
104
- paypalCard?: string
105
- }
106
- onSuccess?: (cartId: string) => void
107
- onError?: (message: string) => void
108
- onPaid?: (result: unknown) => void
109
- }
110
-
111
- export function MedusaNextPayPalAdapter(props: MedusaNextPayPalAdapterProps) {
112
- const {
113
- cartId,
114
- selectedProviderId,
115
- baseUrl,
116
- publishableApiKey,
117
- providerIds,
118
- onSuccess,
119
- onError,
120
- onPaid,
121
- } = props
122
-
123
- const paypalProviderId = providerIds?.paypal || DEFAULT_PAYPAL_PROVIDER_ID
124
- const paypalCardProviderId = providerIds?.paypalCard || DEFAULT_PAYPAL_CARD_PROVIDER_ID
125
-
126
- const shouldRender =
127
- selectedProviderId === paypalProviderId ||
128
- selectedProviderId === paypalCardProviderId
129
-
130
- const { config, loading, error } = usePayPalConfig({
131
- baseUrl,
132
- publishableApiKey,
133
- cartId,
134
- enabled: shouldRender,
135
- })
136
-
137
- const handlePaid = useCallback(
138
- async (captureResult: unknown) => {
139
- onPaid?.(captureResult)
140
- await onSuccess?.(cartId)
141
- },
142
- [cartId, onPaid, onSuccess]
143
- )
144
-
145
- if (!shouldRender) return null
146
-
147
- if (loading) return <PayPalLoadingCard />
148
-
149
- if (error) return <PayPalErrorCard message={error} />
150
-
151
- if (!config) return null
152
-
153
- const isCardProvider = selectedProviderId === paypalCardProviderId
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
- }
160
-
161
- const disableFunding = Array.isArray(config.disable_buttons)
162
- ? config.disable_buttons.join(",")
163
- : undefined
164
-
165
- return (
166
- <div key={selectedProviderId} style={{ display: "grid", gap: 12 }}>
167
- <PayPalCurrencyNotice config={config} />
168
- <PayPalProvider
169
- config={config}
170
- intent={config.intent === "authorize" ? "authorize" : "capture"}
171
- disableFunding={disableFunding}
172
- >
173
- {isCardProvider ? (
174
- <PayPalAdvancedCard
175
- baseUrl={baseUrl}
176
- publishableApiKey={publishableApiKey}
177
- cartId={cartId}
178
- config={config}
179
- onPaid={handlePaid}
180
- onError={onError}
181
- />
182
- ) : (
183
- <PayPalSmartButtons
184
- baseUrl={baseUrl}
185
- publishableApiKey={publishableApiKey}
186
- cartId={cartId}
187
- config={config}
188
- onPaid={handlePaid}
189
- onError={onError}
190
- />
191
- )}
192
- </PayPalProvider>
193
- </div>
194
- )
195
- }
1
+ "use client"
2
+
3
+ import React, { useCallback } from "react"
4
+ import { PayPalCurrencyNotice } from "../components/PayPalCurrencyNotice"
5
+ import { PayPalAdvancedCard } from "../components/PayPalAdvancedCard"
6
+ import { PayPalProvider } from "../components/PayPalProvider"
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"
12
+ import { usePayPalConfig } from "../hooks/usePayPalConfig"
13
+
14
+ const SPIN_STYLE = `@keyframes _pp_spin { to { transform: rotate(360deg) } }`
15
+
16
+ function PayPalLoadingCard() {
17
+ return (
18
+ <div
19
+ role="status"
20
+ aria-label="Connecting to PayPal"
21
+ style={{
22
+ display: "flex",
23
+ alignItems: "center",
24
+ gap: 12,
25
+ padding: "14px 16px",
26
+ background: "#f9fafb",
27
+ border: "1px solid #e5e7eb",
28
+ borderRadius: 10,
29
+ }}
30
+ >
31
+ <style>{SPIN_STYLE}</style>
32
+ <div
33
+ style={{
34
+ width: 22,
35
+ height: 22,
36
+ borderRadius: "50%",
37
+ border: "2.5px solid #e5e7eb",
38
+ borderTopColor: "#0070ba",
39
+ animation: "_pp_spin .7s linear infinite",
40
+ flexShrink: 0,
41
+ }}
42
+ />
43
+ <div>
44
+ <div style={{ fontSize: 13, fontWeight: 500, color: "#111827" }}>
45
+ Connecting to PayPal…
46
+ </div>
47
+ <div style={{ fontSize: 12, color: "#6b7280", marginTop: 2 }}>
48
+ Setting up secure payment
49
+ </div>
50
+ </div>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ function PayPalErrorCard({ message }: { message: string }) {
56
+ return (
57
+ <div
58
+ role="alert"
59
+ style={{
60
+ padding: "12px 16px",
61
+ background: "#fef2f2",
62
+ border: "1px solid #fecaca",
63
+ borderRadius: 10,
64
+ fontSize: 13,
65
+ color: "#b91c1c",
66
+ }}
67
+ >
68
+ {message}
69
+ </div>
70
+ )
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
+
97
+ export type MedusaNextPayPalAdapterProps = {
98
+ cartId: string
99
+ selectedProviderId: string | null | undefined
100
+ baseUrl: string
101
+ publishableApiKey?: string
102
+ providerIds?: {
103
+ paypal?: string
104
+ paypalCard?: string
105
+ }
106
+ onSuccess?: (cartId: string) => void
107
+ onError?: (message: string) => void
108
+ onPaid?: (result: unknown) => void
109
+ }
110
+
111
+ export function MedusaNextPayPalAdapter(props: MedusaNextPayPalAdapterProps) {
112
+ const {
113
+ cartId,
114
+ selectedProviderId,
115
+ baseUrl,
116
+ publishableApiKey,
117
+ providerIds,
118
+ onSuccess,
119
+ onError,
120
+ onPaid,
121
+ } = props
122
+
123
+ const paypalProviderId = providerIds?.paypal || DEFAULT_PAYPAL_PROVIDER_ID
124
+ const paypalCardProviderId =
125
+ providerIds?.paypalCard || DEFAULT_PAYPAL_CARD_PROVIDER_ID
126
+
127
+ const shouldRender =
128
+ selectedProviderId === paypalProviderId ||
129
+ selectedProviderId === paypalCardProviderId
130
+
131
+ const { config, loading, error } = usePayPalConfig({
132
+ baseUrl,
133
+ publishableApiKey,
134
+ cartId,
135
+ enabled: shouldRender,
136
+ })
137
+
138
+ const handlePaid = useCallback(
139
+ async (captureResult: unknown) => {
140
+ onPaid?.(captureResult)
141
+ await onSuccess?.(cartId)
142
+ },
143
+ [cartId, onPaid, onSuccess]
144
+ )
145
+
146
+ if (!shouldRender) return null
147
+
148
+ if (loading) return <PayPalLoadingCard />
149
+
150
+ if (error) return <PayPalErrorCard message={error} />
151
+
152
+ if (!config) return null
153
+
154
+ const isCardProvider = selectedProviderId === paypalCardProviderId
155
+ if (config.paypal_enabled === false && !isCardProvider) {
156
+ return <PayPalUnavailableCard label={config.paypal_title || "PayPal"} />
157
+ }
158
+ if (isCardProvider && config.card_enabled === false) {
159
+ return <PayPalUnavailableCard label={config.card_title || "Card payment"} />
160
+ }
161
+
162
+ const disableFunding = Array.isArray(config.disable_buttons)
163
+ ? config.disable_buttons.join(",")
164
+ : undefined
165
+
166
+ return (
167
+ <div key={selectedProviderId} style={{ display: "grid", gap: 12 }}>
168
+ <PayPalCurrencyNotice config={config} />
169
+ <PayPalProvider
170
+ config={config}
171
+ intent={config.intent === "authorize" ? "authorize" : "capture"}
172
+ disableFunding={disableFunding}
173
+ >
174
+ {isCardProvider ? (
175
+ <PayPalAdvancedCard
176
+ baseUrl={baseUrl}
177
+ publishableApiKey={publishableApiKey}
178
+ cartId={cartId}
179
+ config={config}
180
+ onPaid={handlePaid}
181
+ onError={onError}
182
+ />
183
+ ) : (
184
+ <PayPalSmartButtons
185
+ baseUrl={baseUrl}
186
+ publishableApiKey={publishableApiKey}
187
+ cartId={cartId}
188
+ config={config}
189
+ onPaid={handlePaid}
190
+ onError={onError}
191
+ />
192
+ )}
193
+ </PayPalProvider>
194
+ </div>
195
+ )
196
+ }