feat( auth ): make email optional in signup/login

This commit is contained in:
Abubeker Yasin
2026-07-10 13:38:53 +03:00
parent 4705f5c478
commit 8321c697ec
4 changed files with 60 additions and 21 deletions

View File

@@ -7,10 +7,13 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { useState, Suspense } from 'react';
import Link from 'next/link';
import { Train, ShieldCheck } from 'lucide-react';
import { Train, ShieldCheck, Eye, EyeOff } from 'lucide-react';
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
// Accepts either an email or a phone number. Passengers who registered without an
// email sign in with their phone number, which is sent in the same `email` field —
// the IAM matches on either identifier.
email: z.string().min(1, 'Phone or email is required'),
password: z.string().min(6, 'Password must be at least 6 characters'),
});
@@ -22,6 +25,7 @@ function LoginContent() {
const login = useAuthStore((s) => s.login);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
resolver: zodResolver(loginSchema as any),
@@ -63,12 +67,13 @@ function LoginContent() {
)}
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email</label>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Phone or email</label>
<input
type="email"
type="text"
{...register('email')}
className="input-field"
placeholder="your@email.com"
placeholder="+251912345678 or your@email.com"
autoComplete="username"
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
@@ -77,12 +82,23 @@ function LoginContent() {
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Password</label>
<input
type="password"
{...register('password')}
className="input-field"
placeholder="••••••••"
/>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
{...register('password')}
className="input-field pr-10"
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
aria-label={showPassword ? 'Hide password' : 'Show password'}
tabIndex={-1}
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
)}

View File

@@ -11,7 +11,13 @@ import { Train, ShieldCheck } from 'lucide-react';
const registerSchema = z.object({
fullName: z.string().min(2, 'Full name is required'),
email: z.string().email('Invalid email address'),
// Email is optional. If provided it must be a valid address; if left blank we fall back
// to the phone number as the account identifier (see onSubmit).
email: z
.string()
.email('Invalid email address')
.optional()
.or(z.literal('')),
phone: z.string().min(9, 'Phone number is required'),
});
@@ -31,9 +37,12 @@ export default function RegisterPage() {
setLoading(true);
setError('');
try {
// No email? Use the phone number as the account identifier. The IAM (and our
// relaxed RegisterDto) accept any non-empty string in the email field.
const email = data.email?.trim() ? data.email.trim() : data.phone;
const result = await registerUser({
fullName: data.fullName,
email: data.email,
email,
phone: data.phone,
});
const params = new URLSearchParams({
@@ -89,7 +98,9 @@ export default function RegisterPage() {
</div>
<div>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">Email</label>
<label className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300">
Email <span className="text-gray-400 font-normal">(optional)</span>
</label>
<input
type="email"
{...register('email')}