@easypayment/medusa-paypal 0.5.9 → 0.6.1

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,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="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">
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
  }
@@ -492,8 +492,8 @@ export default function PayPalConnectionPage() {
492
492
  <PayPalTabs />
493
493
 
494
494
  {/* Main container */}
495
- <div className="rounded-md border border-ui-border-base p-4 shadow-sm">
496
- <div className="grid grid-cols-1 gap-y-6 md:grid-cols-[260px_1fr] md:items-start">
495
+ <div className="rounded-xl border border-ui-border-base bg-ui-bg-base shadow-sm">
496
+ <div className="grid grid-cols-1 gap-y-6 p-4 md:grid-cols-[260px_1fr] md:items-start">
497
497
  {/* Environment */}
498
498
  <div className="text-sm font-medium pt-2">Environment</div>
499
499
  <div className="max-w-xl">
@@ -544,7 +544,7 @@ export default function PayPalConnectionPage() {
544
544
  type="button"
545
545
  onClick={handleDisconnect}
546
546
  disabled={onboardingInProgress}
547
- className="rounded-md border border-ui-border-base px-3 py-2 text-sm font-medium transition-colors hover:bg-ui-bg-subtle focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ui-border-interactive disabled:opacity-50 disabled:cursor-not-allowed"
547
+ 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"
548
548
  >
549
549
  Disconnect
550
550
  </button>
@@ -581,7 +581,7 @@ export default function PayPalConnectionPage() {
581
581
  href={finalUrl || "#"}
582
582
  data-paypal-onboard-complete="onboardingCallback"
583
583
  onClick={handleConnectClick}
584
- className="inline-flex items-center justify-center rounded-md bg-ui-button-neutral px-4 py-2 text-sm font-medium text-ui-fg-on-color no-underline shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ui-border-interactive disabled:opacity-60"
584
+ className="transition-fg relative inline-flex w-fit items-center justify-center overflow-hidden rounded-md outline-none no-underline 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"
585
585
  style={{
586
586
  cursor: onboardingInProgress ? "not-allowed" : "pointer",
587
587
  opacity: onboardingInProgress ? 0.6 : 1,
@@ -701,7 +701,7 @@ export default function PayPalConnectionPage() {
701
701
  <div className="md:col-span-2 flex items-center gap-2 mt-2">
702
702
  <button
703
703
  type="button"
704
- className="rounded-md border border-ui-border-base px-3 py-2 text-sm font-medium hover:bg-ui-bg-subtle disabled:opacity-50 disabled:cursor-not-allowed"
704
+ 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"
705
705
  onClick={() => setShowManual(false)}
706
706
  disabled={onboardingInProgress}
707
707
  >
@@ -710,7 +710,7 @@ export default function PayPalConnectionPage() {
710
710
 
711
711
  <button
712
712
  type="button"
713
- className="rounded-md border border-ui-border-base px-3 py-2 text-sm font-medium bg-ui-bg-base hover:bg-ui-bg-subtle disabled:opacity-50 disabled:cursor-not-allowed"
713
+ 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"
714
714
  disabled={!canSaveManual || onboardingInProgress}
715
715
  onClick={handleSaveManual}
716
716
  >