@easypayment/medusa-paypal 0.3.0 → 0.3.2
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/.medusa/server/src/admin/index.js +11 -96
- package/.medusa/server/src/admin/index.mjs +11 -96
- package/package.json +1 -1
- package/src/admin/routes/settings/paypal/additional-settings/page.tsx +0 -16
- package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +220 -227
- package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +510 -599
|
@@ -1,599 +1,510 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
2
|
-
import PayPalTabs from "../_components/Tabs"
|
|
3
|
-
|
|
4
|
-
type AdminFetchOptions = {
|
|
5
|
-
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
|
|
6
|
-
body?: Record<string, unknown>
|
|
7
|
-
query?: Record<string, string>
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async function adminFetch<T = unknown>(path: string, opts: AdminFetchOptions = {}): Promise<T> {
|
|
11
|
-
const { method = "GET", body, query } = opts
|
|
12
|
-
let url = path
|
|
13
|
-
if (query && Object.keys(query).length > 0) {
|
|
14
|
-
const params = new URLSearchParams(query)
|
|
15
|
-
url = `${path}?${params.toString()}`
|
|
16
|
-
}
|
|
17
|
-
const headers: Record<string, string> = { Accept: "application/json" }
|
|
18
|
-
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
19
|
-
if (typeof window !== "undefined") {
|
|
20
|
-
const token = (window as any).__medusa__?.token
|
|
21
|
-
if (token) headers["Authorization"] = `Bearer ${token}`
|
|
22
|
-
}
|
|
23
|
-
const res = await fetch(url, {
|
|
24
|
-
method, headers, credentials: "include",
|
|
25
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
26
|
-
})
|
|
27
|
-
const text = await res.text().catch(() => "")
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
if (res.status === 401) throw new Error("Unauthorized (401) - session may have expired. Please reload and log in again.")
|
|
30
|
-
if (res.status === 403) throw new Error("Forbidden (403) - you do not have permission to perform this action.")
|
|
31
|
-
throw new Error(text || `Request failed with status ${res.status}`)
|
|
32
|
-
}
|
|
33
|
-
if (!text) return {} as T
|
|
34
|
-
try { return JSON.parse(text) as T } catch { return {} as T }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
type ButtonColor = "gold" | "blue" | "silver" | "black" | "white"
|
|
38
|
-
type ButtonShape = "rect" | "pill"
|
|
39
|
-
type ButtonWidth = "small" | "medium" | "large" | "responsive"
|
|
40
|
-
type ButtonLabel = "paypal" | "checkout" | "buynow" | "pay"
|
|
41
|
-
|
|
42
|
-
type
|
|
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
|
-
{ value: "
|
|
83
|
-
{ value: "
|
|
84
|
-
{ value: "
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{ value: "
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
]
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ value: "
|
|
107
|
-
{ value: "
|
|
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
|
-
<div className="
|
|
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
|
-
className="
|
|
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
|
-
</FieldRow>
|
|
512
|
-
|
|
513
|
-
<FieldRow label="Button Color">
|
|
514
|
-
<select
|
|
515
|
-
value={form.buttonColor}
|
|
516
|
-
onChange={(e) => setForm((p) => ({ ...p, buttonColor: e.target.value as ButtonColor }))}
|
|
517
|
-
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
518
|
-
>
|
|
519
|
-
{COLOR_OPTIONS.map((o) => (
|
|
520
|
-
<option key={o.value} value={o.value}>
|
|
521
|
-
{o.label}
|
|
522
|
-
</option>
|
|
523
|
-
))}
|
|
524
|
-
</select>
|
|
525
|
-
</FieldRow>
|
|
526
|
-
|
|
527
|
-
<FieldRow label="Button Shape">
|
|
528
|
-
<select
|
|
529
|
-
value={form.buttonShape}
|
|
530
|
-
onChange={(e) => setForm((p) => ({ ...p, buttonShape: e.target.value as ButtonShape }))}
|
|
531
|
-
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
532
|
-
>
|
|
533
|
-
{SHAPE_OPTIONS.map((o) => (
|
|
534
|
-
<option key={o.value} value={o.value}>
|
|
535
|
-
{o.label}
|
|
536
|
-
</option>
|
|
537
|
-
))}
|
|
538
|
-
</select>
|
|
539
|
-
</FieldRow>
|
|
540
|
-
|
|
541
|
-
<FieldRow label="Button Width">
|
|
542
|
-
<select
|
|
543
|
-
value={form.buttonWidth}
|
|
544
|
-
onChange={(e) => setForm((p) => ({ ...p, buttonWidth: e.target.value as ButtonWidth }))}
|
|
545
|
-
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
546
|
-
>
|
|
547
|
-
{WIDTH_OPTIONS.map((o) => (
|
|
548
|
-
<option key={o.value} value={o.value}>
|
|
549
|
-
{o.label}
|
|
550
|
-
</option>
|
|
551
|
-
))}
|
|
552
|
-
</select>
|
|
553
|
-
</FieldRow>
|
|
554
|
-
|
|
555
|
-
<FieldRow label="Button Height">
|
|
556
|
-
<select
|
|
557
|
-
value={String(form.buttonHeight)}
|
|
558
|
-
onChange={(e) => setForm((p) => ({ ...p, buttonHeight: Number(e.target.value) }))}
|
|
559
|
-
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
560
|
-
>
|
|
561
|
-
{HEIGHT_OPTIONS.map((h) => (
|
|
562
|
-
<option key={h} value={h}>
|
|
563
|
-
{h} px
|
|
564
|
-
</option>
|
|
565
|
-
))}
|
|
566
|
-
</select>
|
|
567
|
-
</FieldRow>
|
|
568
|
-
|
|
569
|
-
<FieldRow label="Button Label">
|
|
570
|
-
<select
|
|
571
|
-
value={form.buttonLabel}
|
|
572
|
-
onChange={(e) => setForm((p) => ({ ...p, buttonLabel: e.target.value as ButtonLabel }))}
|
|
573
|
-
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
574
|
-
>
|
|
575
|
-
{LABEL_OPTIONS.map((o) => (
|
|
576
|
-
<option key={o.value} value={o.value}>
|
|
577
|
-
{o.label}
|
|
578
|
-
</option>
|
|
579
|
-
))}
|
|
580
|
-
</select>
|
|
581
|
-
</FieldRow>
|
|
582
|
-
</div>
|
|
583
|
-
</SectionCard>
|
|
584
|
-
|
|
585
|
-
{/* Optional: preview block (pure UI) */}
|
|
586
|
-
<div className="mt-6 rounded-md border border-ui-border-base bg-ui-bg-subtle p-4">
|
|
587
|
-
<div className="text-sm font-medium text-ui-fg-base">Preview (UI only)</div>
|
|
588
|
-
<div className="mt-2 text-sm text-ui-fg-subtle">
|
|
589
|
-
Color: <span className="text-ui-fg-base">{form.buttonColor}</span> · Shape:{" "}
|
|
590
|
-
<span className="text-ui-fg-base">{form.buttonShape}</span> · Width:{" "}
|
|
591
|
-
<span className="text-ui-fg-base">{form.buttonWidth}</span> · Height:{" "}
|
|
592
|
-
<span className="text-ui-fg-base">{form.buttonHeight}px</span> · Label:{" "}
|
|
593
|
-
<span className="text-ui-fg-base">{form.buttonLabel}</span>
|
|
594
|
-
</div>
|
|
595
|
-
</div>
|
|
596
|
-
</div>
|
|
597
|
-
</div>
|
|
598
|
-
)
|
|
599
|
-
}
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react"
|
|
2
|
+
import PayPalTabs from "../_components/Tabs"
|
|
3
|
+
|
|
4
|
+
type AdminFetchOptions = {
|
|
5
|
+
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"
|
|
6
|
+
body?: Record<string, unknown>
|
|
7
|
+
query?: Record<string, string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function adminFetch<T = unknown>(path: string, opts: AdminFetchOptions = {}): Promise<T> {
|
|
11
|
+
const { method = "GET", body, query } = opts
|
|
12
|
+
let url = path
|
|
13
|
+
if (query && Object.keys(query).length > 0) {
|
|
14
|
+
const params = new URLSearchParams(query)
|
|
15
|
+
url = `${path}?${params.toString()}`
|
|
16
|
+
}
|
|
17
|
+
const headers: Record<string, string> = { Accept: "application/json" }
|
|
18
|
+
if (body !== undefined) headers["Content-Type"] = "application/json"
|
|
19
|
+
if (typeof window !== "undefined") {
|
|
20
|
+
const token = (window as any).__medusa__?.token
|
|
21
|
+
if (token) headers["Authorization"] = `Bearer ${token}`
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method, headers, credentials: "include",
|
|
25
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
26
|
+
})
|
|
27
|
+
const text = await res.text().catch(() => "")
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
if (res.status === 401) throw new Error("Unauthorized (401) - session may have expired. Please reload and log in again.")
|
|
30
|
+
if (res.status === 403) throw new Error("Forbidden (403) - you do not have permission to perform this action.")
|
|
31
|
+
throw new Error(text || `Request failed with status ${res.status}`)
|
|
32
|
+
}
|
|
33
|
+
if (!text) return {} as T
|
|
34
|
+
try { return JSON.parse(text) as T } catch { return {} as T }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type ButtonColor = "gold" | "blue" | "silver" | "black" | "white"
|
|
38
|
+
type ButtonShape = "rect" | "pill"
|
|
39
|
+
type ButtonWidth = "small" | "medium" | "large" | "responsive"
|
|
40
|
+
type ButtonLabel = "paypal" | "checkout" | "buynow" | "pay"
|
|
41
|
+
|
|
42
|
+
type DisabledButton =
|
|
43
|
+
| "paypal"
|
|
44
|
+
| "paylater"
|
|
45
|
+
| "card"
|
|
46
|
+
| "venmo"
|
|
47
|
+
| "applepay"
|
|
48
|
+
| "googlepay"
|
|
49
|
+
|
|
50
|
+
type PayPalSettingsForm = {
|
|
51
|
+
enabled: boolean
|
|
52
|
+
title: string
|
|
53
|
+
description: string
|
|
54
|
+
disableButtons: DisabledButton[]
|
|
55
|
+
buttonColor: ButtonColor
|
|
56
|
+
buttonShape: ButtonShape
|
|
57
|
+
buttonWidth: ButtonWidth
|
|
58
|
+
buttonHeight: number
|
|
59
|
+
buttonLabel: ButtonLabel
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const DISABLE_BUTTON_OPTIONS: { value: DisabledButton; label: string }[] = [
|
|
63
|
+
{ value: "paypal", label: "PayPal" },
|
|
64
|
+
{ value: "paylater", label: "Pay Later" },
|
|
65
|
+
{ value: "card", label: "Debit / Credit Card" },
|
|
66
|
+
{ value: "venmo", label: "Venmo" },
|
|
67
|
+
{ value: "applepay", label: "Apple Pay" },
|
|
68
|
+
{ value: "googlepay", label: "Google Pay" },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
const HIDDEN_DISABLE_BUTTONS = new Set<DisabledButton>(["applepay", "googlepay", "paylater"])
|
|
72
|
+
|
|
73
|
+
const VISIBLE_DISABLE_BUTTON_OPTIONS = DISABLE_BUTTON_OPTIONS.filter(
|
|
74
|
+
(option) => !HIDDEN_DISABLE_BUTTONS.has(option.value)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
function filterHiddenDisableButtons(list: DisabledButton[] = []) {
|
|
78
|
+
return list.filter((value) => !HIDDEN_DISABLE_BUTTONS.has(value))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const COLOR_OPTIONS: { value: ButtonColor; label: string }[] = [
|
|
82
|
+
{ value: "gold", label: "Gold (Recommended)" },
|
|
83
|
+
{ value: "blue", label: "Blue" },
|
|
84
|
+
{ value: "silver", label: "Silver" },
|
|
85
|
+
{ value: "black", label: "Black" },
|
|
86
|
+
{ value: "white", label: "White" },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
const SHAPE_OPTIONS: { value: ButtonShape; label: string }[] = [
|
|
90
|
+
{ value: "rect", label: "Rect (Recommended)" },
|
|
91
|
+
{ value: "pill", label: "Pill" },
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const WIDTH_OPTIONS: { value: ButtonWidth; label: string }[] = [
|
|
95
|
+
{ value: "small", label: "Small" },
|
|
96
|
+
{ value: "medium", label: "Medium" },
|
|
97
|
+
{ value: "large", label: "Large" },
|
|
98
|
+
{ value: "responsive", label: "Responsive" },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const HEIGHT_OPTIONS: number[] = [32, 36, 40, 44, 48, 52, 56]
|
|
102
|
+
|
|
103
|
+
const LABEL_OPTIONS: { value: ButtonLabel; label: string }[] = [
|
|
104
|
+
{ value: "paypal", label: "PayPal (Recommended)" },
|
|
105
|
+
{ value: "checkout", label: "Checkout" },
|
|
106
|
+
{ value: "buynow", label: "Buy Now" },
|
|
107
|
+
{ value: "pay", label: "Pay" },
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
function cx(...parts: Array<string | false | undefined | null>) {
|
|
111
|
+
return parts.filter(Boolean).join(" ")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function Pill({
|
|
115
|
+
children,
|
|
116
|
+
onRemove,
|
|
117
|
+
disabled,
|
|
118
|
+
}: {
|
|
119
|
+
children: React.ReactNode
|
|
120
|
+
onRemove?: () => void
|
|
121
|
+
disabled?: boolean
|
|
122
|
+
}) {
|
|
123
|
+
return (
|
|
124
|
+
<span
|
|
125
|
+
className={cx(
|
|
126
|
+
"inline-flex items-center gap-1 rounded-md border px-2 py-1 text-sm",
|
|
127
|
+
disabled ? "opacity-60" : "opacity-100"
|
|
128
|
+
)}
|
|
129
|
+
>
|
|
130
|
+
{children}
|
|
131
|
+
{onRemove ? (
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={onRemove}
|
|
135
|
+
className="ml-1 rounded px-1 text-ui-fg-subtle hover:text-ui-fg-base"
|
|
136
|
+
aria-label="Remove"
|
|
137
|
+
>
|
|
138
|
+
×
|
|
139
|
+
</button>
|
|
140
|
+
) : null}
|
|
141
|
+
</span>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function SectionCard({
|
|
146
|
+
title,
|
|
147
|
+
description,
|
|
148
|
+
children,
|
|
149
|
+
right,
|
|
150
|
+
}: {
|
|
151
|
+
title: string
|
|
152
|
+
description?: string
|
|
153
|
+
children: React.ReactNode
|
|
154
|
+
right?: React.ReactNode
|
|
155
|
+
}) {
|
|
156
|
+
return (
|
|
157
|
+
<div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
|
|
158
|
+
<div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
|
|
159
|
+
<div>
|
|
160
|
+
<div className="text-base font-semibold text-ui-fg-base">{title}</div>
|
|
161
|
+
{description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
|
|
162
|
+
</div>
|
|
163
|
+
{right}
|
|
164
|
+
</div>
|
|
165
|
+
<div className="p-4">{children}</div>
|
|
166
|
+
</div>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function FieldRow({
|
|
171
|
+
label,
|
|
172
|
+
hint,
|
|
173
|
+
children,
|
|
174
|
+
}: {
|
|
175
|
+
label: string
|
|
176
|
+
hint?: React.ReactNode
|
|
177
|
+
children: React.ReactNode
|
|
178
|
+
}) {
|
|
179
|
+
return (
|
|
180
|
+
<div className="grid grid-cols-12 items-start gap-4 py-3">
|
|
181
|
+
<div className="col-span-12 md:col-span-4">
|
|
182
|
+
<div className="text-sm font-medium text-ui-fg-base">{label}</div>
|
|
183
|
+
{hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
|
|
184
|
+
</div>
|
|
185
|
+
<div className="col-span-12 md:col-span-8">{children}</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default function PayPalSettingsTab() {
|
|
191
|
+
const [form, setForm] = useState<PayPalSettingsForm>({
|
|
192
|
+
enabled: true,
|
|
193
|
+
title: "PayPal",
|
|
194
|
+
description: "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account",
|
|
195
|
+
disableButtons: [],
|
|
196
|
+
buttonColor: "gold",
|
|
197
|
+
buttonShape: "rect",
|
|
198
|
+
buttonWidth: "medium",
|
|
199
|
+
buttonHeight: 48,
|
|
200
|
+
buttonLabel: "paypal",
|
|
201
|
+
})
|
|
202
|
+
const [loading, setLoading] = useState(false)
|
|
203
|
+
const [saving, setSaving] = useState(false)
|
|
204
|
+
const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
|
|
205
|
+
const didInit = useRef(false)
|
|
206
|
+
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (didInit.current) return
|
|
209
|
+
didInit.current = true
|
|
210
|
+
|
|
211
|
+
;(async () => {
|
|
212
|
+
try {
|
|
213
|
+
setLoading(true)
|
|
214
|
+
|
|
215
|
+
// BEFORE:
|
|
216
|
+
// const r = await fetch("/admin/paypal/settings", {
|
|
217
|
+
// credentials: "include",
|
|
218
|
+
// headers: { Accept: "application/json" },
|
|
219
|
+
// })
|
|
220
|
+
// if (!r.ok) return
|
|
221
|
+
// const json = await r.json()
|
|
222
|
+
//
|
|
223
|
+
// AFTER: adminFetch attaches the Bearer token automatically
|
|
224
|
+
const json = await adminFetch<{ data?: { paypal_settings?: PayPalSettingsForm }; paypal_settings?: PayPalSettingsForm }>(
|
|
225
|
+
"/admin/paypal/settings"
|
|
226
|
+
)
|
|
227
|
+
const payload = (json?.data ?? json) as any
|
|
228
|
+
const saved = payload?.paypal_settings
|
|
229
|
+
if (saved && typeof saved === "object") {
|
|
230
|
+
setForm((prev) => ({
|
|
231
|
+
...prev,
|
|
232
|
+
...saved,
|
|
233
|
+
disableButtons: filterHiddenDisableButtons(saved.disableButtons),
|
|
234
|
+
}))
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Silently ignore load errors — the form will use defaults
|
|
238
|
+
} finally {
|
|
239
|
+
setLoading(false)
|
|
240
|
+
}
|
|
241
|
+
})()
|
|
242
|
+
}, [])
|
|
243
|
+
|
|
244
|
+
async function onSave() {
|
|
245
|
+
try {
|
|
246
|
+
setSaving(true)
|
|
247
|
+
const cleaned = {
|
|
248
|
+
...form,
|
|
249
|
+
disableButtons: filterHiddenDisableButtons(form.disableButtons),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// BEFORE:
|
|
253
|
+
// const r = await fetch("/admin/paypal/settings", {
|
|
254
|
+
// method: "POST",
|
|
255
|
+
// credentials: "include",
|
|
256
|
+
// headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
257
|
+
// body: JSON.stringify({ paypal_settings: cleaned }),
|
|
258
|
+
// })
|
|
259
|
+
// if (!r.ok) { ... }
|
|
260
|
+
// const json = await r.json().catch(() => null)
|
|
261
|
+
//
|
|
262
|
+
// AFTER: adminFetch attaches the Bearer token automatically
|
|
263
|
+
const json = await adminFetch<{ data?: { paypal_settings?: PayPalSettingsForm }; paypal_settings?: PayPalSettingsForm }>(
|
|
264
|
+
"/admin/paypal/settings",
|
|
265
|
+
{
|
|
266
|
+
method: "POST",
|
|
267
|
+
body: { paypal_settings: cleaned as unknown as Record<string, unknown> },
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
const payload = (json?.data ?? json) as any
|
|
271
|
+
const saved = payload?.paypal_settings
|
|
272
|
+
if (saved && typeof saved === "object") {
|
|
273
|
+
setForm((prev) => ({
|
|
274
|
+
...prev,
|
|
275
|
+
...saved,
|
|
276
|
+
disableButtons: filterHiddenDisableButtons(saved.disableButtons),
|
|
277
|
+
}))
|
|
278
|
+
}
|
|
279
|
+
setToast({ type: "success", message: "Settings saved" })
|
|
280
|
+
window.setTimeout(() => setToast(null), 2500)
|
|
281
|
+
} catch (e: unknown) {
|
|
282
|
+
setToast({
|
|
283
|
+
type: "error",
|
|
284
|
+
message:
|
|
285
|
+
(e instanceof Error ? e.message : "") ||
|
|
286
|
+
"Failed to save settings.",
|
|
287
|
+
})
|
|
288
|
+
window.setTimeout(() => setToast(null), 3500)
|
|
289
|
+
} finally {
|
|
290
|
+
setSaving(false)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function toggleMulti<T extends string>(key: keyof PayPalSettingsForm, value: T) {
|
|
295
|
+
setForm((prev) => {
|
|
296
|
+
const list = (prev[key] as T[]) || []
|
|
297
|
+
const exists = list.includes(value)
|
|
298
|
+
const next = exists ? list.filter((v) => v !== value) : [...list, value]
|
|
299
|
+
return { ...prev, [key]: next }
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function removeMulti<T extends string>(key: keyof PayPalSettingsForm, value: T) {
|
|
304
|
+
setForm((prev) => {
|
|
305
|
+
const list = (prev[key] as T[]) || []
|
|
306
|
+
return { ...prev, [key]: list.filter((v) => v !== value) }
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div className="p-6">
|
|
312
|
+
<div className="flex flex-col gap-6">
|
|
313
|
+
<div className="flex items-start justify-between gap-4">
|
|
314
|
+
<div>
|
|
315
|
+
<h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1>
|
|
316
|
+
</div>
|
|
317
|
+
<div className="flex items-center gap-2">
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<PayPalTabs />
|
|
322
|
+
|
|
323
|
+
{toast ? (
|
|
324
|
+
<div
|
|
325
|
+
className="fixed right-6 top-6 z-50 rounded-md border border-ui-border-base bg-ui-bg-base px-4 py-3 text-sm shadow-lg"
|
|
326
|
+
role="status"
|
|
327
|
+
aria-live="polite"
|
|
328
|
+
>
|
|
329
|
+
<span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>
|
|
330
|
+
{toast.message}
|
|
331
|
+
</span>
|
|
332
|
+
</div>
|
|
333
|
+
) : null}
|
|
334
|
+
|
|
335
|
+
{/* PayPal Settings */}
|
|
336
|
+
<SectionCard
|
|
337
|
+
title="PayPal Settings"
|
|
338
|
+
description="Enable PayPal and configure checkout title."
|
|
339
|
+
right={(
|
|
340
|
+
<div className="flex items-center gap-3">
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
onClick={onSave}
|
|
344
|
+
disabled={saving || loading}
|
|
345
|
+
className="rounded-md bg-ui-button-neutral px-4 py-2 text-sm font-medium text-ui-fg-on-color shadow-sm hover:opacity-90 disabled:opacity-60"
|
|
346
|
+
>
|
|
347
|
+
{saving ? "Saving..." : "Save settings"}
|
|
348
|
+
</button>
|
|
349
|
+
{loading ? <span className="text-sm text-ui-fg-subtle">Loading…</span> : null}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
<div className="divide-y divide-ui-border-base">
|
|
354
|
+
<FieldRow label="Enable/Disable">
|
|
355
|
+
<label className="inline-flex items-center gap-2">
|
|
356
|
+
<input
|
|
357
|
+
type="checkbox"
|
|
358
|
+
checked={form.enabled}
|
|
359
|
+
onChange={(e) => setForm((p) => ({ ...p, enabled: e.target.checked }))}
|
|
360
|
+
className="h-4 w-4 rounded border-ui-border-base"
|
|
361
|
+
/>
|
|
362
|
+
<span className="text-sm text-ui-fg-base">Enable PayPal</span>
|
|
363
|
+
</label>
|
|
364
|
+
</FieldRow>
|
|
365
|
+
|
|
366
|
+
<FieldRow label="Title">
|
|
367
|
+
<input
|
|
368
|
+
value={form.title}
|
|
369
|
+
onChange={(e) => setForm((p) => ({ ...p, title: e.target.value }))}
|
|
370
|
+
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
371
|
+
placeholder="PayPal"
|
|
372
|
+
/>
|
|
373
|
+
</FieldRow>
|
|
374
|
+
|
|
375
|
+
</div>
|
|
376
|
+
</SectionCard>
|
|
377
|
+
|
|
378
|
+
{/* Button Appearance */}
|
|
379
|
+
<SectionCard
|
|
380
|
+
title="Button Appearance"
|
|
381
|
+
description="Control PayPal Smart Button styling (color/shape/size/label) and optionally disable specific buttons."
|
|
382
|
+
>
|
|
383
|
+
<div className="divide-y divide-ui-border-base">
|
|
384
|
+
<FieldRow
|
|
385
|
+
label="Disable Specific Payment Buttons"
|
|
386
|
+
hint="Hide individual funding sources (ex: Card, Venmo)."
|
|
387
|
+
>
|
|
388
|
+
<div className="flex flex-col gap-2">
|
|
389
|
+
<div className="flex flex-wrap gap-2">
|
|
390
|
+
{filterHiddenDisableButtons(form.disableButtons).map((v) => {
|
|
391
|
+
const opt = VISIBLE_DISABLE_BUTTON_OPTIONS.find((o) => o.value === v)
|
|
392
|
+
return (
|
|
393
|
+
<Pill key={v} onRemove={() => removeMulti<DisabledButton>("disableButtons", v)}>
|
|
394
|
+
{opt?.label ?? v}
|
|
395
|
+
</Pill>
|
|
396
|
+
)
|
|
397
|
+
})}
|
|
398
|
+
{filterHiddenDisableButtons(form.disableButtons).length === 0 ? (
|
|
399
|
+
<span className="text-sm text-ui-fg-subtle">No buttons disabled.</span>
|
|
400
|
+
) : null}
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<div className="rounded-md border border-ui-border-base p-3">
|
|
404
|
+
<div className="grid gap-2 md:grid-cols-2">
|
|
405
|
+
{VISIBLE_DISABLE_BUTTON_OPTIONS.map((o) => {
|
|
406
|
+
const checked = form.disableButtons.includes(o.value)
|
|
407
|
+
return (
|
|
408
|
+
<label key={o.value} className="flex items-center gap-2 rounded-md p-2 hover:bg-ui-bg-subtle">
|
|
409
|
+
<input
|
|
410
|
+
type="checkbox"
|
|
411
|
+
checked={checked}
|
|
412
|
+
onChange={() => toggleMulti<DisabledButton>("disableButtons", o.value)}
|
|
413
|
+
className="h-4 w-4 rounded border-ui-border-base"
|
|
414
|
+
/>
|
|
415
|
+
<span className="text-sm text-ui-fg-base">{o.label}</span>
|
|
416
|
+
</label>
|
|
417
|
+
)
|
|
418
|
+
})}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</FieldRow>
|
|
423
|
+
|
|
424
|
+
<FieldRow label="Button Color">
|
|
425
|
+
<select
|
|
426
|
+
value={form.buttonColor}
|
|
427
|
+
onChange={(e) => setForm((p) => ({ ...p, buttonColor: e.target.value as ButtonColor }))}
|
|
428
|
+
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
429
|
+
>
|
|
430
|
+
{COLOR_OPTIONS.map((o) => (
|
|
431
|
+
<option key={o.value} value={o.value}>
|
|
432
|
+
{o.label}
|
|
433
|
+
</option>
|
|
434
|
+
))}
|
|
435
|
+
</select>
|
|
436
|
+
</FieldRow>
|
|
437
|
+
|
|
438
|
+
<FieldRow label="Button Shape">
|
|
439
|
+
<select
|
|
440
|
+
value={form.buttonShape}
|
|
441
|
+
onChange={(e) => setForm((p) => ({ ...p, buttonShape: e.target.value as ButtonShape }))}
|
|
442
|
+
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
443
|
+
>
|
|
444
|
+
{SHAPE_OPTIONS.map((o) => (
|
|
445
|
+
<option key={o.value} value={o.value}>
|
|
446
|
+
{o.label}
|
|
447
|
+
</option>
|
|
448
|
+
))}
|
|
449
|
+
</select>
|
|
450
|
+
</FieldRow>
|
|
451
|
+
|
|
452
|
+
<FieldRow label="Button Width">
|
|
453
|
+
<select
|
|
454
|
+
value={form.buttonWidth}
|
|
455
|
+
onChange={(e) => setForm((p) => ({ ...p, buttonWidth: e.target.value as ButtonWidth }))}
|
|
456
|
+
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
457
|
+
>
|
|
458
|
+
{WIDTH_OPTIONS.map((o) => (
|
|
459
|
+
<option key={o.value} value={o.value}>
|
|
460
|
+
{o.label}
|
|
461
|
+
</option>
|
|
462
|
+
))}
|
|
463
|
+
</select>
|
|
464
|
+
</FieldRow>
|
|
465
|
+
|
|
466
|
+
<FieldRow label="Button Height">
|
|
467
|
+
<select
|
|
468
|
+
value={String(form.buttonHeight)}
|
|
469
|
+
onChange={(e) => setForm((p) => ({ ...p, buttonHeight: Number(e.target.value) }))}
|
|
470
|
+
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
471
|
+
>
|
|
472
|
+
{HEIGHT_OPTIONS.map((h) => (
|
|
473
|
+
<option key={h} value={h}>
|
|
474
|
+
{h} px
|
|
475
|
+
</option>
|
|
476
|
+
))}
|
|
477
|
+
</select>
|
|
478
|
+
</FieldRow>
|
|
479
|
+
|
|
480
|
+
<FieldRow label="Button Label">
|
|
481
|
+
<select
|
|
482
|
+
value={form.buttonLabel}
|
|
483
|
+
onChange={(e) => setForm((p) => ({ ...p, buttonLabel: e.target.value as ButtonLabel }))}
|
|
484
|
+
className="w-full rounded-md border border-ui-border-base bg-ui-bg-base px-3 py-2 text-sm text-ui-fg-base outline-none focus:ring-2 focus:ring-ui-border-interactive"
|
|
485
|
+
>
|
|
486
|
+
{LABEL_OPTIONS.map((o) => (
|
|
487
|
+
<option key={o.value} value={o.value}>
|
|
488
|
+
{o.label}
|
|
489
|
+
</option>
|
|
490
|
+
))}
|
|
491
|
+
</select>
|
|
492
|
+
</FieldRow>
|
|
493
|
+
</div>
|
|
494
|
+
</SectionCard>
|
|
495
|
+
|
|
496
|
+
{/* Optional: preview block (pure UI) */}
|
|
497
|
+
<div className="mt-6 rounded-md border border-ui-border-base bg-ui-bg-subtle p-4">
|
|
498
|
+
<div className="text-sm font-medium text-ui-fg-base">Preview (UI only)</div>
|
|
499
|
+
<div className="mt-2 text-sm text-ui-fg-subtle">
|
|
500
|
+
Color: <span className="text-ui-fg-base">{form.buttonColor}</span> · Shape:{" "}
|
|
501
|
+
<span className="text-ui-fg-base">{form.buttonShape}</span> · Width:{" "}
|
|
502
|
+
<span className="text-ui-fg-base">{form.buttonWidth}</span> · Height:{" "}
|
|
503
|
+
<span className="text-ui-fg-base">{form.buttonHeight}px</span> · Label:{" "}
|
|
504
|
+
<span className="text-ui-fg-base">{form.buttonLabel}</span>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
)
|
|
510
|
+
}
|