mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: fayda integration paritial
This commit is contained in:
@@ -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';
|
||||
|
||||
110
libs/auth/src/lib/pages/FaydaCallbackPage.tsx
Normal file
110
libs/auth/src/lib/pages/FaydaCallbackPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
139
libs/auth/src/lib/pages/SetPasswordPage.tsx
Normal file
139
libs/auth/src/lib/pages/SetPasswordPage.tsx
Normal 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 & continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</AuthShell>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user