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