diff --git a/apps/portal/src/app/components/ProfileGuard.tsx b/apps/portal/src/app/components/ProfileGuard.tsx new file mode 100644 index 000000000..c9328918a --- /dev/null +++ b/apps/portal/src/app/components/ProfileGuard.tsx @@ -0,0 +1,12 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { authStorage } from '@ema-platform/auth'; + +export function ProfileGuard() { + const profileId = authStorage.getProfileId(); + + if (!profileId) { + return ; + } + + return ; +} diff --git a/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx b/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx index 0c42d7369..5d9c5360c 100644 --- a/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx +++ b/apps/portal/src/app/features/profile-setup/pages/ProfileSetupPage.tsx @@ -19,6 +19,7 @@ import { IconArrowRight, IconCheck, IconCircleCheck, + IconLogout2, IconMapPin, IconUser, } from '@tabler/icons-react'; @@ -28,7 +29,7 @@ import { z } from 'zod'; import { useNavigate } from 'react-router-dom'; import { useApiMutation } from '@ema-platform/api'; import { notify } from '@ema-platform/ui'; -import { authStorage, setUser } from '@ema-platform/auth'; +import { authStorage, setUser, logout } from '@ema-platform/auth'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; const GENDERS = ['MALE', 'FEMALE']; @@ -159,9 +160,12 @@ export function ProfileSetupPage() { const [profileTrigger] = useApiMutation<{ id: string }>(); const [addressTrigger] = useApiMutation(); const [meTrigger] = useApiMutation<{ id: string }>(); + const [profileCheckTrigger] = useApiMutation<{ count: number; items: Array<{ id: string; userId: string }> }>(); const [fetchProfessions] = useApiMutation<{ count: number; items: Array<{ id: string; name: { en: string } }> }>(); const fetched = useRef(false); + const checkedExistingProfile = useRef(false); + useEffect(() => { if (fetched.current) return; fetched.current = true; @@ -172,6 +176,21 @@ export function ProfileSetupPage() { .finally(() => setProfessionsLoading(false)); }, [fetchProfessions]); + useEffect(() => { + if (!user || checkedExistingProfile.current) return; + checkedExistingProfile.current = true; + profileCheckTrigger({ url: '/profiles?take=1&skip=0', method: 'GET' }) + .unwrap() + .then((data) => { + const existing = data.items?.find((p) => p.userId === user.id); + if (existing) { + authStorage.setProfileId(existing.id); + navigate('/dashboard', { replace: true }); + } + }) + .catch(() => {}); + }, [user, navigate, profileCheckTrigger]); + const professionOptions = useMemo( () => professions.map((p) => ({ value: p.id, label: p.name.en })), [professions], @@ -185,6 +204,37 @@ export function ProfileSetupPage() { return map; }, [professions]); + const nameParts = useMemo(() => (user?.name?.en || '').trim().split(/\s+/), [user]); + const profileDefaults: ProfileValues = useMemo(() => ({ + professionId: '', + firstName: nameParts[0] || '', + middleName: nameParts.length > 2 ? nameParts.slice(1, -1).join(' ') : '', + lastName: nameParts.length > 1 ? nameParts[nameParts.length - 1] : '', + gender: '', + dob: '', + pob: '', + maritalStatus: '', + }), [nameParts]); + + const addressDefaults: AddressValues = useMemo(() => ({ + idType: '', + idNumber: '', + nationality: '', + primaryPhoneNumber: user?.phoneNumber || '', + secondaryPhoneNumber: '', + email: user?.email || '', + regionId: '', + cityId: '', + subcityId: '', + woredaId: '', + kebeleId: '', + streetAddress: '', + postalAddress: '', + emergencyContactName: '', + emergencyContactPhone: '', + emergencyContactRelation: '', + }), [user]); + const { register: profileRegister, handleSubmit: profileHandleSubmit, @@ -194,16 +244,7 @@ export function ProfileSetupPage() { trigger: profileTriggerValidation, } = useForm({ resolver: zodResolver(profileSchema), - defaultValues: { - professionId: '', - firstName: '', - middleName: '', - lastName: '', - gender: '', - dob: '', - pob: '', - maritalStatus: '', - }, + defaultValues: profileDefaults, }); const { @@ -215,24 +256,7 @@ export function ProfileSetupPage() { trigger: addressTriggerValidation, } = useForm({ resolver: zodResolver(addressSchema), - defaultValues: { - idType: '', - idNumber: '', - nationality: '', - primaryPhoneNumber: '', - secondaryPhoneNumber: '', - email: '', - regionId: '', - cityId: '', - subcityId: '', - woredaId: '', - kebeleId: '', - streetAddress: '', - postalAddress: '', - emergencyContactName: '', - emergencyContactPhone: '', - emergencyContactRelation: '', - }, + defaultValues: addressDefaults, }); const onNext = async () => { @@ -528,8 +552,16 @@ export function ProfileSetupPage() { )} - {active > 0 && ( diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index 132ae8c00..361e1bf2a 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -3,6 +3,7 @@ import { I18nextProvider } from 'react-i18next'; import { i18n } from './i18n/config'; import { PortalLayout } from './layouts/PortalLayout'; import { ProtectedRoute } from './components/ProtectedRoute'; +import { ProfileGuard } from './components/ProfileGuard'; // Auth (standalone pages, no portal chrome) import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth'; @@ -54,9 +55,11 @@ export const router = createBrowserRouter([ { element: ( - - - + + + + + ), children: [ diff --git a/libs/auth/src/lib/pages/LoginPage.tsx b/libs/auth/src/lib/pages/LoginPage.tsx index 3c33b8f41..169dde10f 100644 --- a/libs/auth/src/lib/pages/LoginPage.tsx +++ b/libs/auth/src/lib/pages/LoginPage.tsx @@ -28,6 +28,7 @@ import { AuthShell } from '../components/AuthShell'; import { loginSuccess, setUser } from '../store/auth.slice'; import type { LoginPayload, AuthUser } from '../types/auth.types'; import { useAuthConfig } from '../AuthConfig'; +import { authStorage } from '../utils/auth-storage'; const schema = z.object({ email: z.string().email({ message: 'Enter a valid email' }), @@ -76,12 +77,14 @@ export function LoginPage() { url: '/profiles?take=10000&skip=0', method: 'GET', }).unwrap(); - const hasProfile = profiles.items?.some((p) => p.userId === me.id); + const userProfile = profiles.items?.find((p) => p.userId === me.id); - if (!hasProfile) { + if (!userProfile) { navigate('/profile-setup'); return; } + + authStorage.setProfileId(userProfile.id); } catch { // profile check failed — proceed to dashboard anyway } diff --git a/libs/auth/src/lib/pages/OTPVerificationPage.tsx b/libs/auth/src/lib/pages/OTPVerificationPage.tsx index f39b93dc7..6c3172355 100644 --- a/libs/auth/src/lib/pages/OTPVerificationPage.tsx +++ b/libs/auth/src/lib/pages/OTPVerificationPage.tsx @@ -37,10 +37,11 @@ export function OTPVerificationPage() { const location = useLocation(); const { loginRedirectPath } = useAuthConfig(); const state = location.state as - | { email?: string; phoneNumber?: string } + | { email?: string; phoneNumber?: string; needsProfile?: boolean } | null; const email = state?.email ?? ''; const phoneNumber = state?.phoneNumber ?? ''; + const needsProfile = state?.needsProfile ?? false; const [verifyTrigger, { isLoading: loading }] = useApiMutation(); const [resendTrigger, { isLoading: resending }] = useApiMutation(); @@ -70,7 +71,7 @@ export function OTPVerificationPage() { }).unwrap(); notify.success('Phone number verified successfully'); - navigate(loginRedirectPath); + navigate(needsProfile ? '/profile-setup' : loginRedirectPath); } catch (err) { const msg = err instanceof Error ? err.message : 'Something went wrong'; notify.error(msg); diff --git a/libs/auth/src/lib/pages/SignupPage.tsx b/libs/auth/src/lib/pages/SignupPage.tsx index 676a82676..853eb1e74 100644 --- a/libs/auth/src/lib/pages/SignupPage.tsx +++ b/libs/auth/src/lib/pages/SignupPage.tsx @@ -27,7 +27,8 @@ import { useDispatch } from 'react-redux'; import { useApiMutation } from '@ema-platform/api'; import { notify } from '@ema-platform/ui'; import { AuthShell } from '../components/AuthShell'; -import { loginSuccess } from '../store/auth.slice'; +import { loginSuccess, setUser } from '../store/auth.slice'; +import type { AuthUser } from '../types/auth.types'; import { useAuthConfig } from '../AuthConfig'; const schema = z @@ -71,6 +72,7 @@ export function SignupPage() { refreshToken: string; isPhoneNumberVerified: boolean; }>(); + const [meTrigger] = useApiMutation(); const { register, @@ -107,11 +109,18 @@ export function SignupPage() { }), ); + const me = await meTrigger({ url: '/auth/me', method: 'GET' }).unwrap(); + dispatch(setUser(me)); + if (data.isPhoneNumberVerified) { - navigate(loginRedirectPath); + navigate('/profile-setup'); } else { navigate('/otp-verify', { - state: { email: values.email, phoneNumber: values.phoneNumber }, + state: { + email: values.email, + phoneNumber: values.phoneNumber, + needsProfile: true, + }, }); } } catch (err) { diff --git a/local-packages/tria-plc-iamui-0.0.3.tgz b/local-packages/tria-plc-iamui-0.1.1.tgz similarity index 50% rename from local-packages/tria-plc-iamui-0.0.3.tgz rename to local-packages/tria-plc-iamui-0.1.1.tgz index 5fb9f439c..6f3ffaf4e 100644 Binary files a/local-packages/tria-plc-iamui-0.0.3.tgz and b/local-packages/tria-plc-iamui-0.1.1.tgz differ diff --git a/package-lock.json b/package-lock.json index 52af32cb4..7c702ace9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@reduxjs/toolkit": "^2.11.2", "@tabler/icons-react": "^3.40.0", "@tanstack/react-query": "^5.99.0", - "@tria-plc/iamui": "file:local-packages/tria-plc-iamui-0.0.3.tgz", + "@tria-plc/iamui": "file:local-packages/tria-plc-iamui-0.1.1.tgz", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.20", @@ -8632,9 +8632,9 @@ "license": "MIT" }, "node_modules/@tria-plc/iamui": { - "version": "0.0.3", - "resolved": "file:local-packages/tria-plc-iamui-0.0.3.tgz", - "integrity": "sha512-aZhIeNq2Uui7TUkG88G9QTBqdhaVB5kvqfd47HLgVE7qKkVyfjlm4nwlSWnHh5MO+CjgP9vRi6ILwMJx1ULO8g==", + "version": "0.1.1", + "resolved": "file:local-packages/tria-plc-iamui-0.1.1.tgz", + "integrity": "sha512-FTihoH0lIqKV/0s+FTJZokQw8xX3XztXIqT8eEYEZgDM2xyzVSCHY8pHCUxw0MQtiR8R97zxZJ/o192eWt+E/g==", "license": "UNLICENSED", "dependencies": { "@emotion/react": "^11.14.0", diff --git a/package.json b/package.json index 052f93161..7d291d660 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@reduxjs/toolkit": "^2.11.2", "@tabler/icons-react": "^3.40.0", "@tanstack/react-query": "^5.99.0", - "@tria-plc/iamui": "file:local-packages/tria-plc-iamui-0.0.3.tgz", + "@tria-plc/iamui": "file:local-packages/tria-plc-iamui-0.1.1.tgz", "clsx": "^2.1.1", "date-fns": "^4.1.0", "dayjs": "^1.11.20",