@easypayment/medusa-paypal-ui 1.0.43 โ 1.0.45
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/README.md +760 -615
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/adapters/MedusaNextPayPalAdapter.tsx +0 -1
- package/src/components/PayPalAdvancedCard.tsx +368 -383
- package/src/components/PayPalPaymentSection.tsx +0 -1
- package/src/components/PayPalProvider.tsx +3 -0
- package/src/components/PayPalSmartButtons.tsx +197 -199
- package/src/hooks/usePayPalConfig.ts +1 -8
- package/src/hooks/usePayPalPaymentMethods.ts +0 -8
package/README.md
CHANGED
|
@@ -1,615 +1,760 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
1
|
+
# PayPal for Medusa Frontend UI
|
|
2
|
+
|
|
3
|
+
**PayPal checkout UI for Medusa v2 storefronts โ Smart Buttons, Advanced Card Fields**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@easypayment/medusa-paypal-ui)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://medusajs.com)
|
|
8
|
+
[](https://nextjs.org)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ๐ Table of Contents
|
|
13
|
+
|
|
14
|
+
- [๐ฆ Overview](#-overview)
|
|
15
|
+
- [โ
Requirements](#-requirements)
|
|
16
|
+
- [๐ Installation](#-installation)
|
|
17
|
+
- [๐ Environment Variables](#-environment-variables)
|
|
18
|
+
- [๐ Integration Guide](#-integration-guide)
|
|
19
|
+
- [Step 1 โ Add the import](#step-1--add-the-import)
|
|
20
|
+
- [Step 2 โ Add PayPal helpers and state](#step-2--add-paypal-helpers-and-state)
|
|
21
|
+
- [Step 3 โ Load PayPal config](#step-3--load-paypal-config)
|
|
22
|
+
- [Step 4 โ Update setPaymentMethod](#step-4--update-setpaymentmethod)
|
|
23
|
+
- [Step 5 โ Filter the payment method list](#step-5--filter-the-payment-method-list)
|
|
24
|
+
- [Step 6 โ Inject admin-configured titles](#step-6--inject-admin-configured-titles)
|
|
25
|
+
- [Step 7 โ Render the PayPal UI](#step-7--render-the-paypal-ui)
|
|
26
|
+
- [Step 8 โ Disable the Continue button](#step-8--disable-the-continue-button)
|
|
27
|
+
- [Step 9 โ Fix the summary label](#step-9--fix-the-summary-label)
|
|
28
|
+
- [๐ Complete File](#-complete-file)
|
|
29
|
+
- [๐งช Testing](#-testing)
|
|
30
|
+
- [๐ License](#-license)
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ๐ฆ Overview
|
|
35
|
+
|
|
36
|
+
`@easypayment/medusa-paypal-ui` is the **storefront UI package** that connects your Next.js (App Router) storefront to the `@easypayment/medusa-paypal` backend plugin. It ships the PayPal adapter used inside your checkout payment step โ your storefront adds the adapter, provider filtering, and backend config handling to the existing Medusa payment UI.
|
|
37
|
+
|
|
38
|
+
| Feature | Details |
|
|
39
|
+
|---|---|
|
|
40
|
+
| ๐ต **PayPal Smart Buttons** | Wallet-based checkout via `pp_paypal_paypal` |
|
|
41
|
+
| ๐ณ **Advanced Card Fields** | Hosted PCI-compliant advanced credit card inputs via `pp_paypal_card_paypal_card` |
|
|
42
|
+
| ๐ **Admin-driven config** | Enable/disable providers and set labels from Medusa Admin |
|
|
43
|
+
| โก **Built-in UX** | Smart Buttons and Advanced Card UI rendered by `MedusaNextPayPalAdapter` |
|
|
44
|
+
| ๐ **Storefront-controlled flow** | Your payment step controls session creation, loading states, and `placeOrder` |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## โ
Requirements
|
|
49
|
+
|
|
50
|
+
- **Node.js** 18+
|
|
51
|
+
- **Next.js** 14+ with App Router
|
|
52
|
+
- **`@easypayment/medusa-paypal`** installed and running on your Medusa server
|
|
53
|
+
- A PayPal account connected in **Medusa Admin โ Settings โ PayPal โ PayPal Connection**
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## ๐ Installation
|
|
58
|
+
|
|
59
|
+
**In your storefront directory**, run:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install @easypayment/medusa-paypal-ui
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## ๐ Environment Variables
|
|
68
|
+
|
|
69
|
+
Add the following to your storefront `.env.local`. Use separate values for development and production.
|
|
70
|
+
|
|
71
|
+
```env
|
|
72
|
+
NEXT_PUBLIC_MEDUSA_BACKEND_URL=http://localhost:9000
|
|
73
|
+
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> **Where to get the publishable key:**
|
|
77
|
+
> Medusa Admin โ **Settings โ API Key Management โ Create API Key**
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## ๐ Integration Guide
|
|
82
|
+
|
|
83
|
+
All changes in this guide are made to **one single file** in your storefront:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
src/modules/checkout/components/payment/index.tsx
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Open that file and follow each step in order.
|
|
90
|
+
|
|
91
|
+
> **Prefer to copy-paste the whole file?** Skip straight to [Complete File](#-complete-file) and replace the entire contents in one go. The complete file has all 9 steps already applied.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### Step 1 โ Add the import
|
|
96
|
+
|
|
97
|
+
**Where:** At the very top of the file, alongside your other imports.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { MedusaNextPayPalAdapter } from "@easypayment/medusa-paypal-ui"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Step 2 โ Add PayPal helpers and state
|
|
106
|
+
|
|
107
|
+
**Where:** At the top of the file, outside the component โ add the constants. Inside the `Payment` component, add the `useState` lines alongside your other state declarations.
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// Outside the component โ add these constants
|
|
111
|
+
const PAYPAL_PROVIDER_ID = "pp_paypal_paypal"
|
|
112
|
+
const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card"
|
|
113
|
+
const PAYPAL_PROVIDER_IDS = [PAYPAL_PROVIDER_ID, PAYPAL_CARD_PROVIDER_ID]
|
|
114
|
+
|
|
115
|
+
const isPayPal = (id: string) => PAYPAL_PROVIDER_IDS.includes(id)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// Inside the Payment component โ add alongside your other useState declarations
|
|
120
|
+
const [paypalEnabled, setPaypalEnabled] = useState(true)
|
|
121
|
+
const [paypalTitle, setPaypalTitle] = useState("PayPal")
|
|
122
|
+
const [cardEnabled, setCardEnabled] = useState(true)
|
|
123
|
+
const [cardTitle, setCardTitle] = useState("Credit or Debit Card")
|
|
124
|
+
const [paypalLoading, setPaypalLoading] = useState(false)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### Step 3 โ Load PayPal config
|
|
130
|
+
|
|
131
|
+
**Where:** Inside the `Payment` component, alongside your other `useEffect` hooks.
|
|
132
|
+
|
|
133
|
+
This fetches PayPal settings from your backend whenever the payment step is opened, so the UI always reflects the latest admin configuration.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!isOpen) return
|
|
138
|
+
|
|
139
|
+
const backendUrl = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
|
|
140
|
+
if (!backendUrl) return
|
|
141
|
+
|
|
142
|
+
const key = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
|
|
143
|
+
const controller = new AbortController()
|
|
144
|
+
|
|
145
|
+
const loadPayPalConfig = async () => {
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetch(`${backendUrl}/store/paypal/config`, {
|
|
148
|
+
headers: key ? { "x-publishable-api-key": key } : {},
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
if (response.status === 403) {
|
|
153
|
+
setPaypalEnabled(false)
|
|
154
|
+
setCardEnabled(false)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!response.ok) return
|
|
159
|
+
|
|
160
|
+
const config = await response.json()
|
|
161
|
+
|
|
162
|
+
if (typeof config?.paypal_enabled === "boolean") setPaypalEnabled(config.paypal_enabled)
|
|
163
|
+
if (typeof config?.paypal_title === "string" && config.paypal_title) setPaypalTitle(config.paypal_title)
|
|
164
|
+
if (typeof config?.card_enabled === "boolean") setCardEnabled(config.card_enabled)
|
|
165
|
+
if (typeof config?.card_title === "string" && config.card_title) setCardTitle(config.card_title)
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if ((err as Error).name !== "AbortError") setPaypalLoading(false)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void loadPayPalConfig()
|
|
172
|
+
return () => controller.abort()
|
|
173
|
+
}, [isOpen])
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Step 4 โ Update setPaymentMethod
|
|
179
|
+
|
|
180
|
+
**Where:** Inside the `Payment` component. Find your existing `setPaymentMethod` function and **replace it entirely** with the version below.
|
|
181
|
+
|
|
182
|
+
The key addition is `paypalLoading` โ it shows a loading indicator while the PayPal payment session is being created in the background.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
const setPaymentMethod = async (method: string) => {
|
|
186
|
+
setError(null)
|
|
187
|
+
setSelectedPaymentMethod(method)
|
|
188
|
+
|
|
189
|
+
if (!isStripeLike(method) && !isPayPal(method)) return
|
|
190
|
+
|
|
191
|
+
if (isPayPal(method)) setPaypalLoading(true)
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await initiatePaymentSession(cart, { provider_id: method })
|
|
195
|
+
} finally {
|
|
196
|
+
if (isPayPal(method)) setPaypalLoading(false)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Step 5 โ Filter the payment method list
|
|
204
|
+
|
|
205
|
+
**Where:** Inside the `Payment` component, alongside your other `useMemo` declarations โ add this before the `return` statement.
|
|
206
|
+
|
|
207
|
+
This hides PayPal or Card from the list if they have been disabled in Medusa Admin.
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
const filteredPaymentMethods = useMemo(
|
|
211
|
+
() =>
|
|
212
|
+
availablePaymentMethods.filter((paymentMethod) => {
|
|
213
|
+
if (paymentMethod.id === PAYPAL_PROVIDER_ID) return paypalEnabled
|
|
214
|
+
if (paymentMethod.id === PAYPAL_CARD_PROVIDER_ID) return cardEnabled
|
|
215
|
+
return true
|
|
216
|
+
}),
|
|
217
|
+
[availablePaymentMethods, cardEnabled, paypalEnabled],
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Then in your JSX, find where you render `availablePaymentMethods.map(...)` and **replace** `availablePaymentMethods` with `filteredPaymentMethods`:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
// Before
|
|
225
|
+
availablePaymentMethods.map((paymentMethod) => ( ... ))
|
|
226
|
+
|
|
227
|
+
// After
|
|
228
|
+
filteredPaymentMethods.map((paymentMethod) => ( ... ))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Step 6 โ Inject admin-configured titles
|
|
234
|
+
|
|
235
|
+
**Where:** Inside the `.map()` loop from Step 5, find your `<PaymentContainer>` component and **replace** its `paymentInfoMap` prop with the version below.
|
|
236
|
+
|
|
237
|
+
This makes the radio button labels show the titles configured in Medusa Admin instead of hardcoded defaults.
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
<PaymentContainer
|
|
241
|
+
paymentInfoMap={{
|
|
242
|
+
...paymentInfoMap,
|
|
243
|
+
...(paymentMethod.id === PAYPAL_PROVIDER_ID
|
|
244
|
+
? { [paymentMethod.id]: { ...(paymentInfoMap[paymentMethod.id] || {}), title: paypalTitle } }
|
|
245
|
+
: {}),
|
|
246
|
+
...(paymentMethod.id === PAYPAL_CARD_PROVIDER_ID
|
|
247
|
+
? { [paymentMethod.id]: { ...(paymentInfoMap[paymentMethod.id] || {}), title: cardTitle } }
|
|
248
|
+
: {}),
|
|
249
|
+
}}
|
|
250
|
+
paymentProviderId={paymentMethod.id}
|
|
251
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
252
|
+
/>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Step 7 โ Render the PayPal UI
|
|
258
|
+
|
|
259
|
+
**Where:** In the JSX, immediately after the closing `</RadioGroup>` tag.
|
|
260
|
+
|
|
261
|
+
The first block shows a loading spinner while the session is being set up. The second block renders the PayPal buttons or card fields once the session is ready.
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
{/* Loading state while PayPal session is being created */}
|
|
265
|
+
{isPayPal(selectedPaymentMethod) && paypalLoading && (
|
|
266
|
+
<div>Setting up payment...</div>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{/* PayPal buttons or card fields */}
|
|
270
|
+
{isPayPal(selectedPaymentMethod) && !paypalLoading && (
|
|
271
|
+
<MedusaNextPayPalAdapter
|
|
272
|
+
cartId={cart.id}
|
|
273
|
+
selectedProviderId={selectedPaymentMethod}
|
|
274
|
+
baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
|
|
275
|
+
publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
|
|
276
|
+
onSuccess={async () => {
|
|
277
|
+
await placeOrder(cart.id)
|
|
278
|
+
}}
|
|
279
|
+
onError={(message) => setError(message)}
|
|
280
|
+
/>
|
|
281
|
+
)}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
### Step 8 โ Disable the Continue button
|
|
287
|
+
|
|
288
|
+
**Where:** In the JSX, find your existing `<Button>` with `data-testid="submit-payment-button"` and **add** `isPayPal(selectedPaymentMethod)` to its `disabled` prop.
|
|
289
|
+
|
|
290
|
+
PayPal handles its own checkout action, so the "Continue to review" button must be hidden from the flow when PayPal is selected.
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
<Button
|
|
294
|
+
size="large"
|
|
295
|
+
className="mt-6"
|
|
296
|
+
onClick={handleSubmit}
|
|
297
|
+
isLoading={isLoading}
|
|
298
|
+
disabled={
|
|
299
|
+
(isStripeLike(selectedPaymentMethod) && !cardComplete) ||
|
|
300
|
+
(!selectedPaymentMethod && !paidByGiftcard) ||
|
|
301
|
+
isPayPal(selectedPaymentMethod) // ๐ add this line
|
|
302
|
+
}
|
|
303
|
+
data-testid="submit-payment-button"
|
|
304
|
+
>
|
|
305
|
+
{!activeSession && isStripeLike(selectedPaymentMethod)
|
|
306
|
+
? "Enter card details"
|
|
307
|
+
: "Continue to review"}
|
|
308
|
+
</Button>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### Step 9 โ Fix the summary label
|
|
314
|
+
|
|
315
|
+
**Where:** In the collapsed summary view (shown after the customer has completed the payment step). Find the `<Text>` with `data-testid="payment-method-summary"` and **replace its content** with the version below.
|
|
316
|
+
|
|
317
|
+
This shows the admin-configured title instead of a hardcoded or missing label.
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
<Text
|
|
321
|
+
className="txt-medium text-ui-fg-subtle"
|
|
322
|
+
data-testid="payment-method-summary"
|
|
323
|
+
>
|
|
324
|
+
{activeSession?.provider_id === "pp_paypal_paypal"
|
|
325
|
+
? paypalTitle
|
|
326
|
+
: activeSession?.provider_id === "pp_paypal_card_paypal_card"
|
|
327
|
+
? cardTitle
|
|
328
|
+
: paymentInfoMap[activeSession?.provider_id]?.title ||
|
|
329
|
+
activeSession?.provider_id}
|
|
330
|
+
</Text>
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## ๐ Complete File
|
|
336
|
+
|
|
337
|
+
If you prefer to copy-paste the entire file at once, replace the full contents of `src/modules/checkout/components/payment/index.tsx` with the following:
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
"use client"
|
|
341
|
+
|
|
342
|
+
import { RadioGroup } from "@headlessui/react"
|
|
343
|
+
import { initiatePaymentSession, placeOrder } from "@lib/data/cart"
|
|
344
|
+
import { isStripeLike, paymentInfoMap } from "@lib/constants"
|
|
345
|
+
import { MedusaNextPayPalAdapter } from "@easypayment/medusa-paypal-ui"
|
|
346
|
+
import { CheckCircleSolid, CreditCard } from "@medusajs/icons"
|
|
347
|
+
import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
|
|
348
|
+
import ErrorMessage from "@modules/checkout/components/error-message"
|
|
349
|
+
import PaymentContainer, {
|
|
350
|
+
StripeCardContainer,
|
|
351
|
+
} from "@modules/checkout/components/payment-container"
|
|
352
|
+
import Divider from "@modules/common/components/divider"
|
|
353
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
354
|
+
import { useCallback, useEffect, useMemo, useState } from "react"
|
|
355
|
+
|
|
356
|
+
const PAYPAL_PROVIDER_ID = "pp_paypal_paypal"
|
|
357
|
+
const PAYPAL_CARD_PROVIDER_ID = "pp_paypal_card_paypal_card"
|
|
358
|
+
const PAYPAL_PROVIDER_IDS = [PAYPAL_PROVIDER_ID, PAYPAL_CARD_PROVIDER_ID]
|
|
359
|
+
|
|
360
|
+
const isPayPal = (id: string) => PAYPAL_PROVIDER_IDS.includes(id)
|
|
361
|
+
|
|
362
|
+
const Payment = ({
|
|
363
|
+
cart,
|
|
364
|
+
availablePaymentMethods,
|
|
365
|
+
}: {
|
|
366
|
+
cart: any
|
|
367
|
+
availablePaymentMethods: any[]
|
|
368
|
+
}) => {
|
|
369
|
+
const activeSession = cart.payment_collection?.payment_sessions?.find(
|
|
370
|
+
(paymentSession: any) => paymentSession.status === "pending",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
374
|
+
const [error, setError] = useState<string | null>(null)
|
|
375
|
+
const [cardBrand, setCardBrand] = useState<string | null>(null)
|
|
376
|
+
const [cardComplete, setCardComplete] = useState(false)
|
|
377
|
+
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(
|
|
378
|
+
activeSession?.provider_id ?? "",
|
|
379
|
+
)
|
|
380
|
+
const [paypalEnabled, setPaypalEnabled] = useState(true)
|
|
381
|
+
const [paypalTitle, setPaypalTitle] = useState("PayPal")
|
|
382
|
+
const [cardEnabled, setCardEnabled] = useState(true)
|
|
383
|
+
const [cardTitle, setCardTitle] = useState("Credit or Debit Card")
|
|
384
|
+
const [paypalLoading, setPaypalLoading] = useState(false)
|
|
385
|
+
|
|
386
|
+
const searchParams = useSearchParams()
|
|
387
|
+
const router = useRouter()
|
|
388
|
+
const pathname = usePathname()
|
|
389
|
+
|
|
390
|
+
const isOpen = searchParams.get("step") === "payment"
|
|
391
|
+
|
|
392
|
+
const filteredPaymentMethods = useMemo(
|
|
393
|
+
() =>
|
|
394
|
+
availablePaymentMethods.filter((paymentMethod) => {
|
|
395
|
+
if (paymentMethod.id === PAYPAL_PROVIDER_ID) return paypalEnabled
|
|
396
|
+
if (paymentMethod.id === PAYPAL_CARD_PROVIDER_ID) return cardEnabled
|
|
397
|
+
return true
|
|
398
|
+
}),
|
|
399
|
+
[availablePaymentMethods, cardEnabled, paypalEnabled],
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
const setPaymentMethod = async (method: string) => {
|
|
403
|
+
setError(null)
|
|
404
|
+
setSelectedPaymentMethod(method)
|
|
405
|
+
|
|
406
|
+
if (!isStripeLike(method) && !isPayPal(method)) return
|
|
407
|
+
|
|
408
|
+
if (isPayPal(method)) setPaypalLoading(true)
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
await initiatePaymentSession(cart, { provider_id: method })
|
|
412
|
+
} finally {
|
|
413
|
+
if (isPayPal(method)) setPaypalLoading(false)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const paidByGiftcard =
|
|
418
|
+
cart?.gift_cards && cart?.gift_cards?.length > 0 && cart?.total === 0
|
|
419
|
+
|
|
420
|
+
const paymentReady =
|
|
421
|
+
(activeSession && cart?.shipping_methods.length !== 0) || paidByGiftcard
|
|
422
|
+
|
|
423
|
+
const createQueryString = useCallback(
|
|
424
|
+
(name: string, value: string) => {
|
|
425
|
+
const params = new URLSearchParams(searchParams)
|
|
426
|
+
params.set(name, value)
|
|
427
|
+
return params.toString()
|
|
428
|
+
},
|
|
429
|
+
[searchParams],
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
const handleEdit = () => {
|
|
433
|
+
router.push(pathname + "?" + createQueryString("step", "payment"), {
|
|
434
|
+
scroll: false,
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const handleSubmit = async () => {
|
|
439
|
+
setIsLoading(true)
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const shouldInputCard =
|
|
443
|
+
isStripeLike(selectedPaymentMethod) && !activeSession
|
|
444
|
+
const checkActiveSession =
|
|
445
|
+
activeSession?.provider_id === selectedPaymentMethod
|
|
446
|
+
|
|
447
|
+
if (!checkActiveSession) {
|
|
448
|
+
await initiatePaymentSession(cart, {
|
|
449
|
+
provider_id: selectedPaymentMethod,
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!shouldInputCard) {
|
|
454
|
+
return router.push(
|
|
455
|
+
pathname + "?" + createQueryString("step", "review"),
|
|
456
|
+
{ scroll: false },
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
} catch (err: any) {
|
|
460
|
+
setError(err.message)
|
|
461
|
+
} finally {
|
|
462
|
+
setIsLoading(false)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
setError(null)
|
|
468
|
+
}, [isOpen])
|
|
469
|
+
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
if (!isOpen) return
|
|
472
|
+
|
|
473
|
+
const backendUrl = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
|
|
474
|
+
if (!backendUrl) return
|
|
475
|
+
|
|
476
|
+
const key = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
|
|
477
|
+
const controller = new AbortController()
|
|
478
|
+
|
|
479
|
+
const loadPayPalConfig = async () => {
|
|
480
|
+
try {
|
|
481
|
+
const response = await fetch(`${backendUrl}/store/paypal/config`, {
|
|
482
|
+
headers: key ? { "x-publishable-api-key": key } : {},
|
|
483
|
+
signal: controller.signal,
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
if (response.status === 403) {
|
|
487
|
+
setPaypalEnabled(false)
|
|
488
|
+
setCardEnabled(false)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!response.ok) return
|
|
493
|
+
|
|
494
|
+
const config = await response.json()
|
|
495
|
+
|
|
496
|
+
if (typeof config?.paypal_enabled === "boolean") setPaypalEnabled(config.paypal_enabled)
|
|
497
|
+
if (typeof config?.paypal_title === "string" && config.paypal_title) setPaypalTitle(config.paypal_title)
|
|
498
|
+
if (typeof config?.card_enabled === "boolean") setCardEnabled(config.card_enabled)
|
|
499
|
+
if (typeof config?.card_title === "string" && config.card_title) setCardTitle(config.card_title)
|
|
500
|
+
} catch (err) {
|
|
501
|
+
if ((err as Error).name !== "AbortError") setPaypalLoading(false)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
void loadPayPalConfig()
|
|
506
|
+
return () => controller.abort()
|
|
507
|
+
}, [isOpen])
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<div className="bg-white">
|
|
511
|
+
<div className="flex flex-row items-center justify-between mb-6">
|
|
512
|
+
<Heading
|
|
513
|
+
level="h2"
|
|
514
|
+
className={clx(
|
|
515
|
+
"flex flex-row text-3xl-regular gap-x-2 items-baseline",
|
|
516
|
+
{
|
|
517
|
+
"opacity-50 pointer-events-none select-none":
|
|
518
|
+
!isOpen && !paymentReady,
|
|
519
|
+
},
|
|
520
|
+
)}
|
|
521
|
+
>
|
|
522
|
+
Payment
|
|
523
|
+
{!isOpen && paymentReady && <CheckCircleSolid />}
|
|
524
|
+
</Heading>
|
|
525
|
+
{!isOpen && paymentReady && (
|
|
526
|
+
<Text>
|
|
527
|
+
<button
|
|
528
|
+
onClick={handleEdit}
|
|
529
|
+
className="text-ui-fg-interactive hover:text-ui-fg-interactive-hover"
|
|
530
|
+
data-testid="edit-payment-button"
|
|
531
|
+
>
|
|
532
|
+
Edit
|
|
533
|
+
</button>
|
|
534
|
+
</Text>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
|
|
538
|
+
<div>
|
|
539
|
+
<div className={isOpen ? "block" : "hidden"}>
|
|
540
|
+
{!paidByGiftcard &&
|
|
541
|
+
filteredPaymentMethods.length > 0 &&
|
|
542
|
+
(paypalEnabled ||
|
|
543
|
+
cardEnabled ||
|
|
544
|
+
availablePaymentMethods.some((method) => !isPayPal(method.id))) && (
|
|
545
|
+
<>
|
|
546
|
+
<RadioGroup
|
|
547
|
+
value={selectedPaymentMethod}
|
|
548
|
+
onChange={(value: string) => setPaymentMethod(value)}
|
|
549
|
+
>
|
|
550
|
+
{filteredPaymentMethods.map((paymentMethod) => (
|
|
551
|
+
<div key={paymentMethod.id}>
|
|
552
|
+
{isStripeLike(paymentMethod.id) ? (
|
|
553
|
+
<StripeCardContainer
|
|
554
|
+
paymentProviderId={paymentMethod.id}
|
|
555
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
556
|
+
paymentInfoMap={paymentInfoMap}
|
|
557
|
+
setCardBrand={setCardBrand}
|
|
558
|
+
setError={setError}
|
|
559
|
+
setCardComplete={setCardComplete}
|
|
560
|
+
/>
|
|
561
|
+
) : (
|
|
562
|
+
<PaymentContainer
|
|
563
|
+
paymentInfoMap={{
|
|
564
|
+
...paymentInfoMap,
|
|
565
|
+
...(paymentMethod.id === PAYPAL_PROVIDER_ID
|
|
566
|
+
? {
|
|
567
|
+
[paymentMethod.id]: {
|
|
568
|
+
...(paymentInfoMap[paymentMethod.id] || {}),
|
|
569
|
+
title: paypalTitle,
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
573
|
+
...(paymentMethod.id === PAYPAL_CARD_PROVIDER_ID
|
|
574
|
+
? {
|
|
575
|
+
[paymentMethod.id]: {
|
|
576
|
+
...(paymentInfoMap[paymentMethod.id] || {}),
|
|
577
|
+
title: cardTitle,
|
|
578
|
+
},
|
|
579
|
+
}
|
|
580
|
+
: {}),
|
|
581
|
+
}}
|
|
582
|
+
paymentProviderId={paymentMethod.id}
|
|
583
|
+
selectedPaymentOptionId={selectedPaymentMethod}
|
|
584
|
+
/>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
))}
|
|
588
|
+
</RadioGroup>
|
|
589
|
+
|
|
590
|
+
{isPayPal(selectedPaymentMethod) && paypalLoading && (
|
|
591
|
+
<div
|
|
592
|
+
style={{
|
|
593
|
+
display: "flex",
|
|
594
|
+
alignItems: "center",
|
|
595
|
+
gap: 12,
|
|
596
|
+
padding: "14px 16px",
|
|
597
|
+
marginTop: 8,
|
|
598
|
+
background: "#f9fafb",
|
|
599
|
+
border: "1px solid #e5e7eb",
|
|
600
|
+
borderRadius: 10,
|
|
601
|
+
}}
|
|
602
|
+
>
|
|
603
|
+
<style>{`@keyframes _idx_spin{to{transform:rotate(360deg)}}`}</style>
|
|
604
|
+
<div
|
|
605
|
+
style={{
|
|
606
|
+
width: 20,
|
|
607
|
+
height: 20,
|
|
608
|
+
borderRadius: "50%",
|
|
609
|
+
border: "2.5px solid #e5e7eb",
|
|
610
|
+
borderTopColor: "#0070ba",
|
|
611
|
+
animation: "_idx_spin .7s linear infinite",
|
|
612
|
+
flexShrink: 0,
|
|
613
|
+
}}
|
|
614
|
+
/>
|
|
615
|
+
<div
|
|
616
|
+
style={{
|
|
617
|
+
fontSize: 13,
|
|
618
|
+
fontWeight: 500,
|
|
619
|
+
color: "#111827",
|
|
620
|
+
}}
|
|
621
|
+
>
|
|
622
|
+
Setting up payment...
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
|
|
627
|
+
{isPayPal(selectedPaymentMethod) && !paypalLoading && (
|
|
628
|
+
<MedusaNextPayPalAdapter
|
|
629
|
+
cartId={cart.id}
|
|
630
|
+
selectedProviderId={selectedPaymentMethod}
|
|
631
|
+
baseUrl={process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!}
|
|
632
|
+
publishableApiKey={process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY}
|
|
633
|
+
onSuccess={async () => {
|
|
634
|
+
await placeOrder(cart.id)
|
|
635
|
+
}}
|
|
636
|
+
onError={(message) => setError(message)}
|
|
637
|
+
/>
|
|
638
|
+
)}
|
|
639
|
+
</>
|
|
640
|
+
)}
|
|
641
|
+
|
|
642
|
+
{paidByGiftcard && (
|
|
643
|
+
<div className="flex flex-col w-1/3">
|
|
644
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
645
|
+
Payment method
|
|
646
|
+
</Text>
|
|
647
|
+
<Text
|
|
648
|
+
className="txt-medium text-ui-fg-subtle"
|
|
649
|
+
data-testid="payment-method-summary"
|
|
650
|
+
>
|
|
651
|
+
Gift card
|
|
652
|
+
</Text>
|
|
653
|
+
</div>
|
|
654
|
+
)}
|
|
655
|
+
|
|
656
|
+
<ErrorMessage
|
|
657
|
+
error={error}
|
|
658
|
+
data-testid="payment-method-error-message"
|
|
659
|
+
/>
|
|
660
|
+
|
|
661
|
+
<Button
|
|
662
|
+
size="large"
|
|
663
|
+
className="mt-6"
|
|
664
|
+
onClick={handleSubmit}
|
|
665
|
+
isLoading={isLoading}
|
|
666
|
+
disabled={
|
|
667
|
+
(isStripeLike(selectedPaymentMethod) && !cardComplete) ||
|
|
668
|
+
(!selectedPaymentMethod && !paidByGiftcard) ||
|
|
669
|
+
isPayPal(selectedPaymentMethod)
|
|
670
|
+
}
|
|
671
|
+
data-testid="submit-payment-button"
|
|
672
|
+
>
|
|
673
|
+
{!activeSession && isStripeLike(selectedPaymentMethod)
|
|
674
|
+
? "Enter card details"
|
|
675
|
+
: "Continue to review"}
|
|
676
|
+
</Button>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
<div className={isOpen ? "hidden" : "block"}>
|
|
680
|
+
{cart && paymentReady && activeSession ? (
|
|
681
|
+
<div className="flex items-start gap-x-1 w-full">
|
|
682
|
+
<div className="flex flex-col w-1/3">
|
|
683
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
684
|
+
Payment method
|
|
685
|
+
</Text>
|
|
686
|
+
<Text
|
|
687
|
+
className="txt-medium text-ui-fg-subtle"
|
|
688
|
+
data-testid="payment-method-summary"
|
|
689
|
+
>
|
|
690
|
+
{activeSession?.provider_id === "pp_paypal_paypal"
|
|
691
|
+
? paypalTitle
|
|
692
|
+
: activeSession?.provider_id === "pp_paypal_card_paypal_card"
|
|
693
|
+
? cardTitle
|
|
694
|
+
: paymentInfoMap[activeSession?.provider_id]?.title ||
|
|
695
|
+
activeSession?.provider_id}
|
|
696
|
+
</Text>
|
|
697
|
+
</div>
|
|
698
|
+
<div className="flex flex-col w-1/3">
|
|
699
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
700
|
+
Payment details
|
|
701
|
+
</Text>
|
|
702
|
+
<div
|
|
703
|
+
className="flex gap-2 txt-medium text-ui-fg-subtle items-center"
|
|
704
|
+
data-testid="payment-details-summary"
|
|
705
|
+
>
|
|
706
|
+
<Container className="flex items-center h-7 w-fit p-2 bg-ui-button-neutral-hover">
|
|
707
|
+
{paymentInfoMap[selectedPaymentMethod]?.icon || <CreditCard />}
|
|
708
|
+
</Container>
|
|
709
|
+
<Text>
|
|
710
|
+
{isStripeLike(selectedPaymentMethod) && cardBrand
|
|
711
|
+
? cardBrand
|
|
712
|
+
: "Another step will appear"}
|
|
713
|
+
</Text>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
) : paidByGiftcard ? (
|
|
718
|
+
<div className="flex flex-col w-1/3">
|
|
719
|
+
<Text className="txt-medium-plus text-ui-fg-base mb-1">
|
|
720
|
+
Payment method
|
|
721
|
+
</Text>
|
|
722
|
+
<Text
|
|
723
|
+
className="txt-medium text-ui-fg-subtle"
|
|
724
|
+
data-testid="payment-method-summary"
|
|
725
|
+
>
|
|
726
|
+
Gift card
|
|
727
|
+
</Text>
|
|
728
|
+
</div>
|
|
729
|
+
) : null}
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
<Divider className="mt-8" />
|
|
733
|
+
</div>
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export default Payment
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## ๐งช Testing
|
|
743
|
+
|
|
744
|
+
Toggle between sandbox and live in **Medusa Admin โ Settings โ PayPal โ PayPal Connection โ Environment**.
|
|
745
|
+
|
|
746
|
+
**Sandbox buyer account** โ log in at [developer.paypal.com](https://developer.paypal.com) โ **Testing โ Sandbox Accounts** to find your auto-generated buyer credentials. Sandbox payments do not charge real money.
|
|
747
|
+
|
|
748
|
+
**Test card for Advanced Card Fields:**
|
|
749
|
+
|
|
750
|
+
```
|
|
751
|
+
Card number 4111 1111 1111 1111
|
|
752
|
+
Expiry Any future date
|
|
753
|
+
CVV Any 3 digits
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## ๐ License
|
|
759
|
+
|
|
760
|
+
MIT ยฉ [Easy Payment](https://www.npmjs.com/package/@easypayment/medusa-paypal-ui)
|