@easypayment/medusa-paypal 0.2.7 → 0.2.9

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.
Files changed (90) hide show
  1. package/.medusa/server/src/admin/index.js +536 -938
  2. package/.medusa/server/src/admin/index.mjs +536 -938
  3. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.d.ts.map +1 -1
  4. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js +1 -0
  5. package/.medusa/server/src/api/store/payment-collections/[id]/payment-sessions/route.js.map +1 -1
  6. package/.medusa/server/src/api/store/paypal/capture-order/route.d.ts.map +1 -1
  7. package/.medusa/server/src/api/store/paypal/capture-order/route.js +61 -74
  8. package/.medusa/server/src/api/store/paypal/capture-order/route.js.map +1 -1
  9. package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
  10. package/.medusa/server/src/api/store/paypal/create-order/route.js +3 -24
  11. package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
  12. package/.medusa/server/src/api/store/paypal/settings/route.d.ts.map +1 -1
  13. package/.medusa/server/src/api/store/paypal/settings/route.js +7 -1
  14. package/.medusa/server/src/api/store/paypal/settings/route.js.map +1 -1
  15. package/.medusa/server/src/api/store/paypal/webhook/route.d.ts.map +1 -1
  16. package/.medusa/server/src/api/store/paypal/webhook/route.js +1 -1
  17. package/.medusa/server/src/api/store/paypal/webhook/route.js.map +1 -1
  18. package/.medusa/server/src/api/store/paypal-complete/route.d.ts.map +1 -1
  19. package/.medusa/server/src/api/store/paypal-complete/route.js +46 -24
  20. package/.medusa/server/src/api/store/paypal-complete/route.js.map +1 -1
  21. package/.medusa/server/src/jobs/paypal-reconcile.d.ts.map +1 -1
  22. package/.medusa/server/src/jobs/paypal-reconcile.js +19 -5
  23. package/.medusa/server/src/jobs/paypal-reconcile.js.map +1 -1
  24. package/.medusa/server/src/jobs/paypal-webhook-retry.d.ts.map +1 -1
  25. package/.medusa/server/src/jobs/paypal-webhook-retry.js +1 -1
  26. package/.medusa/server/src/jobs/paypal-webhook-retry.js.map +1 -1
  27. package/.medusa/server/src/modules/paypal/index.d.ts +0 -14
  28. package/.medusa/server/src/modules/paypal/index.d.ts.map +1 -1
  29. package/.medusa/server/src/modules/paypal/service.d.ts +56 -93
  30. package/.medusa/server/src/modules/paypal/service.d.ts.map +1 -1
  31. package/.medusa/server/src/modules/paypal/service.js +34 -47
  32. package/.medusa/server/src/modules/paypal/service.js.map +1 -1
  33. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts +14 -0
  34. package/.medusa/server/src/modules/paypal/utils/paypal-auth.d.ts.map +1 -0
  35. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js +32 -0
  36. package/.medusa/server/src/modules/paypal/utils/paypal-auth.js.map +1 -0
  37. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts +2 -15
  38. package/.medusa/server/src/modules/paypal/webhook-processor.d.ts.map +1 -1
  39. package/.medusa/server/src/modules/paypal/webhook-processor.js +17 -100
  40. package/.medusa/server/src/modules/paypal/webhook-processor.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/admin/routes/settings/paypal/_components/Tabs.tsx +0 -1
  43. package/src/admin/routes/settings/paypal/additional-settings/page.tsx +226 -346
  44. package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +227 -381
  45. package/src/admin/routes/settings/paypal/audit-logs/page.tsx +127 -131
  46. package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +599 -557
  47. package/src/admin/routes/settings/paypal/reconciliation-status/page.tsx +120 -165
  48. package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +12 -1
  49. package/src/api/store/paypal/capture-order/route.ts +276 -284
  50. package/src/api/store/paypal/create-order/route.ts +2 -32
  51. package/src/api/store/paypal/settings/route.ts +8 -1
  52. package/src/api/store/paypal/webhook/route.ts +1 -2
  53. package/src/api/store/paypal-complete/route.ts +75 -45
  54. package/src/jobs/paypal-reconcile.ts +21 -6
  55. package/src/jobs/paypal-webhook-retry.ts +1 -2
  56. package/src/modules/paypal/service.ts +39 -62
  57. package/src/modules/paypal/utils/paypal-auth.ts +32 -0
  58. package/src/modules/paypal/webhook-processor.ts +18 -116
  59. package/tsconfig.json +1 -1
  60. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.d.ts +0 -3
  61. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.d.ts.map +0 -1
  62. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.js +0 -17
  63. package/.medusa/server/src/api/admin/paypal/disputes/[id]/route.js.map +0 -1
  64. package/.medusa/server/src/api/admin/paypal/disputes/route.d.ts +0 -3
  65. package/.medusa/server/src/api/admin/paypal/disputes/route.d.ts.map +0 -1
  66. package/.medusa/server/src/api/admin/paypal/disputes/route.js +0 -27
  67. package/.medusa/server/src/api/admin/paypal/disputes/route.js.map +0 -1
  68. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.d.ts +0 -3
  69. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.d.ts.map +0 -1
  70. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.js +0 -17
  71. package/.medusa/server/src/api/admin/paypal/disputes/summary/route.js.map +0 -1
  72. package/.medusa/server/src/api/store/paypal/disputes/route.d.ts +0 -3
  73. package/.medusa/server/src/api/store/paypal/disputes/route.d.ts.map +0 -1
  74. package/.medusa/server/src/api/store/paypal/disputes/route.js +0 -46
  75. package/.medusa/server/src/api/store/paypal/disputes/route.js.map +0 -1
  76. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.d.ts +0 -6
  77. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.d.ts.map +0 -1
  78. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.js +0 -43
  79. package/.medusa/server/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.js.map +0 -1
  80. package/.medusa/server/src/modules/paypal/models/paypal_dispute.d.ts +0 -16
  81. package/.medusa/server/src/modules/paypal/models/paypal_dispute.d.ts.map +0 -1
  82. package/.medusa/server/src/modules/paypal/models/paypal_dispute.js +0 -19
  83. package/.medusa/server/src/modules/paypal/models/paypal_dispute.js.map +0 -1
  84. package/src/admin/routes/settings/paypal/disputes/page.tsx +0 -259
  85. package/src/api/admin/paypal/disputes/[id]/route.ts +0 -19
  86. package/src/api/admin/paypal/disputes/route.ts +0 -30
  87. package/src/api/admin/paypal/disputes/summary/route.ts +0 -18
  88. package/src/api/store/paypal/disputes/route.ts +0 -67
  89. package/src/modules/paypal/migrations/20260501090000_create_paypal_dispute.ts +0 -40
  90. package/src/modules/paypal/models/paypal_dispute.ts +0 -18
@@ -1,346 +1,226 @@
1
- import React, {useEffect, useRef, useState} from "react"
2
- import PayPalTabs from "../_components/Tabs"
3
-
4
- type PaymentAction = "capture" | "authorize"
5
- type LandingPage = "no_preference" | "login" | "billing"
6
-
7
- type AdditionalSettingsForm = {
8
- paymentAction: PaymentAction
9
- brandName: string
10
- landingPage: LandingPage
11
-
12
- requireInstantPayment: boolean
13
-
14
- useShippingAsBilling: boolean
15
- sendItemDetails: boolean
16
- skipOrderReviewPage: boolean
17
-
18
- invoicePrefix: string
19
- creditCardStatementName: string
20
-
21
- enableLogging: boolean
22
- logPath?: string
23
- }
24
-
25
- const DEFAULT_FORM: AdditionalSettingsForm = {
26
- paymentAction: "capture",
27
- brandName: "PayPal",
28
- landingPage: "no_preference",
29
- requireInstantPayment: false,
30
- useShippingAsBilling: true,
31
- sendItemDetails: true,
32
- skipOrderReviewPage: true,
33
- invoicePrefix: "WC-",
34
- creditCardStatementName: "PayPal",
35
- enableLogging: true,
36
- logPath: "/uploads/wc-logs/",
37
- }
38
-
39
- function mergeWithDefaults(saved?: Partial<AdditionalSettingsForm> | null) {
40
- if (!saved) return { ...DEFAULT_FORM }
41
- const entries = Object.entries(saved).filter(([, value]) => value !== undefined)
42
- return {
43
- ...DEFAULT_FORM,
44
- ...(Object.fromEntries(entries) as Partial<AdditionalSettingsForm>),
45
- }
46
- }
47
-
48
- function cx(...parts: Array<string | false | undefined | null>) {
49
- return parts.filter(Boolean).join(" ")
50
- }
51
-
52
- function SectionCard({
53
- title,
54
- description,
55
- right,
56
- children,
57
- }: {
58
- title: string
59
- description?: React.ReactNode
60
- right?: React.ReactNode
61
- children: React.ReactNode
62
- }) {
63
- return (
64
- <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
65
- <div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
66
- <div>
67
- <div className="text-base font-semibold text-ui-fg-base">{title}</div>
68
- {description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
69
- </div>
70
- {right}
71
- </div>
72
- <div className="p-4">{children}</div>
73
- </div>
74
- )
75
- }
76
-
77
- function FieldRow({
78
- label,
79
- hint,
80
- children,
81
- }: {
82
- label: string
83
- hint?: React.ReactNode
84
- children: React.ReactNode
85
- }) {
86
- return (
87
- <div className="grid grid-cols-12 items-start gap-4 py-3">
88
- <div className="col-span-12 md:col-span-4">
89
- <div className="text-sm font-medium text-ui-fg-base">{label}</div>
90
- {hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
91
- </div>
92
- <div className="col-span-12 md:col-span-8">{children}</div>
93
- </div>
94
- )
95
- }
96
-
97
- export default function AdditionalSettingsTab() {
98
- const [form, setForm] = useState<AdditionalSettingsForm>(() => ({ ...DEFAULT_FORM }))
99
- const [loading, setLoading] = useState(false)
100
- const [saving, setSaving] = useState(false)
101
- const didInit = useRef(false)
102
-
103
- useEffect(() => {
104
- if (didInit.current) return
105
- didInit.current = true
106
-
107
- ;(async () => {
108
- try {
109
- setLoading(true)
110
- const r = await fetch("/admin/paypal/settings", {
111
- credentials: "include",
112
- headers: { "Accept": "application/json" },
113
- })
114
- if (!r.ok) return
115
- const json = await r.json()
116
- const payload = (json?.data ?? json) as any
117
- const saved = payload?.additional_settings
118
- if (saved && typeof saved === "object") {
119
- setForm(mergeWithDefaults(saved))
120
- }
121
- } finally {
122
- setLoading(false)
123
- }
124
- })()
125
- }, [])
126
-
127
- const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
128
-
129
- async function onSave() {
130
- try {
131
- setSaving(true)
132
- setToast(null)
133
-
134
- const r = await fetch("/admin/paypal/settings", {
135
- method: "POST",
136
- credentials: "include",
137
- headers: {
138
- "Content-Type": "application/json",
139
- "Accept": "application/json",
140
- },
141
- body: JSON.stringify({
142
- additional_settings: form,
143
- }),
144
- })
145
-
146
- if (!r.ok) {
147
- const errText = await r.text().catch(() => "")
148
- throw new Error(errText || "Failed to save settings")
149
- }
150
-
151
- const json = await r.json().catch(() => ({}))
152
- const payload = (json?.data ?? json) as any
153
- const saved = payload?.additional_settings
154
- if (saved && typeof saved === "object") {
155
- setForm(mergeWithDefaults(saved))
156
- }
157
-
158
- setToast({ type: "success", message: "Settings saved" })
159
- window.setTimeout(() => setToast(null), 2500)
160
- } catch (e: any) {
161
- setToast({ type: "error", message: e?.message || "Failed to save settings" })
162
- window.setTimeout(() => setToast(null), 3500)
163
- } finally {
164
- setSaving(false)
165
- }
166
- }
167
- return (
168
- <div className="p-6">
169
- <div className="flex flex-col gap-6">
170
- <div className="flex items-start justify-between gap-4">
171
- <div>
172
- <h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1>
173
-
174
- </div>
175
-
176
-
177
- </div>
178
-
179
- <PayPalTabs />
180
- {toast ? (
181
- <div
182
- 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"
183
- role="status"
184
- aria-live="polite"
185
- >
186
- <span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>
187
- {toast.message}
188
- </span>
189
- </div>
190
- ) : null}
191
-
192
- <SectionCard
193
- title="Additional Settings"
194
- description="These settings control checkout behavior, PayPal experience, and logging."
195
-
196
- right={(
197
- <div className="flex items-center gap-3">
198
- <button
199
- type="button"
200
- onClick={onSave}
201
- disabled={saving || loading}
202
- 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"
203
- >
204
- {saving ? "Saving..." : "Save settings"}
205
- </button>
206
- {loading ? <span className="text-sm text-ui-fg-subtle">Loading…</span> : null}
207
- </div>
208
- )}
209
- >
210
- <div className="divide-y divide-ui-border-base">
211
- <FieldRow label="Payment action">
212
- <select
213
- value={form.paymentAction}
214
- onChange={(e) => setForm((p) => ({ ...p, paymentAction: e.target.value as PaymentAction }))}
215
- 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"
216
- >
217
- <option value="capture">Capture</option>
218
- <option value="authorize">Authorize</option>
219
- </select>
220
- </FieldRow>
221
-
222
- <FieldRow label="Brand Name">
223
- <input
224
- value={form.brandName}
225
- onChange={(e) => setForm((p) => ({ ...p, brandName: e.target.value }))}
226
- 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"
227
- placeholder="PayPal"
228
- />
229
- </FieldRow>
230
-
231
- <FieldRow label="Landing Page">
232
- <select
233
- value={form.landingPage}
234
- onChange={(e) => setForm((p) => ({ ...p, landingPage: e.target.value as LandingPage }))}
235
- 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"
236
- >
237
- <option value="no_preference">No Preference</option>
238
- <option value="login">Login</option>
239
- <option value="billing">Billing</option>
240
- </select>
241
- </FieldRow>
242
-
243
- <FieldRow label="Instant Payments">
244
- <label className="inline-flex items-center gap-2">
245
- <input
246
- type="checkbox"
247
- checked={form.requireInstantPayment}
248
- onChange={(e) => setForm((p) => ({ ...p, requireInstantPayment: e.target.checked }))}
249
- className="h-4 w-4 rounded border-ui-border-base"
250
- />
251
- <span className="text-sm text-ui-fg-base">Require Instant Payment</span>
252
- </label>
253
- </FieldRow>
254
-
255
- <FieldRow
256
- label="Billing Address"
257
- hint="If the billing address is empty and PayPal provides a shipping address, the order will use the shipping address as the billing address."
258
- >
259
- <label className="inline-flex items-center gap-2">
260
- <input
261
- type="checkbox"
262
- checked={form.useShippingAsBilling}
263
- onChange={(e) => setForm((p) => ({ ...p, useShippingAsBilling: e.target.checked }))}
264
- className="h-4 w-4 rounded border-ui-border-base"
265
- />
266
- <span className="text-sm text-ui-fg-base">Use PayPal Shipping Address as Billing</span>
267
- </label>
268
- </FieldRow>
269
-
270
- <FieldRow
271
- label="Send Item Details"
272
- hint="Include all line item details in the payment request to PayPal so that they can be seen from the PayPal transaction details page."
273
- >
274
- <label className="inline-flex items-center gap-2">
275
- <input
276
- type="checkbox"
277
- checked={form.sendItemDetails}
278
- onChange={(e) => setForm((p) => ({ ...p, sendItemDetails: e.target.checked }))}
279
- className="h-4 w-4 rounded border-ui-border-base"
280
- />
281
- <span className="text-sm text-ui-fg-base">Send line item details to PayPal</span>
282
- </label>
283
- </FieldRow>
284
-
285
- <FieldRow
286
- label="Order Review Page"
287
- hint="Payments from the Product or Cart page skip the review step and go straight to the Thank You page."
288
- >
289
- <label className="inline-flex items-center gap-2">
290
- <input
291
- type="checkbox"
292
- checked={form.skipOrderReviewPage}
293
- onChange={(e) => setForm((p) => ({ ...p, skipOrderReviewPage: e.target.checked }))}
294
- className="h-4 w-4 rounded border-ui-border-base"
295
- />
296
- <span className="text-sm text-ui-fg-base">Skip Order Review Page</span>
297
- </label>
298
- </FieldRow>
299
-
300
- <FieldRow label="Invoice prefix">
301
- <input
302
- value={form.invoicePrefix}
303
- onChange={(e) => setForm((p) => ({ ...p, invoicePrefix: e.target.value }))}
304
- 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"
305
- placeholder="WC-"
306
- />
307
- </FieldRow>
308
-
309
- <FieldRow label="Credit Card Statement Name">
310
- <input
311
- value={form.creditCardStatementName}
312
- onChange={(e) => setForm((p) => ({ ...p, creditCardStatementName: e.target.value }))}
313
- 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"
314
- placeholder="PayPal"
315
- />
316
- </FieldRow>
317
-
318
- <FieldRow
319
- label="Debug log"
320
- hint={
321
- <span>
322
- Log PayPal events such as Webhook, Payment, Refund.{" "}
323
- {form.logPath ? (
324
- <>
325
- Log location: <span className="font-mono">{form.logPath}</span>
326
- </>
327
- ) : null}
328
- </span>
329
- }
330
- >
331
- <label className="inline-flex items-center gap-2">
332
- <input
333
- type="checkbox"
334
- checked={form.enableLogging}
335
- onChange={(e) => setForm((p) => ({ ...p, enableLogging: e.target.checked }))}
336
- className="h-4 w-4 rounded border-ui-border-base"
337
- />
338
- <span className="text-sm text-ui-fg-base">Enable logging</span>
339
- </label>
340
- </FieldRow>
341
- </div>
342
- </SectionCard>
343
- </div>
344
- </div>
345
- )
346
- }
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 PaymentAction = "capture" | "authorize"
38
+ type LandingPage = "no_preference" | "login" | "billing"
39
+
40
+ type AdditionalSettingsForm = {
41
+ paymentAction: PaymentAction
42
+ brandName: string
43
+ landingPage: LandingPage
44
+ requireInstantPayment: boolean
45
+ useShippingAsBilling: boolean
46
+ sendItemDetails: boolean
47
+ skipOrderReviewPage: boolean
48
+ invoicePrefix: string
49
+ creditCardStatementName: string
50
+ enableLogging: boolean
51
+ logPath?: string
52
+ }
53
+
54
+ const DEFAULT_FORM: AdditionalSettingsForm = {
55
+ paymentAction: "capture",
56
+ brandName: "PayPal",
57
+ landingPage: "no_preference",
58
+ requireInstantPayment: false,
59
+ useShippingAsBilling: true,
60
+ sendItemDetails: true,
61
+ skipOrderReviewPage: true,
62
+ invoicePrefix: "WC-",
63
+ creditCardStatementName: "PayPal",
64
+ enableLogging: true,
65
+ logPath: "/uploads/wc-logs/",
66
+ }
67
+
68
+ function mergeWithDefaults(saved?: Partial<AdditionalSettingsForm> | null) {
69
+ if (!saved) return { ...DEFAULT_FORM }
70
+ const entries = Object.entries(saved).filter(([, value]) => value !== undefined)
71
+ return { ...DEFAULT_FORM, ...(Object.fromEntries(entries) as Partial<AdditionalSettingsForm>) }
72
+ }
73
+
74
+ function SectionCard({ title, description, right, children }: { title: string; description?: React.ReactNode; right?: React.ReactNode; children: React.ReactNode }) {
75
+ return (
76
+ <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
77
+ <div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
78
+ <div>
79
+ <div className="text-base font-semibold text-ui-fg-base">{title}</div>
80
+ {description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
81
+ </div>
82
+ {right}
83
+ </div>
84
+ <div className="p-4">{children}</div>
85
+ </div>
86
+ )
87
+ }
88
+
89
+ function FieldRow({ label, hint, children }: { label: string; hint?: React.ReactNode; children: React.ReactNode }) {
90
+ return (
91
+ <div className="grid grid-cols-12 items-start gap-4 py-3">
92
+ <div className="col-span-12 md:col-span-4">
93
+ <div className="text-sm font-medium text-ui-fg-base">{label}</div>
94
+ {hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
95
+ </div>
96
+ <div className="col-span-12 md:col-span-8">{children}</div>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ export default function AdditionalSettingsTab() {
102
+ const [form, setForm] = useState<AdditionalSettingsForm>(() => ({ ...DEFAULT_FORM }))
103
+ const [loading, setLoading] = useState(false)
104
+ const [saving, setSaving] = useState(false)
105
+ const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
106
+ const didInit = useRef(false)
107
+
108
+ useEffect(() => {
109
+ if (didInit.current) return
110
+ didInit.current = true
111
+ ;(async () => {
112
+ try {
113
+ setLoading(true)
114
+ const json = await adminFetch<any>("/admin/paypal/settings")
115
+ const payload = json?.data ?? json
116
+ const saved = payload?.additional_settings
117
+ if (saved && typeof saved === "object") setForm(mergeWithDefaults(saved))
118
+ } catch {
119
+ // use defaults
120
+ } finally {
121
+ setLoading(false)
122
+ }
123
+ })()
124
+ }, [])
125
+
126
+ async function onSave() {
127
+ try {
128
+ setSaving(true)
129
+ setToast(null)
130
+ const json = await adminFetch<any>("/admin/paypal/settings", { method: "POST", body: { additional_settings: form as unknown as Record<string, unknown> } })
131
+ const payload = json?.data ?? json
132
+ const saved = payload?.additional_settings
133
+ if (saved && typeof saved === "object") setForm(mergeWithDefaults(saved))
134
+ setToast({ type: "success", message: "Settings saved" })
135
+ window.setTimeout(() => setToast(null), 2500)
136
+ } catch (e: unknown) {
137
+ setToast({ type: "error", message: e instanceof Error ? e.message : "Failed to save settings" })
138
+ window.setTimeout(() => setToast(null), 3500)
139
+ } finally {
140
+ setSaving(false)
141
+ }
142
+ }
143
+
144
+ return (
145
+ <div className="p-6">
146
+ <div className="flex flex-col gap-6">
147
+ <div className="flex items-start justify-between gap-4">
148
+ <div><h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1></div>
149
+ </div>
150
+ <PayPalTabs />
151
+ {toast ? (
152
+ <div 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" role="status" aria-live="polite">
153
+ <span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>{toast.message}</span>
154
+ </div>
155
+ ) : null}
156
+ <SectionCard
157
+ title="Additional Settings"
158
+ description="These settings control checkout behavior, PayPal experience, and logging."
159
+ right={(
160
+ <div className="flex items-center gap-3">
161
+ <button type="button" onClick={onSave} disabled={saving || loading} 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">
162
+ {saving ? "Saving..." : "Save settings"}
163
+ </button>
164
+ {loading ? <span className="text-sm text-ui-fg-subtle">Loading...</span> : null}
165
+ </div>
166
+ )}
167
+ >
168
+ <div className="divide-y divide-ui-border-base">
169
+ <FieldRow label="Payment action">
170
+ <select value={form.paymentAction} onChange={(e) => setForm((p) => ({ ...p, paymentAction: e.target.value as PaymentAction }))} 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">
171
+ <option value="capture">Capture</option>
172
+ <option value="authorize">Authorize</option>
173
+ </select>
174
+ </FieldRow>
175
+ <FieldRow label="Brand Name">
176
+ <input value={form.brandName} onChange={(e) => setForm((p) => ({ ...p, brandName: e.target.value }))} 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" placeholder="PayPal" />
177
+ </FieldRow>
178
+ <FieldRow label="Landing Page">
179
+ <select value={form.landingPage} onChange={(e) => setForm((p) => ({ ...p, landingPage: e.target.value as LandingPage }))} 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">
180
+ <option value="no_preference">No Preference</option>
181
+ <option value="login">Login</option>
182
+ <option value="billing">Billing</option>
183
+ </select>
184
+ </FieldRow>
185
+ <FieldRow label="Instant Payments">
186
+ <label className="inline-flex items-center gap-2">
187
+ <input type="checkbox" checked={form.requireInstantPayment} onChange={(e) => setForm((p) => ({ ...p, requireInstantPayment: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
188
+ <span className="text-sm text-ui-fg-base">Require Instant Payment</span>
189
+ </label>
190
+ </FieldRow>
191
+ <FieldRow label="Billing Address" hint="If the billing address is empty and PayPal provides a shipping address, the order will use the shipping address as the billing address.">
192
+ <label className="inline-flex items-center gap-2">
193
+ <input type="checkbox" checked={form.useShippingAsBilling} onChange={(e) => setForm((p) => ({ ...p, useShippingAsBilling: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
194
+ <span className="text-sm text-ui-fg-base">Use PayPal Shipping Address as Billing</span>
195
+ </label>
196
+ </FieldRow>
197
+ <FieldRow label="Send Item Details" hint="Include all line item details in the payment request to PayPal so that they can be seen from the PayPal transaction details page.">
198
+ <label className="inline-flex items-center gap-2">
199
+ <input type="checkbox" checked={form.sendItemDetails} onChange={(e) => setForm((p) => ({ ...p, sendItemDetails: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
200
+ <span className="text-sm text-ui-fg-base">Send line item details to PayPal</span>
201
+ </label>
202
+ </FieldRow>
203
+ <FieldRow label="Order Review Page" hint="Payments from the Product or Cart page skip the review step and go straight to the Thank You page.">
204
+ <label className="inline-flex items-center gap-2">
205
+ <input type="checkbox" checked={form.skipOrderReviewPage} onChange={(e) => setForm((p) => ({ ...p, skipOrderReviewPage: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
206
+ <span className="text-sm text-ui-fg-base">Skip Order Review Page</span>
207
+ </label>
208
+ </FieldRow>
209
+ <FieldRow label="Invoice prefix">
210
+ <input value={form.invoicePrefix} onChange={(e) => setForm((p) => ({ ...p, invoicePrefix: e.target.value }))} 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" placeholder="WC-" />
211
+ </FieldRow>
212
+ <FieldRow label="Credit Card Statement Name">
213
+ <input value={form.creditCardStatementName} onChange={(e) => setForm((p) => ({ ...p, creditCardStatementName: e.target.value }))} 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" placeholder="PayPal" />
214
+ </FieldRow>
215
+ <FieldRow label="Debug log" hint={<span>Log PayPal events such as Webhook, Payment, Refund. {form.logPath ? <>Log location: <span className="font-mono">{form.logPath}</span></> : null}</span>}>
216
+ <label className="inline-flex items-center gap-2">
217
+ <input type="checkbox" checked={form.enableLogging} onChange={(e) => setForm((p) => ({ ...p, enableLogging: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
218
+ <span className="text-sm text-ui-fg-base">Enable logging</span>
219
+ </label>
220
+ </FieldRow>
221
+ </div>
222
+ </SectionCard>
223
+ </div>
224
+ </div>
225
+ )
226
+ }