2026-05-19 00:30:20 +02:00
|
|
|
'use client';
|
|
|
|
|
|
2026-05-21 00:36:56 +02:00
|
|
|
import { Suspense, useEffect, useState } from 'react';
|
2026-05-19 00:30:20 +02:00
|
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
|
|
|
import { Logo } from '@/components/logo';
|
|
|
|
|
import { apiFetch } from '@/lib/api';
|
|
|
|
|
|
2026-05-21 00:36:56 +02:00
|
|
|
function CallbackInner() {
|
2026-05-19 00:30:20 +02:00
|
|
|
const router = useRouter();
|
|
|
|
|
const params = useSearchParams();
|
|
|
|
|
const token = params.get('token');
|
|
|
|
|
const [state, setState] = useState<'verifying' | 'ok' | 'error'>('verifying');
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!token) {
|
|
|
|
|
setState('error');
|
|
|
|
|
setError('Missing token');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await apiFetch('/v1/auth/verify', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ token }),
|
|
|
|
|
});
|
|
|
|
|
setState('ok');
|
|
|
|
|
setTimeout(() => router.replace('/dashboard'), 200);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setState('error');
|
|
|
|
|
setError((err as Error).message);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}, [token, router]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex min-h-screen items-center justify-center px-6">
|
|
|
|
|
<div className="w-full max-w-sm text-center">
|
|
|
|
|
<Logo className="mx-auto mb-8" />
|
|
|
|
|
{state === 'verifying' && (
|
|
|
|
|
<p className="text-[13px] text-[--color-fg-muted]">Verifying your magic link…</p>
|
|
|
|
|
)}
|
|
|
|
|
{state === 'ok' && (
|
|
|
|
|
<p className="text-[13px] text-emerald-300">Signed in. Redirecting…</p>
|
|
|
|
|
)}
|
|
|
|
|
{state === 'error' && (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-[13px] text-[--color-danger]">Could not verify magic link.</p>
|
|
|
|
|
{error && <p className="mt-2 mono text-[11px] text-[--color-fg-subtle]">{error}</p>}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-21 00:36:56 +02:00
|
|
|
|
|
|
|
|
// useSearchParams() requires a Suspense boundary or `next build` cannot
|
|
|
|
|
// statically render this route.
|
|
|
|
|
export default function CallbackPage() {
|
|
|
|
|
return (
|
|
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
|
|
|
|
<div className="flex min-h-screen items-center justify-center px-6">
|
|
|
|
|
<p className="text-[13px] text-[--color-fg-muted]">Verifying your magic link…</p>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<CallbackInner />
|
|
|
|
|
</Suspense>
|
|
|
|
|
);
|
|
|
|
|
}
|