@easypayment/medusa-paypal 0.6.0 → 0.6.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.
@@ -1,376 +1,376 @@
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 PayPalSettingsForm = {
43
- enabled: boolean
44
- title: string
45
- description: string
46
- buttonColor: ButtonColor
47
- buttonShape: ButtonShape
48
- buttonWidth: ButtonWidth
49
- buttonHeight: number
50
- buttonLabel: ButtonLabel
51
- }
52
-
53
- const COLOR_OPTIONS: { value: ButtonColor; label: string }[] = [
54
- { value: "gold", label: "Gold (Recommended)" },
55
- { value: "blue", label: "Blue" },
56
- { value: "silver", label: "Silver" },
57
- { value: "black", label: "Black" },
58
- { value: "white", label: "White" },
59
- ]
60
-
61
- const SHAPE_OPTIONS: { value: ButtonShape; label: string }[] = [
62
- { value: "rect", label: "Rect (Recommended)" },
63
- { value: "pill", label: "Pill" },
64
- ]
65
-
66
- const WIDTH_OPTIONS: { value: ButtonWidth; label: string }[] = [
67
- { value: "small", label: "Small" },
68
- { value: "medium", label: "Medium" },
69
- { value: "large", label: "Large" },
70
- { value: "responsive", label: "Responsive" },
71
- ]
72
-
73
- const HEIGHT_OPTIONS: number[] = [32, 36, 40, 44, 48, 52, 56]
74
-
75
- const LABEL_OPTIONS: { value: ButtonLabel; label: string }[] = [
76
- { value: "paypal", label: "PayPal" },
77
- { value: "checkout", label: "Checkout" },
78
- { value: "buynow", label: "Buy Now" },
79
- { value: "pay", label: "Pay" },
80
- ]
81
-
82
- function SectionCard({
83
- title,
84
- description,
85
- children,
86
- right,
87
- }: {
88
- title: string
89
- description?: string
90
- children: React.ReactNode
91
- right?: React.ReactNode
92
- }) {
93
- return (
94
- <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
95
- <div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
96
- <div>
97
- <div className="text-base font-semibold text-ui-fg-base">{title}</div>
98
- {description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
99
- </div>
100
- {right}
101
- </div>
102
- <div className="p-4">{children}</div>
103
- </div>
104
- )
105
- }
106
-
107
- function FieldRow({
108
- label,
109
- hint,
110
- children,
111
- }: {
112
- label: string
113
- hint?: React.ReactNode
114
- children: React.ReactNode
115
- }) {
116
- return (
117
- <div className="grid grid-cols-12 items-start gap-4 py-3">
118
- <div className="col-span-12 md:col-span-4">
119
- <div className="text-sm font-medium text-ui-fg-base">{label}</div>
120
- {hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
121
- </div>
122
- <div className="col-span-12 md:col-span-8">{children}</div>
123
- </div>
124
- )
125
- }
126
-
127
- export default function PayPalSettingsTab() {
128
- const [form, setForm] = useState<PayPalSettingsForm>({
129
- enabled: true,
130
- title: "PayPal",
131
- description: "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account",
132
- buttonColor: "gold",
133
- buttonShape: "rect",
134
- buttonWidth: "medium",
135
- buttonHeight: 48,
136
- buttonLabel: "paypal",
137
- })
138
- const [loading, setLoading] = useState(false)
139
- const [saving, setSaving] = useState(false)
140
- const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
141
- const didInit = useRef(false)
142
-
143
- useEffect(() => {
144
- if (didInit.current) return
145
- didInit.current = true
146
-
147
- ;(async () => {
148
- try {
149
- setLoading(true)
150
-
151
- // BEFORE:
152
- // const r = await fetch("/admin/paypal/settings", {
153
- // credentials: "include",
154
- // headers: { Accept: "application/json" },
155
- // })
156
- // if (!r.ok) return
157
- // const json = await r.json()
158
- //
159
- // AFTER: adminFetch attaches the Bearer token automatically
160
- const json = await adminFetch<{ data?: { paypal_settings?: PayPalSettingsForm }; paypal_settings?: PayPalSettingsForm }>(
161
- "/admin/paypal/settings"
162
- )
163
- const payload = (json?.data ?? json) as any
164
- const saved = payload?.paypal_settings
165
- if (saved && typeof saved === "object") {
166
- setForm((prev) => ({
167
- ...prev,
168
- ...saved,
169
- }))
170
- }
171
- } catch {
172
- // Silently ignore load errors — the form will use defaults
173
- } finally {
174
- setLoading(false)
175
- }
176
- })()
177
- }, [])
178
-
179
- async function onSave() {
180
- try {
181
- setSaving(true)
182
- const cleaned = { ...form }
183
-
184
- // BEFORE:
185
- // const r = await fetch("/admin/paypal/settings", {
186
- // method: "POST",
187
- // credentials: "include",
188
- // headers: { "Content-Type": "application/json", Accept: "application/json" },
189
- // body: JSON.stringify({ paypal_settings: cleaned }),
190
- // })
191
- // if (!r.ok) { ... }
192
- // const json = await r.json().catch(() => null)
193
- //
194
- // AFTER: adminFetch attaches the Bearer token automatically
195
- const json = await adminFetch<{ data?: { paypal_settings?: PayPalSettingsForm }; paypal_settings?: PayPalSettingsForm }>(
196
- "/admin/paypal/settings",
197
- {
198
- method: "POST",
199
- body: { paypal_settings: cleaned as unknown as Record<string, unknown> },
200
- }
201
- )
202
- const payload = (json?.data ?? json) as any
203
- const saved = payload?.paypal_settings
204
- if (saved && typeof saved === "object") {
205
- setForm((prev) => ({
206
- ...prev,
207
- ...saved,
208
- }))
209
- }
210
- setToast({ type: "success", message: "Settings saved" })
211
- window.setTimeout(() => setToast(null), 2500)
212
- } catch (e: unknown) {
213
- setToast({
214
- type: "error",
215
- message:
216
- (e instanceof Error ? e.message : "") ||
217
- "Failed to save settings.",
218
- })
219
- window.setTimeout(() => setToast(null), 3500)
220
- } finally {
221
- setSaving(false)
222
- }
223
- }
224
-
225
-
226
- return (
227
- <div className="p-6">
228
- <div className="flex flex-col gap-6">
229
- <div className="flex items-start justify-between gap-4">
230
- <div>
231
- <h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1>
232
- </div>
233
- <div className="flex items-center gap-2">
234
- </div>
235
- </div>
236
-
237
- <PayPalTabs />
238
-
239
- {toast ? (
240
- <div
241
- 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"
242
- role="status"
243
- aria-live="polite"
244
- >
245
- <span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>
246
- {toast.message}
247
- </span>
248
- </div>
249
- ) : null}
250
-
251
- {/* PayPal Settings */}
252
- <SectionCard
253
- title="PayPal Settings"
254
- description="Enable PayPal and configure checkout title."
255
- right={(
256
- <div className="flex items-center gap-3">
257
- <button
258
- type="button"
259
- onClick={onSave}
260
- disabled={saving || loading}
261
- 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"
262
- >
263
- {saving ? "Saving..." : "Save settings"}
264
- </button>
265
- {loading ? <span className="text-sm text-ui-fg-subtle">Loading…</span> : null}
266
- </div>
267
- )}
268
- >
269
- <div className="divide-y divide-ui-border-base">
270
- <FieldRow label="Enable/Disable">
271
- <label className="inline-flex items-center gap-2">
272
- <input
273
- type="checkbox"
274
- checked={form.enabled}
275
- onChange={(e) => setForm((p) => ({ ...p, enabled: e.target.checked }))}
276
- className="h-4 w-4 rounded border-ui-border-base"
277
- />
278
- <span className="text-sm text-ui-fg-base">Enable PayPal</span>
279
- </label>
280
- </FieldRow>
281
-
282
- <FieldRow label="Title">
283
- <input
284
- value={form.title}
285
- onChange={(e) => setForm((p) => ({ ...p, title: e.target.value }))}
286
- 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"
287
- placeholder="PayPal"
288
- />
289
- </FieldRow>
290
-
291
- </div>
292
- </SectionCard>
293
-
294
- {/* Button Appearance */}
295
- <SectionCard
296
- title="Button Appearance"
297
- description="Control PayPal Smart Button styling (color/shape/size/label)."
298
- >
299
- <div className="divide-y divide-ui-border-base">
300
-
301
- <FieldRow label="Button Color">
302
- <select
303
- value={form.buttonColor}
304
- onChange={(e) => setForm((p) => ({ ...p, buttonColor: e.target.value as ButtonColor }))}
305
- 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"
306
- >
307
- {COLOR_OPTIONS.map((o) => (
308
- <option key={o.value} value={o.value}>
309
- {o.label}
310
- </option>
311
- ))}
312
- </select>
313
- </FieldRow>
314
-
315
- <FieldRow label="Button Shape">
316
- <select
317
- value={form.buttonShape}
318
- onChange={(e) => setForm((p) => ({ ...p, buttonShape: e.target.value as ButtonShape }))}
319
- 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"
320
- >
321
- {SHAPE_OPTIONS.map((o) => (
322
- <option key={o.value} value={o.value}>
323
- {o.label}
324
- </option>
325
- ))}
326
- </select>
327
- </FieldRow>
328
-
329
- <FieldRow label="Button Width">
330
- <select
331
- value={form.buttonWidth}
332
- onChange={(e) => setForm((p) => ({ ...p, buttonWidth: e.target.value as ButtonWidth }))}
333
- 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"
334
- >
335
- {WIDTH_OPTIONS.map((o) => (
336
- <option key={o.value} value={o.value}>
337
- {o.label}
338
- </option>
339
- ))}
340
- </select>
341
- </FieldRow>
342
-
343
- <FieldRow label="Button Height">
344
- <select
345
- value={String(form.buttonHeight)}
346
- onChange={(e) => setForm((p) => ({ ...p, buttonHeight: Number(e.target.value) }))}
347
- 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"
348
- >
349
- {HEIGHT_OPTIONS.map((h) => (
350
- <option key={h} value={h}>
351
- {h} px
352
- </option>
353
- ))}
354
- </select>
355
- </FieldRow>
356
-
357
- <FieldRow label="Button Label">
358
- <select
359
- value={form.buttonLabel}
360
- onChange={(e) => setForm((p) => ({ ...p, buttonLabel: e.target.value as ButtonLabel }))}
361
- 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"
362
- >
363
- {LABEL_OPTIONS.map((o) => (
364
- <option key={o.value} value={o.value}>
365
- {o.label}
366
- </option>
367
- ))}
368
- </select>
369
- </FieldRow>
370
- </div>
371
- </SectionCard>
372
-
373
- </div>
374
- </div>
375
- )
376
- }
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 PayPalSettingsForm = {
43
+ enabled: boolean
44
+ title: string
45
+ description: string
46
+ buttonColor: ButtonColor
47
+ buttonShape: ButtonShape
48
+ buttonWidth: ButtonWidth
49
+ buttonHeight: number
50
+ buttonLabel: ButtonLabel
51
+ }
52
+
53
+ const COLOR_OPTIONS: { value: ButtonColor; label: string }[] = [
54
+ { value: "gold", label: "Gold (Recommended)" },
55
+ { value: "blue", label: "Blue" },
56
+ { value: "silver", label: "Silver" },
57
+ { value: "black", label: "Black" },
58
+ { value: "white", label: "White" },
59
+ ]
60
+
61
+ const SHAPE_OPTIONS: { value: ButtonShape; label: string }[] = [
62
+ { value: "rect", label: "Rect (Recommended)" },
63
+ { value: "pill", label: "Pill" },
64
+ ]
65
+
66
+ const WIDTH_OPTIONS: { value: ButtonWidth; label: string }[] = [
67
+ { value: "small", label: "Small" },
68
+ { value: "medium", label: "Medium" },
69
+ { value: "large", label: "Large" },
70
+ { value: "responsive", label: "Responsive" },
71
+ ]
72
+
73
+ const HEIGHT_OPTIONS: number[] = [32, 36, 40, 44, 48, 52, 56]
74
+
75
+ const LABEL_OPTIONS: { value: ButtonLabel; label: string }[] = [
76
+ { value: "paypal", label: "PayPal" },
77
+ { value: "checkout", label: "Checkout" },
78
+ { value: "buynow", label: "Buy Now" },
79
+ { value: "pay", label: "Pay" },
80
+ ]
81
+
82
+ function SectionCard({
83
+ title,
84
+ description,
85
+ children,
86
+ right,
87
+ }: {
88
+ title: string
89
+ description?: string
90
+ children: React.ReactNode
91
+ right?: React.ReactNode
92
+ }) {
93
+ return (
94
+ <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
95
+ <div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
96
+ <div>
97
+ <div className="text-base font-semibold text-ui-fg-base">{title}</div>
98
+ {description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
99
+ </div>
100
+ {right}
101
+ </div>
102
+ <div className="p-4">{children}</div>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ function FieldRow({
108
+ label,
109
+ hint,
110
+ children,
111
+ }: {
112
+ label: string
113
+ hint?: React.ReactNode
114
+ children: React.ReactNode
115
+ }) {
116
+ return (
117
+ <div className="grid grid-cols-12 items-start gap-4 py-3">
118
+ <div className="col-span-12 md:col-span-4">
119
+ <div className="text-sm font-medium text-ui-fg-base">{label}</div>
120
+ {hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
121
+ </div>
122
+ <div className="col-span-12 md:col-span-8">{children}</div>
123
+ </div>
124
+ )
125
+ }
126
+
127
+ export default function PayPalSettingsTab() {
128
+ const [form, setForm] = useState<PayPalSettingsForm>({
129
+ enabled: true,
130
+ title: "PayPal",
131
+ description: "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account",
132
+ buttonColor: "gold",
133
+ buttonShape: "rect",
134
+ buttonWidth: "medium",
135
+ buttonHeight: 48,
136
+ buttonLabel: "paypal",
137
+ })
138
+ const [loading, setLoading] = useState(false)
139
+ const [saving, setSaving] = useState(false)
140
+ const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
141
+ const didInit = useRef(false)
142
+
143
+ useEffect(() => {
144
+ if (didInit.current) return
145
+ didInit.current = true
146
+
147
+ ;(async () => {
148
+ try {
149
+ setLoading(true)
150
+
151
+ // BEFORE:
152
+ // const r = await fetch("/admin/paypal/settings", {
153
+ // credentials: "include",
154
+ // headers: { Accept: "application/json" },
155
+ // })
156
+ // if (!r.ok) return
157
+ // const json = await r.json()
158
+ //
159
+ // AFTER: adminFetch attaches the Bearer token automatically
160
+ const json = await adminFetch<{ data?: { paypal_settings?: PayPalSettingsForm }; paypal_settings?: PayPalSettingsForm }>(
161
+ "/admin/paypal/settings"
162
+ )
163
+ const payload = (json?.data ?? json) as any
164
+ const saved = payload?.paypal_settings
165
+ if (saved && typeof saved === "object") {
166
+ setForm((prev) => ({
167
+ ...prev,
168
+ ...saved,
169
+ }))
170
+ }
171
+ } catch {
172
+ // Silently ignore load errors — the form will use defaults
173
+ } finally {
174
+ setLoading(false)
175
+ }
176
+ })()
177
+ }, [])
178
+
179
+ async function onSave() {
180
+ try {
181
+ setSaving(true)
182
+ const cleaned = { ...form }
183
+
184
+ // BEFORE:
185
+ // const r = await fetch("/admin/paypal/settings", {
186
+ // method: "POST",
187
+ // credentials: "include",
188
+ // headers: { "Content-Type": "application/json", Accept: "application/json" },
189
+ // body: JSON.stringify({ paypal_settings: cleaned }),
190
+ // })
191
+ // if (!r.ok) { ... }
192
+ // const json = await r.json().catch(() => null)
193
+ //
194
+ // AFTER: adminFetch attaches the Bearer token automatically
195
+ const json = await adminFetch<{ data?: { paypal_settings?: PayPalSettingsForm }; paypal_settings?: PayPalSettingsForm }>(
196
+ "/admin/paypal/settings",
197
+ {
198
+ method: "POST",
199
+ body: { paypal_settings: cleaned as unknown as Record<string, unknown> },
200
+ }
201
+ )
202
+ const payload = (json?.data ?? json) as any
203
+ const saved = payload?.paypal_settings
204
+ if (saved && typeof saved === "object") {
205
+ setForm((prev) => ({
206
+ ...prev,
207
+ ...saved,
208
+ }))
209
+ }
210
+ setToast({ type: "success", message: "Settings saved" })
211
+ window.setTimeout(() => setToast(null), 2500)
212
+ } catch (e: unknown) {
213
+ setToast({
214
+ type: "error",
215
+ message:
216
+ (e instanceof Error ? e.message : "") ||
217
+ "Failed to save settings.",
218
+ })
219
+ window.setTimeout(() => setToast(null), 3500)
220
+ } finally {
221
+ setSaving(false)
222
+ }
223
+ }
224
+
225
+
226
+ return (
227
+ <div className="p-6">
228
+ <div className="flex flex-col gap-6">
229
+ <div className="flex items-start justify-between gap-4">
230
+ <div>
231
+ <h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1>
232
+ </div>
233
+ <div className="flex items-center gap-2">
234
+ </div>
235
+ </div>
236
+
237
+ <PayPalTabs />
238
+
239
+ {toast ? (
240
+ <div
241
+ 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"
242
+ role="status"
243
+ aria-live="polite"
244
+ >
245
+ <span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>
246
+ {toast.message}
247
+ </span>
248
+ </div>
249
+ ) : null}
250
+
251
+ {/* PayPal Settings */}
252
+ <SectionCard
253
+ title="PayPal Settings"
254
+ description="Enable PayPal and configure checkout title."
255
+ right={(
256
+ <div className="flex items-center gap-3">
257
+ <button
258
+ type="button"
259
+ onClick={onSave}
260
+ disabled={saving || loading}
261
+ className="transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none shadow-buttons-neutral text-ui-fg-base bg-ui-button-neutral after:transition-fg after:absolute after:inset-0 after:content-[''] after:button-neutral-gradient hover:bg-ui-button-neutral-hover hover:after:button-neutral-hover-gradient active:bg-ui-button-neutral-pressed active:after:button-neutral-pressed-gradient focus-visible:shadow-buttons-neutral-focus disabled:bg-ui-bg-disabled disabled:border-ui-border-base disabled:text-ui-fg-disabled disabled:shadow-buttons-neutral disabled:after:hidden txt-compact-small-plus px-3 py-1.5"
262
+ >
263
+ {saving ? "Saving..." : "Save settings"}
264
+ </button>
265
+ {loading ? <span className="text-sm text-ui-fg-subtle">Loading…</span> : null}
266
+ </div>
267
+ )}
268
+ >
269
+ <div className="divide-y divide-ui-border-base">
270
+ <FieldRow label="Enable/Disable">
271
+ <label className="inline-flex items-center gap-2">
272
+ <input
273
+ type="checkbox"
274
+ checked={form.enabled}
275
+ onChange={(e) => setForm((p) => ({ ...p, enabled: e.target.checked }))}
276
+ className="h-4 w-4 rounded border-ui-border-base"
277
+ />
278
+ <span className="text-sm text-ui-fg-base">Enable PayPal</span>
279
+ </label>
280
+ </FieldRow>
281
+
282
+ <FieldRow label="Title">
283
+ <input
284
+ value={form.title}
285
+ onChange={(e) => setForm((p) => ({ ...p, title: e.target.value }))}
286
+ 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"
287
+ placeholder="PayPal"
288
+ />
289
+ </FieldRow>
290
+
291
+ </div>
292
+ </SectionCard>
293
+
294
+ {/* Button Appearance */}
295
+ <SectionCard
296
+ title="Button Appearance"
297
+ description="Control PayPal Smart Button styling (color/shape/size/label)."
298
+ >
299
+ <div className="divide-y divide-ui-border-base">
300
+
301
+ <FieldRow label="Button Color">
302
+ <select
303
+ value={form.buttonColor}
304
+ onChange={(e) => setForm((p) => ({ ...p, buttonColor: e.target.value as ButtonColor }))}
305
+ 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"
306
+ >
307
+ {COLOR_OPTIONS.map((o) => (
308
+ <option key={o.value} value={o.value}>
309
+ {o.label}
310
+ </option>
311
+ ))}
312
+ </select>
313
+ </FieldRow>
314
+
315
+ <FieldRow label="Button Shape">
316
+ <select
317
+ value={form.buttonShape}
318
+ onChange={(e) => setForm((p) => ({ ...p, buttonShape: e.target.value as ButtonShape }))}
319
+ 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"
320
+ >
321
+ {SHAPE_OPTIONS.map((o) => (
322
+ <option key={o.value} value={o.value}>
323
+ {o.label}
324
+ </option>
325
+ ))}
326
+ </select>
327
+ </FieldRow>
328
+
329
+ <FieldRow label="Button Width">
330
+ <select
331
+ value={form.buttonWidth}
332
+ onChange={(e) => setForm((p) => ({ ...p, buttonWidth: e.target.value as ButtonWidth }))}
333
+ 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"
334
+ >
335
+ {WIDTH_OPTIONS.map((o) => (
336
+ <option key={o.value} value={o.value}>
337
+ {o.label}
338
+ </option>
339
+ ))}
340
+ </select>
341
+ </FieldRow>
342
+
343
+ <FieldRow label="Button Height">
344
+ <select
345
+ value={String(form.buttonHeight)}
346
+ onChange={(e) => setForm((p) => ({ ...p, buttonHeight: Number(e.target.value) }))}
347
+ 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"
348
+ >
349
+ {HEIGHT_OPTIONS.map((h) => (
350
+ <option key={h} value={h}>
351
+ {h} px
352
+ </option>
353
+ ))}
354
+ </select>
355
+ </FieldRow>
356
+
357
+ <FieldRow label="Button Label">
358
+ <select
359
+ value={form.buttonLabel}
360
+ onChange={(e) => setForm((p) => ({ ...p, buttonLabel: e.target.value as ButtonLabel }))}
361
+ 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"
362
+ >
363
+ {LABEL_OPTIONS.map((o) => (
364
+ <option key={o.value} value={o.value}>
365
+ {o.label}
366
+ </option>
367
+ ))}
368
+ </select>
369
+ </FieldRow>
370
+ </div>
371
+ </SectionCard>
372
+
373
+ </div>
374
+ </div>
375
+ )
376
+ }