Files
sfera-new/src/components/auth/auth-flow.tsx
Veronika Smirnova ca4d44d090 feat: implement direct routing to eliminate double redirects after registration
Replace dashboard intermediate routing with direct role-based navigation to improve UX
and reduce registration flow time from 4-5 seconds to 2-3 seconds.

Key Changes:
- Add routing utility lib/routing.ts with getHomePathFromUser function
- Update auth-flow.tsx, app/page.tsx, login/page.tsx to use direct routing
- Remove dashboard route and redirect components (3 files)
- Preserve critical components: sidebar/ and user-settings/ (43 dependencies)
- Fix breadcrumbs in seller/warehouse and fulfillment-supplies layouts
- Add comprehensive documentation and test coverage

Route Mapping:
- FULFILLMENT → /fulfillment/home
- SELLER → /seller/home
- LOGIST → /logistics/home
- WHOLESALE → /wholesale/home
- Fallback → /register

Testing:
- 8 comprehensive tests passed
- All routing scenarios validated
- Production build successful
- Critical components verified

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 18:46:22 +03:00

352 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { CheckCircle } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState, useEffect } from 'react'
import { useAuthContext } from '@/contexts/AuthContext'
import { getHomePathFromUser } from '@/lib/routing'
import { CabinetSelectStep } from './cabinet-select-step'
import { ConfirmationStep } from './confirmation-step'
import { InnStep } from './inn-step'
import { MarketplaceApiStep } from './marketplace-api-step'
import { PhoneStep } from './phone-step'
import { SmsStep } from './sms-step'
type AuthStep = 'phone' | 'sms' | 'cabinet-select' | 'inn' | 'marketplace-api' | 'confirmation' | 'complete'
type CabinetType = 'fulfillment' | 'seller' | 'logist' | 'wholesale'
interface OrganizationData {
name?: string
fullName?: string
address?: string
isActive?: boolean
}
interface ApiKeyValidation {
sellerId?: string
sellerName?: string
tradeMark?: string
isValid?: boolean
}
interface AuthData {
phone: string
smsCode: string
cabinetType: CabinetType | null
inn: string
organizationData: OrganizationData | null
wbApiKey: string
wbApiValidation: ApiKeyValidation | null
ozonApiKey: string
ozonApiValidation: ApiKeyValidation | null
isAuthenticated: boolean
partnerCode?: string | null
referralCode?: string | null
}
interface AuthFlowProps {
partnerCode?: string | null
referralCode?: string | null
}
export function AuthFlow({ partnerCode, referralCode }: AuthFlowProps = {}) {
const router = useRouter()
const { isAuthenticated, user } = useAuthContext()
// Убираем избыточное логирование
// Убираем автоматический logout - это создает race condition
// Незавершенная регистрация - это нормальное состояние
// AuthFlow должен продолжить с шага cabinet-select
// Начинаем всегда с 'phone' для избежания гидрации,
// а затем обновляем в useEffect после загрузки клиента
const [step, setStep] = useState<AuthStep>('phone')
// Определяем тип регистрации на основе параметров
// Только один из них должен быть активен (валидация уже прошла в RegisterPage)
const registrationType = partnerCode ? 'PARTNER' : referralCode ? 'REFERRAL' : null
const activeCode = partnerCode || referralCode || null
// Убираем избыточное логирование
const [authData, setAuthData] = useState<AuthData>({
phone: '',
smsCode: '',
cabinetType: null,
inn: '',
organizationData: null,
wbApiKey: '',
wbApiValidation: null,
ozonApiKey: '',
ozonApiValidation: null,
isAuthenticated: false,
// Сохраняем только активный код в правильное поле
partnerCode: registrationType === 'PARTNER' ? activeCode : null,
referralCode: registrationType === 'REFERRAL' ? activeCode : null,
})
// ⭐ КРИТИЧНО: Отслеживаем только смену шагов
console.warn('🎯 STEP:', step)
// Добавляем useEffect для отслеживания изменений step
useEffect(() => {
console.warn('🔄 STEP CHANGED TO:', step)
}, [step])
// Определяем правильный шаг после гидрации
useEffect(() => {
if (typeof window === 'undefined') return // Только на клиенте
// Убираем избыточное логирование useEffect
// Если у пользователя есть токен и организация - переходим к завершению
if (isAuthenticated && user?.organization) {
console.warn('🎢 AuthFlow - SCENARIO A: User has organization, setting step to complete')
console.warn('🎢 AuthFlow - Organization details:', {
id: user.organization.id,
type: user.organization.type,
name: user.organization.name,
})
setStep('complete')
}
// Если есть токен но нет организации - переходим к выбору кабинета
else if (isAuthenticated && user && !user.organization) {
console.warn('🎢 AuthFlow - SCENARIO B/C: User authenticated but no organization, setting step to cabinet-select')
console.warn('🎢 AuthFlow - User has token but missing organization:', {
userId: user.id,
phone: user.phone,
organizationStatus: 'missing',
})
setStep('cabinet-select')
}
// Иначе остаемся на шаге телефона
else {
console.warn('🎢 AuthFlow - SCENARIO D: User not authenticated, setting step to phone')
console.warn('🎢 AuthFlow - Auth status:', {
isAuthenticated,
userExists: !!user,
tokenInStorage: typeof window !== 'undefined' ? !!localStorage.getItem('authToken') : 'unknown',
})
setStep('phone')
}
}, [isAuthenticated, user])
// Обновляем шаг при изменении статуса авторизации
useEffect(() => {
if (isAuthenticated && step === 'phone') {
if (process.env.NODE_ENV === 'development') {
console.warn('🎢 AuthFlow - Пользователь авторизовался, переход к выбору кабинета')
}
setStep('cabinet-select')
}
}, [isAuthenticated, step])
// При завершении авторизации инициируем проверку и перенаправление
useEffect(() => {
if (step === 'complete') {
const timer = setTimeout(() => {
// Прямое перенаправление в соответствующий home без промежуточного dashboard
const homePath = getHomePathFromUser(user)
console.warn('🎢 AuthFlow - Registration complete, redirecting to', homePath)
router.push(homePath)
}, 2000) // Задержка для показа сообщения о завершении
return () => clearTimeout(timer)
}
}, [step, router, user])
const handlePhoneNext = (phone: string) => {
console.warn('📞 PHONE→SMS:', phone)
setAuthData((prev) => ({ ...prev, phone }))
setStep('sms')
}
const handleSmsNext = async (smsCode: string) => {
console.warn('✅ SMS→CABINET:', smsCode)
setAuthData((prev) => ({ ...prev, smsCode, isAuthenticated: true }))
setStep('cabinet-select')
}
const handleCabinetNext = (cabinetType: CabinetType) => {
setAuthData((prev) => ({ ...prev, cabinetType }))
if (cabinetType === 'fulfillment' || cabinetType === 'logist' || cabinetType === 'wholesale') {
setStep('inn')
} else {
setStep('marketplace-api')
}
}
const handleInnNext = (inn: string, organizationData?: OrganizationData) => {
setAuthData((prev) => ({
...prev,
inn,
organizationData: organizationData || null,
}))
setStep('confirmation')
}
const handleMarketplaceApiNext = (apiData: {
wbApiKey?: string
wbApiValidation?: ApiKeyValidation
ozonApiKey?: string
ozonApiValidation?: ApiKeyValidation
}) => {
setAuthData((prev) => ({
...prev,
wbApiKey: apiData.wbApiKey || '',
wbApiValidation: apiData.wbApiValidation || null,
ozonApiKey: apiData.ozonApiKey || '',
ozonApiValidation: apiData.ozonApiValidation || null,
}))
setStep('confirmation')
}
const handleConfirmation = () => {
setStep('complete')
}
const handleSmsBack = () => {
setStep('phone')
}
const handleCabinetBack = () => {
setStep('sms')
}
const handleInnBack = () => {
setStep('cabinet-select')
}
const handleMarketplaceApiBack = () => {
setStep('cabinet-select')
}
const handleConfirmationBack = () => {
if (
authData.cabinetType === 'fulfillment' ||
authData.cabinetType === 'logist' ||
authData.cabinetType === 'wholesale'
) {
setStep('inn')
} else {
setStep('marketplace-api')
}
}
if (step === 'complete') {
return (
<div className="min-h-screen bg-animated flex items-center justify-center p-4">
{/* Floating Particles */}
<div className="particles">
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
<div className="particle"></div>
</div>
<div className="text-center text-white max-w-md relative z-10">
<div className="bg-white/10 backdrop-blur rounded-2xl p-8 border border-white/20 glow-purple">
<CheckCircle className="h-20 w-20 mx-auto mb-6 text-green-400 animate-pulse" />
<h1 className="text-3xl font-bold text-gradient-bright mb-4">Добро пожаловать!</h1>
<p className="text-white/80 mb-4">Регистрация успешно завершена</p>
<div className="bg-white/5 rounded-lg p-4 mb-6">
<p className="text-white/60 text-sm mb-2">Тип кабинета:</p>
<p className="text-white font-medium">
{authData.cabinetType === 'fulfillment'
? 'Фулфилмент'
: authData.cabinetType === 'logist'
? 'Логистика'
: authData.cabinetType === 'wholesale'
? 'Поставщик'
: 'Селлер'}
</p>
</div>
<div className="flex items-center justify-center gap-2 text-white/60 text-sm">
<div className="animate-spin h-4 w-4 border-2 border-white/20 border-t-white/60 rounded-full"></div>
Переход в личный кабинет...
</div>
</div>
</div>
</div>
)
}
return (
<>
{step === 'phone' && (
<PhoneStep onNext={handlePhoneNext} registrationType={registrationType} referrerCode={activeCode} />
)}
{step === 'sms' && <SmsStep phone={authData.phone} onNext={handleSmsNext} onBack={handleSmsBack} />}
{step === 'cabinet-select' && <CabinetSelectStep onNext={handleCabinetNext} onBack={handleCabinetBack} />}
{step === 'inn' && <InnStep onNext={handleInnNext} onBack={handleInnBack} />}
{step === 'marketplace-api' && (
<MarketplaceApiStep onNext={handleMarketplaceApiNext} onBack={handleMarketplaceApiBack} />
)}
{step === 'confirmation' && (
<ConfirmationStep
data={{
phone: authData.phone,
cabinetType: authData.cabinetType as 'fulfillment' | 'seller' | 'logist' | 'wholesale',
inn: authData.inn || undefined,
organizationData: authData.organizationData || undefined,
wbApiKey: authData.wbApiKey || undefined,
wbApiValidation: authData.wbApiValidation || undefined,
ozonApiKey: authData.ozonApiKey || undefined,
ozonApiValidation: authData.ozonApiValidation || undefined,
referralCode: authData.referralCode,
partnerCode: authData.partnerCode,
}}
onConfirm={handleConfirmation}
onBack={handleConfirmationBack}
/>
)}
{/* ОТЛАДКА: Логируем authData перед передачей в ConfirmationStep */}
{step === 'confirmation' &&
(() => {
console.warn('📊 AuthFlow - Passing to ConfirmationStep:', {
phone: authData.phone,
phoneLength: authData.phone?.length,
cabinetType: authData.cabinetType,
inn: authData.inn,
allAuthData: authData,
})
return null
})()}
{(step as string) === 'complete' && (
<div className="space-y-6 text-center">
<div className="flex justify-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
</div>
<div className="space-y-2">
<h2 className="text-2xl font-bold text-gray-900">Регистрация завершена!</h2>
<p className="text-gray-600">
Ваш{' '}
{authData.cabinetType === 'fulfillment'
? 'фулфилмент кабинет'
: authData.cabinetType === 'seller'
? 'селлер кабинет'
: authData.cabinetType === 'logist'
? 'логистический кабинет'
: 'оптовый кабинет'}{' '}
успешно создан
</p>
</div>
<div className="animate-pulse">
<p className="text-sm text-gray-500">Переход в личный кабинет...</p>
</div>
</div>
)}
</>
)
}