@easypayment/medusa-paypal-ui 1.2.8 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/LICENSE +21 -1
- package/README.md +8 -2
- package/dist/index.cjs +605 -676
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -28
- package/dist/index.d.ts +101 -28
- package/dist/index.mjs +601 -676
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs +11 -8
- package/dist/order.cjs.map +1 -1
- package/dist/order.d.cts +1 -1
- package/dist/order.d.ts +1 -1
- package/dist/order.mjs +10 -8
- package/dist/order.mjs.map +1 -1
- package/package.json +43 -4
- package/src/adapters/MedusaNextPayPalAdapter.tsx +48 -10
- package/src/client/config-cache.ts +96 -0
- package/src/client/http.ts +24 -5
- package/src/client/paypal.ts +28 -6
- package/src/components/PayPalAdvancedCard.tsx +88 -214
- package/src/components/PayPalPaymentSection.tsx +17 -6
- package/src/components/PayPalProvider.tsx +4 -1
- package/src/components/PayPalSmartButtons.tsx +92 -224
- package/src/components/internal/ui.tsx +221 -0
- package/src/hooks/usePayPalConfig.ts +43 -32
- package/src/hooks/usePayPalPaymentMethods.ts +59 -54
- package/src/index.ts +4 -0
- package/src/order.ts +16 -8
- package/src/utils/processing-overlay.ts +25 -21
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
showProcessingOverlay,
|
|
14
14
|
hideProcessingOverlay,
|
|
15
15
|
} from "../utils/processing-overlay"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import {
|
|
17
|
+
AnimationStyles,
|
|
18
|
+
ErrorBanner,
|
|
19
|
+
LoadingCard,
|
|
20
|
+
ProcessingOverlayView,
|
|
21
|
+
} from "./internal/ui"
|
|
21
22
|
|
|
22
23
|
const BUTTON_WIDTH_MAP: Record<string, string> = {
|
|
23
24
|
small: "300px",
|
|
@@ -29,19 +30,35 @@ const BUTTON_WIDTH_MAP: Record<string, string> = {
|
|
|
29
30
|
export function PayPalSmartButtons(props: {
|
|
30
31
|
baseUrl: string
|
|
31
32
|
publishableApiKey?: string
|
|
33
|
+
/** Forwarded to every API call — see `HttpOptions.credentials`. */
|
|
34
|
+
credentials?: RequestCredentials
|
|
32
35
|
cartId: string
|
|
33
36
|
config: PayPalConfig
|
|
34
|
-
onPaid?: (result: Record<string, unknown>) => void
|
|
37
|
+
onPaid?: (result: Record<string, unknown>) => void | Promise<void>
|
|
35
38
|
onError?: (message: string) => void
|
|
36
39
|
}) {
|
|
37
|
-
const {
|
|
40
|
+
const {
|
|
41
|
+
baseUrl,
|
|
42
|
+
publishableApiKey,
|
|
43
|
+
credentials,
|
|
44
|
+
cartId,
|
|
45
|
+
config,
|
|
46
|
+
onPaid,
|
|
47
|
+
onError,
|
|
48
|
+
} = props
|
|
38
49
|
const api = useMemo(
|
|
39
|
-
() => createPayPalStoreApi({ baseUrl, publishableApiKey }),
|
|
40
|
-
[baseUrl, publishableApiKey]
|
|
50
|
+
() => createPayPalStoreApi({ baseUrl, publishableApiKey, credentials }),
|
|
51
|
+
[baseUrl, publishableApiKey, credentials]
|
|
41
52
|
)
|
|
42
53
|
|
|
43
54
|
const [error, setError] = useState<string | null>(null)
|
|
44
55
|
const [processing, setProcessing] = useState(false)
|
|
56
|
+
// Synchronous mirror of `processing` — React state updates are async, so a
|
|
57
|
+
// second click in the same tick would slip past a state-based guard.
|
|
58
|
+
const processingRef = useRef(false)
|
|
59
|
+
// True while the shared full-page DOM overlay is mounted, so the component
|
|
60
|
+
// doesn't also render its own copy on top of it.
|
|
61
|
+
const [domOverlayUp, setDomOverlayUp] = useState(false)
|
|
45
62
|
const [buttonsReady, setButtonsReady] = useState(false)
|
|
46
63
|
const [loadTimedOut, setLoadTimedOut] = useState(false)
|
|
47
64
|
// Once a capture succeeded, the buyer's money is taken — any later failure
|
|
@@ -51,6 +68,23 @@ export function PayPalSmartButtons(props: {
|
|
|
51
68
|
const [completionPending, setCompletionPending] = useState(false)
|
|
52
69
|
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
|
|
53
70
|
|
|
71
|
+
const startProcessing = useCallback(() => {
|
|
72
|
+
processingRef.current = true
|
|
73
|
+
setProcessing(true)
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const stopProcessing = useCallback(() => {
|
|
77
|
+
hideProcessingOverlay()
|
|
78
|
+
setDomOverlayUp(false)
|
|
79
|
+
processingRef.current = false
|
|
80
|
+
setProcessing(false)
|
|
81
|
+
}, [])
|
|
82
|
+
|
|
83
|
+
const showOverlay = useCallback(() => {
|
|
84
|
+
showProcessingOverlay()
|
|
85
|
+
setDomOverlayUp(true)
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
54
88
|
const handleInit = useCallback(() => {
|
|
55
89
|
setButtonsReady(true)
|
|
56
90
|
}, [])
|
|
@@ -60,7 +94,11 @@ export function PayPalSmartButtons(props: {
|
|
|
60
94
|
// behind the in-component overlay.
|
|
61
95
|
useEffect(() => {
|
|
62
96
|
const onPageShow = (e: PageTransitionEvent) => {
|
|
63
|
-
if (e.persisted)
|
|
97
|
+
if (e.persisted) {
|
|
98
|
+
processingRef.current = false
|
|
99
|
+
setProcessing(false)
|
|
100
|
+
setDomOverlayUp(false)
|
|
101
|
+
}
|
|
64
102
|
}
|
|
65
103
|
window.addEventListener("pageshow", onPageShow)
|
|
66
104
|
return () => window.removeEventListener("pageshow", onPageShow)
|
|
@@ -93,15 +131,14 @@ export function PayPalSmartButtons(props: {
|
|
|
93
131
|
const finalizeCapturedPayment = useCallback(async () => {
|
|
94
132
|
const captured = capturedRef.current
|
|
95
133
|
if (!captured) return
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
startProcessing()
|
|
135
|
+
showOverlay()
|
|
98
136
|
setError(null)
|
|
99
137
|
try {
|
|
100
|
-
const completeResult = await markPaymentComplete(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
138
|
+
const completeResult = await markPaymentComplete(baseUrl, cartId, {
|
|
139
|
+
publishableApiKey,
|
|
140
|
+
credentials,
|
|
141
|
+
})
|
|
105
142
|
// The order is finalized — the persisted "captured" flag has served its
|
|
106
143
|
// purpose. Clear it before the consumer callback (which may redirect).
|
|
107
144
|
clearCartCaptured(cartId)
|
|
@@ -109,15 +146,24 @@ export function PayPalSmartButtons(props: {
|
|
|
109
146
|
setCompletionPending(false)
|
|
110
147
|
} catch (e: unknown) {
|
|
111
148
|
if (isNextRouterError(e)) return
|
|
112
|
-
|
|
113
|
-
setProcessing(false)
|
|
149
|
+
stopProcessing()
|
|
114
150
|
setCompletionPending(true)
|
|
115
151
|
const msg =
|
|
116
152
|
"Your payment was received, but finalizing your order failed. Please use the Retry button below — do not pay again."
|
|
117
153
|
setError(msg)
|
|
118
154
|
onError?.(msg)
|
|
119
155
|
}
|
|
120
|
-
}, [
|
|
156
|
+
}, [
|
|
157
|
+
baseUrl,
|
|
158
|
+
cartId,
|
|
159
|
+
publishableApiKey,
|
|
160
|
+
credentials,
|
|
161
|
+
onPaid,
|
|
162
|
+
onError,
|
|
163
|
+
startProcessing,
|
|
164
|
+
showOverlay,
|
|
165
|
+
stopProcessing,
|
|
166
|
+
])
|
|
121
167
|
|
|
122
168
|
const showSpinner =
|
|
123
169
|
(isPending || (isResolved && !buttonsReady)) && !loadTimedOut
|
|
@@ -129,188 +175,26 @@ export function PayPalSmartButtons(props: {
|
|
|
129
175
|
|
|
130
176
|
return (
|
|
131
177
|
<div style={{ width: containerWidth, position: "relative" }}>
|
|
132
|
-
<
|
|
178
|
+
<AnimationStyles />
|
|
133
179
|
|
|
134
|
-
{processing && (
|
|
135
|
-
<
|
|
136
|
-
role="status"
|
|
137
|
-
aria-label="Processing payment"
|
|
138
|
-
style={{
|
|
139
|
-
position: "fixed",
|
|
140
|
-
inset: 0,
|
|
141
|
-
zIndex: 9999,
|
|
142
|
-
background: "rgba(255,255,255,0.96)",
|
|
143
|
-
display: "flex",
|
|
144
|
-
flexDirection: "column",
|
|
145
|
-
alignItems: "center",
|
|
146
|
-
justifyContent: "center",
|
|
147
|
-
gap: 20,
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
<div style={{ position: "relative", width: 56, height: 56 }}>
|
|
151
|
-
<div
|
|
152
|
-
style={{
|
|
153
|
-
position: "absolute",
|
|
154
|
-
inset: 0,
|
|
155
|
-
borderRadius: "50%",
|
|
156
|
-
border: "3px solid #e5e7eb",
|
|
157
|
-
}}
|
|
158
|
-
/>
|
|
159
|
-
<div
|
|
160
|
-
style={{
|
|
161
|
-
position: "absolute",
|
|
162
|
-
inset: 0,
|
|
163
|
-
borderRadius: "50%",
|
|
164
|
-
border: "3px solid transparent",
|
|
165
|
-
borderTopColor: "#0070ba",
|
|
166
|
-
animation: "_pp_spin .8s linear infinite",
|
|
167
|
-
}}
|
|
168
|
-
/>
|
|
169
|
-
<svg
|
|
170
|
-
viewBox="0 0 24 24"
|
|
171
|
-
fill="none"
|
|
172
|
-
stroke="#0070ba"
|
|
173
|
-
strokeWidth="1.8"
|
|
174
|
-
strokeLinecap="round"
|
|
175
|
-
strokeLinejoin="round"
|
|
176
|
-
style={{
|
|
177
|
-
position: "absolute",
|
|
178
|
-
top: "50%",
|
|
179
|
-
left: "50%",
|
|
180
|
-
transform: "translate(-50%, -50%)",
|
|
181
|
-
width: 24,
|
|
182
|
-
height: 24,
|
|
183
|
-
}}
|
|
184
|
-
>
|
|
185
|
-
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
186
|
-
</svg>
|
|
187
|
-
</div>
|
|
188
|
-
<div style={{ textAlign: "center" }}>
|
|
189
|
-
<div
|
|
190
|
-
style={{
|
|
191
|
-
fontSize: 18,
|
|
192
|
-
fontWeight: 600,
|
|
193
|
-
color: "#111827",
|
|
194
|
-
letterSpacing: "-0.01em",
|
|
195
|
-
}}
|
|
196
|
-
>
|
|
197
|
-
Processing your payment
|
|
198
|
-
</div>
|
|
199
|
-
<div
|
|
200
|
-
style={{
|
|
201
|
-
fontSize: 14,
|
|
202
|
-
color: "#6b7280",
|
|
203
|
-
marginTop: 6,
|
|
204
|
-
lineHeight: 1.5,
|
|
205
|
-
}}
|
|
206
|
-
>
|
|
207
|
-
This may take a few moments. Please do not close
|
|
208
|
-
<br />
|
|
209
|
-
or refresh this page.
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
<div
|
|
213
|
-
style={{
|
|
214
|
-
width: 200,
|
|
215
|
-
height: 3,
|
|
216
|
-
background: "#e5e7eb",
|
|
217
|
-
borderRadius: 3,
|
|
218
|
-
overflow: "hidden",
|
|
219
|
-
marginTop: 4,
|
|
220
|
-
}}
|
|
221
|
-
>
|
|
222
|
-
<div
|
|
223
|
-
style={{
|
|
224
|
-
width: "40%",
|
|
225
|
-
height: "100%",
|
|
226
|
-
background: "linear-gradient(90deg, #0070ba, #003087)",
|
|
227
|
-
borderRadius: 3,
|
|
228
|
-
animation: "_pp_progress 1.5s ease-in-out infinite",
|
|
229
|
-
}}
|
|
230
|
-
/>
|
|
231
|
-
</div>
|
|
232
|
-
</div>
|
|
180
|
+
{processing && !domOverlayUp && (
|
|
181
|
+
<ProcessingOverlayView accent="#0070ba" />
|
|
233
182
|
)}
|
|
234
183
|
|
|
235
|
-
{showSpinner &&
|
|
236
|
-
<div
|
|
237
|
-
role="status"
|
|
238
|
-
aria-label="Loading PayPal"
|
|
239
|
-
style={{
|
|
240
|
-
display: "flex",
|
|
241
|
-
alignItems: "center",
|
|
242
|
-
justifyContent: "center",
|
|
243
|
-
gap: 10,
|
|
244
|
-
padding: "18px 16px",
|
|
245
|
-
background: "#f9fafb",
|
|
246
|
-
border: "1px solid #e5e7eb",
|
|
247
|
-
borderRadius: 8,
|
|
248
|
-
minHeight: 55,
|
|
249
|
-
}}
|
|
250
|
-
>
|
|
251
|
-
<div
|
|
252
|
-
style={{
|
|
253
|
-
width: 22,
|
|
254
|
-
height: 22,
|
|
255
|
-
borderRadius: "50%",
|
|
256
|
-
border: "2.5px solid #e5e7eb",
|
|
257
|
-
borderTopColor: "#0070ba",
|
|
258
|
-
animation: "_pp_spin .7s linear infinite",
|
|
259
|
-
flexShrink: 0,
|
|
260
|
-
}}
|
|
261
|
-
/>
|
|
262
|
-
<div style={{ fontSize: 13, fontWeight: 500, color: "#6b7280" }}>
|
|
263
|
-
Loading PayPal…
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
184
|
+
{showSpinner && <LoadingCard label="Loading PayPal…" />}
|
|
267
185
|
|
|
268
186
|
{loadTimedOut && !buttonsReady && !isRejected && (
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
alignItems: "flex-start",
|
|
274
|
-
gap: 8,
|
|
275
|
-
padding: "10px 14px",
|
|
276
|
-
background: "#fef2f2",
|
|
277
|
-
border: "1px solid #fecaca",
|
|
278
|
-
borderRadius: 8,
|
|
279
|
-
fontSize: 13,
|
|
280
|
-
color: "#b91c1c",
|
|
281
|
-
lineHeight: 1.5,
|
|
282
|
-
}}
|
|
283
|
-
>
|
|
284
|
-
<span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
|
|
285
|
-
<span>
|
|
286
|
-
PayPal is not available for this purchase. Please try a different
|
|
287
|
-
payment method.
|
|
288
|
-
</span>
|
|
289
|
-
</div>
|
|
187
|
+
<ErrorBanner>
|
|
188
|
+
PayPal is not available for this purchase. Please try a different
|
|
189
|
+
payment method.
|
|
190
|
+
</ErrorBanner>
|
|
290
191
|
)}
|
|
291
192
|
|
|
292
193
|
{isRejected && (
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
alignItems: "flex-start",
|
|
298
|
-
gap: 8,
|
|
299
|
-
padding: "10px 14px",
|
|
300
|
-
background: "#fef2f2",
|
|
301
|
-
border: "1px solid #fecaca",
|
|
302
|
-
borderRadius: 8,
|
|
303
|
-
fontSize: 13,
|
|
304
|
-
color: "#b91c1c",
|
|
305
|
-
lineHeight: 1.5,
|
|
306
|
-
}}
|
|
307
|
-
>
|
|
308
|
-
<span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
|
|
309
|
-
<span>
|
|
310
|
-
PayPal failed to load. Please refresh the page or try a different
|
|
311
|
-
payment method.
|
|
312
|
-
</span>
|
|
313
|
-
</div>
|
|
194
|
+
<ErrorBanner>
|
|
195
|
+
PayPal failed to load. Please refresh the page or try a different
|
|
196
|
+
payment method.
|
|
197
|
+
</ErrorBanner>
|
|
314
198
|
)}
|
|
315
199
|
|
|
316
200
|
{config.environment === "sandbox" && buttonsReady && (
|
|
@@ -337,6 +221,7 @@ export function PayPalSmartButtons(props: {
|
|
|
337
221
|
stroke="#1e40af"
|
|
338
222
|
strokeWidth="2"
|
|
339
223
|
strokeLinecap="round"
|
|
224
|
+
aria-hidden="true"
|
|
340
225
|
style={{ flexShrink: 0, marginTop: 1 }}
|
|
341
226
|
>
|
|
342
227
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -373,7 +258,11 @@ export function PayPalSmartButtons(props: {
|
|
|
373
258
|
height: config.button_height,
|
|
374
259
|
}}
|
|
375
260
|
createOrder={async () => {
|
|
376
|
-
|
|
261
|
+
// Ref, not state: a double click lands before React applies the
|
|
262
|
+
// state update, so only the synchronous ref reliably blocks the
|
|
263
|
+
// second createOrder (and its duplicate PayPal order).
|
|
264
|
+
if (processingRef.current)
|
|
265
|
+
throw new Error("Payment already processing")
|
|
377
266
|
if (capturedRef.current) {
|
|
378
267
|
// Money was already captured for this cart — retry the order
|
|
379
268
|
// finalization instead of creating (and charging) a new order.
|
|
@@ -383,19 +272,19 @@ export function PayPalSmartButtons(props: {
|
|
|
383
272
|
)
|
|
384
273
|
}
|
|
385
274
|
setError(null)
|
|
386
|
-
|
|
275
|
+
startProcessing()
|
|
387
276
|
try {
|
|
388
277
|
const r = await api.createOrder(cartId)
|
|
389
278
|
return r.id
|
|
390
279
|
} catch (e: unknown) {
|
|
391
|
-
|
|
280
|
+
stopProcessing()
|
|
392
281
|
throw e
|
|
393
282
|
}
|
|
394
283
|
}}
|
|
395
284
|
onApprove={async (data: { orderID?: string }) => {
|
|
396
285
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
286
|
+
startProcessing()
|
|
287
|
+
showOverlay()
|
|
399
288
|
setError(null)
|
|
400
289
|
const orderId = String(data?.orderID || "")
|
|
401
290
|
if (!orderId)
|
|
@@ -409,8 +298,7 @@ export function PayPalSmartButtons(props: {
|
|
|
409
298
|
markCartCaptured(cartId)
|
|
410
299
|
} catch (e: unknown) {
|
|
411
300
|
if (isNextRouterError(e)) return
|
|
412
|
-
|
|
413
|
-
setProcessing(false)
|
|
301
|
+
stopProcessing()
|
|
414
302
|
const msg =
|
|
415
303
|
e instanceof Error ? e.message : "Payment capture failed"
|
|
416
304
|
setError(msg)
|
|
@@ -422,12 +310,10 @@ export function PayPalSmartButtons(props: {
|
|
|
422
310
|
await finalizeCapturedPayment()
|
|
423
311
|
}}
|
|
424
312
|
onCancel={() => {
|
|
425
|
-
|
|
426
|
-
setProcessing(false)
|
|
313
|
+
stopProcessing()
|
|
427
314
|
}}
|
|
428
315
|
onError={(err: Error | { message?: string }) => {
|
|
429
|
-
|
|
430
|
-
setProcessing(false)
|
|
316
|
+
stopProcessing()
|
|
431
317
|
const msg =
|
|
432
318
|
err instanceof Error
|
|
433
319
|
? err.message
|
|
@@ -464,25 +350,7 @@ export function PayPalSmartButtons(props: {
|
|
|
464
350
|
)}
|
|
465
351
|
|
|
466
352
|
{error ? (
|
|
467
|
-
<
|
|
468
|
-
role="alert"
|
|
469
|
-
style={{
|
|
470
|
-
marginTop: 10,
|
|
471
|
-
display: "flex",
|
|
472
|
-
alignItems: "flex-start",
|
|
473
|
-
gap: 8,
|
|
474
|
-
padding: "10px 14px",
|
|
475
|
-
background: "#fef2f2",
|
|
476
|
-
border: "1px solid #fecaca",
|
|
477
|
-
borderRadius: 8,
|
|
478
|
-
fontSize: 13,
|
|
479
|
-
color: "#b91c1c",
|
|
480
|
-
lineHeight: 1.5,
|
|
481
|
-
}}
|
|
482
|
-
>
|
|
483
|
-
<span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
|
|
484
|
-
<span>{error}</span>
|
|
485
|
-
</div>
|
|
353
|
+
<ErrorBanner style={{ marginTop: 10 }}>{error}</ErrorBanner>
|
|
486
354
|
) : null}
|
|
487
355
|
</div>
|
|
488
356
|
)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared presentational pieces for the payment components. One definition of
|
|
7
|
+
* the keyframes/animation CSS (previously copied into four files) and the
|
|
8
|
+
* overlay/spinner/error markup used by both PayPalSmartButtons and
|
|
9
|
+
* PayPalAdvancedCard.
|
|
10
|
+
*
|
|
11
|
+
* Animations are applied through class names so a single
|
|
12
|
+
* `prefers-reduced-motion` rule can disable them for buyers who ask for less
|
|
13
|
+
* motion, without a global selector leaking into the host page.
|
|
14
|
+
*/
|
|
15
|
+
export const ANIMATION_CSS = `
|
|
16
|
+
@keyframes _pp_spin { to { transform: rotate(360deg) } }
|
|
17
|
+
@keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
|
|
18
|
+
._pp-spin { animation: _pp_spin .8s linear infinite }
|
|
19
|
+
._pp-spin-sm { animation: _pp_spin .7s linear infinite }
|
|
20
|
+
._pp-progress { animation: _pp_progress 1.5s ease-in-out infinite }
|
|
21
|
+
@media (prefers-reduced-motion: reduce) {
|
|
22
|
+
._pp-spin, ._pp-spin-sm, ._pp-progress { animation: none }
|
|
23
|
+
}
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
export function AnimationStyles() {
|
|
27
|
+
return <style>{ANIMATION_CSS}</style>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ShieldIcon({
|
|
31
|
+
color,
|
|
32
|
+
size = 24,
|
|
33
|
+
style,
|
|
34
|
+
}: {
|
|
35
|
+
color: string
|
|
36
|
+
size?: number
|
|
37
|
+
style?: React.CSSProperties
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<svg
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
fill="none"
|
|
43
|
+
stroke={color}
|
|
44
|
+
strokeWidth="1.8"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
aria-hidden="true"
|
|
48
|
+
style={{ width: size, height: size, ...style }}
|
|
49
|
+
>
|
|
50
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
51
|
+
</svg>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Full-screen "Processing your payment" overlay. role="status" makes screen
|
|
57
|
+
* readers announce the visible text — deliberately no aria-label, which would
|
|
58
|
+
* override that text.
|
|
59
|
+
*/
|
|
60
|
+
export function ProcessingOverlayView({ accent }: { accent: string }) {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
role="status"
|
|
64
|
+
style={{
|
|
65
|
+
position: "fixed",
|
|
66
|
+
inset: 0,
|
|
67
|
+
zIndex: 9999,
|
|
68
|
+
background: "rgba(255,255,255,0.96)",
|
|
69
|
+
display: "flex",
|
|
70
|
+
flexDirection: "column",
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
justifyContent: "center",
|
|
73
|
+
gap: 20,
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<div style={{ position: "relative", width: 56, height: 56 }}>
|
|
77
|
+
<div
|
|
78
|
+
style={{
|
|
79
|
+
position: "absolute",
|
|
80
|
+
inset: 0,
|
|
81
|
+
borderRadius: "50%",
|
|
82
|
+
border: "3px solid #e5e7eb",
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
<div
|
|
86
|
+
className="_pp-spin"
|
|
87
|
+
style={{
|
|
88
|
+
position: "absolute",
|
|
89
|
+
inset: 0,
|
|
90
|
+
borderRadius: "50%",
|
|
91
|
+
border: "3px solid transparent",
|
|
92
|
+
borderTopColor: accent,
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
<ShieldIcon
|
|
96
|
+
color={accent}
|
|
97
|
+
style={{
|
|
98
|
+
position: "absolute",
|
|
99
|
+
top: "50%",
|
|
100
|
+
left: "50%",
|
|
101
|
+
transform: "translate(-50%, -50%)",
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<div style={{ textAlign: "center" }}>
|
|
106
|
+
<div
|
|
107
|
+
style={{
|
|
108
|
+
fontSize: 18,
|
|
109
|
+
fontWeight: 600,
|
|
110
|
+
color: "#111827",
|
|
111
|
+
letterSpacing: "-0.01em",
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
Processing your payment
|
|
115
|
+
</div>
|
|
116
|
+
<div
|
|
117
|
+
style={{
|
|
118
|
+
fontSize: 14,
|
|
119
|
+
color: "#6b7280",
|
|
120
|
+
marginTop: 6,
|
|
121
|
+
lineHeight: 1.5,
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
This may take a few moments. Please do not close
|
|
125
|
+
<br />
|
|
126
|
+
or refresh this page.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div
|
|
130
|
+
style={{
|
|
131
|
+
width: 200,
|
|
132
|
+
height: 3,
|
|
133
|
+
background: "#e5e7eb",
|
|
134
|
+
borderRadius: 3,
|
|
135
|
+
overflow: "hidden",
|
|
136
|
+
marginTop: 4,
|
|
137
|
+
}}
|
|
138
|
+
aria-hidden="true"
|
|
139
|
+
>
|
|
140
|
+
<div
|
|
141
|
+
className="_pp-progress"
|
|
142
|
+
style={{
|
|
143
|
+
width: "40%",
|
|
144
|
+
height: "100%",
|
|
145
|
+
background: `linear-gradient(90deg, ${accent}, #003087)`,
|
|
146
|
+
borderRadius: 3,
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Small inline loading card ("Loading PayPal…", "Loading card fields…"). */
|
|
155
|
+
export function LoadingCard({ label }: { label: string }) {
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
role="status"
|
|
159
|
+
style={{
|
|
160
|
+
display: "flex",
|
|
161
|
+
alignItems: "center",
|
|
162
|
+
justifyContent: "center",
|
|
163
|
+
gap: 10,
|
|
164
|
+
padding: "18px 16px",
|
|
165
|
+
background: "#f9fafb",
|
|
166
|
+
border: "1px solid #e5e7eb",
|
|
167
|
+
borderRadius: 8,
|
|
168
|
+
minHeight: 55,
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<div
|
|
172
|
+
className="_pp-spin-sm"
|
|
173
|
+
aria-hidden="true"
|
|
174
|
+
style={{
|
|
175
|
+
width: 22,
|
|
176
|
+
height: 22,
|
|
177
|
+
borderRadius: "50%",
|
|
178
|
+
border: "2.5px solid #e5e7eb",
|
|
179
|
+
borderTopColor: "#0070ba",
|
|
180
|
+
flexShrink: 0,
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
<div style={{ fontSize: 13, fontWeight: 500, color: "#6b7280" }}>
|
|
184
|
+
{label}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Red role="alert" banner with a decorative warning icon. */
|
|
191
|
+
export function ErrorBanner({
|
|
192
|
+
children,
|
|
193
|
+
style,
|
|
194
|
+
}: {
|
|
195
|
+
children: React.ReactNode
|
|
196
|
+
style?: React.CSSProperties
|
|
197
|
+
}) {
|
|
198
|
+
return (
|
|
199
|
+
<div
|
|
200
|
+
role="alert"
|
|
201
|
+
style={{
|
|
202
|
+
display: "flex",
|
|
203
|
+
alignItems: "flex-start",
|
|
204
|
+
gap: 8,
|
|
205
|
+
padding: "10px 14px",
|
|
206
|
+
background: "#fef2f2",
|
|
207
|
+
border: "1px solid #fecaca",
|
|
208
|
+
borderRadius: 8,
|
|
209
|
+
fontSize: 13,
|
|
210
|
+
color: "#b91c1c",
|
|
211
|
+
lineHeight: 1.5,
|
|
212
|
+
...style,
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
<span aria-hidden="true" style={{ flexShrink: 0, fontSize: 15 }}>
|
|
216
|
+
⚠️
|
|
217
|
+
</span>
|
|
218
|
+
<span>{children}</span>
|
|
219
|
+
</div>
|
|
220
|
+
)
|
|
221
|
+
}
|