frontend phase 1: auth — phone-OTP login, role routing & session refresh

Replace the username/password stub with Balinyaar's real credential (phone-OTP)
and add the role router every authenticated screen sits behind.

- services/auth rewritten for OTP over the b2 contract: types/keys/constants,
  clientApi+mockApi behind a config'd seam, hooks (useRequestOtp/useVerifyOtp/
  useMe/useRefresh/useLogout/useSelectRole/useSessionRoleSync). Stub removed.
- A1/A2 customer login + B1/B2 nurse switch as one OTP flow at /login
  (PhoneStep/OtpStep: auto-verify, resend countdown, wrong/expired/lockout states).
- Role router: pure resolveRoleDestination + RoleRouter -> family / nurse /
  admin / select-role, with a splash while /me loads (no wrong-shell flash).
- SelectRole first-use screen at /select-role.
- Widened AuthState (roles via SessionUser), hydrated from /me by useSessionRoleSync.
- Fetch-layer silent token refresh (single-flight + one retry) + shared
  persistAuthTokens/clearAuthTokens; useRefresh as the on-demand path.
- auth i18n namespace in both locales; tests for routing branches, countdown,
  OtpStep state machine, RoleRouter branches; fixed jest @/ -> src alias.
- Docs: client/CLAUDE.md, frontend STATUS/report, for-backend REQ-002..004.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-02 11:58:52 +03:30
parent 3a51305343
commit 17a82832ab
50 changed files with 1815 additions and 108 deletions
+62
View File
@@ -0,0 +1,62 @@
'use client';
import { FunctionComponent, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { APP_ROLES, type AppRole } from '@/constants';
import { OTP_RESEND_FALLBACK_SECONDS } from '@/services/auth/constants';
import AuthCard from './AuthCard';
import PhoneStep from './PhoneStep';
import OtpStep from './OtpStep';
import RoleRouter from './RoleRouter';
type Step = 'phone' | 'otp' | 'routing';
/**
* The single login stack for both actors (A1/A2 customer, B1/B2 nurse). One OTP mechanism,
* parameterised by `intendedRole` (seeded from `?role=nurse`) — no forked login trees. After a
* successful verify it hands off to the role router.
* @component LoginFlow
*/
const LoginFlow: FunctionComponent = () => {
const searchParams = useSearchParams();
const [intendedRole, setIntendedRole] = useState<AppRole>(
searchParams.get('role') === APP_ROLES.NURSE ? APP_ROLES.NURSE : APP_ROLES.CUSTOMER,
);
const [step, setStep] = useState<Step>('phone');
const [phone, setPhone] = useState('');
const [resendSeconds, setResendSeconds] = useState(OTP_RESEND_FALLBACK_SECONDS);
if (step === 'routing') {
return <RoleRouter intendedRole={intendedRole} />;
}
return (
<AuthCard>
{step === 'phone' ? (
<PhoneStep
intendedRole={intendedRole}
onSwitchRole={() =>
setIntendedRole((role) =>
role === APP_ROLES.NURSE ? APP_ROLES.CUSTOMER : APP_ROLES.NURSE,
)
}
onSent={(nextPhone, result) => {
setPhone(nextPhone);
setResendSeconds(result.resendAvailableInSeconds);
setStep('otp');
}}
/>
) : (
<OtpStep
phone={phone}
intendedRole={intendedRole}
resendSeconds={resendSeconds}
onVerified={() => setStep('routing')}
onChangeNumber={() => setStep('phone')}
/>
)}
</AuthCard>
);
};
export default LoginFlow;