Files
IdP/apps/frontend/app/auth/register/page.tsx
lendry a4b4577c55 fix
2026-07-10 10:37:22 +03:00

210 lines
8.7 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 Link from 'next/link';
import { FormEvent, useState } from 'react';
import { useRouter } from 'next/navigation';
import { ChevronLeft, DoorOpen, UserRound } from 'lucide-react';
import { BrandLogo } from '@/components/id/brand-logo';
import { useAuth } from '@/components/id/auth-provider';
import { useToast } from '@/components/id/toast-provider';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { PinInput } from '@/components/ui/pin-input';
import { isPinInputComplete } from '@/lib/pin-input';
import { OtpInput } from '@/components/ui/otp-input';
import { PhoneInput, phoneCountries, toE164 } from '@/components/ui/phone-input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
type Step = 'contact' | 'otp' | 'pin';
export default function RegisterPage() {
const router = useRouter();
const { sendLoginOtp, verifyLoginOtp, completePin, logout } = useAuth();
const { showToast } = useToast();
const [authTab, setAuthTab] = useState<'email' | 'phone'>('email');
const [email, setEmail] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [country, setCountry] = useState(phoneCountries[0]);
const [recipient, setRecipient] = useState('');
const [maskedTarget, setMaskedTarget] = useState('');
const [otp, setOtp] = useState('');
const [pin, setPin] = useState('');
const [pendingSessionId, setPendingSessionId] = useState<string | null>(null);
const [step, setStep] = useState<Step>('contact');
const [isSubmitting, setIsSubmitting] = useState(false);
function getIdentifier() {
return authTab === 'email' ? email.trim() : toE164(country, phoneNumber);
}
function finishRegistration(pinVerified: boolean, sessionId: string) {
if (!pinVerified) {
setPendingSessionId(sessionId);
setStep('pin');
return;
}
showToast('Добро пожаловать! ID успешно создан.');
router.push('/');
}
async function handleSendCode(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setIsSubmitting(true);
try {
const identifier = getIdentifier();
setRecipient(identifier);
const response = await sendLoginOtp(identifier);
setMaskedTarget(response.maskedTarget);
setOtp('');
setStep('otp');
showToast('Код отправлен. Введите его ниже.');
} catch (error) {
showToast(error instanceof Error ? error.message : 'Не удалось отправить код');
} finally {
setIsSubmitting(false);
}
}
async function verifyOtp(code: string) {
setIsSubmitting(true);
try {
const response = await verifyLoginOtp(recipient, code);
if (response.auth) {
finishRegistration(response.auth.pinVerified, response.auth.sessionId);
} else {
showToast('Не удалось завершить регистрацию');
}
} catch (error) {
setOtp('');
showToast(error instanceof Error ? error.message : 'Неверный код');
} finally {
setIsSubmitting(false);
}
}
async function handlePinSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!pendingSessionId) return;
setIsSubmitting(true);
try {
await completePin(pendingSessionId, pin);
showToast('Добро пожаловать! ID успешно создан.');
router.push('/');
} catch (error) {
showToast(error instanceof Error ? error.message : 'Не удалось проверить PIN-код');
} finally {
setIsSubmitting(false);
}
}
function resetToContact() {
setStep('contact');
setOtp('');
setPin('');
setPendingSessionId(null);
}
return (
<main className="flex min-h-screen items-center justify-center bg-[radial-gradient(circle_at_center,#5d6578_0%,#2c2736_48%,#111016_100%)] px-5">
<section className="w-full max-w-[430px] rounded-[34px] bg-[#1f2028] px-9 py-10 text-white shadow-2xl">
<div className="flex flex-col items-center text-center">
<BrandLogo size="lg" variant="light" />
<h1 className="mt-8 text-xl font-bold">Создайте ID</h1>
<p className="mt-2 text-sm text-[#b9bdc9]">
{step === 'contact' ? 'Введите почту или телефон. Пароль не нужен.' : 'Подтвердите код — аккаунт создастся автоматически.'}
</p>
</div>
{step !== 'contact' ? (
<button type="button" onClick={resetToContact} className="mt-6 flex items-center gap-1 text-sm text-[#b9bdc9] hover:text-white">
<ChevronLeft className="h-4 w-4" />
Изменить почту или телефон
</button>
) : null}
{step === 'contact' ? (
<form className="mt-8 space-y-3" onSubmit={handleSendCode}>
<Tabs value={authTab} onValueChange={(value) => setAuthTab(value as 'email' | 'phone')} className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="email">Почта</TabsTrigger>
<TabsTrigger value="phone">Телефон</TabsTrigger>
</TabsList>
<TabsContent value="email">
<Input
className="h-[56px] border-[#555762] bg-transparent text-white placeholder:text-[#8f92a0]"
placeholder="name@example.com"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
required={authTab === 'email'}
/>
</TabsContent>
<TabsContent value="phone">
<PhoneInput country={country} value={phoneNumber} onCountryChange={setCountry} onValueChange={setPhoneNumber} />
</TabsContent>
</Tabs>
<Button variant="white" size="lg" className="w-full rounded-[18px]" disabled={isSubmitting}>
{isSubmitting ? 'Отправляем...' : 'Получить код'}
</Button>
</form>
) : null}
{step === 'otp' ? (
<div className="mt-8">
<div className="mb-5 flex items-center gap-3 rounded-[20px] bg-[#2a2c36] p-3">
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-white/10">
<UserRound className="h-5 w-5" />
</div>
<div className="min-w-0 text-left">
<p className="text-sm text-[#b9bdc9]">Код отправлен на</p>
<p className="truncate text-sm font-semibold">{maskedTarget || recipient}</p>
</div>
</div>
<OtpInput value={otp} onChange={setOtp} onComplete={verifyOtp} disabled={isSubmitting} />
{isSubmitting ? <p className="mt-3 text-center text-sm text-[#b9bdc9]">Создаём ваш ID...</p> : null}
</div>
) : null}
{step === 'pin' && pendingSessionId ? (
<form onSubmit={handlePinSubmit} className="relative mt-8 space-y-3">
<button
type="button"
onClick={() => {
logout();
setPendingSessionId(null);
setPin('');
setStep('contact');
}}
disabled={isSubmitting}
title="Выйти из аккаунта"
aria-label="Выйти из аккаунта"
className="absolute -top-1 right-0 flex h-9 w-9 items-center justify-center rounded-full text-[#b9bdc9] transition hover:bg-white/10 hover:text-white disabled:opacity-50"
>
<DoorOpen className="h-4 w-4" />
</button>
<p className="text-center text-sm text-[#b9bdc9]">Установите PIN для завершения регистрации</p>
<PinInput
className="h-[58px] border-[#555762] bg-transparent text-center text-lg tracking-[0.4em] text-white placeholder:text-[#8f92a0]"
placeholder="PIN"
value={pin}
onChange={setPin}
required
minLength={4}
maxLength={6}
/>
<Button variant="white" size="lg" className="w-full rounded-[18px] text-base" disabled={isSubmitting || !isPinInputComplete(pin)}>
{isSubmitting ? 'Проверяем...' : 'Подтвердить PIN'}
</Button>
</form>
) : null}
{step === 'contact' ? (
<Button asChild variant="ghost" className="mt-5 w-full text-white hover:bg-[#2a2c36]">
<Link href="/auth/login">У меня уже есть ID</Link>
</Button>
) : null}
</section>
</main>
);
}