nxtgauge-admin-solid/src/routes/admin/payment-gateway.tsx
Tracewebstudio Dev 69079caa1d
Some checks failed
build-and-release / build (push) Failing after 0s
feat: admin invoice/credit/pricing UI overhaul with auth token injection
- Inject Bearer token from sessionStorage into all API requests
- Add typed API helpers for invoices, credits, discounts, ledger, orders, pricing, tax
- Overhaul invoice, credit, pricing, tax admin routes with full CRUD UI
- Remove committed log files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:51:08 +02:00

252 lines
9.9 KiB
TypeScript

import { Show, createSignal, onMount } from "solid-js";
type PaymentGatewayConfig = {
provider: string;
mode: 'sandbox' | 'live';
enabled: boolean;
baseUrl: string;
callbackUrl: string;
webhookUrl: string;
merchantId: string;
apiKey: string;
secretKey: string;
clientId: string;
clientSecret: string;
};
const DEFAULT_CONFIG: PaymentGatewayConfig = {
provider: 'PayU',
mode: 'sandbox',
enabled: true,
baseUrl: 'https://test.payu.in',
callbackUrl: '',
webhookUrl: '',
merchantId: '',
apiKey: '',
secretKey: '',
clientId: '',
clientSecret: '',
};
const READ_ENDPOINTS = [
'/api/admin/payment-gateway-config',
];
const WRITE_ENDPOINTS = [
'/api/admin/payment-gateway-config',
];
function authHeaders() {
const token = typeof sessionStorage !== 'undefined'
? (sessionStorage.getItem('nxtgauge_admin_access_token') || '')
: '';
return {
'Content-Type': 'application/json',
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
function normalizePayload(payload: any): PaymentGatewayConfig {
const src = payload?.config || payload?.data || payload || {};
return {
provider: String(src.provider || DEFAULT_CONFIG.provider),
mode: String(src.mode || DEFAULT_CONFIG.mode).toLowerCase() === 'live' ? 'live' : 'sandbox',
enabled: src.enabled !== false,
baseUrl: String(src.baseUrl || src.base_url || ''),
callbackUrl: String(src.callbackUrl || src.callback_url || ''),
webhookUrl: String(src.webhookUrl || src.webhook_url || ''),
merchantId: String(src.merchantId || src.merchant_id || ''),
apiKey: String(src.apiKey || src.api_key || ''),
secretKey: String(src.secretKey || src.secret_key || ''),
clientId: String(src.clientId || src.client_id || ''),
clientSecret: String(src.clientSecret || src.client_secret || ''),
};
}
export default function PaymentGatewayManagementPage() {
const [loading, setLoading] = createSignal(false);
const [saving, setSaving] = createSignal(false);
const [error, setError] = createSignal('');
const [success, setSuccess] = createSignal('');
const [showSecret, setShowSecret] = createSignal(false);
const [showClientSecret, setShowClientSecret] = createSignal(false);
const [cfg, setCfg] = createSignal<PaymentGatewayConfig>(DEFAULT_CONFIG);
const load = async () => {
setLoading(true);
setError('');
try {
let loaded = false;
for (const endpoint of READ_ENDPOINTS) {
const res = await fetch(endpoint, { method: 'GET', headers: authHeaders(), credentials: 'include' }).catch(() => null);
if (!res || !res.ok) continue;
const payload = await res.json().catch(() => ({}));
setCfg(normalizePayload(payload));
loaded = true;
break;
}
if (!loaded) setCfg(DEFAULT_CONFIG);
} catch (e: any) {
setError(e?.message || 'Failed to load payment gateway configuration.');
} finally {
setLoading(false);
}
};
onMount(() => void load());
const setField = <K extends keyof PaymentGatewayConfig>(key: K, value: PaymentGatewayConfig[K]) => {
setCfg((prev) => ({ ...prev, [key]: value }));
};
const save = async (e: Event) => {
e.preventDefault();
setSaving(true);
setError('');
setSuccess('');
try {
const payload = {
provider: cfg().provider.trim(),
mode: cfg().mode,
enabled: cfg().enabled,
base_url: cfg().baseUrl.trim(),
callback_url: cfg().callbackUrl.trim(),
webhook_url: cfg().webhookUrl.trim(),
merchant_id: cfg().merchantId.trim(),
api_key: cfg().apiKey.trim(),
secret_key: cfg().secretKey.trim(),
client_id: cfg().clientId.trim(),
client_secret: cfg().clientSecret.trim(),
};
let saved = false;
for (const endpoint of WRITE_ENDPOINTS) {
const methods: Array<'PUT' | 'PATCH' | 'POST'> = ['PUT', 'PATCH', 'POST'];
for (const method of methods) {
const res = await fetch(endpoint, {
method,
headers: authHeaders(),
credentials: 'include',
body: JSON.stringify(payload),
}).catch(() => null);
if (!res || !res.ok) continue;
saved = true;
break;
}
if (saved) break;
}
if (!saved) throw new Error('Could not save configuration. Please verify backend endpoint wiring.');
setSuccess('Payment gateway configuration saved successfully.');
await load();
} catch (e: any) {
setError(e?.message || 'Failed to save payment gateway configuration.');
} finally {
setSaving(false);
}
};
const inputCls = 'w-full rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13]';
const labelCls = 'mb-1.5 block text-sm font-medium text-gray-700';
return (
<div class="w-full space-y-6 pb-8">
<div style="margin-bottom:1.5rem">
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Payment Gateway Management</h1>
<p class="mt-1 text-[14px] text-[#6B7280]">Manage PayU credentials, mode, and callback URLs for platform payments.</p>
</div>
<Show when={error()}>
<div class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<Show when={success()}>
<div class="rounded-lg border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700">{success()}</div>
</Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm p-6">
<Show when={loading()}>
<p class="text-sm text-gray-500">Loading configuration...</p>
</Show>
<form onSubmit={save} class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label class={labelCls}>Provider</label>
<input class={inputCls} value={cfg().provider} onInput={(e) => setField('provider', e.currentTarget.value)} placeholder="PayU" />
</div>
<div>
<label class={labelCls}>Mode</label>
<select class={inputCls} value={cfg().mode} onChange={(e) => setField('mode', e.currentTarget.value === 'live' ? 'live' : 'sandbox')}>
<option value="sandbox">Sandbox (Test)</option>
<option value="live">Live (Production)</option>
</select>
</div>
<div>
<label class={labelCls}>Base API URL</label>
<input class={inputCls} value={cfg().baseUrl} onInput={(e) => setField('baseUrl', e.currentTarget.value)} placeholder="https://test.payu.in" />
</div>
<div>
<label class={labelCls}>Merchant Key</label>
<input class={inputCls} value={cfg().apiKey} onInput={(e) => setField('apiKey', e.currentTarget.value)} placeholder="PayU merchant key" />
</div>
<div>
<label class={labelCls}>Merchant Salt</label>
<div class="flex gap-2">
<input
type={showSecret() ? 'text' : 'password'}
class={inputCls}
value={cfg().secretKey}
onInput={(e) => setField('secretKey', e.currentTarget.value)}
placeholder="PayU merchant salt"
/>
<button type="button" class="rounded-lg border border-gray-200 px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50" onClick={() => setShowSecret((v) => !v)}>
{showSecret() ? 'Hide' : 'Show'}
</button>
</div>
</div>
<div>
<label class={labelCls}>Client ID (optional)</label>
<input class={inputCls} value={cfg().clientId} onInput={(e) => setField('clientId', e.currentTarget.value)} placeholder="PayU OAuth client id" />
</div>
<div>
<label class={labelCls}>Client Secret (optional)</label>
<div class="flex gap-2">
<input
type={showClientSecret() ? 'text' : 'password'}
class={inputCls}
value={cfg().clientSecret}
onInput={(e) => setField('clientSecret', e.currentTarget.value)}
placeholder="PayU OAuth client secret"
/>
<button type="button" class="rounded-lg border border-gray-200 px-3 py-2 text-xs font-semibold text-gray-700 hover:bg-gray-50" onClick={() => setShowClientSecret((v) => !v)}>
{showClientSecret() ? 'Hide' : 'Show'}
</button>
</div>
</div>
<div>
<label class={labelCls}>Callback URL (surl/furl)</label>
<input class={inputCls} value={cfg().callbackUrl} onInput={(e) => setField('callbackUrl', e.currentTarget.value)} placeholder="https://yourapp.com/api/payments/payu-return" />
</div>
<div>
<label class={labelCls}>Webhook URL</label>
<input class={inputCls} value={cfg().webhookUrl} onInput={(e) => setField('webhookUrl', e.currentTarget.value)} placeholder="https://yourapp.com/api/payments/payu/webhook" />
</div>
<div class="sm:col-span-2">
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" checked={cfg().enabled} onChange={(e) => setField('enabled', e.currentTarget.checked)} />
Enable payment gateway
</label>
</div>
<div class="sm:col-span-2 flex items-center justify-end gap-2 border-t border-gray-100 pt-4">
<button type="button" class="rounded-lg border border-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50" onClick={() => void load()}>
Reload
</button>
<button type="submit" class="rounded-lg bg-[#0D0D2A] px-4 py-2 text-sm font-semibold text-white hover:bg-[#17173f]" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Configuration'}
</button>
</div>
</form>
</section>
</div>
);
}