@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.
- package/CHANGELOG.md +7 -0
- package/dist/index.cjs +197 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +197 -88
- package/dist/index.mjs.map +1 -1
- package/dist/order.cjs.map +1 -1
- package/dist/order.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/MedusaNextPayPalAdapter.tsx +196 -195
- package/src/client/http.ts +214 -204
- package/src/client/paypal.ts +91 -88
- package/src/client/types.ts +40 -40
- package/src/components/PayPalAdvancedCard.tsx +646 -611
- package/src/components/PayPalCurrencyNotice.tsx +24 -19
- package/src/components/PayPalPaymentSection.tsx +5 -1
- package/src/components/PayPalProvider.tsx +44 -38
- package/src/components/PayPalSmartButtons.tsx +489 -445
- package/src/hooks/usePayPalConfig.ts +100 -96
- package/src/hooks/usePayPalPaymentMethods.ts +146 -145
- package/src/index.ts +19 -16
- package/src/order.ts +140 -134
- package/src/utils/next-errors.ts +21 -21
- package/src/utils/processing-overlay.ts +96 -96
|
@@ -1,611 +1,646 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useMemo, useRef, useState } from "react"
|
|
4
|
-
import {
|
|
5
|
-
PayPalCardFieldsProvider,
|
|
6
|
-
PayPalNumberField,
|
|
7
|
-
PayPalExpiryField,
|
|
8
|
-
PayPalCVVField,
|
|
9
|
-
usePayPalCardFields,
|
|
10
|
-
usePayPalScriptReducer,
|
|
11
|
-
} from "@paypal/react-paypal-js"
|
|
12
|
-
|
|
13
|
-
import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
|
|
14
|
-
import type { PayPalConfig } from "../client/types"
|
|
15
|
-
import {
|
|
16
|
-
clearCartCaptured,
|
|
17
|
-
markCartCaptured,
|
|
18
|
-
wasCartCaptured,
|
|
19
|
-
} from "../utils/captured-state"
|
|
20
|
-
import { isNextRouterError } from "../utils/next-errors"
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
</
|
|
383
|
-
</div>
|
|
384
|
-
<div
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
<
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
style={
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
>
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useRef, useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
PayPalCardFieldsProvider,
|
|
6
|
+
PayPalNumberField,
|
|
7
|
+
PayPalExpiryField,
|
|
8
|
+
PayPalCVVField,
|
|
9
|
+
usePayPalCardFields,
|
|
10
|
+
usePayPalScriptReducer,
|
|
11
|
+
} from "@paypal/react-paypal-js"
|
|
12
|
+
|
|
13
|
+
import { createPayPalStoreApi, markPaymentComplete } from "../client/paypal"
|
|
14
|
+
import type { PayPalConfig } from "../client/types"
|
|
15
|
+
import {
|
|
16
|
+
clearCartCaptured,
|
|
17
|
+
markCartCaptured,
|
|
18
|
+
wasCartCaptured,
|
|
19
|
+
} from "../utils/captured-state"
|
|
20
|
+
import { isNextRouterError } from "../utils/next-errors"
|
|
21
|
+
import {
|
|
22
|
+
showProcessingOverlay,
|
|
23
|
+
hideProcessingOverlay,
|
|
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
|
+
`
|
|
30
|
+
|
|
31
|
+
const cardStyle = {
|
|
32
|
+
input: {
|
|
33
|
+
"font-size": "15px",
|
|
34
|
+
"font-family":
|
|
35
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
36
|
+
"font-weight": "400",
|
|
37
|
+
color: "#111827",
|
|
38
|
+
padding: "0 14px",
|
|
39
|
+
height: "42px",
|
|
40
|
+
"border-radius": "8px",
|
|
41
|
+
border: "1px solid #d1d5db",
|
|
42
|
+
background: "#ffffff",
|
|
43
|
+
"box-shadow": "none",
|
|
44
|
+
transition: "border-color 0.15s ease",
|
|
45
|
+
},
|
|
46
|
+
"::placeholder": {
|
|
47
|
+
color: "#9ca3af",
|
|
48
|
+
},
|
|
49
|
+
".invalid": {
|
|
50
|
+
color: "#dc2626",
|
|
51
|
+
border: "1px solid #fca5a5",
|
|
52
|
+
background: "#fff7f7",
|
|
53
|
+
"box-shadow": "none",
|
|
54
|
+
},
|
|
55
|
+
".valid": {
|
|
56
|
+
color: "#111827",
|
|
57
|
+
border: "1px solid #d1d5db",
|
|
58
|
+
"box-shadow": "none",
|
|
59
|
+
},
|
|
60
|
+
"input:focus": {
|
|
61
|
+
outline: "none",
|
|
62
|
+
border: "1px solid #2563eb",
|
|
63
|
+
"box-shadow": "0 0 0 3px rgba(37,99,235,0.12)",
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const labelStyle: React.CSSProperties = {
|
|
68
|
+
display: "block",
|
|
69
|
+
fontSize: 13,
|
|
70
|
+
fontWeight: 500,
|
|
71
|
+
color: "#374151",
|
|
72
|
+
marginBottom: 6,
|
|
73
|
+
letterSpacing: "0.01em",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function SubmitButton({
|
|
77
|
+
disabled,
|
|
78
|
+
label,
|
|
79
|
+
onSubmit,
|
|
80
|
+
onSubmitError,
|
|
81
|
+
}: {
|
|
82
|
+
disabled: boolean
|
|
83
|
+
label: string
|
|
84
|
+
onSubmit: () => void
|
|
85
|
+
onSubmitError: (message: string) => void
|
|
86
|
+
}) {
|
|
87
|
+
const { cardFieldsForm } = usePayPalCardFields()
|
|
88
|
+
const isDisabled = disabled || !cardFieldsForm
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
disabled={isDisabled}
|
|
94
|
+
aria-busy={disabled}
|
|
95
|
+
onClick={() => {
|
|
96
|
+
onSubmit()
|
|
97
|
+
// submit() returns a promise and its rejection is NOT routed to the
|
|
98
|
+
// provider's onError — PayPal's contract is that the caller catches
|
|
99
|
+
// it. A sync try/catch here would miss it entirely, leaving the buyer
|
|
100
|
+
// stuck under the full-screen processing overlay forever (e.g. on a
|
|
101
|
+
// card-field validation error).
|
|
102
|
+
Promise.resolve(cardFieldsForm?.submit()).catch((e: unknown) => {
|
|
103
|
+
const msg =
|
|
104
|
+
e instanceof Error && e.message
|
|
105
|
+
? e.message
|
|
106
|
+
: "Card payment could not be submitted. Please check your card details."
|
|
107
|
+
onSubmitError(msg)
|
|
108
|
+
})
|
|
109
|
+
}}
|
|
110
|
+
style={{
|
|
111
|
+
width: "100%",
|
|
112
|
+
height: 48,
|
|
113
|
+
padding: "0 20px",
|
|
114
|
+
borderRadius: 8,
|
|
115
|
+
border: "none",
|
|
116
|
+
background: isDisabled
|
|
117
|
+
? "#e5e7eb"
|
|
118
|
+
: "linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%)",
|
|
119
|
+
color: isDisabled ? "#9ca3af" : "#ffffff",
|
|
120
|
+
fontSize: 15,
|
|
121
|
+
fontWeight: 600,
|
|
122
|
+
fontFamily:
|
|
123
|
+
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
124
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
125
|
+
letterSpacing: "0.01em",
|
|
126
|
+
boxShadow: isDisabled
|
|
127
|
+
? "none"
|
|
128
|
+
: "0 1px 3px rgba(37,99,235,0.3), 0 1px 2px rgba(0,0,0,0.06)",
|
|
129
|
+
transition: "all 0.15s ease",
|
|
130
|
+
}}
|
|
131
|
+
onMouseEnter={(e) => {
|
|
132
|
+
if (!isDisabled) {
|
|
133
|
+
;(e.target as HTMLButtonElement).style.background =
|
|
134
|
+
"linear-gradient(180deg, #1d4ed8 0%, #1e40af 100%)"
|
|
135
|
+
;(e.target as HTMLButtonElement).style.boxShadow =
|
|
136
|
+
"0 4px 6px rgba(37,99,235,0.35), 0 2px 4px rgba(0,0,0,0.06)"
|
|
137
|
+
}
|
|
138
|
+
}}
|
|
139
|
+
onMouseLeave={(e) => {
|
|
140
|
+
if (!isDisabled) {
|
|
141
|
+
;(e.target as HTMLButtonElement).style.background =
|
|
142
|
+
"linear-gradient(180deg, #2563eb 0%, #1d4ed8 100%)"
|
|
143
|
+
;(e.target as HTMLButtonElement).style.boxShadow =
|
|
144
|
+
"0 1px 3px rgba(37,99,235,0.3), 0 1px 2px rgba(0,0,0,0.06)"
|
|
145
|
+
}
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{label}
|
|
149
|
+
</button>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function PayPalAdvancedCard(props: {
|
|
154
|
+
baseUrl: string
|
|
155
|
+
publishableApiKey?: string
|
|
156
|
+
cartId: string
|
|
157
|
+
config: PayPalConfig
|
|
158
|
+
onPaid?: (result: Record<string, unknown>) => void
|
|
159
|
+
onError?: (message: string) => void
|
|
160
|
+
}) {
|
|
161
|
+
const { baseUrl, publishableApiKey, cartId, config, onPaid, onError } = props
|
|
162
|
+
|
|
163
|
+
const api = useMemo(
|
|
164
|
+
() => createPayPalStoreApi({ baseUrl, publishableApiKey }),
|
|
165
|
+
[baseUrl, publishableApiKey]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const [error, setError] = useState<string | null>(null)
|
|
169
|
+
const [submitting, setSubmitting] = useState(false)
|
|
170
|
+
const submittingRef = useRef(false)
|
|
171
|
+
const creatingOrderRef = useRef(false)
|
|
172
|
+
// Once a capture succeeded the buyer's money is taken: any later failure
|
|
173
|
+
// must retry ONLY the order finalization, never a fresh order/capture.
|
|
174
|
+
const capturedRef = useRef<Record<string, unknown> | null>(null)
|
|
175
|
+
const [completionPending, setCompletionPending] = useState(false)
|
|
176
|
+
const [{ isPending, isResolved, isRejected }] = usePayPalScriptReducer()
|
|
177
|
+
|
|
178
|
+
const resetSubmitState = React.useCallback(() => {
|
|
179
|
+
hideProcessingOverlay()
|
|
180
|
+
creatingOrderRef.current = false
|
|
181
|
+
submittingRef.current = false
|
|
182
|
+
setSubmitting(false)
|
|
183
|
+
}, [])
|
|
184
|
+
|
|
185
|
+
// bfcache restore (Back button after a redirect) revives this page with the
|
|
186
|
+
// old React state — clear the submitting flag so the buyer isn't stuck
|
|
187
|
+
// behind the in-component overlay.
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
const onPageShow = (e: PageTransitionEvent) => {
|
|
190
|
+
if (e.persisted) {
|
|
191
|
+
creatingOrderRef.current = false
|
|
192
|
+
submittingRef.current = false
|
|
193
|
+
setSubmitting(false)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
window.addEventListener("pageshow", onPageShow)
|
|
197
|
+
return () => window.removeEventListener("pageshow", onPageShow)
|
|
198
|
+
}, [])
|
|
199
|
+
|
|
200
|
+
// A reload/crash between capture and finalization loses the in-memory
|
|
201
|
+
// capturedRef — restore it from the persisted per-cart flag so the buyer
|
|
202
|
+
// gets the finalize-only Retry button instead of card fields that would
|
|
203
|
+
// charge (or confusingly fail) again.
|
|
204
|
+
React.useEffect(() => {
|
|
205
|
+
if (capturedRef.current || !wasCartCaptured(cartId)) return
|
|
206
|
+
capturedRef.current = {}
|
|
207
|
+
setCompletionPending(true)
|
|
208
|
+
setError(
|
|
209
|
+
"Your payment was already received for this cart. Please use the button below to finish placing your order — do not pay again."
|
|
210
|
+
)
|
|
211
|
+
}, [cartId])
|
|
212
|
+
|
|
213
|
+
const finalizeCapturedPayment = React.useCallback(async () => {
|
|
214
|
+
const captured = capturedRef.current
|
|
215
|
+
if (!captured) return
|
|
216
|
+
setSubmitting(true)
|
|
217
|
+
showProcessingOverlay()
|
|
218
|
+
setError(null)
|
|
219
|
+
try {
|
|
220
|
+
const completeResult = await markPaymentComplete(
|
|
221
|
+
baseUrl,
|
|
222
|
+
cartId,
|
|
223
|
+
publishableApiKey
|
|
224
|
+
)
|
|
225
|
+
// The order is finalized — the persisted "captured" flag has served its
|
|
226
|
+
// purpose. Clear it before the consumer callback (which may redirect).
|
|
227
|
+
clearCartCaptured(cartId)
|
|
228
|
+
await onPaid?.({ ...captured, ...completeResult })
|
|
229
|
+
setCompletionPending(false)
|
|
230
|
+
} catch (e: unknown) {
|
|
231
|
+
if (isNextRouterError(e)) return
|
|
232
|
+
resetSubmitState()
|
|
233
|
+
setCompletionPending(true)
|
|
234
|
+
const msg =
|
|
235
|
+
"Your payment was received, but finalizing your order failed. Please use the Retry button below — do not pay again."
|
|
236
|
+
setError(msg)
|
|
237
|
+
onError?.(msg)
|
|
238
|
+
}
|
|
239
|
+
}, [baseUrl, cartId, publishableApiKey, onPaid, onError, resetSubmitState])
|
|
240
|
+
|
|
241
|
+
if (!config.currency_supported) return null
|
|
242
|
+
|
|
243
|
+
if (isRejected) {
|
|
244
|
+
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>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!config.client_token) {
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
style={{
|
|
273
|
+
padding: "12px 16px",
|
|
274
|
+
background: "#fefce8",
|
|
275
|
+
border: "1px solid #fde68a",
|
|
276
|
+
borderRadius: 8,
|
|
277
|
+
fontSize: 13,
|
|
278
|
+
color: "#92400e",
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
Card fields unavailable — missing client token from backend.
|
|
282
|
+
</div>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const isSandbox = config.environment === "sandbox"
|
|
287
|
+
|
|
288
|
+
if (isPending) {
|
|
289
|
+
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>
|
|
320
|
+
</div>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!isResolved) return null
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div style={{ position: "relative" }}>
|
|
328
|
+
<style>{SPIN_STYLE}</style>
|
|
329
|
+
|
|
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>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
<PayPalCardFieldsProvider
|
|
432
|
+
style={cardStyle}
|
|
433
|
+
createOrder={async () => {
|
|
434
|
+
if (creatingOrderRef.current)
|
|
435
|
+
throw new Error("Payment already processing")
|
|
436
|
+
if (capturedRef.current) {
|
|
437
|
+
// Money was already captured for this cart — retry the order
|
|
438
|
+
// finalization instead of creating (and charging) a new order.
|
|
439
|
+
void finalizeCapturedPayment()
|
|
440
|
+
throw new Error(
|
|
441
|
+
"Your payment was already received — finalizing your order."
|
|
442
|
+
)
|
|
443
|
+
}
|
|
444
|
+
creatingOrderRef.current = true
|
|
445
|
+
try {
|
|
446
|
+
setError(null)
|
|
447
|
+
const r = await api.createOrder(cartId, true)
|
|
448
|
+
return r.id
|
|
449
|
+
} catch (e) {
|
|
450
|
+
creatingOrderRef.current = false
|
|
451
|
+
throw e
|
|
452
|
+
}
|
|
453
|
+
}}
|
|
454
|
+
onApprove={async (data: { orderID?: string }) => {
|
|
455
|
+
try {
|
|
456
|
+
setError(null)
|
|
457
|
+
showProcessingOverlay()
|
|
458
|
+
const orderId = String(data?.orderID || "")
|
|
459
|
+
if (!orderId)
|
|
460
|
+
throw new Error(
|
|
461
|
+
"PayPal order ID is missing from approval response"
|
|
462
|
+
)
|
|
463
|
+
const result = await api.captureOrder(cartId, orderId)
|
|
464
|
+
capturedRef.current = (result || {}) as Record<string, unknown>
|
|
465
|
+
// Persist across reloads: from here on, only finalization may
|
|
466
|
+
// ever be retried for this cart — never a fresh payment.
|
|
467
|
+
markCartCaptured(cartId)
|
|
468
|
+
} catch (e: unknown) {
|
|
469
|
+
if (isNextRouterError(e)) return
|
|
470
|
+
resetSubmitState()
|
|
471
|
+
const msg = e instanceof Error ? e.message : "Card payment failed"
|
|
472
|
+
setError(msg)
|
|
473
|
+
onError?.(msg)
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
// Past this point the buyer has been charged: completion failures
|
|
477
|
+
// surface a finalize-only Retry, never a fresh payment.
|
|
478
|
+
await finalizeCapturedPayment()
|
|
479
|
+
}}
|
|
480
|
+
onCancel={() => {
|
|
481
|
+
hideProcessingOverlay()
|
|
482
|
+
creatingOrderRef.current = false
|
|
483
|
+
submittingRef.current = false
|
|
484
|
+
setSubmitting(false)
|
|
485
|
+
}}
|
|
486
|
+
onError={(e: Error | { message?: string }) => {
|
|
487
|
+
const msg =
|
|
488
|
+
e instanceof Error ? e.message : e?.message || "CardFields error"
|
|
489
|
+
setError(msg)
|
|
490
|
+
onError?.(msg)
|
|
491
|
+
hideProcessingOverlay()
|
|
492
|
+
creatingOrderRef.current = false
|
|
493
|
+
submittingRef.current = false
|
|
494
|
+
setSubmitting(false)
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
<div
|
|
498
|
+
style={{
|
|
499
|
+
display: "flex",
|
|
500
|
+
flexDirection: "column",
|
|
501
|
+
gap: 16,
|
|
502
|
+
width: "100%",
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
{isSandbox && (
|
|
506
|
+
<div
|
|
507
|
+
style={{
|
|
508
|
+
display: "flex",
|
|
509
|
+
alignItems: "flex-start",
|
|
510
|
+
gap: 10,
|
|
511
|
+
padding: "10px 14px",
|
|
512
|
+
background: "#eff6ff",
|
|
513
|
+
border: "1px solid #bfdbfe",
|
|
514
|
+
borderRadius: 8,
|
|
515
|
+
fontSize: 13,
|
|
516
|
+
color: "#1e40af",
|
|
517
|
+
lineHeight: 1.55,
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
<span style={{ fontSize: 15, flexShrink: 0, marginTop: 1 }}>
|
|
521
|
+
🧪
|
|
522
|
+
</span>
|
|
523
|
+
<span>
|
|
524
|
+
Sandbox mode — use test card{" "}
|
|
525
|
+
<strong
|
|
526
|
+
style={{ fontFamily: "monospace", letterSpacing: "0.05em" }}
|
|
527
|
+
>
|
|
528
|
+
4111 1111 1111 1111
|
|
529
|
+
</strong>{" "}
|
|
530
|
+
with any future date and any CVV.
|
|
531
|
+
</span>
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
534
|
+
|
|
535
|
+
<div>
|
|
536
|
+
<label style={labelStyle}>Card number</label>
|
|
537
|
+
<PayPalNumberField />
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div
|
|
541
|
+
style={{
|
|
542
|
+
display: "grid",
|
|
543
|
+
gridTemplateColumns: "1fr 1fr",
|
|
544
|
+
gap: 16,
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<div>
|
|
548
|
+
<label style={labelStyle}>Expiration date</label>
|
|
549
|
+
<PayPalExpiryField />
|
|
550
|
+
</div>
|
|
551
|
+
<div>
|
|
552
|
+
<label style={labelStyle}>Security code</label>
|
|
553
|
+
<PayPalCVVField />
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
<div
|
|
558
|
+
style={{
|
|
559
|
+
display: "flex",
|
|
560
|
+
alignItems: "center",
|
|
561
|
+
gap: 6,
|
|
562
|
+
fontSize: 12,
|
|
563
|
+
color: "#6b7280",
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
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>
|
|
578
|
+
<span>Your payment is secured with 256-bit SSL encryption</span>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
{completionPending ? (
|
|
582
|
+
<button
|
|
583
|
+
type="button"
|
|
584
|
+
onClick={() => void finalizeCapturedPayment()}
|
|
585
|
+
disabled={submitting}
|
|
586
|
+
aria-busy={submitting}
|
|
587
|
+
style={{
|
|
588
|
+
width: "100%",
|
|
589
|
+
height: 48,
|
|
590
|
+
padding: "0 20px",
|
|
591
|
+
borderRadius: 8,
|
|
592
|
+
border: "none",
|
|
593
|
+
background: "#0070ba",
|
|
594
|
+
color: "#ffffff",
|
|
595
|
+
fontSize: 15,
|
|
596
|
+
fontWeight: 600,
|
|
597
|
+
cursor: submitting ? "not-allowed" : "pointer",
|
|
598
|
+
opacity: submitting ? 0.7 : 1,
|
|
599
|
+
}}
|
|
600
|
+
>
|
|
601
|
+
Retry — finish placing my order
|
|
602
|
+
</button>
|
|
603
|
+
) : (
|
|
604
|
+
<SubmitButton
|
|
605
|
+
disabled={submitting}
|
|
606
|
+
label={submitting ? "Processing…" : "Pay by Card"}
|
|
607
|
+
onSubmit={() => {
|
|
608
|
+
if (submittingRef.current) return
|
|
609
|
+
submittingRef.current = true
|
|
610
|
+
setError(null)
|
|
611
|
+
setSubmitting(true)
|
|
612
|
+
showProcessingOverlay()
|
|
613
|
+
}}
|
|
614
|
+
onSubmitError={(msg) => {
|
|
615
|
+
resetSubmitState()
|
|
616
|
+
setError(msg)
|
|
617
|
+
onError?.(msg)
|
|
618
|
+
}}
|
|
619
|
+
/>
|
|
620
|
+
)}
|
|
621
|
+
|
|
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
|
+
)}
|
|
642
|
+
</div>
|
|
643
|
+
</PayPalCardFieldsProvider>
|
|
644
|
+
</div>
|
|
645
|
+
)
|
|
646
|
+
}
|