diff --git a/src/features/auth/api/registrationApi.ts b/src/features/auth/api/registrationApi.ts index b1b7811..3bf1721 100644 --- a/src/features/auth/api/registrationApi.ts +++ b/src/features/auth/api/registrationApi.ts @@ -12,9 +12,14 @@ export interface RegistrationCompletePayload { code: string } -export interface LoginPayload { +export interface LoginStartPayload { + email: string +} + +export interface LoginCompletePayload { email: string password: string + code: string } export interface AuthResponse { @@ -29,8 +34,12 @@ export function registrationComplete(payload: RegistrationCompletePayload): Prom return api.post('/auth/registration/complete', payload) } -export function login(payload: LoginPayload): Promise { - return api.post('/auth/login', payload) +export function loginStart(payload: LoginStartPayload): Promise { + return api.post('/auth/login/start', payload) +} + +export function loginComplete(payload: LoginCompletePayload): Promise { + return api.post('/auth/login/complete', payload) } export async function logout(): Promise { diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 444233b..c373a4c 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -1,4 +1,4 @@ -export { registrationStart, registrationComplete, login } from './api/registrationApi' -export type { RegistrationStartPayload, RegistrationCompletePayload, LoginPayload, AuthResponse } from './api/registrationApi' +export { registrationStart, registrationComplete, loginStart, loginComplete } from './api/registrationApi' +export type { RegistrationStartPayload, RegistrationCompletePayload, LoginStartPayload, LoginCompletePayload, AuthResponse } from './api/registrationApi' export { useIsAuthenticated } from './hooks/useIsAuthenticated' export { useAuth, AUTH_QUERY_KEY } from './hooks/useAuth' diff --git a/src/widgets/login-form/model/useLoginForm.ts b/src/widgets/login-form/model/useLoginForm.ts index e5b770c..1a3fa4f 100644 --- a/src/widgets/login-form/model/useLoginForm.ts +++ b/src/widgets/login-form/model/useLoginForm.ts @@ -1,21 +1,33 @@ import { useState } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate, useLocation } from 'react-router-dom' -import { login } from '@features/auth' +import { loginStart, loginComplete, AUTH_QUERY_KEY } from '@features/auth' import { tokenStore } from '@shared/api/tokenStore' -import { AUTH_QUERY_KEY } from '@features/auth' import { ROUTES } from '@shared/config/routes' +import type { ApiErrorResponse } from '@shared/api/types' + +function extractErrorMessage(error: unknown): string { + const apiError = error as ApiErrorResponse + return apiError?.detail?.[0]?.msg ?? 'Произошла ошибка' +} export function useLoginForm() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') + const [verificationCode, setVerificationCode] = useState('') + const [codeSent, setCodeSent] = useState(false) const navigate = useNavigate() const location = useLocation() const queryClient = useQueryClient() const from = (location.state as { from?: { pathname: string } })?.from?.pathname ?? ROUTES.WALLET - const mutation = useMutation({ - mutationFn: login, + const startMutation = useMutation({ + mutationFn: loginStart, + onSuccess: () => setCodeSent(true), + }) + + const completeMutation = useMutation({ + mutationFn: loginComplete, onSuccess: ({ access_token }) => { tokenStore.set(access_token) queryClient.setQueryData(AUTH_QUERY_KEY, access_token) @@ -23,14 +35,29 @@ export function useLoginForm() { }, }) + const handleRequestCode = () => { + if (!email) return + startMutation.mutate({ email }) + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + completeMutation.mutate({ email, password, code: verificationCode }) + } + + const error = + (startMutation.isError ? extractErrorMessage(startMutation.error) : null) ?? + (completeMutation.isError ? extractErrorMessage(completeMutation.error) : null) + return { email, setEmail, password, setPassword, - isLoading: mutation.isPending, - error: mutation.isError ? 'Неверный email или пароль' : null, - handleSubmit: (e: React.FormEvent) => { - e.preventDefault() - mutation.mutate({ email, password }) - }, + verificationCode, setVerificationCode, + codeSent, + isLoadingCode: startMutation.isPending, + isLoadingSubmit: completeMutation.isPending, + error, + handleRequestCode, + handleSubmit, } } diff --git a/src/widgets/login-form/ui/LoginForm.module.css b/src/widgets/login-form/ui/LoginForm.module.css index ae8f032..aaf56f4 100644 --- a/src/widgets/login-form/ui/LoginForm.module.css +++ b/src/widgets/login-form/ui/LoginForm.module.css @@ -2,9 +2,9 @@ background: var(--glass-bg); border: 1px solid var(--glass-border); border-radius: 24px; - padding: 48px 40px; + padding: 32px; width: 100%; - max-width: 480px; + max-width: 600px; } .logo { @@ -26,10 +26,48 @@ line-height: 1.3; } -.fields { +.twoCol { + display: grid; + grid-template-columns: 1fr; + gap: 20px 24px; + align-items: start; +} + +.leftCol { display: flex; flex-direction: column; - gap: 12px; + gap: 20px; +} + +.rightCol { + display: flex; + flex-direction: column; + gap: 8px; +} + +.codeHint { + font-size: 12px; + color: var(--text-secondary); + text-decoration: underline; + cursor: pointer; +} + +.error { + color: #ff5a5a; + font-size: 13px; + margin-top: 12px; + text-align: center; +} + +.submitWrapper { + margin-top: 28px; +} + +.footer { + display: flex; + flex-direction: column; + gap: 0; + margin-top: 16px; } .forgot { @@ -37,7 +75,7 @@ text-align: right; font-size: 13px; color: var(--interactive); - margin: 8px 0 16px; + margin-bottom: 16px; cursor: pointer; text-decoration: none; transition: color 0.2s; @@ -47,17 +85,11 @@ color: var(--highlight); } -.actions { - display: flex; - flex-direction: column; - gap: 0; -} - .divider { display: flex; align-items: center; gap: 16px; - margin: 24px 0; + margin: 16px 0; } .divider::before, @@ -73,14 +105,13 @@ color: var(--text-secondary); } -@media (max-width: 520px) { +@media (max-width: 560px) { .card { padding: 32px 20px; - min-height: 100vh; - min-width: 100%; border-radius: 0; - display: flex; - flex-direction: column; - justify-content: center; } -} \ No newline at end of file + + .twoCol { + grid-template-columns: 1fr; + } +} diff --git a/src/widgets/login-form/ui/LoginForm.tsx b/src/widgets/login-form/ui/LoginForm.tsx index 1b1bf10..f3c14c7 100644 --- a/src/widgets/login-form/ui/LoginForm.tsx +++ b/src/widgets/login-form/ui/LoginForm.tsx @@ -8,7 +8,17 @@ import styles from './LoginForm.module.css' import { useLoginForm } from '../model/useLoginForm' export function LoginForm() { - const { email, setEmail, password, setPassword, isLoading, error, handleSubmit } = useLoginForm() + const { + email, setEmail, + password, setPassword, + verificationCode, setVerificationCode, + codeSent, + isLoadingCode, + isLoadingSubmit, + error, + handleRequestCode, + handleSubmit, + } = useLoginForm() const navigate = useNavigate() return ( @@ -18,33 +28,57 @@ export function LoginForm() {

Войти в кошелёк ЭКСА

-
- - +
+
+ + +
+ +
+ + Код не пришёл + +
{error &&

{error}

} - Забыли пароль? +
+ +
-
- +
+ Забыли пароль?
или
-