Files
2026-06-09 02:02:40 +05:30

164 lines
4.9 KiB
TypeScript

'use client';
import { useState } from 'react';
interface Props {
token: string;
defaultScopes: string[];
defaultRetentionDays: number;
}
type Step = 'phone' | 'code' | 'done';
export function OnboardingForm({ token, defaultScopes, defaultRetentionDays }: Props) {
const [step, setStep] = useState<Step>('phone');
const [phone, setPhone] = useState('');
const [challengeId, setChallengeId] = useState<string | null>(null);
const [code, setCode] = useState('');
const [retentionDays, setRetentionDays] = useState(defaultRetentionDays);
const [scopes, setScopes] = useState<string[]>(defaultScopes);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
async function requestOtp() {
setError(null);
setBusy(true);
try {
const res = await fetch(`${process.env['NEXT_PUBLIC_API_URL'] ?? 'http://localhost:3001'}/public/auth/request-otp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ onboardingToken: token, phone }),
});
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { message?: string };
setError(body.message ?? 'Failed to send code');
return;
}
const data = (await res.json()) as { challengeId: string };
setChallengeId(data.challengeId);
setStep('code');
} finally {
setBusy(false);
}
}
async function verifyOtp() {
setError(null);
setBusy(true);
try {
const res = await fetch('/api/onboard/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
onboardingToken: token,
challengeId,
phone,
code,
scopes,
retentionDays,
}),
});
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { message?: string };
setError(body.message ?? 'Verification failed');
return;
}
setStep('done');
} finally {
setBusy(false);
}
}
if (step === 'done') {
return (
<div>
<p className="text-sm text-green-700 mb-4">You&apos;re verified. Your session is set.</p>
<a href="/my" className="inline-block px-4 py-2 bg-blue-600 text-white rounded text-sm">
Go to your portal
</a>
</div>
);
}
if (step === 'phone') {
return (
<div className="space-y-3">
<label className="block text-sm">
<span className="font-medium">Your WhatsApp phone number</span>
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+1 234 567 8900"
className="mt-1 w-full border rounded px-3 py-2"
/>
</label>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="button"
onClick={requestOtp}
disabled={busy || phone.length < 6}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
Send verification code
</button>
<p className="text-xs text-gray-500">
We&apos;ll DM a 6-digit code to your WhatsApp.
</p>
</div>
);
}
return (
<div className="space-y-3">
<label className="block text-sm">
<span className="font-medium">Verification code</span>
<input
type="text"
inputMode="numeric"
pattern="[0-9]{6}"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="123456"
className="mt-1 w-full border rounded px-3 py-2"
/>
</label>
<fieldset className="text-sm">
<legend className="font-medium mb-1">Scopes</legend>
{['INGEST', 'ARCHIVE', 'REPLICATE', 'DISPLAY'].map((s) => (
<label key={s} className="block">
<input
type="checkbox"
checked={scopes.includes(s)}
onChange={(e) =>
setScopes((prev) => (e.target.checked ? [...prev, s] : prev.filter((x) => x !== s)))
}
/>{' '}
{s}
</label>
))}
</fieldset>
<label className="block text-sm">
<span className="font-medium">Retention (days)</span>
<input
type="number"
min={1}
max={3650}
value={retentionDays}
onChange={(e) => setRetentionDays(Number(e.target.value))}
className="mt-1 w-32 border rounded px-3 py-2"
/>
</label>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="button"
onClick={verifyOtp}
disabled={busy || code.length < 6}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
Verify and join
</button>
</div>
);
}