Files
loyaly-merchant/src/features/settings/ApiWebhooksManager.tsx
2026-08-05 18:34:37 +05:30

381 lines
9.7 KiB
TypeScript

'use client';
import {useState} from 'react';
import {proportional, pixel} from '@astryxdesign/core/Table';
import {ResponsiveTable} from '@/components/patterns/ResponsiveTable';
import type {TableColumn} from '@astryxdesign/core/Table';
import {VStack, HStack} from '@astryxdesign/core/Layout';
import {Text} from '@astryxdesign/core/Text';
import {Badge} from '@astryxdesign/core/Badge';
import {Button} from '@astryxdesign/core/Button';
import {TextInput} from '@astryxdesign/core/TextInput';
import {useToast} from '@astryxdesign/core/Toast';
import {StaticPanel} from '@/components/patterns/PanelCard';
export interface ApiKeyItem extends Record<string, unknown> {
id: string;
name: string;
keyPrefix: string;
created: string;
lastUsed: string;
status: 'active' | 'revoked';
}
export interface WebhookEndpointItem extends Record<string, unknown> {
id: string;
url: string;
events: string;
secret: string;
status: 'healthy' | 'failing';
}
export interface WebhookLogItem extends Record<string, unknown> {
id: string;
event: string;
url: string;
code: number;
latency: string;
timestamp: string;
}
const INITIAL_KEYS: ApiKeyItem[] = [
{
id: 'key-1',
name: 'Production POS Key',
keyPrefix: 'lmer_live_9f8a••••••••4b12',
created: '10 Jan 2026',
lastUsed: 'Just now',
status: 'active',
},
{
id: 'key-2',
name: 'Staging Integration Key',
keyPrefix: 'lmer_test_3d2c••••••••8e91',
created: '15 Jun 2026',
lastUsed: 'Yesterday',
status: 'active',
},
];
const INITIAL_WEBHOOKS: WebhookEndpointItem[] = [
{
id: 'wh-1',
url: 'https://api.nearle.in/webhooks/loyaly',
events: 'order.created, lyt.redeemed',
secret: 'whsec_8841••••••••9932',
status: 'healthy',
},
{
id: 'wh-2',
url: 'https://hooks.zapier.com/hooks/catch/19284',
events: 'staff.checkin, store.updated',
secret: 'whsec_7712••••••••1102',
status: 'healthy',
},
];
const WEBHOOK_LOGS: WebhookLogItem[] = [
{
id: 'log-101',
event: 'lyt.redeemed',
url: 'https://api.nearle.in/webhooks/loyaly',
code: 200,
latency: '142ms',
timestamp: 'Today, 15:42:01',
},
{
id: 'log-102',
event: 'order.created',
url: 'https://api.nearle.in/webhooks/loyaly',
code: 200,
latency: '185ms',
timestamp: 'Today, 15:38:12',
},
{
id: 'log-103',
event: 'staff.checkin',
url: 'https://hooks.zapier.com/hooks/catch/19284',
code: 200,
latency: '95ms',
timestamp: 'Today, 09:01:44',
},
];
export function ApiWebhooksManager() {
const toast = useToast();
const [keys, setKeys] = useState<ApiKeyItem[]>(INITIAL_KEYS);
const [webhooks, setWebhooks] = useState<WebhookEndpointItem[]>(INITIAL_WEBHOOKS);
const [newKeyName, setNewKeyName] = useState('');
const [isGenerating, setIsGenerating] = useState(false);
const [newWebhookUrl, setNewWebhookUrl] = useState('');
const handleGenerateKey = () => {
if (!newKeyName.trim()) {
toast({type: 'error', body: 'Key name is required'});
return;
}
const createdKey: ApiKeyItem = {
id: `key-${Date.now()}`,
name: newKeyName,
keyPrefix: `lmer_live_${Math.random().toString(36).substring(2, 6)}••••••••${Math.random().toString(36).substring(2, 6)}`,
created: 'Just now',
lastUsed: 'Never',
status: 'active',
};
setKeys((prev) => [createdKey, ...prev]);
setNewKeyName('');
setIsGenerating(false);
toast({body: `API Key "${newKeyName}" generated successfully`});
};
const handleRevokeKey = (id: string, name: string) => {
setKeys((prev) => prev.filter((k) => k.id !== id));
toast({body: `Revoked API Key "${name}"`});
};
const handleAddWebhook = () => {
if (!newWebhookUrl.trim()) {
toast({type: 'error', body: 'Webhook URL is required'});
return;
}
const newWh: WebhookEndpointItem = {
id: `wh-${Date.now()}`,
url: newWebhookUrl,
events: 'order.created, lyt.redeemed, staff.checkin',
secret: `whsec_${Math.random().toString(36).substring(2, 8)}••••`,
status: 'healthy',
};
setWebhooks((prev) => [newWh, ...prev]);
setNewWebhookUrl('');
toast({body: 'Webhook endpoint registered successfully'});
};
const keyColumns: TableColumn<ApiKeyItem>[] = [
{
key: 'name',
header: 'Key Name',
width: proportional(1.5),
renderCell: (row) => (
<VStack gap={0}>
<Text size="sm" weight="medium">
{row.name}
</Text>
<Text size="sm" color="secondary">
{row.keyPrefix}
</Text>
</VStack>
),
},
{
key: 'created',
header: 'Created Date',
width: proportional(1.2),
renderCell: (row) => <Text size="sm">{row.created}</Text>,
},
{
key: 'lastUsed',
header: 'Last Active',
width: proportional(1.2),
renderCell: (row) => (
<Text size="sm" color="secondary">
{row.lastUsed}
</Text>
),
},
{
key: 'status',
header: 'Status',
width: pixel(100),
renderCell: (row) => (
<Badge
variant={row.status === 'active' ? 'success' : 'error'}
label={row.status}
/>
),
},
{
key: 'actions',
header: 'Action',
width: pixel(100),
renderCell: (row) => (
<Button
size="sm"
variant="ghost"
label="Revoke"
onClick={() => handleRevokeKey(row.id, row.name)}
/>
),
},
];
const webhookColumns: TableColumn<WebhookEndpointItem>[] = [
{
key: 'url',
header: 'Endpoint URL',
width: proportional(2),
renderCell: (row) => (
<VStack gap={0}>
<Text size="sm" weight="medium">
{row.url}
</Text>
<Text size="sm" color="secondary">
Events: {row.events}
</Text>
</VStack>
),
},
{
key: 'secret',
header: 'Signing Secret',
width: proportional(1.2),
renderCell: (row) => (
<Text size="sm" color="secondary">
{row.secret}
</Text>
),
},
{
key: 'status',
header: 'Health',
width: pixel(100),
renderCell: (row) => (
<Badge
variant={row.status === 'healthy' ? 'success' : 'error'}
label={row.status}
/>
),
},
];
const logColumns: TableColumn<WebhookLogItem>[] = [
{
key: 'event',
header: 'Event',
width: proportional(1.2),
renderCell: (row) => (
<Text size="sm" weight="medium">
{row.event}
</Text>
),
},
{
key: 'url',
header: 'Target URL',
width: proportional(2),
renderCell: (row) => (
<Text size="sm" color="secondary">
{row.url}
</Text>
),
},
{
key: 'code',
header: 'HTTP Code',
width: pixel(100),
renderCell: (row) => (
<Badge
variant={row.code === 200 ? 'success' : 'error'}
label={`${row.code} OK`}
/>
),
},
{
key: 'latency',
header: 'Latency',
width: pixel(90),
renderCell: (row) => <Text size="sm">{row.latency}</Text>,
},
{
key: 'timestamp',
header: 'Timestamp',
width: proportional(1.2),
renderCell: (row) => (
<Text size="sm" color="secondary">
{row.timestamp}
</Text>
),
},
];
return (
<VStack gap={5}>
<StaticPanel
title="Developer API Keys"
subtitle="Manage authentication tokens for custom POS clients and server SDKs."
actions={
<Button
variant="primary"
size="sm"
label={isGenerating ? 'Cancel' : 'Generate Secret Key'}
onClick={() => setIsGenerating(!isGenerating)}
/>
}
>
<VStack gap={4}>
{isGenerating ? (
<HStack gap={3} vAlign="end" wrap="wrap">
<TextInput
label="API Key Description"
value={newKeyName}
onChange={setNewKeyName}
placeholder="e.g. Indiranagar Counter 2 POS"
/>
<Button
variant="primary"
size="sm"
label="Generate Token"
onClick={handleGenerateKey}
/>
</HStack>
) : null}
<ResponsiveTable
columns={keyColumns}
data={keys}
primaryKey="name"
/>
</VStack>
</StaticPanel>
<StaticPanel
title="Webhook Subscriptions & Endpoints"
subtitle="Real-time HTTP POST callbacks for order, redemption and staff events."
>
<VStack gap={4}>
<HStack gap={3} vAlign="end" wrap="wrap">
<TextInput
label="Add Webhook Endpoint URL"
value={newWebhookUrl}
onChange={setNewWebhookUrl}
placeholder="https://yourdomain.com/webhooks/loyaly"
/>
<Button
variant="primary"
size="sm"
label="Add Endpoint"
onClick={handleAddWebhook}
/>
</HStack>
<ResponsiveTable
columns={webhookColumns}
data={webhooks}
primaryKey="url"
/>
</VStack>
</StaticPanel>
<StaticPanel
title="Webhook Delivery Logs"
subtitle="Recent HTTP delivery attempts, latency and status response codes."
>
<ResponsiveTable
columns={logColumns}
data={WEBHOOK_LOGS}
primaryKey="event"
/>
</StaticPanel>
</VStack>
);
}