From 2c8895fed82640d6e0739134efe10e96aad62195 Mon Sep 17 00:00:00 2001 From: estifanos Date: Thu, 20 Aug 2026 11:08:03 +0000 Subject: [PATCH] feat: support phone number input for password recovery and move route to public access --- apps/portal/src/app/router.tsx | 12 ++++------ .../auth/src/lib/pages/ForgotPasswordPage.tsx | 22 +++++++++++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index a877f4cea..292950be2 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -70,6 +70,10 @@ export const router = createBrowserRouter([ { path: "/set-password", element: }, { path: "/reset-password", element: }, + // Reached from the login page by a logged-out user, so it must stay + // public — ProtectedRoute would bounce them straight to the landing page. + { path: "/forgot-password", element: }, + // Protected auth pages { element: ( @@ -79,14 +83,6 @@ export const router = createBrowserRouter([ ), path: "/otp-verify", }, - { - element: ( - - - - ), - path: "/forgot-password", - }, // The two-step setup wizard is gone. Signing up lands on the dashboard, and // profile details are collected where they are actually needed: on /profile, // via the dashboard nudge, or inline in an application flow. The path stays diff --git a/libs/auth/src/lib/pages/ForgotPasswordPage.tsx b/libs/auth/src/lib/pages/ForgotPasswordPage.tsx index e8b1eec06..2894c354f 100644 --- a/libs/auth/src/lib/pages/ForgotPasswordPage.tsx +++ b/libs/auth/src/lib/pages/ForgotPasswordPage.tsx @@ -23,11 +23,25 @@ import { z } from 'zod'; import { Link } from 'react-router-dom'; 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 { useAuthConfig } from '../AuthConfig'; +// Same email-or-phone rule as LoginPage: a phone-looking value normalizes to +// E.164 (bare Ethiopian national numbers default to +251) so the backend +// always gets a value it can look the account up by, under the `email` key. +const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const schema = z.object({ - email: z.string().email({ message: 'Enter a valid email' }), + email: z + .string() + .trim() + .transform((value) => { + if (emailRegex.test(value)) return value; + return parsePhoneNumberFromString(value, 'ET')?.number ?? value; + }) + .refine((value) => emailRegex.test(value) || isValidPhoneNumber(value), { + message: 'Enter a valid email or phone number', + }), }); type FormValues = z.infer; @@ -156,8 +170,8 @@ export function ForgotPasswordPage() { Forgot your password? - Enter the email linked to your account and we'll send you a link - to reset your password. + Enter the email or phone number linked to your account and + we'll send you a link to reset your password. @@ -170,7 +184,7 @@ export function ForgotPasswordPage() {
}