feat: implement reusable PhoneInput component with validation support and integrate across auth and portal modules.

This commit is contained in:
estifanos
2026-08-20 07:31:51 +00:00
parent 524131fe42
commit 481ff57ad1
16 changed files with 941 additions and 1006 deletions

View File

@@ -28,6 +28,7 @@ import { useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
import { useApiMutation } from "@ema-platform/api";
import { notify, useErrorHandler } from "@ema-platform/ui";
import { isValidPhoneNumber, parsePhoneNumberFromString } from "libphonenumber-js";
import { AuthShell } from "../components/AuthShell";
import { loginSuccess, setUser, setCurrentProfile } from "../store/auth.slice";
import type {
@@ -67,24 +68,23 @@ export function LoginPage() {
.string()
.trim()
.transform((value) => {
// Convert 09xxxxxxxx -> +2519xxxxxxxx
if (/^09\d{8}$/.test(value)) {
return `+251${value.substring(1)}`;
}
return value;
// A phone-looking value normalizes to E.164 (bare Ethiopian
// national numbers, e.g. 09xxxxxxxx, default to +251) so the
// international check below can validate it; anything else
// (an email) passes through untouched.
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(value)) return value;
return parsePhoneNumberFromString(value, "ET")?.number ?? value;
})
.refine(
(value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneRegex = /^\+2519\d{8}$/;
return emailRegex.test(value) || phoneRegex.test(value);
return emailRegex.test(value) || isValidPhoneNumber(value);
},
{
message: t(
"login.emailOrPhoneInvalid",
"Enter a valid email or phone number (+2519xxxxxxxx)",
"Enter a valid email or phone number",
),
},
),

View File

@@ -17,7 +17,6 @@ import {
IconArrowLeft,
IconArrowRight,
IconAt,
IconDeviceMobile,
IconLock,
IconMail,
IconUser,
@@ -29,7 +28,7 @@ import { useNavigate, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useApiMutation } from '@ema-platform/api';
import { useErrorHandler, passwordSchema, PasswordRequirements } from '@ema-platform/ui';
import { useErrorHandler, passwordSchema, PasswordRequirements, phoneNumber, PhoneInput } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
import { loginSuccess, setUser } from '../store/auth.slice';
import type { AuthUser } from '../types/auth.types';
@@ -88,7 +87,7 @@ export function SignupPage() {
.object({
email: z.string().email(),
username: z.string().min(3, { message: t('signup.usernameMinLength', 'Username must be at least 3 characters') }),
phoneNumber: z.string().min(1, { message: t('signup.phoneRequired', 'Phone number is required') }),
phoneNumber,
userType: z.literal('individual'),
nameEn: z
.string()
@@ -111,6 +110,7 @@ export function SignupPage() {
register,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
@@ -247,12 +247,12 @@ export function SignupPage() {
/>
</SimpleGrid>
<TextInput
<PhoneInput
label={t('signup.phoneLabel', 'Phone number')}
placeholder={t('signup.phonePlaceholder', '+251 911 234 567')}
leftSection={<IconDeviceMobile size={18} />}
placeholder={t('signup.phonePlaceholder', '9XX XXX XXX')}
value={watch('phoneNumber') || ''}
onChange={(val) => setValue('phoneNumber', val, { shouldValidate: true })}
error={errors.phoneNumber?.message}
{...register('phoneNumber')}
/>
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">