Files
edr-platform/apps/edr-passenger-web/portal/src/components/FaydaSetupWizard.tsx

257 lines
10 KiB
TypeScript

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Train, ShieldCheck, CheckCircle, Info, ArrowLeft, ArrowRight } from 'lucide-react';
import { iamAuthApi } from '@/lib/api/auth';
interface FaydaSetupWizardProps {
// Prefilled OTP when landing from the SMS link (/set-password?verificationCode=...)
initialOtp?: string;
}
type Outcome = 'success' | 'hasPassword' | null;
export default function FaydaSetupWizard({ initialOtp }: FaydaSetupWizardProps) {
const router = useRouter();
const [step, setStep] = useState<1 | 2>(initialOtp ? 2 : 1);
const [phone, setPhone] = useState('');
const [otp, setOtp] = useState(initialOtp || '');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [outcome, setOutcome] = useState<Outcome>(null);
const handleRequestCode = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await iamAuthApi.faydaRequestPasswordSetup(phone);
setStep(2);
} catch (err: any) {
setError(err.response?.data?.message || 'Failed to send the code. Please try again.');
} finally {
setLoading(false);
}
};
const handleSetPassword = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (newPassword.length < 6) {
setError('Password must be at least 6 characters.');
return;
}
if (newPassword !== confirmPassword) {
setError('Passwords do not match.');
return;
}
setLoading(true);
try {
const res = await iamAuthApi.faydaVerifyAndLogin({ phoneNumber: phone, otp });
const data = (res.data as any)?.data ?? res.data;
if (!data.requiresPassword) {
setOutcome('hasPassword');
return;
}
await iamAuthApi.setFaydaPassword(
{ userId: data.iamUserId, newPassword, confirmPassword },
data.token,
);
setOutcome('success');
setTimeout(() => router.push('/login'), 2500);
} catch (err: any) {
const msg = err.response?.data?.message || err.message || '';
setError(msg || 'Verification failed. The code may be wrong or expired.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-[rgb(20_113_76)] from-10% via-transparent to-[rgb(20_113_76)] to-90% dark:from-gray-900 dark:to-gray-800 flex items-center justify-center py-12 px-4">
<div className="max-w-md w-full">
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<div className="w-12 h-12 bg-[rgb(20_113_76)] rounded-lg flex items-center justify-center">
<Train className="w-6 h-6 text-white" />
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 flex items-center justify-center gap-2">
<ShieldCheck className="w-7 h-7 text-[rgb(20_113_76)] dark:text-emerald-400" />
Fayda account setup
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-2">
Already verified with Fayda? Set a password to access your account online.
</p>
</div>
<div className="card">
{outcome === 'success' ? (
<div className="space-y-4">
<div className="flex items-start gap-3 bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 text-emerald-700 dark:text-emerald-300 px-4 py-3 rounded">
<CheckCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<p className="text-sm">
Your password has been set and your account is now active. Redirecting to sign in
</p>
</div>
<Link href="/login" className="btn-primary w-full flex items-center justify-center gap-2">
Go to sign in
<ArrowRight className="w-4 h-4" />
</Link>
</div>
) : outcome === 'hasPassword' ? (
<div className="space-y-4">
<div className="flex items-start gap-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 px-4 py-3 rounded">
<Info className="w-5 h-5 flex-shrink-0 mt-0.5" />
<p className="text-sm">
This account already has a password. Sign in with your email or phone number,
or use forgot password if you can&apos;t remember it.
</p>
</div>
<Link href="/login" className="btn-primary w-full flex items-center justify-center">
Sign in
</Link>
<Link
href="/forgot-password"
className="flex items-center justify-center text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
>
Forgot password?
</Link>
</div>
) : step === 1 ? (
<form onSubmit={handleRequestCode} className="space-y-4">
{error && (
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone number</label>
<input
type="tel"
value={phone}
onChange={(e) => { setPhone(e.target.value); setError(''); }}
className="input-field"
placeholder="+251912345678"
autoComplete="tel"
required
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
The phone number you used during Fayda verification.
</p>
</div>
<button type="submit" className="btn-primary w-full" disabled={loading || !phone}>
{loading ? 'Sending...' : 'Send verification code'}
</button>
</form>
) : (
<form onSubmit={handleSetPassword} className="space-y-4">
<div className="flex items-start gap-3 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300 px-4 py-3 rounded">
<Info className="w-5 h-5 flex-shrink-0 mt-0.5" />
<p className="text-sm">
If this phone number is Fayda-verified, an SMS with a verification code has been sent.
Enter it below with your new password.
</p>
</div>
{error && (
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 px-4 py-3 rounded">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone number</label>
<input
type="tel"
value={phone}
onChange={(e) => { setPhone(e.target.value); setError(''); }}
className="input-field"
placeholder="+251912345678"
autoComplete="tel"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Verification code</label>
<input
type="text"
value={otp}
onChange={(e) => { setOtp(e.target.value); setError(''); }}
className="input-field"
placeholder="6-character code from SMS"
maxLength={6}
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">New password</label>
<input
type="password"
value={newPassword}
onChange={(e) => { setNewPassword(e.target.value); setError(''); }}
className="input-field"
placeholder="••••••••"
autoComplete="new-password"
minLength={6}
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Confirm new password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => { setConfirmPassword(e.target.value); setError(''); }}
className="input-field"
placeholder="••••••••"
autoComplete="new-password"
minLength={6}
required
/>
</div>
<button
type="submit"
className="btn-primary w-full"
disabled={loading || !phone || !otp || !newPassword || !confirmPassword}
>
{loading ? 'Verifying...' : 'Verify & set password'}
</button>
<button
type="button"
onClick={() => { setStep(1); setError(''); setOtp(''); }}
className="w-full text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
>
Didn&apos;t get a code? Send again
</button>
</form>
)}
{outcome === null && (
<div className="mt-6 text-center">
<Link
href="/login"
className="inline-flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-[rgb(20_113_76)] dark:hover:text-emerald-400 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
Back to sign in
</Link>
</div>
)}
</div>
</div>
</div>
);
}