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() {