@easypayment/medusa-paypal 0.4.6 → 0.4.8
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 +7 -7
- package/.medusa/server/src/admin/index.mjs +7 -7
- package/.medusa/server/src/api/store/paypal/create-order/route.d.ts.map +1 -1
- package/.medusa/server/src/api/store/paypal/create-order/route.js +10 -3
- package/.medusa/server/src/api/store/paypal/create-order/route.js.map +1 -1
- package/.medusa/server/src/modules/paypal/migrations/20260115120000_create_paypal_connection.js +22 -22
- package/.medusa/server/src/modules/paypal/migrations/20260123090000_create_paypal_settings.js +11 -11
- package/.medusa/server/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.js +18 -18
- package/.medusa/server/src/modules/paypal/migrations/20260401090000_create_paypal_metric.js +16 -16
- package/.medusa/server/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.js +20 -20
- package/.medusa/server/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.js +14 -14
- package/.medusa/server/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.js +15 -15
- package/README.md +142 -142
- package/package.json +75 -75
- package/src/admin/index.ts +7 -7
- package/src/admin/routes/settings/paypal/_components/Tabs.tsx +52 -52
- package/src/admin/routes/settings/paypal/_components/Toast.tsx +51 -51
- package/src/admin/routes/settings/paypal/additional-settings/page.tsx +200 -200
- package/src/admin/routes/settings/paypal/advanced-card-payments/page.tsx +183 -183
- package/src/admin/routes/settings/paypal/apple-pay/page.tsx +5 -5
- package/src/admin/routes/settings/paypal/connection/page.tsx +754 -754
- package/src/admin/routes/settings/paypal/google-pay/page.tsx +5 -5
- package/src/admin/routes/settings/paypal/pay-later-messaging/page.tsx +5 -5
- package/src/admin/routes/settings/paypal/paypal-settings/page.tsx +376 -376
- package/src/api/admin/payment-collections/[id]/payment-sessions/route.ts +24 -24
- package/src/api/admin/paypal/disconnect/route.ts +8 -8
- package/src/api/admin/paypal/environment/route.ts +25 -25
- package/src/api/admin/paypal/onboard-complete/route.ts +44 -44
- package/src/api/admin/paypal/onboarding-link/route.ts +45 -45
- package/src/api/admin/paypal/onboarding-status/route.ts +18 -18
- package/src/api/admin/paypal/rotate-credentials/route.ts +8 -8
- package/src/api/admin/paypal/save-credentials/route.ts +14 -14
- package/src/api/admin/paypal/settings/route.ts +14 -14
- package/src/api/admin/paypal/status/route.ts +12 -12
- package/src/api/store/payment-collections/[id]/payment-sessions/route.ts +65 -65
- package/src/api/store/paypal/capture-order/route.ts +276 -276
- package/src/api/store/paypal/config/route.ts +102 -102
- package/src/api/store/paypal/create-order/route.ts +13 -3
- package/src/api/store/paypal/settings/route.ts +19 -19
- package/src/api/store/paypal/webhook/route.ts +246 -246
- package/src/api/store/paypal-complete/route.ts +75 -75
- package/src/jobs/paypal-reconcile.ts +112 -112
- package/src/jobs/paypal-webhook-retry.ts +85 -85
- package/src/modules/paypal/clients/paypal-seller.client.ts +59 -59
- package/src/modules/paypal/index.ts +8 -8
- package/src/modules/paypal/migrations/20260115120000_create_paypal_connection.ts +33 -33
- package/src/modules/paypal/migrations/20260123090000_create_paypal_settings.ts +22 -22
- package/src/modules/paypal/migrations/20260201090000_create_paypal_webhook_event.ts +29 -29
- package/src/modules/paypal/migrations/20260401090000_create_paypal_metric.ts +27 -27
- package/src/modules/paypal/migrations/20260701090000_add_paypal_webhook_event_processing.ts +31 -31
- package/src/modules/paypal/migrations/20261101090000_remove_paypal_reconciliation_status.ts +25 -25
- package/src/modules/paypal/migrations/20261201090000_remove_paypal_audit_log.ts +26 -26
- package/src/modules/paypal/migrations/20270101090000_set_paypal_environment_default_live.ts +11 -11
- package/src/modules/paypal/models/paypal_connection.ts +21 -21
- package/src/modules/paypal/models/paypal_metric.ts +9 -9
- package/src/modules/paypal/models/paypal_settings.ts +8 -8
- package/src/modules/paypal/models/paypal_webhook_event.ts +19 -19
- package/src/modules/paypal/payment-provider/README.md +22 -22
- package/src/modules/paypal/payment-provider/card-service.ts +760 -760
- package/src/modules/paypal/payment-provider/index.ts +19 -19
- package/src/modules/paypal/payment-provider/service.ts +1121 -1121
- package/src/modules/paypal/payment-provider/webhook-utils.ts +88 -88
- package/src/modules/paypal/service.ts +1247 -1247
- package/src/modules/paypal/types/config.ts +47 -47
- package/src/modules/paypal/utils/amounts.ts +41 -41
- package/src/modules/paypal/utils/crypto.ts +51 -51
- package/src/modules/paypal/utils/currencies.ts +84 -84
- package/src/modules/paypal/utils/paypal-auth.ts +32 -32
- package/src/modules/paypal/utils/provider-ids.ts +15 -15
- package/src/modules/paypal/webhook-processor.ts +215 -215
|
@@ -1,184 +1,184 @@
|
|
|
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 ThreeDSContingency = "sli" | "when_required" | "always"
|
|
38
|
-
|
|
39
|
-
type AdvancedCardPaymentsForm = {
|
|
40
|
-
enabled: boolean
|
|
41
|
-
title: string
|
|
42
|
-
threeDS: ThreeDSContingency
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const DEFAULT_FORM: AdvancedCardPaymentsForm = {
|
|
46
|
-
enabled: true,
|
|
47
|
-
title: "Credit or Debit Card",
|
|
48
|
-
threeDS: "when_required",
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function mergeWithDefaults(saved?: Partial<AdvancedCardPaymentsForm> | null) {
|
|
52
|
-
if (!saved) return { ...DEFAULT_FORM }
|
|
53
|
-
const entries = Object.entries(saved).filter(([, value]) => value !== undefined)
|
|
54
|
-
return { ...DEFAULT_FORM, ...(Object.fromEntries(entries) as Partial<AdvancedCardPaymentsForm>) }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const THREE_DS_OPTIONS: { value: ThreeDSContingency; label: string; hint?: string }[] = [
|
|
58
|
-
{ value: "when_required", label: "3D Secure when required", hint: "Triggers 3DS only when the card / issuer requires it." },
|
|
59
|
-
{ value: "sli", label: "3D Secure (SCA) / liability shift (recommended)", hint: "Attempts to optimize for liability shift while remaining compliant." },
|
|
60
|
-
{ value: "always", label: "Always request 3D Secure", hint: "Forces 3DS challenge whenever possible (may reduce conversion)." },
|
|
61
|
-
]
|
|
62
|
-
|
|
63
|
-
function SectionCard({ title, description, right, children }: { title: string; description?: string; right?: React.ReactNode; children: React.ReactNode }) {
|
|
64
|
-
return (
|
|
65
|
-
<div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
|
|
66
|
-
<div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
|
|
67
|
-
<div>
|
|
68
|
-
<div className="text-base font-semibold text-ui-fg-base">{title}</div>
|
|
69
|
-
{description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
|
|
70
|
-
</div>
|
|
71
|
-
{right}
|
|
72
|
-
</div>
|
|
73
|
-
<div className="p-4">{children}</div>
|
|
74
|
-
</div>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function FieldRow({ label, hint, children }: { label: string; hint?: React.ReactNode; children: React.ReactNode }) {
|
|
79
|
-
return (
|
|
80
|
-
<div className="grid grid-cols-12 items-start gap-4 py-3">
|
|
81
|
-
<div className="col-span-12 md:col-span-4">
|
|
82
|
-
<div className="text-sm font-medium text-ui-fg-base">{label}</div>
|
|
83
|
-
{hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
|
|
84
|
-
</div>
|
|
85
|
-
<div className="col-span-12 md:col-span-8">{children}</div>
|
|
86
|
-
</div>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export default function AdvancedCardPaymentsTab() {
|
|
91
|
-
const [form, setForm] = useState<AdvancedCardPaymentsForm>(() => ({ ...DEFAULT_FORM }))
|
|
92
|
-
const [loading, setLoading] = useState(false)
|
|
93
|
-
const [saving, setSaving] = useState(false)
|
|
94
|
-
const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
|
|
95
|
-
const didInit = useRef(false)
|
|
96
|
-
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (didInit.current) return
|
|
99
|
-
didInit.current = true
|
|
100
|
-
;(async () => {
|
|
101
|
-
try {
|
|
102
|
-
setLoading(true)
|
|
103
|
-
const json = await adminFetch<any>("/admin/paypal/settings")
|
|
104
|
-
const payload = json?.data ?? json
|
|
105
|
-
const saved = payload?.advanced_card_payments
|
|
106
|
-
if (saved && typeof saved === "object") setForm(mergeWithDefaults(saved))
|
|
107
|
-
} catch {
|
|
108
|
-
// use defaults
|
|
109
|
-
} finally {
|
|
110
|
-
setLoading(false)
|
|
111
|
-
}
|
|
112
|
-
})()
|
|
113
|
-
}, [])
|
|
114
|
-
|
|
115
|
-
async function onSave() {
|
|
116
|
-
try {
|
|
117
|
-
setSaving(true)
|
|
118
|
-
const json = await adminFetch<any>("/admin/paypal/settings", {
|
|
119
|
-
method: "POST",
|
|
120
|
-
body: { advanced_card_payments: form as unknown as Record<string, unknown> },
|
|
121
|
-
})
|
|
122
|
-
const payload = json?.data ?? json
|
|
123
|
-
const saved = payload?.advanced_card_payments
|
|
124
|
-
if (saved && typeof saved === "object") setForm(mergeWithDefaults(saved))
|
|
125
|
-
setToast({ type: "success", message: "Settings saved" })
|
|
126
|
-
window.setTimeout(() => setToast(null), 2500)
|
|
127
|
-
} catch (e: unknown) {
|
|
128
|
-
setToast({ type: "error", message: (e instanceof Error ? e.message : "") || "Failed to save settings." })
|
|
129
|
-
window.setTimeout(() => setToast(null), 3500)
|
|
130
|
-
} finally {
|
|
131
|
-
setSaving(false)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<div className="p-6">
|
|
137
|
-
<div className="flex flex-col gap-6">
|
|
138
|
-
<div className="flex items-start justify-between gap-4">
|
|
139
|
-
<div><h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1></div>
|
|
140
|
-
</div>
|
|
141
|
-
<PayPalTabs />
|
|
142
|
-
{toast ? (
|
|
143
|
-
<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">
|
|
144
|
-
<span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>{toast.message}</span>
|
|
145
|
-
</div>
|
|
146
|
-
) : null}
|
|
147
|
-
<SectionCard
|
|
148
|
-
title="Advanced Card Payments"
|
|
149
|
-
description="Control card checkout settings and 3D Secure behavior."
|
|
150
|
-
right={(
|
|
151
|
-
<div className="flex items-center gap-3">
|
|
152
|
-
<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">
|
|
153
|
-
{saving ? "Saving..." : "Save settings"}
|
|
154
|
-
</button>
|
|
155
|
-
{loading ? <span className="text-sm text-ui-fg-subtle">Loading...</span> : null}
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
>
|
|
159
|
-
<div className="divide-y divide-ui-border-base">
|
|
160
|
-
<FieldRow label="Enable/Disable">
|
|
161
|
-
<label className="inline-flex items-center gap-2">
|
|
162
|
-
<input type="checkbox" checked={form.enabled} onChange={(e) => setForm((p) => ({ ...p, enabled: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
|
|
163
|
-
<span className="text-sm text-ui-fg-base">Enable Advanced Credit/Debit Card</span>
|
|
164
|
-
</label>
|
|
165
|
-
</FieldRow>
|
|
166
|
-
<FieldRow label="Title">
|
|
167
|
-
<input value={form.title} onChange={(e) => setForm((p) => ({ ...p, title: 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="Credit or Debit Card" />
|
|
168
|
-
</FieldRow>
|
|
169
|
-
<FieldRow label="Contingency for 3D Secure" hint="Choose when 3D Secure should be triggered during card payments.">
|
|
170
|
-
<div className="flex flex-col gap-2">
|
|
171
|
-
<select value={form.threeDS} onChange={(e) => setForm((p) => ({ ...p, threeDS: e.target.value as ThreeDSContingency }))} 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">
|
|
172
|
-
{THREE_DS_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
173
|
-
</select>
|
|
174
|
-
{THREE_DS_OPTIONS.find((o) => o.value === form.threeDS)?.hint
|
|
175
|
-
? <div className="text-xs text-ui-fg-subtle">{THREE_DS_OPTIONS.find((o) => o.value === form.threeDS)?.hint}</div>
|
|
176
|
-
: null}
|
|
177
|
-
</div>
|
|
178
|
-
</FieldRow>
|
|
179
|
-
</div>
|
|
180
|
-
</SectionCard>
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
)
|
|
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 ThreeDSContingency = "sli" | "when_required" | "always"
|
|
38
|
+
|
|
39
|
+
type AdvancedCardPaymentsForm = {
|
|
40
|
+
enabled: boolean
|
|
41
|
+
title: string
|
|
42
|
+
threeDS: ThreeDSContingency
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_FORM: AdvancedCardPaymentsForm = {
|
|
46
|
+
enabled: true,
|
|
47
|
+
title: "Credit or Debit Card",
|
|
48
|
+
threeDS: "when_required",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function mergeWithDefaults(saved?: Partial<AdvancedCardPaymentsForm> | null) {
|
|
52
|
+
if (!saved) return { ...DEFAULT_FORM }
|
|
53
|
+
const entries = Object.entries(saved).filter(([, value]) => value !== undefined)
|
|
54
|
+
return { ...DEFAULT_FORM, ...(Object.fromEntries(entries) as Partial<AdvancedCardPaymentsForm>) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const THREE_DS_OPTIONS: { value: ThreeDSContingency; label: string; hint?: string }[] = [
|
|
58
|
+
{ value: "when_required", label: "3D Secure when required", hint: "Triggers 3DS only when the card / issuer requires it." },
|
|
59
|
+
{ value: "sli", label: "3D Secure (SCA) / liability shift (recommended)", hint: "Attempts to optimize for liability shift while remaining compliant." },
|
|
60
|
+
{ value: "always", label: "Always request 3D Secure", hint: "Forces 3DS challenge whenever possible (may reduce conversion)." },
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
function SectionCard({ title, description, right, children }: { title: string; description?: string; right?: React.ReactNode; children: React.ReactNode }) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
|
|
66
|
+
<div className="flex items-start justify-between gap-4 border-b border-ui-border-base p-4">
|
|
67
|
+
<div>
|
|
68
|
+
<div className="text-base font-semibold text-ui-fg-base">{title}</div>
|
|
69
|
+
{description ? <div className="mt-1 text-sm text-ui-fg-subtle">{description}</div> : null}
|
|
70
|
+
</div>
|
|
71
|
+
{right}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="p-4">{children}</div>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function FieldRow({ label, hint, children }: { label: string; hint?: React.ReactNode; children: React.ReactNode }) {
|
|
79
|
+
return (
|
|
80
|
+
<div className="grid grid-cols-12 items-start gap-4 py-3">
|
|
81
|
+
<div className="col-span-12 md:col-span-4">
|
|
82
|
+
<div className="text-sm font-medium text-ui-fg-base">{label}</div>
|
|
83
|
+
{hint ? <div className="mt-1 text-xs text-ui-fg-subtle">{hint}</div> : null}
|
|
84
|
+
</div>
|
|
85
|
+
<div className="col-span-12 md:col-span-8">{children}</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function AdvancedCardPaymentsTab() {
|
|
91
|
+
const [form, setForm] = useState<AdvancedCardPaymentsForm>(() => ({ ...DEFAULT_FORM }))
|
|
92
|
+
const [loading, setLoading] = useState(false)
|
|
93
|
+
const [saving, setSaving] = useState(false)
|
|
94
|
+
const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null)
|
|
95
|
+
const didInit = useRef(false)
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (didInit.current) return
|
|
99
|
+
didInit.current = true
|
|
100
|
+
;(async () => {
|
|
101
|
+
try {
|
|
102
|
+
setLoading(true)
|
|
103
|
+
const json = await adminFetch<any>("/admin/paypal/settings")
|
|
104
|
+
const payload = json?.data ?? json
|
|
105
|
+
const saved = payload?.advanced_card_payments
|
|
106
|
+
if (saved && typeof saved === "object") setForm(mergeWithDefaults(saved))
|
|
107
|
+
} catch {
|
|
108
|
+
// use defaults
|
|
109
|
+
} finally {
|
|
110
|
+
setLoading(false)
|
|
111
|
+
}
|
|
112
|
+
})()
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
async function onSave() {
|
|
116
|
+
try {
|
|
117
|
+
setSaving(true)
|
|
118
|
+
const json = await adminFetch<any>("/admin/paypal/settings", {
|
|
119
|
+
method: "POST",
|
|
120
|
+
body: { advanced_card_payments: form as unknown as Record<string, unknown> },
|
|
121
|
+
})
|
|
122
|
+
const payload = json?.data ?? json
|
|
123
|
+
const saved = payload?.advanced_card_payments
|
|
124
|
+
if (saved && typeof saved === "object") setForm(mergeWithDefaults(saved))
|
|
125
|
+
setToast({ type: "success", message: "Settings saved" })
|
|
126
|
+
window.setTimeout(() => setToast(null), 2500)
|
|
127
|
+
} catch (e: unknown) {
|
|
128
|
+
setToast({ type: "error", message: (e instanceof Error ? e.message : "") || "Failed to save settings." })
|
|
129
|
+
window.setTimeout(() => setToast(null), 3500)
|
|
130
|
+
} finally {
|
|
131
|
+
setSaving(false)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="p-6">
|
|
137
|
+
<div className="flex flex-col gap-6">
|
|
138
|
+
<div className="flex items-start justify-between gap-4">
|
|
139
|
+
<div><h1 className="text-xl font-semibold text-ui-fg-base">PayPal Gateway By Easy Payment</h1></div>
|
|
140
|
+
</div>
|
|
141
|
+
<PayPalTabs />
|
|
142
|
+
{toast ? (
|
|
143
|
+
<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">
|
|
144
|
+
<span className={toast.type === "success" ? "text-ui-fg-base" : "text-ui-fg-error"}>{toast.message}</span>
|
|
145
|
+
</div>
|
|
146
|
+
) : null}
|
|
147
|
+
<SectionCard
|
|
148
|
+
title="Advanced Card Payments"
|
|
149
|
+
description="Control card checkout settings and 3D Secure behavior."
|
|
150
|
+
right={(
|
|
151
|
+
<div className="flex items-center gap-3">
|
|
152
|
+
<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">
|
|
153
|
+
{saving ? "Saving..." : "Save settings"}
|
|
154
|
+
</button>
|
|
155
|
+
{loading ? <span className="text-sm text-ui-fg-subtle">Loading...</span> : null}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
<div className="divide-y divide-ui-border-base">
|
|
160
|
+
<FieldRow label="Enable/Disable">
|
|
161
|
+
<label className="inline-flex items-center gap-2">
|
|
162
|
+
<input type="checkbox" checked={form.enabled} onChange={(e) => setForm((p) => ({ ...p, enabled: e.target.checked }))} className="h-4 w-4 rounded border-ui-border-base" />
|
|
163
|
+
<span className="text-sm text-ui-fg-base">Enable Advanced Credit/Debit Card</span>
|
|
164
|
+
</label>
|
|
165
|
+
</FieldRow>
|
|
166
|
+
<FieldRow label="Title">
|
|
167
|
+
<input value={form.title} onChange={(e) => setForm((p) => ({ ...p, title: 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="Credit or Debit Card" />
|
|
168
|
+
</FieldRow>
|
|
169
|
+
<FieldRow label="Contingency for 3D Secure" hint="Choose when 3D Secure should be triggered during card payments.">
|
|
170
|
+
<div className="flex flex-col gap-2">
|
|
171
|
+
<select value={form.threeDS} onChange={(e) => setForm((p) => ({ ...p, threeDS: e.target.value as ThreeDSContingency }))} 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">
|
|
172
|
+
{THREE_DS_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
|
173
|
+
</select>
|
|
174
|
+
{THREE_DS_OPTIONS.find((o) => o.value === form.threeDS)?.hint
|
|
175
|
+
? <div className="text-xs text-ui-fg-subtle">{THREE_DS_OPTIONS.find((o) => o.value === form.threeDS)?.hint}</div>
|
|
176
|
+
: null}
|
|
177
|
+
</div>
|
|
178
|
+
</FieldRow>
|
|
179
|
+
</div>
|
|
180
|
+
</SectionCard>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
184
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Navigate } from "react-router-dom"
|
|
2
|
-
|
|
3
|
-
export default function PayPalApplePayPage() {
|
|
4
|
-
return <Navigate to="/settings/paypal/connection" replace />
|
|
5
|
-
}
|
|
1
|
+
import { Navigate } from "react-router-dom"
|
|
2
|
+
|
|
3
|
+
export default function PayPalApplePayPage() {
|
|
4
|
+
return <Navigate to="/settings/paypal/connection" replace />
|
|
5
|
+
}
|