Skip to content

Commit 0fea0fd

Browse files
authored
Merge pull request #388 from infimum90/feat--fix-issues-76-215-226-216
2 parents 1f03cb2 + 2fe21bb commit 0fea0fd

14 files changed

Lines changed: 656 additions & 256 deletions

File tree

app/(admin)/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use client";
22

33
import { useState } from 'react';
4-
import { AdminSidebar, adminNavItems } from '@/components/layout';
4+
import { AdminSidebar } from '@/components/layout';
5+
import { adminNavItems } from '@/lib/navigation/adminNav';
56
import { PageTransition } from '@/components/shared';
67
import { MobileNavDrawer } from '@/components/layout';
78
import { Topbar } from '@/components/layout';

app/(merchant)/developers/page.tsx

Lines changed: 3 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui';
66
import { Input } from '@/components/ui';
77
import { Skeleton } from '@/components/ui';
88
import { NetworkTooltip } from '@/components/ui';
9-
import { Copy, Eye, EyeOff, Plus, RefreshCcw, Key, Globe, BookOpen, Zap, CheckCircle2, AlertCircle, Code2, Terminal, Trash2 } from 'lucide-react';
9+
import { Copy, Eye, EyeOff, Plus, Key, Globe, BookOpen, Code2, Terminal, Trash2 } from 'lucide-react';
1010
import { useNotify } from '@/lib/hooks/useNotify';
1111
import {
1212
Select,
@@ -19,6 +19,7 @@ import { useOfflineStore } from '@/lib/store/offlineStore';
1919
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui';
2020
import { Label } from '@/components/ui';
2121
import { RateLimitDisplay } from '@/components/developers/RateLimitDisplay';
22+
import { WebhookTester } from '@/components/developers/WebhookTester';
2223

2324
const CodeExample = dynamic(() => import('@/components/developers/CodeExample').then(m => ({ default: m.CodeExample })), {
2425
loading: () => <Skeleton className="h-64 rounded-xl" />,
@@ -29,61 +30,9 @@ const initialKeys = [
2930
{ id: 'key_02', name: 'Sandbox Key', prefix: 'bp_test_', suffix: '...c2d8', created: '2026-01-05', lastUsed: '5 days ago', type: 'test', scope: 'read_write' },
3031
];
3132

32-
const EVENT_TYPES = [
33-
{ value: 'payment.received', label: 'payment.received' },
34-
{ value: 'settlement.completed', label: 'settlement.completed' },
35-
{ value: 'payment.failed', label: 'payment.failed' },
36-
];
37-
38-
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
39-
40-
const SAMPLE_PAYLOADS: Record<string, JsonValue> = {
41-
'payment.received': {
42-
"id": "evt_123456",
43-
"type": "payment.received",
44-
"data": {
45-
"payment_id": "pay_987654",
46-
"amount": 5000,
47-
"currency": "USDC",
48-
"status": "completed",
49-
"customer": {
50-
"email": "customer@example.com"
51-
}
52-
},
53-
"created_at": "2026-07-23T12:00:00Z"
54-
},
55-
'settlement.completed': {
56-
"id": "evt_234567",
57-
"type": "settlement.completed",
58-
"data": {
59-
"settlement_id": "set_112233",
60-
"amount": 4900,
61-
"currency": "USDC",
62-
"fee": 100,
63-
"status": "processed"
64-
},
65-
"created_at": "2026-07-23T13:00:00Z"
66-
},
67-
'payment.failed': {
68-
"id": "evt_345678",
69-
"type": "payment.failed",
70-
"data": {
71-
"payment_id": "pay_failed_111",
72-
"amount": 5000,
73-
"currency": "USDC",
74-
"status": "failed",
75-
"reason": "insufficient_funds"
76-
},
77-
"created_at": "2026-07-23T14:00:00Z"
78-
}
79-
};
80-
8133
export default function DevelopersPage() {
8234
const [keys, setKeys] = useState(initialKeys);
8335
const [showKey, setShowKey] = useState<string | null>(null);
84-
const [selectedEvent, setSelectedEvent] = useState<string>('payment.received');
85-
const [isSimulating, setIsSimulating] = useState(false);
86-
const [simulationResult, setSimulationResult] = useState<{ status: number; message: string } | null>(null);
8736
const isOnline = useOfflineStore((s) => s.isOnline);
8837
const notify = useNotify();
8938

@@ -123,20 +72,6 @@ export default function DevelopersPage() {
12372
notify.info('API key revoked');
12473
};
12574

126-
const handleSendTest = () => {
127-
setIsSimulating(true);
128-
setSimulationResult(null);
129-
130-
setTimeout(() => {
131-
setIsSimulating(false);
132-
setSimulationResult({
133-
status: 200,
134-
message: 'Webhook delivered successfully with signature verification pass'
135-
});
136-
notify.success('Test webhook delivered');
137-
}, 1200);
138-
};
139-
14075
return (
14176
<div className="space-y-8 pb-8">
14277
<div>
@@ -269,87 +204,7 @@ export default function DevelopersPage() {
269204
</CardContent>
270205
</Card>
271206

272-
{/* Test Webhook Simulator Section */}
273-
<Card className="border border-border bg-card shadow-sm">
274-
<CardHeader>
275-
<CardTitle className="text-base font-semibold text-foreground flex items-center gap-2">
276-
<Zap className="w-4 h-4 text-primary" /> Test Webhook Simulator
277-
</CardTitle>
278-
<CardDescription>
279-
Simulate webhook events to verify signature verification and payload handlers.
280-
</CardDescription>
281-
</CardHeader>
282-
<CardContent className="space-y-6">
283-
<div className="flex flex-col sm:flex-row gap-4 items-end">
284-
<div className="flex-1 space-y-2">
285-
<label className="text-xs font-semibold text-foreground">Event Type</label>
286-
<Select value={selectedEvent} onValueChange={(value) => value && setSelectedEvent(value)}>
287-
<SelectTrigger className="w-full h-10 border-border rounded-xl bg-muted">
288-
<SelectValue placeholder="Select event type" />
289-
</SelectTrigger>
290-
<SelectContent>
291-
{EVENT_TYPES.map((type) => (
292-
<SelectItem key={type.value} value={type.value}>
293-
{type.label}
294-
</SelectItem>
295-
))}
296-
</SelectContent>
297-
</Select>
298-
</div>
299-
<Button
300-
onClick={handleSendTest}
301-
disabled={isSimulating}
302-
className="bg-foreground hover:bg-foreground/90 text-background rounded-xl h-10 px-6 text-sm font-semibold min-w-[140px]"
303-
>
304-
{isSimulating ? (
305-
<>
306-
<RefreshCcw className="w-3.5 h-3.5 mr-2 animate-spin" />
307-
Sending...
308-
</>
309-
) : (
310-
'Send Test'
311-
)}
312-
</Button>
313-
</div>
314-
315-
<div className="space-y-2">
316-
<div className="flex items-center justify-between">
317-
<label className="text-xs font-semibold text-foreground">Simulated Payload</label>
318-
<Button
319-
variant="ghost"
320-
size="sm"
321-
className="min-h-[44px] text-xs text-muted-foreground hover:text-foreground"
322-
onClick={() => handleCopy(JSON.stringify(SAMPLE_PAYLOADS[selectedEvent], null, 2))}
323-
>
324-
<Copy className="w-3 h-3 mr-1" /> Copy JSON
325-
</Button>
326-
</div>
327-
<div className="bg-foreground rounded-xl p-4 overflow-x-auto border border-border">
328-
<pre className="text-xs text-emerald-400 font-mono leading-relaxed">
329-
{JSON.stringify(SAMPLE_PAYLOADS[selectedEvent], null, 2)}
330-
</pre>
331-
</div>
332-
</div>
333-
334-
{simulationResult && (
335-
<div className={`p-4 rounded-xl border flex items-start gap-3 animate-in fade-in slide-in-from-top-2 duration-300 ${
336-
simulationResult.status === 200
337-
? 'bg-success/10 border-success/20 text-success'
338-
: 'bg-destructive/10 border-destructive/20 text-destructive'
339-
}`}>
340-
{simulationResult.status === 200 ? (
341-
<CheckCircle2 className="w-5 h-5 text-success shrink-0 mt-0.5" />
342-
) : (
343-
<AlertCircle className="w-5 h-5 text-destructive shrink-0 mt-0.5" />
344-
)}
345-
<div>
346-
<p className="text-sm font-semibold">Response Status: {simulationResult.status} OK</p>
347-
<p className="text-xs opacity-80">{simulationResult.message}</p>
348-
</div>
349-
</div>
350-
)}
351-
</CardContent>
352-
</Card>
207+
<WebhookTester />
353208

354209
{/* New API Key Dialog */}
355210
<Dialog open={isCreateKeyOpen} onOpenChange={setIsCreateKeyOpen}>

app/(merchant)/layout.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
import { useCallback, useState, useEffect } from "react";
44
import { useRouter } from "next/navigation";
55
import { AlertTriangle } from "lucide-react";
6-
import { MerchantSidebar, merchantNavItems } from "@/components/layout";
6+
import { MerchantSidebar } from "@/components/layout";
7+
import { merchantNavItems } from "@/lib/navigation/merchantNav";
78
import { PageTransition } from "@/components/shared";
89
import { MobileNavDrawer } from "@/components/layout";
910
import { Topbar } from "@/components/layout";
1011
import Footer from "@/components/layout";
1112
import { MobileBottomNav } from "@/components/layout";
1213
import { OnboardingWizard } from "@/components/onboarding/OnboardingWizard";
1314
import { useWalletStore } from "@/lib/store/walletStore";
15+
import { useAuthStore } from "@/lib/store/authStore";
16+
import { useSessionTimeout } from "@/lib/hooks/useSessionTimeout";
17+
import { SessionTimeoutModal } from "@/components/SessionTimeoutModal";
1418

1519
export default function MerchantLayout({
1620
children,
@@ -21,11 +25,32 @@ export default function MerchantLayout({
2125
const router = useRouter();
2226
const network = useWalletStore((s) => s.network);
2327
const isTestnet = network === 'testnet';
28+
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
29+
const logout = useAuthStore((s) => s.logout);
2430

2531
const closeMobileMenu = useCallback(() => {
2632
setMobileMenuOpen(false);
2733
}, []);
2834

35+
const handleTimeoutLogout = useCallback(() => {
36+
logout();
37+
router.push('/auth/login');
38+
}, [logout, router]);
39+
40+
const { showWarning, secondsRemaining, dismissWarning } = useSessionTimeout({
41+
onTimeout: handleTimeoutLogout,
42+
});
43+
44+
const handleExtend = useCallback(async () => {
45+
try {
46+
await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' });
47+
dismissWarning();
48+
} catch {
49+
logout();
50+
router.push('/auth/login');
51+
}
52+
}, [logout, router, dismissWarning]);
53+
2954
useEffect(() => {
3055
router.prefetch("/transactions");
3156
router.prefetch("/payments");
@@ -65,6 +90,15 @@ export default function MerchantLayout({
6590

6691
<Footer />
6792
</div>
93+
94+
{isAuthenticated && (
95+
<SessionTimeoutModal
96+
open={showWarning}
97+
secondsRemaining={secondsRemaining}
98+
onExtend={handleExtend}
99+
onLogout={handleTimeoutLogout}
100+
/>
101+
)}
68102
</div>
69103
);
70104
}

components/SessionTimeoutModal.tsx

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,72 @@ import {
99
DialogTitle,
1010
} from '@/components/ui/dialog';
1111
import { Button } from '@/components/ui';
12+
import { AlertTriangle, Clock, LogOut, RefreshCcw } from 'lucide-react';
1213

1314
interface SessionTimeoutModalProps {
1415
open: boolean;
1516
secondsRemaining: number;
16-
onDismiss: () => void;
17+
onExtend: () => void;
18+
onLogout: () => void;
19+
}
20+
21+
function formatCountdown(seconds: number): string {
22+
const m = Math.floor(seconds / 60);
23+
const s = seconds % 60;
24+
return `${m}:${s.toString().padStart(2, '0')}`;
1725
}
1826

1927
export function SessionTimeoutModal({
2028
open,
2129
secondsRemaining,
22-
onDismiss,
30+
onExtend,
31+
onLogout,
2332
}: SessionTimeoutModalProps) {
2433
return (
25-
<Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) onDismiss(); }}>
26-
<DialogContent className="sm:max-w-md">
34+
<Dialog open={open} onOpenChange={() => {}}>
35+
<DialogContent className="sm:max-w-md" showCloseButton={false}>
2736
<DialogHeader>
28-
<DialogTitle>Session Expiring</DialogTitle>
29-
<DialogDescription>
30-
Your session will expire due to inactivity in{' '}
31-
<span className="font-semibold text-foreground">
32-
{secondsRemaining} second{secondsRemaining !== 1 ? 's' : ''}
33-
</span>
34-
.
35-
</DialogDescription>
37+
<div className="flex items-center gap-3 mb-2">
38+
<div className="w-10 h-10 rounded-full bg-warning/20 flex items-center justify-center">
39+
<AlertTriangle className="w-5 h-5 text-warning" />
40+
</div>
41+
<div>
42+
<DialogTitle>Session Expiring</DialogTitle>
43+
<DialogDescription>
44+
Your session will expire due to inactivity.
45+
</DialogDescription>
46+
</div>
47+
</div>
3648
</DialogHeader>
37-
<DialogFooter>
38-
<Button onClick={onDismiss}>Stay Logged In</Button>
49+
50+
<div className="flex flex-col items-center py-4">
51+
<div className="relative w-24 h-24 flex items-center justify-center">
52+
<Clock className="w-12 h-12 text-warning/60 absolute" />
53+
<span className="text-2xl font-bold text-foreground z-10">
54+
{formatCountdown(secondsRemaining)}
55+
</span>
56+
</div>
57+
<p className="text-sm text-muted-foreground mt-2">
58+
Time remaining before automatic logout
59+
</p>
60+
</div>
61+
62+
<DialogFooter className="flex-col sm:flex-row gap-2">
63+
<Button
64+
variant="outline"
65+
onClick={onLogout}
66+
className="w-full sm:w-auto rounded-xl border-destructive/30 text-destructive hover:bg-destructive/10"
67+
>
68+
<LogOut className="w-4 h-4 mr-2" />
69+
Logout
70+
</Button>
71+
<Button
72+
onClick={onExtend}
73+
className="w-full sm:w-auto rounded-xl bg-primary hover:bg-primary/90 text-primary-foreground"
74+
>
75+
<RefreshCcw className="w-4 h-4 mr-2" />
76+
Extend Session
77+
</Button>
3978
</DialogFooter>
4079
</DialogContent>
4180
</Dialog>

0 commit comments

Comments
 (0)