diff --git a/apps/portal/src/app/app.tsx b/apps/portal/src/app/app.tsx
index 8a0b8df61..58451087d 100644
--- a/apps/portal/src/app/app.tsx
+++ b/apps/portal/src/app/app.tsx
@@ -1,16 +1,17 @@
-// src/main.tsx or App.tsx
-import { AppProviders, configureIam } from "@tria-plc/iamui-common";
+import { configureIam } from "@tria-plc/iamui-common";
+import { BrowserRouter } from 'react-router-dom';
import "@tria-plc/iamui-common/styles.css";
-// import { AppProviders } from './providers/AppProviders';
+import { AppProviders } from './providers/AppProviders';
import { AppRouter } from './router';
-configureIam({ apiUrl: 'http://localhost:3001/api'});
-// import './styles.css';
+
+configureIam({ apiUrl: 'http://localhost:3001/api' });
export function App() {
return (
- // 2. Wrap with Providers so hooks like useAuthUser() work
-
+
+
+
);
}
diff --git a/apps/portal/src/app/features/auth/pages/LoginPage.tsx b/apps/portal/src/app/features/auth/pages/LoginPage.tsx
index f0153f2ef..ae6344ab1 100644
--- a/apps/portal/src/app/features/auth/pages/LoginPage.tsx
+++ b/apps/portal/src/app/features/auth/pages/LoginPage.tsx
@@ -14,7 +14,9 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate, Link } from 'react-router-dom';
-import { useAuth } from '../hooks/useAuth';
+import { useAppDispatch } from '../../../store/hooks';
+import { loginSuccess, setUser } from '../store/auth.slice';
+import type { LoginPayload, AuthUser } from '../types/auth.types';
import { notify } from '@ema-platform/ui';
const BASE_API_URL =
@@ -30,7 +32,7 @@ type FormValues = z.infer;
export function LoginPage() {
const navigate = useNavigate();
- const { login } = useAuth();
+ const dispatch = useAppDispatch();
const [isLoading, setIsLoading] = useState(false);
const {
@@ -50,9 +52,23 @@ export function LoginPage() {
body: JSON.stringify(values),
});
if (!res.ok) throw new Error('Login failed');
- const data = (await res.json()) as Parameters[0];
- login(data);
- navigate('/dashboard');
+ const data = (await res.json()) as LoginPayload;
+ dispatch(loginSuccess(data));
+
+ const meRes = await fetch(`${BASE_API_URL}/auth/me`, {
+ headers: { Authorization: `Bearer ${data.token}` },
+ });
+ if (!meRes.ok) throw new Error('Failed to fetch user');
+ const me = (await meRes.json()) as AuthUser;
+ dispatch(setUser(me));
+
+ if (me.status === 'accepted') {
+ navigate('/dashboard');
+ } else {
+ navigate('/set-password', {
+ state: { email: me.email, phoneNumber: me.phoneNumber },
+ });
+ }
} catch {
notify.error('Invalid email or password');
} finally {
diff --git a/apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx b/apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx
index eb2837420..f3c6eae87 100644
--- a/apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx
+++ b/apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx
@@ -1,7 +1,6 @@
import {
Paper,
TextInput,
- PasswordInput,
Button,
Stack,
Title,
@@ -19,23 +18,15 @@ import {
setPasswordFailure,
} from '../store/set-password.slice';
import { notify } from '@ema-platform/ui';
+import { useState } from 'react';
const BASE_API_URL =
(import.meta as { env?: Record }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3001/api';
-const schema = z
- .object({
- userId: z.string().min(1, 'User ID is required'),
- email: z.string().email('Enter a valid email'),
- verificationCode: z.string().min(1, 'Verification code is required'),
- newPassword: z.string().min(6, 'Password must be at least 6 characters'),
- confirmPassword: z.string().min(6, 'Confirm your password'),
- })
- .refine((data) => data.newPassword === data.confirmPassword, {
- message: 'Passwords do not match',
- path: ['confirmPassword'],
- });
+const schema = z.object({
+ verificationCode: z.string().min(1, 'Verification code is required'),
+});
type FormValues = z.infer;
@@ -44,7 +35,12 @@ export function SetPasswordPage() {
const dispatch = useAppDispatch();
const location = useLocation();
const { loading } = useAppSelector((s) => s.setPassword);
- const signupEmail = (location.state as { email?: string } | null)?.email ?? '';
+ const state = location.state as
+ | { email?: string; phoneNumber?: string }
+ | null;
+ const email = state?.email ?? '';
+ const phoneNumber = state?.phoneNumber ?? '';
+ const [resending, setResending] = useState(false);
const {
register,
@@ -52,31 +48,28 @@ export function SetPasswordPage() {
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
- defaultValues: { email: signupEmail },
});
const onSubmit = async (values: FormValues) => {
dispatch(setPasswordStart());
try {
- const res = await fetch(`${BASE_API_URL}/auth/set-password`, {
+ const res = await fetch(`${BASE_API_URL}/auth/verify-phone-number`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- userId: values.userId,
- email: values.email,
+ email,
+ phoneNumber,
verificationCode: values.verificationCode,
- newPassword: values.newPassword,
- confirmPassword: values.confirmPassword,
}),
});
if (!res.ok) {
const errorData = await res.json().catch(() => null);
- throw new Error(errorData?.message ?? 'Failed to set password');
+ throw new Error(errorData?.message ?? 'Verification failed');
}
dispatch(setPasswordSuccess());
- notify.success('Password set successfully');
+ notify.success('Phone number verified successfully');
navigate('/dashboard');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
@@ -85,52 +78,60 @@ export function SetPasswordPage() {
}
};
+ const handleResendOtp = async () => {
+ setResending(true);
+ try {
+ const res = await fetch(`${BASE_API_URL}/auth/resend-otp`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email }),
+ });
+
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => null);
+ throw new Error(errorData?.message ?? 'Failed to resend OTP');
+ }
+
+ notify.success('Verification code resent to your email');
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : 'Something went wrong';
+ notify.error(msg);
+ } finally {
+ setResending(false);
+ }
+ };
+
return (
- Set your password
+ Verify your email
- A verification code has been sent to your email. Enter it below along
- with your new password to complete registration.
+ A verification code has been sent to {email || 'your email'}. Enter it
+ below to complete your registration.
+
diff --git a/apps/portal/src/app/features/auth/pages/SignupPage.tsx b/apps/portal/src/app/features/auth/pages/SignupPage.tsx
index 2a277e457..ed5716dc5 100644
--- a/apps/portal/src/app/features/auth/pages/SignupPage.tsx
+++ b/apps/portal/src/app/features/auth/pages/SignupPage.tsx
@@ -1,6 +1,7 @@
import {
Paper,
TextInput,
+ PasswordInput,
Button,
Stack,
Title,
@@ -22,13 +23,21 @@ const BASE_API_URL =
(import.meta as { env?: Record }).env?.['VITE_BASE_API_URL'] ??
'http://localhost:3001/api';
-const schema = z.object({
- email: z.string().email('Enter a valid email'),
- username: z.string().min(3, 'Username must be at least 3 characters'),
- phoneNumber: z.string().min(1, 'Phone number is required'),
- userType: z.string().min(1, 'User type is required'),
- name: z.string().min(1, 'Name is required'),
-});
+const schema = z
+ .object({
+ email: z.string().email('Enter a valid email'),
+ username: z.string().min(3, 'Username must be at least 3 characters'),
+ phoneNumber: z.string().min(1, 'Phone number is required'),
+ userType: z.literal('individual'),
+ nameEn: z.string().min(1, 'Name (English) is required'),
+ nameAm: z.string().optional(),
+ password: z.string().min(8, 'Password must be at least 8 characters'),
+ confirmPassword: z.string().min(8, 'Confirm your password'),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: 'Passwords do not match',
+ path: ['confirmPassword'],
+ });
type FormValues = z.infer;
@@ -41,6 +50,8 @@ interface SignupPayload {
am: string;
en: string;
};
+ password: string;
+ confirmPassword: string;
}
export function SignupPage() {
@@ -54,6 +65,7 @@ export function SignupPage() {
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
+ defaultValues: { userType: 'individual' },
});
const onSubmit = async (values: FormValues) => {
@@ -64,13 +76,12 @@ export function SignupPage() {
username: values.username,
phoneNumber: values.phoneNumber,
userType: values.userType,
- name: {
- am: '',
- en: values.name,
- },
+ name: { en: values.nameEn, am: values.nameAm ?? '' },
+ password: values.password,
+ confirmPassword: values.confirmPassword,
};
- const res = await fetch(`${BASE_API_URL}/auth/signup`, {
+ const res = await fetch(`${BASE_API_URL}/auth/signup-with-pwd`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
@@ -81,14 +92,16 @@ export function SignupPage() {
throw new Error(errorData?.message ?? 'Signup failed');
}
- const data = (await res.json()) as { token: string; refreshToken: string };
+ const data = (await res.json()) as { token: string; refreshToken: string; isPhoneNumberVerified: boolean };
authStorage.setToken(data.token);
authStorage.setRefreshToken(data.refreshToken);
dispatch(hydrateAuth());
dispatch(signupSuccess());
- navigate('/set-password', { state: { email: values.email } });
+ navigate('/set-password', {
+ state: { email: values.email, phoneNumber: values.phoneNumber },
+ });
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
dispatch(signupFailure(msg));
@@ -104,10 +117,16 @@ export function SignupPage() {