diff --git a/apps/backoffice/src/app/router/index.tsx b/apps/backoffice/src/app/router/index.tsx
index b91446d3e..4518adb80 100644
--- a/apps/backoffice/src/app/router/index.tsx
+++ b/apps/backoffice/src/app/router/index.tsx
@@ -61,7 +61,7 @@ const router = createBrowserRouter([
),
children: [
{ path: '/login', element: },
- { path: '/set-password', element: },
+ { path: '/otp-verify', element: },
],
},
{ path: '/404', element:
Page not found
},
diff --git a/apps/portal/src/app/features/auth/hooks/useAuth.ts b/apps/portal/src/app/features/auth/hooks/useAuth.ts
deleted file mode 100644
index 260f46bd0..000000000
--- a/apps/portal/src/app/features/auth/hooks/useAuth.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useAppDispatch, useAppSelector } from '../../../store/hooks';
-import { loginSuccess, logout as logoutAction, hydrateAuth } from '../store/auth.slice';
-import type { LoginPayload } from '../types/auth.types';
-
-export function useAuth() {
- const dispatch = useAppDispatch();
- const { user, token, isAuthenticated } = useAppSelector((s) => s.auth);
-
- return {
- user,
- token,
- isAuthenticated,
- login: (p: LoginPayload) => dispatch(loginSuccess(p)),
- logout: () => dispatch(logoutAction()),
- hydrate: () => dispatch(hydrateAuth()),
- };
-}
diff --git a/apps/portal/src/app/features/auth/pages/LoginPage.tsx b/apps/portal/src/app/features/auth/pages/LoginPage.tsx
index ae6344ab1..a9f26f342 100644
--- a/apps/portal/src/app/features/auth/pages/LoginPage.tsx
+++ b/apps/portal/src/app/features/auth/pages/LoginPage.tsx
@@ -17,15 +17,12 @@ import { useNavigate, Link } from 'react-router-dom';
import { useAppDispatch } from '../../../store/hooks';
import { loginSuccess, setUser } from '../store/auth.slice';
import type { LoginPayload, AuthUser } from '../types/auth.types';
+import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
-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'),
- password: z.string().min(0, 'Password must be at least 6 characters'),
+ email: z.string().email({ message: 'Enter a valid email' }),
+ password: z.string().min(5, { message: 'Password must be at least 6 characters' }),
});
type FormValues = z.infer;
@@ -34,6 +31,8 @@ export function LoginPage() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [isLoading, setIsLoading] = useState(false);
+ const [loginTrigger] = useApiMutation();
+ const [meTrigger] = useApiMutation();
const {
register,
@@ -46,26 +45,23 @@ export function LoginPage() {
const onSubmit = async (values: FormValues) => {
setIsLoading(true);
try {
- const res = await fetch(`${BASE_API_URL}/auth/login`, {
+ const data = await loginTrigger({
+ url: '/auth/login',
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(values),
- });
- if (!res.ok) throw new Error('Login failed');
- const data = (await res.json()) as LoginPayload;
+ body: values,
+ }).unwrap() 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;
+ const me = await meTrigger({
+ url: '/auth/me',
+ method: 'GET',
+ }).unwrap() as AuthUser;
dispatch(setUser(me));
if (me.status === 'accepted') {
navigate('/dashboard');
} else {
- navigate('/set-password', {
+ navigate('/otp-verify', {
state: { email: me.email, phoneNumber: me.phoneNumber },
});
}
diff --git a/apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx b/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx
similarity index 61%
rename from apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx
rename to apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx
index f3c6eae87..c786d40fe 100644
--- a/apps/portal/src/app/features/auth/pages/SetPasswordPage.tsx
+++ b/apps/portal/src/app/features/auth/pages/OTPVerificationPage.tsx
@@ -11,36 +11,26 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate, useLocation } from 'react-router-dom';
-import { useAppDispatch, useAppSelector } from '../../../store/hooks';
-import {
- setPasswordStart,
- setPasswordSuccess,
- setPasswordFailure,
-} from '../store/set-password.slice';
+import { useApiMutation } from '@ema-platform/api';
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({
- verificationCode: z.string().min(1, 'Verification code is required'),
+ verificationCode: z.string().min(1, { message: 'Verification code is required' }),
});
type FormValues = z.infer;
-export function SetPasswordPage() {
+export function OTPVerificationPage() {
const navigate = useNavigate();
- const dispatch = useAppDispatch();
const location = useLocation();
- const { loading } = useAppSelector((s) => s.setPassword);
const state = location.state as
| { email?: string; phoneNumber?: string }
| null;
const email = state?.email ?? '';
const phoneNumber = state?.phoneNumber ?? '';
- const [resending, setResending] = useState(false);
+
+ const [verifyTrigger, { isLoading: loading }] = useApiMutation();
+ const [resendTrigger, { isLoading: resending }] = useApiMutation();
const {
register,
@@ -51,53 +41,33 @@ export function SetPasswordPage() {
});
const onSubmit = async (values: FormValues) => {
- dispatch(setPasswordStart());
try {
- const res = await fetch(`${BASE_API_URL}/auth/verify-phone-number`, {
+ await verifyTrigger({
+ url: '/auth/verify-phone-number',
method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- email,
- phoneNumber,
- verificationCode: values.verificationCode,
- }),
- });
+ body: { email, phoneNumber, verificationCode: values.verificationCode },
+ }).unwrap();
- if (!res.ok) {
- const errorData = await res.json().catch(() => null);
- throw new Error(errorData?.message ?? 'Verification failed');
- }
-
- dispatch(setPasswordSuccess());
notify.success('Phone number verified successfully');
navigate('/dashboard');
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
- dispatch(setPasswordFailure(msg));
notify.error(msg);
}
};
const handleResendOtp = async () => {
- setResending(true);
try {
- const res = await fetch(`${BASE_API_URL}/auth/resend-otp`, {
+ await resendTrigger({
+ 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');
- }
+ body: { email },
+ }).unwrap();
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);
}
};
diff --git a/apps/portal/src/app/features/auth/pages/SignupPage.tsx b/apps/portal/src/app/features/auth/pages/SignupPage.tsx
index ed5716dc5..16bc78945 100644
--- a/apps/portal/src/app/features/auth/pages/SignupPage.tsx
+++ b/apps/portal/src/app/features/auth/pages/SignupPage.tsx
@@ -13,26 +13,21 @@ import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate, Link } from 'react-router-dom';
-import { useAppDispatch, useAppSelector } from '../../../store/hooks';
-import { signupStart, signupSuccess, signupFailure } from '../store/signup.slice';
-import { hydrateAuth } from '../store/auth.slice';
-import { authStorage } from '../utils/auth-storage';
+import { useAppDispatch } from '../../../store/hooks';
+import { loginSuccess } from '../store/auth.slice';
+import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
-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'),
+ email: z.string().email(),
+ username: z.string().min(3, { message: 'Username must be at least 3 characters' }),
+ phoneNumber: z.string().min(1, { message: 'Phone number is required' }),
userType: z.literal('individual'),
- nameEn: z.string().min(1, 'Name (English) is required'),
+ nameEn: z.string().min(1, { message: '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'),
+ password: z.string().min(8, { message: 'Password must be at least 8 characters' }),
+ confirmPassword: z.string().min(8, { message: 'Confirm your password' }),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
@@ -57,7 +52,11 @@ interface SignupPayload {
export function SignupPage() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
- const { loading } = useAppSelector((s) => s.signup);
+ const [signupTrigger, { isLoading: loading }] = useApiMutation<{
+ token: string;
+ refreshToken: string;
+ isPhoneNumberVerified: boolean;
+ }>();
const {
register,
@@ -69,7 +68,6 @@ export function SignupPage() {
});
const onSubmit = async (values: FormValues) => {
- dispatch(signupStart());
try {
const payload: SignupPayload = {
email: values.email,
@@ -81,30 +79,25 @@ export function SignupPage() {
confirmPassword: values.confirmPassword,
};
- const res = await fetch(`${BASE_API_URL}/auth/signup-with-pwd`, {
+ const data = await signupTrigger({
+ url: '/auth/signup-with-pwd',
method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload),
- });
+ body: payload,
+ }).unwrap() as { token: string; refreshToken: string; isPhoneNumberVerified: boolean };
- if (!res.ok) {
- const errorData = await res.json().catch(() => null);
- throw new Error(errorData?.message ?? 'Signup failed');
- }
+ dispatch(
+ loginSuccess({
+ token: data.token,
+ refreshToken: data.refreshToken,
+ isPhoneNumberVerified: data.isPhoneNumberVerified,
+ }),
+ );
- 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', {
+ navigate('/otp-verify', {
state: { email: values.email, phoneNumber: values.phoneNumber },
});
} catch (err) {
const msg = err instanceof Error ? err.message : 'Something went wrong';
- dispatch(signupFailure(msg));
notify.error(msg);
}
};
diff --git a/apps/portal/src/app/features/auth/store/set-password.slice.ts b/apps/portal/src/app/features/auth/store/set-password.slice.ts
deleted file mode 100644
index bdcf8d745..000000000
--- a/apps/portal/src/app/features/auth/store/set-password.slice.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
-
-interface SetPasswordState {
- loading: boolean;
- success: boolean;
- error: string | null;
-}
-
-const initialState: SetPasswordState = {
- loading: false,
- success: false,
- error: null,
-};
-
-const setPasswordSlice = createSlice({
- name: 'setPassword',
- initialState,
- reducers: {
- setPasswordStart(state) {
- state.loading = true;
- state.error = null;
- state.success = false;
- },
- setPasswordSuccess(state) {
- state.loading = false;
- state.success = true;
- },
- setPasswordFailure(state, action: PayloadAction) {
- state.loading = false;
- state.error = action.payload;
- },
- resetSetPassword(state) {
- state.loading = false;
- state.success = false;
- state.error = null;
- },
- },
-});
-
-export const { setPasswordStart, setPasswordSuccess, setPasswordFailure, resetSetPassword } =
- setPasswordSlice.actions;
-export const setPasswordReducer = setPasswordSlice.reducer;
diff --git a/apps/portal/src/app/features/auth/store/signup.slice.ts b/apps/portal/src/app/features/auth/store/signup.slice.ts
deleted file mode 100644
index 92a5f4294..000000000
--- a/apps/portal/src/app/features/auth/store/signup.slice.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
-
-interface SignupState {
- loading: boolean;
- success: boolean;
- error: string | null;
-}
-
-const initialState: SignupState = {
- loading: false,
- success: false,
- error: null,
-};
-
-const signupSlice = createSlice({
- name: 'signup',
- initialState,
- reducers: {
- signupStart(state) {
- state.loading = true;
- state.error = null;
- state.success = false;
- },
- signupSuccess(state) {
- state.loading = false;
- state.success = true;
- },
- signupFailure(state, action: PayloadAction) {
- state.loading = false;
- state.error = action.payload;
- },
- resetSignup(state) {
- state.loading = false;
- state.success = false;
- state.error = null;
- },
- },
-});
-
-export const { signupStart, signupSuccess, signupFailure, resetSignup } = signupSlice.actions;
-export const signupReducer = signupSlice.reducer;
diff --git a/apps/portal/src/app/layouts/PortalLayout.tsx b/apps/portal/src/app/layouts/PortalLayout.tsx
index 4e90c93c9..50742ff64 100644
--- a/apps/portal/src/app/layouts/PortalLayout.tsx
+++ b/apps/portal/src/app/layouts/PortalLayout.tsx
@@ -1,24 +1,25 @@
import { AppShell, Group, Text, Button } from '@mantine/core';
import { Outlet, useNavigate } from 'react-router-dom';
+import { useAppDispatch, useAppSelector } from '../store/hooks';
+import { logout } from '../features/auth/store/auth.slice';
export function PortalLayout() {
const navigate = useNavigate();
- const isLoggedIn = !!localStorage.getItem('ema-portal-auth-token');
+ const dispatch = useAppDispatch();
+ const isAuthenticated = useAppSelector((s) => s.auth.isAuthenticated);
+
+ const handleLogout = () => {
+ dispatch(logout());
+ navigate('/login');
+ };
return (
EMA Portal
- {isLoggedIn ? (
-