@easypayment/medusa-paypal-ui 1.2.8 → 1.3.0

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.
@@ -22,11 +22,13 @@ import {
22
22
  showProcessingOverlay,
23
23
  hideProcessingOverlay,
24
24
  } from "../utils/processing-overlay"
25
-
26
- const SPIN_STYLE = `
27
- @keyframes _pp_spin { to { transform: rotate(360deg) } }
28
- @keyframes _pp_progress { 0% { transform: translateX(-100%) } 100% { transform: translateX(350%) } }
29
- `
25
+ import {
26
+ AnimationStyles,
27
+ ErrorBanner,
28
+ LoadingCard,
29
+ ProcessingOverlayView,
30
+ ShieldIcon,
31
+ } from "./internal/ui"
30
32
 
31
33
  const cardStyle = {
32
34
  input: {
@@ -75,11 +77,13 @@ const labelStyle: React.CSSProperties = {
75
77
 
76
78
  function SubmitButton({
77
79
  disabled,
80
+ busy,
78
81
  label,
79
82
  onSubmit,
80
83
  onSubmitError,
81
84
  }: {
82
85
  disabled: boolean
86
+ busy: boolean
83
87
  label: string
84
88
  onSubmit: () => void
85
89
  onSubmitError: (message: string) => void
@@ -91,7 +95,7 @@ function SubmitButton({
91
95
  <button
92
96
  type="button"
93
97
  disabled={isDisabled}
94
- aria-busy={disabled}
98
+ aria-busy={busy}
95
99
  onClick={() => {
96
100
  onSubmit()
97
101
  // submit() returns a promise and its rejection is NOT routed to the
@@ -153,30 +157,49 @@ function SubmitButton({
153
157
  export function PayPalAdvancedCard(props: {
154
158
  baseUrl: string
155
159
  publishableApiKey?: string
160
+ /** Forwarded to every API call — see `HttpOptions.credentials`. */
161
+ credentials?: RequestCredentials
156
162
  cartId: string
157
163
  config: PayPalConfig
158
- onPaid?: (result: Record<string, unknown>) => void
164
+ onPaid?: (result: Record<string, unknown>) => void | Promise<void>
159
165
  onError?: (message: string) => void
160
166
  }) {
161
- const { baseUrl, publishableApiKey, cartId, config, onPaid, onError } = props
167
+ const {
168
+ baseUrl,
169
+ publishableApiKey,
170
+ credentials,
171
+ cartId,
172
+ config,
173
+ onPaid,
174
+ onError,
175
+ } = props
162
176
 
163
177
  const api = useMemo(
164
- () => createPayPalStoreApi({ baseUrl, publishableApiKey }),
165
- [baseUrl, publishableApiKey]
178
+ () => createPayPalStoreApi({ baseUrl, publishableApiKey, credentials }),
179
+ [baseUrl, publishableApiKey, credentials]
166
180
  )
167
181
 
168
182
  const [error, setError] = useState<string | null>(null)
169
183
  const [submitting, setSubmitting] = useState(false)
170
184
  const submittingRef = useRef(false)
171
185
  const creatingOrderRef = useRef(false)
186
+ // True while the shared full-page DOM overlay is mounted, so the component
187
+ // doesn't also render its own copy on top of it.
188
+ const [domOverlayUp, setDomOverlayUp] = useState(false)
172
189
  // Once a capture succeeded the buyer's money is taken: any later failure
173
190
  // must retry ONLY the order finalization, never a fresh order/capture.
174
191
  const capturedRef = useRef<Record<string, unknown> | null>(null)
175
192
  const [completionPending, setCompletionPending] = useState(false)
176
193
  const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
177
194
 
195
+ const showOverlay = React.useCallback(() => {
196
+ showProcessingOverlay()
197
+ setDomOverlayUp(true)
198
+ }, [])
199
+
178
200
  const resetSubmitState = React.useCallback(() => {
179
201
  hideProcessingOverlay()
202
+ setDomOverlayUp(false)
180
203
  creatingOrderRef.current = false
181
204
  submittingRef.current = false
182
205
  setSubmitting(false)
@@ -191,6 +214,7 @@ export function PayPalAdvancedCard(props: {
191
214
  creatingOrderRef.current = false
192
215
  submittingRef.current = false
193
216
  setSubmitting(false)
217
+ setDomOverlayUp(false)
194
218
  }
195
219
  }
196
220
  window.addEventListener("pageshow", onPageShow)
@@ -214,14 +238,13 @@ export function PayPalAdvancedCard(props: {
214
238
  const captured = capturedRef.current
215
239
  if (!captured) return
216
240
  setSubmitting(true)
217
- showProcessingOverlay()
241
+ showOverlay()
218
242
  setError(null)
219
243
  try {
220
- const completeResult = await markPaymentComplete(
221
- baseUrl,
222
- cartId,
223
- publishableApiKey
224
- )
244
+ const completeResult = await markPaymentComplete(baseUrl, cartId, {
245
+ publishableApiKey,
246
+ credentials,
247
+ })
225
248
  // The order is finalized — the persisted "captured" flag has served its
226
249
  // purpose. Clear it before the consumer callback (which may redirect).
227
250
  clearCartCaptured(cartId)
@@ -236,33 +259,25 @@ export function PayPalAdvancedCard(props: {
236
259
  setError(msg)
237
260
  onError?.(msg)
238
261
  }
239
- }, [baseUrl, cartId, publishableApiKey, onPaid, onError, resetSubmitState])
262
+ }, [
263
+ baseUrl,
264
+ cartId,
265
+ publishableApiKey,
266
+ credentials,
267
+ onPaid,
268
+ onError,
269
+ showOverlay,
270
+ resetSubmitState,
271
+ ])
240
272
 
241
273
  if (!config.currency_supported) return null
242
274
 
243
275
  if (isRejected) {
244
276
  return (
245
- <div
246
- role="alert"
247
- style={{
248
- display: "flex",
249
- alignItems: "flex-start",
250
- gap: 8,
251
- padding: "10px 14px",
252
- background: "#fef2f2",
253
- border: "1px solid #fecaca",
254
- borderRadius: 8,
255
- fontSize: 13,
256
- color: "#b91c1c",
257
- lineHeight: 1.5,
258
- }}
259
- >
260
- <span style={{ flexShrink: 0, fontSize: 15 }}>⚠️</span>
261
- <span>
262
- PayPal failed to load. Please refresh the page or try a different
263
- payment method.
264
- </span>
265
- </div>
277
+ <ErrorBanner>
278
+ PayPal failed to load. Please refresh the page or try a different
279
+ payment method.
280
+ </ErrorBanner>
266
281
  )
267
282
  }
268
283
 
@@ -287,36 +302,9 @@ export function PayPalAdvancedCard(props: {
287
302
 
288
303
  if (isPending) {
289
304
  return (
290
- <div
291
- role="status"
292
- aria-label="Loading PayPal"
293
- style={{
294
- display: "flex",
295
- alignItems: "center",
296
- justifyContent: "center",
297
- gap: 10,
298
- padding: "18px 16px",
299
- background: "#f9fafb",
300
- border: "1px solid #e5e7eb",
301
- borderRadius: 8,
302
- minHeight: 55,
303
- }}
304
- >
305
- <style>{SPIN_STYLE}</style>
306
- <div
307
- style={{
308
- width: 22,
309
- height: 22,
310
- borderRadius: "50%",
311
- border: "2.5px solid #e5e7eb",
312
- borderTopColor: "#0070ba",
313
- animation: "_pp_spin .7s linear infinite",
314
- flexShrink: 0,
315
- }}
316
- />
317
- <div style={{ fontSize: 13, fontWeight: 500, color: "#6b7280" }}>
318
- Loading card fields…
319
- </div>
305
+ <div>
306
+ <AnimationStyles />
307
+ <LoadingCard label="Loading card fields…" />
320
308
  </div>
321
309
  )
322
310
  }
@@ -325,107 +313,10 @@ export function PayPalAdvancedCard(props: {
325
313
 
326
314
  return (
327
315
  <div style={{ position: "relative" }}>
328
- <style>{SPIN_STYLE}</style>
316
+ <AnimationStyles />
329
317
 
330
- {submitting && (
331
- <div
332
- role="status"
333
- aria-label="Processing payment"
334
- style={{
335
- position: "fixed",
336
- inset: 0,
337
- zIndex: 9999,
338
- background: "rgba(255,255,255,0.96)",
339
- display: "flex",
340
- flexDirection: "column",
341
- alignItems: "center",
342
- justifyContent: "center",
343
- gap: 20,
344
- }}
345
- >
346
- <div style={{ position: "relative", width: 56, height: 56 }}>
347
- <div
348
- style={{
349
- position: "absolute",
350
- inset: 0,
351
- borderRadius: "50%",
352
- border: "3px solid #e5e7eb",
353
- }}
354
- />
355
- <div
356
- style={{
357
- position: "absolute",
358
- inset: 0,
359
- borderRadius: "50%",
360
- border: "3px solid transparent",
361
- borderTopColor: "#2563eb",
362
- animation: "_pp_spin .8s linear infinite",
363
- }}
364
- />
365
- <svg
366
- viewBox="0 0 24 24"
367
- fill="none"
368
- stroke="#2563eb"
369
- strokeWidth="1.8"
370
- strokeLinecap="round"
371
- strokeLinejoin="round"
372
- style={{
373
- position: "absolute",
374
- top: "50%",
375
- left: "50%",
376
- transform: "translate(-50%, -50%)",
377
- width: 24,
378
- height: 24,
379
- }}
380
- >
381
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
382
- </svg>
383
- </div>
384
- <div style={{ textAlign: "center" }}>
385
- <div
386
- style={{
387
- fontSize: 18,
388
- fontWeight: 600,
389
- color: "#111827",
390
- letterSpacing: "-0.01em",
391
- }}
392
- >
393
- Processing your payment
394
- </div>
395
- <div
396
- style={{
397
- fontSize: 14,
398
- color: "#6b7280",
399
- marginTop: 6,
400
- lineHeight: 1.5,
401
- }}
402
- >
403
- This may take a few moments. Please do not close
404
- <br />
405
- or refresh this page.
406
- </div>
407
- </div>
408
- <div
409
- style={{
410
- width: 200,
411
- height: 3,
412
- background: "#e5e7eb",
413
- borderRadius: 3,
414
- overflow: "hidden",
415
- marginTop: 4,
416
- }}
417
- >
418
- <div
419
- style={{
420
- width: "40%",
421
- height: "100%",
422
- background: "linear-gradient(90deg, #2563eb, #1d4ed8)",
423
- borderRadius: 3,
424
- animation: "_pp_progress 1.5s ease-in-out infinite",
425
- }}
426
- />
427
- </div>
428
- </div>
318
+ {submitting && !domOverlayUp && (
319
+ <ProcessingOverlayView accent="#2563eb" />
429
320
  )}
430
321
 
431
322
  <PayPalCardFieldsProvider
@@ -454,7 +345,7 @@ export function PayPalAdvancedCard(props: {
454
345
  onApprove={async (data: { orderID?: string }) => {
455
346
  try {
456
347
  setError(null)
457
- showProcessingOverlay()
348
+ showOverlay()
458
349
  const orderId = String(data?.orderID || "")
459
350
  if (!orderId)
460
351
  throw new Error(
@@ -478,20 +369,14 @@ export function PayPalAdvancedCard(props: {
478
369
  await finalizeCapturedPayment()
479
370
  }}
480
371
  onCancel={() => {
481
- hideProcessingOverlay()
482
- creatingOrderRef.current = false
483
- submittingRef.current = false
484
- setSubmitting(false)
372
+ resetSubmitState()
485
373
  }}
486
374
  onError={(e: Error | { message?: string }) => {
487
375
  const msg =
488
376
  e instanceof Error ? e.message : e?.message || "CardFields error"
489
377
  setError(msg)
490
378
  onError?.(msg)
491
- hideProcessingOverlay()
492
- creatingOrderRef.current = false
493
- submittingRef.current = false
494
- setSubmitting(false)
379
+ resetSubmitState()
495
380
  }}
496
381
  >
497
382
  <div
@@ -517,7 +402,10 @@ export function PayPalAdvancedCard(props: {
517
402
  lineHeight: 1.55,
518
403
  }}
519
404
  >
520
- <span style={{ fontSize: 15, flexShrink: 0, marginTop: 1 }}>
405
+ <span
406
+ aria-hidden="true"
407
+ style={{ fontSize: 15, flexShrink: 0, marginTop: 1 }}
408
+ >
521
409
  🧪
522
410
  </span>
523
411
  <span>
@@ -533,7 +421,12 @@ export function PayPalAdvancedCard(props: {
533
421
  )}
534
422
 
535
423
  <div>
536
- <label style={labelStyle}>Card number</label>
424
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control --
425
+ the PayPal hosted field below is a cross-origin iframe; it cannot be
426
+ associated via htmlFor, and PayPal provides the accessible name inside */}
427
+ <label style={labelStyle} id="_pp-label-card-number">
428
+ Card number
429
+ </label>
537
430
  <PayPalNumberField />
538
431
  </div>
539
432
 
@@ -545,11 +438,21 @@ export function PayPalAdvancedCard(props: {
545
438
  }}
546
439
  >
547
440
  <div>
548
- <label style={labelStyle}>Expiration date</label>
441
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control --
442
+ the PayPal hosted field below is a cross-origin iframe; it cannot be
443
+ associated via htmlFor, and PayPal provides the accessible name inside */}
444
+ <label style={labelStyle} id="_pp-label-expiry">
445
+ Expiration date
446
+ </label>
549
447
  <PayPalExpiryField />
550
448
  </div>
551
449
  <div>
552
- <label style={labelStyle}>Security code</label>
450
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control --
451
+ the PayPal hosted field below is a cross-origin iframe; it cannot be
452
+ associated via htmlFor, and PayPal provides the accessible name inside */}
453
+ <label style={labelStyle} id="_pp-label-cvv">
454
+ Security code
455
+ </label>
553
456
  <PayPalCVVField />
554
457
  </div>
555
458
  </div>
@@ -563,18 +466,7 @@ export function PayPalAdvancedCard(props: {
563
466
  color: "#6b7280",
564
467
  }}
565
468
  >
566
- <svg
567
- width="14"
568
- height="14"
569
- viewBox="0 0 24 24"
570
- fill="none"
571
- stroke="#10b981"
572
- strokeWidth="2.5"
573
- strokeLinecap="round"
574
- strokeLinejoin="round"
575
- >
576
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
577
- </svg>
469
+ <ShieldIcon color="#10b981" size={14} />
578
470
  <span>Your payment is secured with 256-bit SSL encryption</span>
579
471
  </div>
580
472
 
@@ -603,13 +495,14 @@ export function PayPalAdvancedCard(props: {
603
495
  ) : (
604
496
  <SubmitButton
605
497
  disabled={submitting}
498
+ busy={submitting}
606
499
  label={submitting ? "Processing…" : "Pay by Card"}
607
500
  onSubmit={() => {
608
501
  if (submittingRef.current) return
609
502
  submittingRef.current = true
610
503
  setError(null)
611
504
  setSubmitting(true)
612
- showProcessingOverlay()
505
+ showOverlay()
613
506
  }}
614
507
  onSubmitError={(msg) => {
615
508
  resetSubmitState()
@@ -619,26 +512,7 @@ export function PayPalAdvancedCard(props: {
619
512
  />
620
513
  )}
621
514
 
622
- {error && (
623
- <div
624
- role="alert"
625
- style={{
626
- display: "flex",
627
- alignItems: "flex-start",
628
- gap: 10,
629
- padding: "10px 14px",
630
- background: "#fef2f2",
631
- border: "1px solid #fecaca",
632
- borderRadius: 8,
633
- fontSize: 13,
634
- color: "#b91c1c",
635
- lineHeight: 1.5,
636
- }}
637
- >
638
- <span style={{ flexShrink: 0, marginTop: 1 }}>⚠️</span>
639
- <span>{error}</span>
640
- </div>
641
- )}
515
+ {error && <ErrorBanner>{error}</ErrorBanner>}
642
516
  </div>
643
517
  </PayPalCardFieldsProvider>
644
518
  </div>
@@ -16,13 +16,12 @@ export {
16
16
  isPayPalProviderId,
17
17
  }
18
18
 
19
- const SPIN_STYLE = `@keyframes _pp_section_spin { to { transform: rotate(360deg) } }`
19
+ import { AnimationStyles } from "./internal/ui"
20
20
 
21
21
  function SessionInitCard() {
22
22
  return (
23
23
  <div
24
24
  role="status"
25
- aria-label="Setting up payment"
26
25
  style={{
27
26
  display: "flex",
28
27
  alignItems: "center",
@@ -34,15 +33,16 @@ function SessionInitCard() {
34
33
  borderRadius: 10,
35
34
  }}
36
35
  >
37
- <style>{SPIN_STYLE}</style>
36
+ <AnimationStyles />
38
37
  <div
38
+ className="_pp-spin-sm"
39
+ aria-hidden="true"
39
40
  style={{
40
41
  width: 20,
41
42
  height: 20,
42
43
  borderRadius: "50%",
43
44
  border: "2.5px solid #e5e7eb",
44
45
  borderTopColor: "#0070ba",
45
- animation: "_pp_section_spin .7s linear infinite",
46
46
  flexShrink: 0,
47
47
  }}
48
48
  />
@@ -58,10 +58,17 @@ export type PayPalPaymentSectionProps = {
58
58
  selectedProviderId: string | null | undefined
59
59
  baseUrl: string
60
60
  publishableApiKey?: string
61
+ /** Forwarded to every API call — see `HttpOptions.credentials`. */
62
+ credentials?: RequestCredentials
63
+ /**
64
+ * The cart's currency code (e.g. "usd"). Recommended: busts the config
65
+ * cache when the buyer switches region/currency on the same cart.
66
+ */
67
+ currencyCode?: string
61
68
  sessionLoading?: boolean
62
- onSuccess?: (cartId: string) => void
69
+ onSuccess?: (cartId: string) => void | Promise<void>
63
70
  onError?: (message: string) => void
64
- onPaid?: (result: unknown) => void
71
+ onPaid?: (result: unknown) => void | Promise<void>
65
72
  }
66
73
 
67
74
  /**
@@ -77,6 +84,8 @@ export function PayPalPaymentSection({
77
84
  selectedProviderId,
78
85
  baseUrl,
79
86
  publishableApiKey,
87
+ credentials,
88
+ currencyCode,
80
89
  sessionLoading = false,
81
90
  onSuccess,
82
91
  onError,
@@ -91,6 +100,8 @@ export function PayPalPaymentSection({
91
100
  selectedProviderId={selectedProviderId}
92
101
  baseUrl={baseUrl}
93
102
  publishableApiKey={publishableApiKey}
103
+ credentials={credentials}
104
+ currencyCode={currencyCode}
94
105
  onSuccess={onSuccess}
95
106
  onError={onError}
96
107
  onPaid={onPaid}
@@ -5,7 +5,10 @@ import { PayPalScriptProvider } from "@paypal/react-paypal-js"
5
5
  import type { ReactPayPalScriptOptions } from "@paypal/react-paypal-js"
6
6
  import type { PayPalConfig } from "../client/types"
7
7
 
8
- const BN_CODE = "MBJTechnolabs_SI_SPB"
8
+ // "MBJ_Medusa_SI" is the Medusa-platform BN code PayPal assigned on 2026-10-06
9
+ // (replacing "MBJTechnolabs_SI_SPB"). Must stay in sync with the backend's
10
+ // PAYPAL_PARTNER_ATTRIBUTION_ID so SDK and REST attribution match.
11
+ const BN_CODE = "MBJ_Medusa_SI"
9
12
 
10
13
  export function PayPalProvider(props: {
11
14
  config: PayPalConfig