@easypayment/medusa-paypal-ui 1.0.51 → 1.0.52
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 +431 -81
- 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 +439 -90
- 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 +164 -26
- 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,37 @@ 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 [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
|
|
140
153
|
|
|
141
154
|
if (!config.currency_supported) return null
|
|
142
155
|
|
|
156
|
+
if (isRejected) {
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
role="alert"
|
|
160
|
+
style={{
|
|
161
|
+
display: "flex",
|
|
162
|
+
alignItems: "flex-start",
|
|
163
|
+
gap: 8,
|
|
164
|
+
padding: "10px 14px",
|
|
165
|
+
background: "#fef2f2",
|
|
166
|
+
border: "1px solid #fecaca",
|
|
167
|
+
borderRadius: 8,
|
|
168
|
+
fontSize: 13,
|
|
169
|
+
color: "#b91c1c",
|
|
170
|
+
lineHeight: 1.5,
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
|
|
174
|
+
<span>
|
|
175
|
+
PayPal failed to load. Please refresh the page or try a different
|
|
176
|
+
payment method.
|
|
177
|
+
</span>
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
143
182
|
if (!config.client_token) {
|
|
144
183
|
return (
|
|
145
184
|
<div
|
|
@@ -159,50 +198,138 @@ export function PayPalAdvancedCard(props: {
|
|
|
159
198
|
|
|
160
199
|
const isSandbox = config.environment === "sandbox"
|
|
161
200
|
|
|
201
|
+
if (isPending) {
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
role="status"
|
|
205
|
+
aria-label="Loading PayPal"
|
|
206
|
+
style={{
|
|
207
|
+
display: "flex",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
justifyContent: "center",
|
|
210
|
+
gap: 10,
|
|
211
|
+
padding: "18px 16px",
|
|
212
|
+
background: "#f9fafb",
|
|
213
|
+
border: "1px solid #e5e7eb",
|
|
214
|
+
borderRadius: 8,
|
|
215
|
+
minHeight: 55,
|
|
216
|
+
}}
|
|
217
|
+
>
|
|
218
|
+
<style>{SPIN_STYLE}</style>
|
|
219
|
+
<div
|
|
220
|
+
style={{
|
|
221
|
+
width: 22,
|
|
222
|
+
height: 22,
|
|
223
|
+
borderRadius: "50%",
|
|
224
|
+
border: "2.5px solid #e5e7eb",
|
|
225
|
+
borderTopColor: "#0070ba",
|
|
226
|
+
animation: "_pp_spin .7s linear infinite",
|
|
227
|
+
flexShrink: 0,
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
<div style={{ fontSize: 13, fontWeight: 500, color: "#6b7280" }}>
|
|
231
|
+
Loading card fields…
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!isResolved) return null
|
|
238
|
+
|
|
162
239
|
return (
|
|
163
240
|
<div style={{ position: "relative" }}>
|
|
164
241
|
<style>{SPIN_STYLE}</style>
|
|
165
242
|
|
|
166
243
|
{submitting && (
|
|
167
244
|
<div
|
|
245
|
+
role="status"
|
|
246
|
+
aria-label="Processing payment"
|
|
168
247
|
style={{
|
|
169
|
-
position: "
|
|
248
|
+
position: "fixed",
|
|
170
249
|
inset: 0,
|
|
171
|
-
zIndex:
|
|
172
|
-
background: "rgba(255,255,255,0.
|
|
173
|
-
borderRadius: 12,
|
|
250
|
+
zIndex: 9999,
|
|
251
|
+
background: "rgba(255,255,255,0.96)",
|
|
174
252
|
display: "flex",
|
|
175
253
|
flexDirection: "column",
|
|
176
254
|
alignItems: "center",
|
|
177
255
|
justifyContent: "center",
|
|
178
|
-
gap:
|
|
179
|
-
minHeight: 180,
|
|
256
|
+
gap: 20,
|
|
180
257
|
}}
|
|
181
258
|
>
|
|
182
|
-
<div
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
259
|
+
<div style={{ position: "relative", width: 56, height: 56 }}>
|
|
260
|
+
<div
|
|
261
|
+
style={{
|
|
262
|
+
position: "absolute",
|
|
263
|
+
inset: 0,
|
|
264
|
+
borderRadius: "50%",
|
|
265
|
+
border: "3px solid #e5e7eb",
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
<div
|
|
269
|
+
style={{
|
|
270
|
+
position: "absolute",
|
|
271
|
+
inset: 0,
|
|
272
|
+
borderRadius: "50%",
|
|
273
|
+
border: "3px solid transparent",
|
|
274
|
+
borderTopColor: "#2563eb",
|
|
275
|
+
animation: "_pp_spin .8s linear infinite",
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
<svg
|
|
279
|
+
viewBox="0 0 24 24"
|
|
280
|
+
fill="none"
|
|
281
|
+
stroke="#2563eb"
|
|
282
|
+
strokeWidth="1.8"
|
|
283
|
+
strokeLinecap="round"
|
|
284
|
+
strokeLinejoin="round"
|
|
285
|
+
style={{
|
|
286
|
+
position: "absolute",
|
|
287
|
+
top: "50%",
|
|
288
|
+
left: "50%",
|
|
289
|
+
transform: "translate(-50%, -50%)",
|
|
290
|
+
width: 24,
|
|
291
|
+
height: 24,
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
295
|
+
</svg>
|
|
296
|
+
</div>
|
|
192
297
|
<div style={{ textAlign: "center" }}>
|
|
193
|
-
<div style={{ fontSize:
|
|
194
|
-
Processing your payment
|
|
298
|
+
<div style={{ fontSize: 18, fontWeight: 600, color: "#111827", letterSpacing: "-0.01em" }}>
|
|
299
|
+
Processing your payment
|
|
195
300
|
</div>
|
|
196
|
-
<div style={{ fontSize:
|
|
197
|
-
Please do not close
|
|
301
|
+
<div style={{ fontSize: 14, color: "#6b7280", marginTop: 6, lineHeight: 1.5 }}>
|
|
302
|
+
This may take a few moments. Please do not close<br />
|
|
303
|
+
or refresh this page.
|
|
198
304
|
</div>
|
|
199
305
|
</div>
|
|
306
|
+
<div
|
|
307
|
+
style={{
|
|
308
|
+
width: 200,
|
|
309
|
+
height: 3,
|
|
310
|
+
background: "#e5e7eb",
|
|
311
|
+
borderRadius: 3,
|
|
312
|
+
overflow: "hidden",
|
|
313
|
+
marginTop: 4,
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
<div
|
|
317
|
+
style={{
|
|
318
|
+
width: "40%",
|
|
319
|
+
height: "100%",
|
|
320
|
+
background: "linear-gradient(90deg, #2563eb, #1d4ed8)",
|
|
321
|
+
borderRadius: 3,
|
|
322
|
+
animation: "_pp_progress 1.5s ease-in-out infinite",
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
</div>
|
|
200
326
|
</div>
|
|
201
327
|
)}
|
|
202
328
|
|
|
203
329
|
<PayPalCardFieldsProvider
|
|
204
330
|
style={cardStyle}
|
|
205
331
|
createOrder={async () => {
|
|
332
|
+
if (submittingRef.current) throw new Error("Payment already processing")
|
|
206
333
|
setError(null)
|
|
207
334
|
const r = await api.createOrder(cartId, true)
|
|
208
335
|
return r.id
|
|
@@ -210,27 +337,34 @@ export function PayPalAdvancedCard(props: {
|
|
|
210
337
|
onApprove={async (data: { orderID?: string }) => {
|
|
211
338
|
try {
|
|
212
339
|
setError(null)
|
|
340
|
+
showProcessingOverlay()
|
|
213
341
|
const orderId = String(data?.orderID || "")
|
|
214
342
|
const result = await api.captureOrder(cartId, orderId)
|
|
215
343
|
|
|
216
344
|
const completeResult = await markPaymentComplete(baseUrl, cartId, publishableApiKey)
|
|
217
345
|
|
|
218
|
-
onPaid?.({ ...result, ...completeResult })
|
|
346
|
+
await onPaid?.({ ...result, ...completeResult })
|
|
219
347
|
} catch (e: unknown) {
|
|
348
|
+
if (isNextRouterError(e)) throw e
|
|
349
|
+
hideProcessingOverlay()
|
|
350
|
+
submittingRef.current = false
|
|
351
|
+
setSubmitting(false)
|
|
220
352
|
const msg = e instanceof Error ? e.message : "Card payment failed"
|
|
221
353
|
setError(msg)
|
|
222
354
|
onError?.(msg)
|
|
223
|
-
} finally {
|
|
224
|
-
setSubmitting(false)
|
|
225
355
|
}
|
|
226
356
|
}}
|
|
227
357
|
onCancel={() => {
|
|
358
|
+
hideProcessingOverlay()
|
|
359
|
+
submittingRef.current = false
|
|
228
360
|
setSubmitting(false)
|
|
229
361
|
}}
|
|
230
362
|
onError={(e: Error | { message?: string }) => {
|
|
231
363
|
const msg = e instanceof Error ? e.message : e?.message || "CardFields error"
|
|
232
364
|
setError(msg)
|
|
233
365
|
onError?.(msg)
|
|
366
|
+
hideProcessingOverlay()
|
|
367
|
+
submittingRef.current = false
|
|
234
368
|
setSubmitting(false)
|
|
235
369
|
}}
|
|
236
370
|
>
|
|
@@ -318,13 +452,17 @@ export function PayPalAdvancedCard(props: {
|
|
|
318
452
|
disabled={submitting}
|
|
319
453
|
label={submitting ? "Processing…" : "Pay by Card"}
|
|
320
454
|
onSubmit={() => {
|
|
455
|
+
if (submittingRef.current) return
|
|
456
|
+
submittingRef.current = true
|
|
321
457
|
setError(null)
|
|
322
458
|
setSubmitting(true)
|
|
459
|
+
showProcessingOverlay()
|
|
323
460
|
}}
|
|
324
461
|
/>
|
|
325
462
|
|
|
326
463
|
{error && (
|
|
327
464
|
<div
|
|
465
|
+
role="alert"
|
|
328
466
|
style={{
|
|
329
467
|
display: "flex",
|
|
330
468
|
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
|
}
|