feat: fayda integration paritial

This commit is contained in:
mengstabketemaw
2026-07-11 10:09:18 +03:00
parent 9a249acf31
commit e8c02ab853
7 changed files with 295 additions and 5 deletions

View File

@@ -6,10 +6,10 @@ export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/backoffice',
server: {
port: 4201,
port: Number(process.env.PORT) || 4201,
host: 'localhost',
},
preview: { port: 4201, host: 'localhost' },
preview: { port: Number(process.env.PORT) || 4201, host: 'localhost' },
plugins: [react(), nxViteTsPaths()],
resolve: {
dedupe: ['react', 'react-dom', 'react-router-dom', '@tanstack/react-query'],

View File

@@ -7,7 +7,7 @@ import { ProfileGuard } from './components/ProfileGuard';
import { SmartDashboard } from './components/SmartDashboard';
// Auth (standalone pages, no portal chrome)
import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth';
import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage, FaydaCallbackPage, SetPasswordPage } from '@ema-platform/auth';
// Profile setup
import { ProfileSetupPage } from './features/profile-setup/pages/ProfileSetupPage';
@@ -51,6 +51,12 @@ export const router = createBrowserRouter([
// Public auth pages
{ path: '/forgot-password', element: <ForgotPasswordPage appName="Portal" /> },
// Fayda / National ID callback
{ path: '/callback', element: <FaydaCallbackPage /> },
// Set password (after Fayda signup)
{ path: '/set-password', element: <SetPasswordPage /> },
// Protected auth pages
{
element: <ProtectedRoute><OTPVerificationPage /></ProtectedRoute>,

View File

@@ -5,8 +5,8 @@ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/portal',
server: { port: 4200, host: 'localhost' },
preview: { port: 4200, host: 'localhost' },
server: { port: Number(process.env.PORT) || 3000, host: 'localhost' },
preview: { port: Number(process.env.PORT) || 3000, host: 'localhost' },
plugins: [react(), nxViteTsPaths()],
resolve: {
dedupe: ['react', 'react-dom', 'react-router-dom', '@tanstack/react-query'],

View File

@@ -4,6 +4,8 @@ export { LoginPage } from './lib/pages/LoginPage';
export { SignupPage } from './lib/pages/SignupPage';
export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage';
export { OTPVerificationPage } from './lib/pages/OTPVerificationPage';
export { FaydaCallbackPage } from './lib/pages/FaydaCallbackPage';
export { SetPasswordPage } from './lib/pages/SetPasswordPage';
export { authReducer, loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } from './lib/store/auth.slice';
export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice';
export { authStorage } from './lib/utils/auth-storage';

View File

@@ -0,0 +1,110 @@
import { useEffect, useRef, useState } from 'react';
import { Alert, Center, Loader, Stack, Text, Title } from '@mantine/core';
import { IconAlertCircle, IconShieldCheck } from '@tabler/icons-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
interface FaydaUserInfo {
name: string;
email: string;
sub: string;
birthDate: string;
gender: string;
picture: string;
phone: string;
address: string;
}
export function FaydaCallbackPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const calledRef = useRef(false);
const [error, setError] = useState<string | null>(null);
const [getUserInfoTrigger] = useApiMutation<FaydaUserInfo>();
useEffect(() => {
if (calledRef.current) return;
calledRef.current = true;
const code = searchParams.get('code');
const state = searchParams.get('state');
if (!code || !state) {
const msg = 'Invalid response from National ID service. Missing code or state.';
setError(msg);
notify.error(msg);
return;
}
const authId = sessionStorage.getItem('fayda_authId');
sessionStorage.removeItem('fayda_authId');
if (!authId) {
const msg = 'Session expired. Please try signing up again.';
setError(msg);
notify.error(msg);
return;
}
const handleCallback = async () => {
try {
const userInfo = await getUserInfoTrigger({
url: '/utilities/get-national-id-user',
method: 'POST',
body: { state, code, authId },
}).unwrap();
sessionStorage.setItem('fayda_userId', userInfo.sub);
sessionStorage.setItem('fayda_userInfo', JSON.stringify(userInfo));
notify.success('National ID verified successfully');
navigate('/set-password', { replace: true });
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Failed to complete National ID verification');
setError(msg);
notify.error(msg);
}
};
handleCallback();
}, [searchParams, getUserInfoTrigger, navigate]);
return (
<AuthShell
brandTitle="Verifying your National ID..."
brandSubtitle="Please wait while we securely verify your identity with the National ID service."
>
<Stack gap="lg" align="center">
<IconShieldCheck size={56} stroke={1.5} />
<div>
<Title order={2} fz={30} ta="center">
{error ? 'Verification failed' : 'Verifying your identity'}
</Title>
<Text c="dimmed" mt={6} ta="center">
{error
? error
: 'We are securely processing your National ID information. This should only take a moment.'}
</Text>
</div>
{!error && <Loader size="lg" />}
{error && (
<>
<Alert variant="light" color="red" icon={<IconAlertCircle size={18} />}>
{error}
</Alert>
<Text size="sm" c="dimmed" ta="center">
Please try again or use the standard sign-up method instead.
</Text>
</>
)}
</Stack>
</AuthShell>
);
}

View File

@@ -0,0 +1,139 @@
import { useState } from 'react';
import { Alert, Button, PasswordInput, Stack, Text, ThemeIcon, Title } from '@mantine/core';
import { IconArrowRight, IconLock, IconLockOpen } from '@tabler/icons-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell';
const schema = z
.object({
newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }),
confirmPassword: z.string().min(8, { message: 'Confirm your password' }),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});
type FormValues = z.infer<typeof schema>;
export function SetPasswordPage() {
const navigate = useNavigate();
const [serverError, setServerError] = useState<string | null>(null);
const [setPasswordTrigger, { isLoading }] = useApiMutation();
const userId = sessionStorage.getItem('fayda_userId');
const userInfoRaw = sessionStorage.getItem('fayda_userInfo');
let userName = 'your account';
if (userInfoRaw) {
try {
const info = JSON.parse(userInfoRaw);
userName = info.name || 'your account';
} catch {
// ignore
}
}
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(schema),
});
const onSubmit = async (values: FormValues) => {
if (!userId) {
notify.error('Session expired. Please sign up again.');
navigate('/signup', { replace: true });
return;
}
try {
await setPasswordTrigger({
url: '/v1/auth/set-fayda-password',
method: 'PATCH',
body: {
userId,
newPassword: values.newPassword,
confirmPassword: values.confirmPassword,
},
}).unwrap();
sessionStorage.removeItem('fayda_userId');
sessionStorage.removeItem('fayda_userInfo');
notify.success('Password set successfully');
navigate('/login', { replace: true });
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
setServerError(msg);
notify.error(msg);
}
};
return (
<AuthShell
brandTitle="Set your account password."
brandSubtitle="Choose a strong password to secure your account and access all Portal features."
>
<Stack gap="lg">
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
<IconLockOpen size={30} />
</ThemeIcon>
<div>
<Title order={2} fz={30}>
Set your password
</Title>
<Text c="dimmed" mt={6}>
Welcome, <Text span fw={600}>{userName}</Text>. Your National ID has been verified successfully.
Choose a password to complete your account setup.
</Text>
</div>
{serverError && (
<Alert variant="light" color="red" withCloseButton onClose={() => setServerError(null)}>
{serverError}
</Alert>
)}
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<PasswordInput
label="New password"
placeholder="At least 8 characters"
leftSection={<IconLock size={18} />}
error={errors.newPassword?.message}
{...register('newPassword')}
/>
<PasswordInput
label="Confirm password"
placeholder="Re-enter password"
leftSection={<IconLock size={18} />}
error={errors.confirmPassword?.message}
{...register('confirmPassword')}
/>
<Button
type="submit"
loading={isLoading}
fullWidth
size="md"
rightSection={<IconArrowRight size={18} />}
>
Set password &amp; continue
</Button>
</Stack>
</form>
</Stack>
</AuthShell>
);
}

View File

@@ -4,6 +4,7 @@ import {
Anchor,
Button,
Checkbox,
Divider,
Group,
PasswordInput,
SimpleGrid,
@@ -16,6 +17,7 @@ import {
IconArrowRight,
IconAt,
IconDeviceMobile,
IconId,
IconLock,
IconMail,
IconUser,
@@ -77,6 +79,7 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
isPhoneNumberVerified: boolean;
}>();
const [meTrigger] = useApiMutation<AuthUser>();
const [faydaInitTrigger] = useApiMutation<{ url: string; authId: string }>();
const {
register,
@@ -87,6 +90,23 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
defaultValues: { userType: 'individual' },
});
const handleFaydaSignup = async () => {
try {
const data = await faydaInitTrigger({
url: '/utilities/init-national-id-auth',
method: 'GET',
}).unwrap();
sessionStorage.setItem('fayda_authId', data.authId);
window.location.href = data.url;
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Failed to initiate National ID verification');
setServerError(msg);
notify.error(msg);
}
};
const onSubmit = async (values: FormValues) => {
try {
const payload: SignupPayload = {
@@ -246,6 +266,19 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
>
Create account
</Button>
<Divider label="or" labelPosition="center" variant="dashed" />
<Button
type="button"
variant="default"
fullWidth
size="md"
leftSection={<IconId size={18} />}
onClick={handleFaydaSignup}
>
Sign up with National ID (Fayda)
</Button>
</Stack>
</form>