@easypayment/medusa-paypal-ui 1.0.51 → 1.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +444 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.mjs +452 -93
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +20 -7
- package/dist/order.cjs.map +1 -1
- package/dist/order.mjs +20 -7
- package/dist/order.mjs.map +1 -1
- package/package.json +11 -1
- package/src/adapters/MedusaNextPayPalAdapter.tsx +5 -2
- package/src/client/http.ts +18 -1
- package/src/client/paypal.ts +7 -17
- package/src/components/PayPalAdvancedCard.tsx +177 -29
- package/src/components/PayPalCurrencyNotice.tsx +1 -1
- package/src/components/PayPalPaymentSection.tsx +7 -2
- package/src/components/PayPalProvider.tsx +1 -1
- package/src/components/PayPalSmartButtons.tsx +133 -30
- package/src/hooks/usePayPalConfig.ts +13 -4
- package/src/hooks/usePayPalPaymentMethods.ts +14 -4
- package/src/index.ts +1 -0
- package/src/order.ts +20 -7
- package/src/utils/next-errors.ts +17 -0
- package/src/utils/processing-overlay.ts +55 -0
package/src/client/paypal.ts
CHANGED
|
@@ -5,23 +5,13 @@ export async function markPaymentComplete(
|
|
|
5
5
|
baseUrl: string,
|
|
6
6
|
cartId: string,
|
|
7
7
|
publishableApiKey?: string
|
|
8
|
-
): Promise<Record<string,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
body: JSON.stringify({ cart_id: cartId }),
|
|
17
|
-
credentials: "include",
|
|
18
|
-
})
|
|
19
|
-
const data = await resp.json().catch(() => ({}))
|
|
20
|
-
return data || {}
|
|
21
|
-
} catch (e) {
|
|
22
|
-
console.warn("[PayPal] paypal-complete call failed:", e)
|
|
23
|
-
return {}
|
|
24
|
-
}
|
|
8
|
+
): Promise<Record<string, unknown>> {
|
|
9
|
+
const http = createHttpClient({ baseUrl, publishableApiKey })
|
|
10
|
+
return http.request<Record<string, unknown>>(`/store/paypal-complete`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify({ cart_id: cartId }),
|
|
14
|
+
})
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
export function createPayPalStoreApi(opts: HttpOptions) {
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { useMemo, useState } from "react"
|
|
3
|
+
import React, { useMemo, useRef, useState } from "react"
|
|
4
4
|
import {
|
|
5
5
|
PayPalCardFieldsProvider,
|
|
6
6
|
PayPalNumberField,
|
|
7
7
|
PayPalExpiryField,
|
|
8
8
|
PayPalCVVField,
|
|
9
9
|
usePayPalCardFields,
|
|
10
|
+
usePayPalScriptReducer,
|
|
10
11
|
} from "@paypal/react-paypal-js"
|
|
11
12
|
|
|
12
13
|
import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
|
|
13
14
|
import type { PayPalConfig } from "../client/types"
|
|
15
|
+
import { isNextRouterError } from "../utils/next-errors"
|
|
16
|
+
import { showProcessingOverlay, hideProcessingOverlay } from "../utils/processing-overlay"
|
|
14
17
|
|
|
15
|
-
const SPIN_STYLE =
|
|
18
|
+
const SPIN_STYLE = `
|
|
19
|
+
@keyframes _pp_spin { to { transform: rotate(360deg) } }
|
|
20
|
+
@keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
|
|
21
|
+
`
|
|
16
22
|
|
|
17
23
|
const cardStyle = {
|
|
18
24
|
input: {
|
|
@@ -74,9 +80,14 @@ function SubmitButton({
|
|
|
74
80
|
<button
|
|
75
81
|
type="button"
|
|
76
82
|
disabled={isDisabled}
|
|
83
|
+
aria-busy={disabled}
|
|
77
84
|
onClick={() => {
|
|
78
85
|
onSubmit()
|
|
79
|
-
|
|
86
|
+
try {
|
|
87
|
+
cardFieldsForm?.submit()
|
|
88
|
+
} catch {
|
|
89
|
+
// submit() error is handled by PayPalCardFieldsProvider onError
|
|
90
|
+
}
|
|
80
91
|
}}
|
|
81
92
|
style={{
|
|
82
93
|
width: "100%",
|
|
@@ -137,9 +148,38 @@ export function PayPalAdvancedCard(props: {
|
|
|
137
148
|
|
|
138
149
|
const [error, setError] = useState<string | null>(null)
|
|
139
150
|
const [submitting, setSubmitting] = useState(false)
|
|
151
|
+
const submittingRef = useRef(false)
|
|
152
|
+
const creatingOrderRef = useRef(false)
|
|
153
|
+
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
|
|
140
154
|
|
|
141
155
|
if (!config.currency_supported) return null
|
|
142
156
|
|
|
157
|
+
if (isRejected) {
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
role="alert"
|
|
161
|
+
style={{
|
|
162
|
+
display: "flex",
|
|
163
|
+
alignItems: "flex-start",
|
|
164
|
+
gap: 8,
|
|
165
|
+
padding: "10px 14px",
|
|
166
|
+
background: "#fef2f2",
|
|
167
|
+
border: "1px solid #fecaca",
|
|
168
|
+
borderRadius: 8,
|
|
169
|
+
fontSize: 13,
|
|
170
|
+
color: "#b91c1c",
|
|
171
|
+
lineHeight: 1.5,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
<span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
|
|
175
|
+
<span>
|
|
176
|
+
PayPal failed to load. Please refresh the page or try a different
|
|
177
|
+
payment method.
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
143
183
|
if (!config.client_token) {
|
|
144
184
|
return (
|
|
145
185
|
<div
|
|
@@ -159,78 +199,182 @@ export function PayPalAdvancedCard(props: {
|
|
|
159
199
|
|
|
160
200
|
const isSandbox = config.environment === "sandbox"
|
|
161
201
|
|
|
202
|
+
if (isPending) {
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
role="status"
|
|
206
|
+
aria-label="Loading PayPal"
|
|
207
|
+
style={{
|
|
208
|
+
display: "flex",
|
|
209
|
+
alignItems: "center",
|
|
210
|
+
justifyContent: "center",
|
|
211
|
+
gap: 10,
|
|
212
|
+
padding: "18px 16px",
|
|
213
|
+
background: "#f9fafb",
|
|
214
|
+
border: "1px solid #e5e7eb",
|
|
215
|
+
borderRadius: 8,
|
|
216
|
+
minHeight: 55,
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
<style>{SPIN_STYLE}</style>
|
|
220
|
+
<div
|
|
221
|
+
style={{
|
|
222
|
+
width: 22,
|
|
223
|
+
height: 22,
|
|
224
|
+
borderRadius: "50%",
|
|
225
|
+
border: "2.5px solid #e5e7eb",
|
|
226
|
+
borderTopColor: "#0070ba",
|
|
227
|
+
animation: "_pp_spin .7s linear infinite",
|
|
228
|
+
flexShrink: 0,
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
<div style={{ fontSize: 13, fontWeight: 500, color: "#6b7280" }}>
|
|
232
|
+
Loading card fields…
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!isResolved) return null
|
|
239
|
+
|
|
162
240
|
return (
|
|
163
241
|
<div style={{ position: "relative" }}>
|
|
164
242
|
<style>{SPIN_STYLE}</style>
|
|
165
243
|
|
|
166
244
|
{submitting && (
|
|
167
245
|
<div
|
|
246
|
+
role="status"
|
|
247
|
+
aria-label="Processing payment"
|
|
168
248
|
style={{
|
|
169
|
-
position: "
|
|
249
|
+
position: "fixed",
|
|
170
250
|
inset: 0,
|
|
171
|
-
zIndex:
|
|
172
|
-
background: "rgba(255,255,255,0.
|
|
173
|
-
borderRadius: 12,
|
|
251
|
+
zIndex: 9999,
|
|
252
|
+
background: "rgba(255,255,255,0.96)",
|
|
174
253
|
display: "flex",
|
|
175
254
|
flexDirection: "column",
|
|
176
255
|
alignItems: "center",
|
|
177
256
|
justifyContent: "center",
|
|
178
|
-
gap:
|
|
179
|
-
minHeight: 180,
|
|
257
|
+
gap: 20,
|
|
180
258
|
}}
|
|
181
259
|
>
|
|
182
|
-
<div
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
260
|
+
<div style={{ position: "relative", width: 56, height: 56 }}>
|
|
261
|
+
<div
|
|
262
|
+
style={{
|
|
263
|
+
position: "absolute",
|
|
264
|
+
inset: 0,
|
|
265
|
+
borderRadius: "50%",
|
|
266
|
+
border: "3px solid #e5e7eb",
|
|
267
|
+
}}
|
|
268
|
+
/>
|
|
269
|
+
<div
|
|
270
|
+
style={{
|
|
271
|
+
position: "absolute",
|
|
272
|
+
inset: 0,
|
|
273
|
+
borderRadius: "50%",
|
|
274
|
+
border: "3px solid transparent",
|
|
275
|
+
borderTopColor: "#2563eb",
|
|
276
|
+
animation: "_pp_spin .8s linear infinite",
|
|
277
|
+
}}
|
|
278
|
+
/>
|
|
279
|
+
<svg
|
|
280
|
+
viewBox="0 0 24 24"
|
|
281
|
+
fill="none"
|
|
282
|
+
stroke="#2563eb"
|
|
283
|
+
strokeWidth="1.8"
|
|
284
|
+
strokeLinecap="round"
|
|
285
|
+
strokeLinejoin="round"
|
|
286
|
+
style={{
|
|
287
|
+
position: "absolute",
|
|
288
|
+
top: "50%",
|
|
289
|
+
left: "50%",
|
|
290
|
+
transform: "translate(-50%, -50%)",
|
|
291
|
+
width: 24,
|
|
292
|
+
height: 24,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
296
|
+
</svg>
|
|
297
|
+
</div>
|
|
192
298
|
<div style={{ textAlign: "center" }}>
|
|
193
|
-
<div style={{ fontSize:
|
|
194
|
-
Processing your payment
|
|
299
|
+
<div style={{ fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }}>
|
|
300
|
+
Processing your payment
|
|
195
301
|
</div>
|
|
196
|
-
<div style={{ fontSize:
|
|
197
|
-
Please do not close
|
|
302
|
+
<div style={{ fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }}>
|
|
303
|
+
This may take a few moments. Please do not close<br />
|
|
304
|
+
or refresh this page.
|
|
198
305
|
</div>
|
|
199
306
|
</div>
|
|
307
|
+
<div
|
|
308
|
+
style={{
|
|
309
|
+
width: 200,
|
|
310
|
+
height: 3,
|
|
311
|
+
background: "#e5e7eb",
|
|
312
|
+
borderRadius: 3,
|
|
313
|
+
overflow: "hidden",
|
|
314
|
+
marginTop: 4,
|
|
315
|
+
}}
|
|
316
|
+
>
|
|
317
|
+
<div
|
|
318
|
+
style={{
|
|
319
|
+
width: "40%",
|
|
320
|
+
height: "100%",
|
|
321
|
+
background: "linear-gradient(90deg, #2563eb, #1d4ed8)",
|
|
322
|
+
borderRadius: 3,
|
|
323
|
+
animation: "_pp_progress 1.5s ease-in-out infinite",
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
200
327
|
</div>
|
|
201
328
|
)}
|
|
202
329
|
|
|
203
330
|
<PayPalCardFieldsProvider
|
|
204
331
|
style={cardStyle}
|
|
205
332
|
createOrder={async () => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
333
|
+
if (creatingOrderRef.current) throw new Error("Payment already processing")
|
|
334
|
+
creatingOrderRef.current = true
|
|
335
|
+
try {
|
|
336
|
+
setError(null)
|
|
337
|
+
const r = await api.createOrder(cartId, true)
|
|
338
|
+
return r.id
|
|
339
|
+
} catch (e) {
|
|
340
|
+
creatingOrderRef.current = false
|
|
341
|
+
throw e
|
|
342
|
+
}
|
|
209
343
|
}}
|
|
210
344
|
onApprove={async (data: { orderID?: string }) => {
|
|
211
345
|
try {
|
|
212
346
|
setError(null)
|
|
347
|
+
showProcessingOverlay()
|
|
213
348
|
const orderId = String(data?.orderID || "")
|
|
214
349
|
const result = await api.captureOrder(cartId, orderId)
|
|
215
350
|
|
|
216
351
|
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
|
|
217
352
|
|
|
218
|
-
onPaid?.({ ...result, ...completeResult })
|
|
353
|
+
await onPaid?.({ ...result, ...completeResult })
|
|
219
354
|
} catch (e: unknown) {
|
|
355
|
+
if (isNextRouterError(e)) throw e
|
|
356
|
+
hideProcessingOverlay()
|
|
357
|
+
creatingOrderRef.current = false
|
|
358
|
+
submittingRef.current = false
|
|
359
|
+
setSubmitting(false)
|
|
220
360
|
const msg = e instanceof Error ? e.message : "Card payment failed"
|
|
221
361
|
setError(msg)
|
|
222
362
|
onError?.(msg)
|
|
223
|
-
} finally {
|
|
224
|
-
setSubmitting(false)
|
|
225
363
|
}
|
|
226
364
|
}}
|
|
227
365
|
onCancel={() => {
|
|
366
|
+
hideProcessingOverlay()
|
|
367
|
+
creatingOrderRef.current = false
|
|
368
|
+
submittingRef.current = false
|
|
228
369
|
setSubmitting(false)
|
|
229
370
|
}}
|
|
230
371
|
onError={(e: Error | { message?: string }) => {
|
|
231
372
|
const msg = e instanceof Error ? e.message : e?.message || "CardFields error"
|
|
232
373
|
setError(msg)
|
|
233
374
|
onError?.(msg)
|
|
375
|
+
hideProcessingOverlay()
|
|
376
|
+
creatingOrderRef.current = false
|
|
377
|
+
submittingRef.current = false
|
|
234
378
|
setSubmitting(false)
|
|
235
379
|
}}
|
|
236
380
|
>
|
|
@@ -318,13 +462,17 @@ export function PayPalAdvancedCard(props: {
|
|
|
318
462
|
disabled={submitting}
|
|
319
463
|
label={submitting ? "Processing…" : "Pay by Card"}
|
|
320
464
|
onSubmit={() => {
|
|
465
|
+
if (submittingRef.current) return
|
|
466
|
+
submittingRef.current = true
|
|
321
467
|
setError(null)
|
|
322
468
|
setSubmitting(true)
|
|
469
|
+
showProcessingOverlay()
|
|
323
470
|
}}
|
|
324
471
|
/>
|
|
325
472
|
|
|
326
473
|
{error && (
|
|
327
474
|
<div
|
|
475
|
+
role="alert"
|
|
328
476
|
style={{
|
|
329
477
|
display: "flex",
|
|
330
478
|
alignItems: "flex-start",
|
|
@@ -7,7 +7,7 @@ export function PayPalCurrencyNotice({ config }: { config: PayPalConfig }) {
|
|
|
7
7
|
if (config.currency_supported) return null
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
|
-
<div style={{ padding: 12, border: "1px solid #ddd", borderRadius: 10 }}>
|
|
10
|
+
<div role="alert" style={{ padding: 12, border: "1px solid #ddd", borderRadius: 10 }}>
|
|
11
11
|
<div style={{ fontWeight: 600, marginBottom: 6 }}>PayPal currency issue</div>
|
|
12
12
|
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
|
13
13
|
{(config.currency_errors || []).map((e) => (
|
|
@@ -25,6 +25,8 @@ const SPIN_STYLE = `@keyframes _pp_section_spin { to { transform: rotate(360deg)
|
|
|
25
25
|
function SessionInitCard() {
|
|
26
26
|
return (
|
|
27
27
|
<div
|
|
28
|
+
role="status"
|
|
29
|
+
aria-label="Setting up payment"
|
|
28
30
|
style={{
|
|
29
31
|
display: "flex",
|
|
30
32
|
alignItems: "center",
|
|
@@ -58,6 +60,8 @@ function SessionInitCard() {
|
|
|
58
60
|
function ConfigLoadingCard() {
|
|
59
61
|
return (
|
|
60
62
|
<div
|
|
63
|
+
role="status"
|
|
64
|
+
aria-label="Connecting to PayPal"
|
|
61
65
|
style={{
|
|
62
66
|
display: "flex",
|
|
63
67
|
alignItems: "center",
|
|
@@ -95,6 +99,7 @@ function ConfigLoadingCard() {
|
|
|
95
99
|
function ErrorCard({ message }: { message: string }) {
|
|
96
100
|
return (
|
|
97
101
|
<div
|
|
102
|
+
role="alert"
|
|
98
103
|
style={{
|
|
99
104
|
padding: "12px 16px",
|
|
100
105
|
background: "#fef2f2",
|
|
@@ -140,9 +145,9 @@ export function PayPalPaymentSection({
|
|
|
140
145
|
})
|
|
141
146
|
|
|
142
147
|
const handlePaid = useCallback(
|
|
143
|
-
(captureResult: unknown) => {
|
|
148
|
+
async (captureResult: unknown) => {
|
|
144
149
|
onPaid?.(captureResult)
|
|
145
|
-
onSuccess?.(cartId)
|
|
150
|
+
await onSuccess?.(cartId)
|
|
146
151
|
},
|
|
147
152
|
[cartId, onPaid, onSuccess]
|
|
148
153
|
)
|
|
@@ -25,7 +25,7 @@ export function PayPalProvider(props: {
|
|
|
25
25
|
"disable-funding": disableFunding,
|
|
26
26
|
"data-partner-attribution-id": BN_CODE,
|
|
27
27
|
}
|
|
28
|
-
}, [config, intent, disableFunding])
|
|
28
|
+
}, [config.client_id, config.currency, config.client_token, intent, disableFunding])
|
|
29
29
|
|
|
30
30
|
return (
|
|
31
31
|
<PayPalScriptProvider
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
"use client"
|
|
2
|
-
import React, { useMemo, useState } from "react"
|
|
2
|
+
import React, { useCallback, useMemo, useState } from "react"
|
|
3
3
|
import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js"
|
|
4
4
|
import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
|
|
5
5
|
import type { PayPalConfig } from "../client/types"
|
|
6
|
+
import { isNextRouterError } from "../utils/next-errors"
|
|
7
|
+
import { showProcessingOverlay, hideProcessingOverlay } from "../utils/processing-overlay"
|
|
6
8
|
|
|
7
|
-
const SPIN_STYLE =
|
|
9
|
+
const SPIN_STYLE = `
|
|
10
|
+
@keyframes _pp_spin { to { transform: rotate(360deg) } }
|
|
11
|
+
@keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
|
|
12
|
+
`
|
|
8
13
|
|
|
9
14
|
const BUTTON_WIDTH_MAP: Record<string, string> = {
|
|
10
15
|
small: "300px",
|
|
@@ -29,7 +34,14 @@ export function PayPalSmartButtons(props: {
|
|
|
29
34
|
|
|
30
35
|
const [error, setError] = useState<string | null>(null)
|
|
31
36
|
const [processing, setProcessing] = useState(false)
|
|
32
|
-
const [
|
|
37
|
+
const [buttonsReady, setButtonsReady] = useState(false)
|
|
38
|
+
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
|
|
39
|
+
|
|
40
|
+
const handleInit = useCallback(() => {
|
|
41
|
+
setButtonsReady(true)
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
const showSpinner = isPending || (isResolved && !buttonsReady)
|
|
33
45
|
|
|
34
46
|
if (!config.currency_supported) return null
|
|
35
47
|
|
|
@@ -41,43 +53,94 @@ export function PayPalSmartButtons(props: {
|
|
|
41
53
|
|
|
42
54
|
{processing && (
|
|
43
55
|
<div
|
|
56
|
+
role="status"
|
|
57
|
+
aria-label="Processing payment"
|
|
44
58
|
style={{
|
|
45
|
-
position: "
|
|
59
|
+
position: "fixed",
|
|
46
60
|
inset: 0,
|
|
47
|
-
zIndex:
|
|
48
|
-
background: "rgba(255,255,255,0.
|
|
49
|
-
borderRadius: 8,
|
|
61
|
+
zIndex: 9999,
|
|
62
|
+
background: "rgba(255,255,255,0.96)",
|
|
50
63
|
display: "flex",
|
|
51
64
|
flexDirection: "column",
|
|
52
65
|
alignItems: "center",
|
|
53
66
|
justifyContent: "center",
|
|
54
|
-
gap:
|
|
55
|
-
minHeight: 60,
|
|
67
|
+
gap: 20,
|
|
56
68
|
}}
|
|
57
69
|
>
|
|
58
|
-
<div
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
<div style={{ position: "relative", width: 56, height: 56 }}>
|
|
71
|
+
<div
|
|
72
|
+
style={{
|
|
73
|
+
position: "absolute",
|
|
74
|
+
inset: 0,
|
|
75
|
+
borderRadius: "50%",
|
|
76
|
+
border: "3px solid #e5e7eb",
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
<div
|
|
80
|
+
style={{
|
|
81
|
+
position: "absolute",
|
|
82
|
+
inset: 0,
|
|
83
|
+
borderRadius: "50%",
|
|
84
|
+
border: "3px solid transparent",
|
|
85
|
+
borderTopColor: "#0070ba",
|
|
86
|
+
animation: "_pp_spin .8s linear infinite",
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
<svg
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="#0070ba"
|
|
93
|
+
strokeWidth="1.8"
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
style={{
|
|
97
|
+
position: "absolute",
|
|
98
|
+
top: "50%",
|
|
99
|
+
left: "50%",
|
|
100
|
+
transform: "translate(-50%, -50%)",
|
|
101
|
+
width: 24,
|
|
102
|
+
height: 24,
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
106
|
+
</svg>
|
|
107
|
+
</div>
|
|
68
108
|
<div style={{ textAlign: "center" }}>
|
|
69
|
-
<div style={{ fontSize:
|
|
70
|
-
Processing your payment
|
|
109
|
+
<div style={{ fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }}>
|
|
110
|
+
Processing your payment
|
|
71
111
|
</div>
|
|
72
|
-
<div style={{ fontSize:
|
|
73
|
-
Please do not close
|
|
112
|
+
<div style={{ fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }}>
|
|
113
|
+
This may take a few moments. Please do not close<br />
|
|
114
|
+
or refresh this page.
|
|
74
115
|
</div>
|
|
75
116
|
</div>
|
|
117
|
+
<div
|
|
118
|
+
style={{
|
|
119
|
+
width: 200,
|
|
120
|
+
height: 3,
|
|
121
|
+
background: "#e5e7eb",
|
|
122
|
+
borderRadius: 3,
|
|
123
|
+
overflow: "hidden",
|
|
124
|
+
marginTop: 4,
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
style={{
|
|
129
|
+
width: "40%",
|
|
130
|
+
height: "100%",
|
|
131
|
+
background: "linear-gradient(90deg, #0070ba, #003087)",
|
|
132
|
+
borderRadius: 3,
|
|
133
|
+
animation: "_pp_progress 1.5s ease-in-out infinite",
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
76
137
|
</div>
|
|
77
138
|
)}
|
|
78
139
|
|
|
79
|
-
{
|
|
140
|
+
{showSpinner && (
|
|
80
141
|
<div
|
|
142
|
+
role="status"
|
|
143
|
+
aria-label="Loading PayPal"
|
|
81
144
|
style={{
|
|
82
145
|
display: "flex",
|
|
83
146
|
alignItems: "center",
|
|
@@ -107,7 +170,31 @@ export function PayPalSmartButtons(props: {
|
|
|
107
170
|
</div>
|
|
108
171
|
)}
|
|
109
172
|
|
|
110
|
-
{
|
|
173
|
+
{isRejected && (
|
|
174
|
+
<div
|
|
175
|
+
role="alert"
|
|
176
|
+
style={{
|
|
177
|
+
display: "flex",
|
|
178
|
+
alignItems: "flex-start",
|
|
179
|
+
gap: 8,
|
|
180
|
+
padding: "10px 14px",
|
|
181
|
+
background: "#fef2f2",
|
|
182
|
+
border: "1px solid #fecaca",
|
|
183
|
+
borderRadius: 8,
|
|
184
|
+
fontSize: 13,
|
|
185
|
+
color: "#b91c1c",
|
|
186
|
+
lineHeight: 1.5,
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
|
|
190
|
+
<span>
|
|
191
|
+
PayPal failed to load. Please refresh the page or try a different
|
|
192
|
+
payment method.
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{config.environment === "sandbox" && buttonsReady && (
|
|
111
198
|
<div
|
|
112
199
|
style={{
|
|
113
200
|
display: "flex",
|
|
@@ -144,8 +231,10 @@ export function PayPalSmartButtons(props: {
|
|
|
144
231
|
)}
|
|
145
232
|
|
|
146
233
|
{isResolved && (
|
|
234
|
+
<div style={buttonsReady ? undefined : { position: "absolute", left: -9999, opacity: 0, pointerEvents: "none" }}>
|
|
147
235
|
<PayPalButtons
|
|
148
236
|
forceReRender={[config.currency, config.intent, cartId]}
|
|
237
|
+
onInit={handleInit}
|
|
149
238
|
style={{
|
|
150
239
|
layout: "vertical",
|
|
151
240
|
color: config.button_color,
|
|
@@ -154,42 +243,56 @@ export function PayPalSmartButtons(props: {
|
|
|
154
243
|
height: config.button_height,
|
|
155
244
|
}}
|
|
156
245
|
createOrder={async () => {
|
|
246
|
+
if (processing) throw new Error("Payment already processing")
|
|
157
247
|
setError(null)
|
|
158
|
-
|
|
159
|
-
|
|
248
|
+
setProcessing(true)
|
|
249
|
+
try {
|
|
250
|
+
const r = await api.createOrder(cartId)
|
|
251
|
+
return r.id
|
|
252
|
+
} catch (e: unknown) {
|
|
253
|
+
setProcessing(false)
|
|
254
|
+
throw e
|
|
255
|
+
}
|
|
160
256
|
}}
|
|
161
257
|
onApprove={async (data: { orderID?: string }) => {
|
|
162
258
|
try {
|
|
163
259
|
setProcessing(true)
|
|
260
|
+
showProcessingOverlay()
|
|
164
261
|
setError(null)
|
|
165
262
|
const orderId = String(data?.orderID || "")
|
|
263
|
+
if (!orderId) throw new Error("PayPal order ID is missing from approval response")
|
|
166
264
|
const result = await api.captureOrder(cartId, orderId)
|
|
167
265
|
|
|
168
266
|
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
|
|
169
267
|
|
|
170
|
-
onPaid?.({ ...result, ...completeResult })
|
|
268
|
+
await onPaid?.({ ...result, ...completeResult })
|
|
171
269
|
} catch (e: unknown) {
|
|
270
|
+
if (isNextRouterError(e)) throw e
|
|
271
|
+
hideProcessingOverlay()
|
|
272
|
+
setProcessing(false)
|
|
172
273
|
const msg = e instanceof Error ? e.message : "Payment capture failed"
|
|
173
274
|
setError(msg)
|
|
174
275
|
onError?.(msg)
|
|
175
|
-
} finally {
|
|
176
|
-
setProcessing(false)
|
|
177
276
|
}
|
|
178
277
|
}}
|
|
179
278
|
onCancel={() => {
|
|
279
|
+
hideProcessingOverlay()
|
|
180
280
|
setProcessing(false)
|
|
181
281
|
}}
|
|
182
282
|
onError={(err: Error | { message?: string }) => {
|
|
283
|
+
hideProcessingOverlay()
|
|
183
284
|
setProcessing(false)
|
|
184
285
|
const msg = err instanceof Error ? err.message : err?.message || "PayPal error"
|
|
185
286
|
setError(msg)
|
|
186
287
|
onError?.(msg)
|
|
187
288
|
}}
|
|
188
289
|
/>
|
|
290
|
+
</div>
|
|
189
291
|
)}
|
|
190
292
|
|
|
191
293
|
{error ? (
|
|
192
294
|
<div
|
|
295
|
+
role="alert"
|
|
193
296
|
style={{
|
|
194
297
|
marginTop: 10,
|
|
195
298
|
display: "flex",
|
|
@@ -11,9 +11,18 @@ type Args = {
|
|
|
11
11
|
enabled?: boolean
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
const MAX_CACHE_ENTRIES = 50
|
|
14
15
|
const _cache = new Map<string, { config: PayPalConfig; at: number }>()
|
|
15
16
|
const CACHE_TTL = 5 * 60 * 1000
|
|
16
17
|
|
|
18
|
+
function cacheSet(key: string, value: { config: PayPalConfig; at: number }) {
|
|
19
|
+
if (_cache.size >= MAX_CACHE_ENTRIES) {
|
|
20
|
+
const oldest = _cache.keys().next().value
|
|
21
|
+
if (oldest !== undefined) _cache.delete(oldest)
|
|
22
|
+
}
|
|
23
|
+
_cache.set(key, value)
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
function cacheKey(baseUrl: string, cartId?: string) {
|
|
18
27
|
return `${baseUrl}::${cartId ?? ""}`
|
|
19
28
|
}
|
|
@@ -66,12 +75,12 @@ export function usePayPalConfig({
|
|
|
66
75
|
try {
|
|
67
76
|
const cfg = await api.getConfig(cartId, controller.signal)
|
|
68
77
|
if (!mounted || id !== fetchIdRef.current) return
|
|
69
|
-
|
|
78
|
+
cacheSet(k, { config: cfg, at: Date.now() })
|
|
70
79
|
setConfig(cfg)
|
|
71
|
-
} catch (e:
|
|
72
|
-
if (e
|
|
80
|
+
} catch (e: unknown) {
|
|
81
|
+
if (e instanceof Error && e.name === "AbortError") return
|
|
73
82
|
if (!mounted || id !== fetchIdRef.current) return
|
|
74
|
-
setError(e
|
|
83
|
+
setError(e instanceof Error ? e.message : "Failed to load PayPal config")
|
|
75
84
|
} finally {
|
|
76
85
|
if (mounted && id === fetchIdRef.current) setLoading(false)
|
|
77
86
|
}
|