buhapp-mobile/src/screens/auth/RegisterScreen.tsx
ga 4bd3ebae99 Sprint 4: React Native mobile app (iOS+Android)
Screens:
- LoginScreen / RegisterScreen (with mandatory consents checkboxes)
- MapScreen — MAIN screen after login (Google Maps on Android, Apple Maps on iOS)
  * shows nearby users within radius
  * visibility toggle (hide/show myself on map)
  * radius selector 1/5/10/25/50 km
  * GPS auto-update with permission flow
- ChatListScreen / ChatScreen with WebSocket realtime
  * long-press message: edit/delete
  * 'ред.' marker for edited messages
  * mark as read
- ProfileScreen — bio, city, gender, drinks, activities, purposes

State: Zustand (auth, map stores)
HTTP: axios + JWT interceptor + token refresh
WS: socket.io-client
Storage: AsyncStorage for tokens
Maps: react-native-maps
Permissions: react-native-permissions
2026-08-20 15:56:17 +00:00

180 lines
7.3 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.

import React, { useState } from 'react';
import {
View, Text, TextInput, TouchableOpacity, ScrollView, Switch, StyleSheet, Alert,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useAuth } from '@/store/auth';
import { Colors, Spacing, FontSizes, Radius } from '@/theme/colors';
import { Config } from '@/config';
import type { Gender } from '@/types/api';
export default function RegisterScreen() {
const nav = useNavigation<any>();
const { register, loading, error } = useAuth();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
const [birthdate, setBirthdate] = useState('');
const [gender, setGender] = useState<Gender | ''>('');
const [city, setCity] = useState('');
const [cAdult, setCAdult] = useState(false);
const [cTerms, setCTerms] = useState(false);
const [cPrivacy, setCPrivacy] = useState(false);
const [cDisclaimer, setCDisclaimer] = useState(false);
const allConsents = cAdult && cTerms && cPrivacy && cDisclaimer;
const canSubmit =
name.trim().length > 0 &&
(email.trim() || phone.trim()) &&
password.length >= Config.MIN_PASSWORD_LENGTH &&
allConsents &&
!loading;
const onSubmit = async () => {
if (!allConsents) {
Alert.alert('Согласия', 'Подтвердите все 4 пункта: 18+, соглашение, политика, отказ');
return;
}
try {
await register({
email: email.trim() || undefined,
phone: phone.trim() || undefined,
password,
name: name.trim(),
birthdate: birthdate.trim() || undefined,
gender: gender || undefined,
city: city.trim() || undefined,
consents: {
adult: cAdult,
terms: cTerms,
privacy: cPrivacy,
disclaimer: cDisclaimer,
},
});
} catch (e: any) {
Alert.alert('Ошибка', e?.response?.data?.error ?? 'не удалось зарегистрироваться');
}
};
return (
<ScrollView style={styles.root} contentContainerStyle={styles.content}>
<Text style={styles.title}>Регистрация</Text>
<Text style={styles.subtitle}>BuhApp для совершеннолетних</Text>
<Field label="Имя" value={name} onChange={setName} placeholder="Как тебя зовут" />
<Field label="Email" value={email} onChange={setEmail} placeholder="you@example.com" keyboardType="email-address" autoCapitalize="none" />
<Field label="Телефон (опционально)" value={phone} onChange={setPhone} placeholder="+79991234567" keyboardType="phone-pad" />
<Field label="Пароль" value={password} onChange={setPassword} placeholder={`минимум ${Config.MIN_PASSWORD_LENGTH} символов`} secureTextEntry />
<Field label="Дата рождения (ГГГГ-ММ-ДД)" value={birthdate} onChange={setBirthdate} placeholder="1995-06-15" autoCapitalize="none" />
<Field label="Город" value={city} onChange={setCity} placeholder="Москва" />
<Text style={styles.label}>Пол</Text>
<View style={styles.row}>
{(['m','f','o'] as Gender[]).map((g) => (
<TouchableOpacity
key={g}
style={[styles.chip, gender === g && styles.chipActive]}
onPress={() => setGender(g)}>
<Text style={[styles.chipText, gender === g && styles.chipTextActive]}>
{g === 'm' ? 'Мужской' : g === 'f' ? 'Женский' : 'Другое'}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={[styles.label, { marginTop: Spacing.l }]}>Согласия (обязательно)</Text>
<ConsentRow
label={`Мне исполнилось 18 лет`}
value={cAdult} onChange={setCAdult}
/>
<ConsentRow
label={`Пользовательское соглашение (версия 1.0)`}
value={cTerms} onChange={setCTerms}
/>
<ConsentRow
label={`Политика конфиденциальности (152-ФЗ, версия 1.0)`}
value={cPrivacy} onChange={setCPrivacy}
/>
<ConsentRow
label={`Отказ от ответственности (версия 1.0)`}
value={cDisclaimer} onChange={setCDisclaimer}
/>
{error && <Text style={styles.error}>{error}</Text>}
<TouchableOpacity
style={[styles.btn, !canSubmit && styles.btnDisabled]}
disabled={!canSubmit}
onPress={onSubmit}>
<Text style={styles.btnText}>{loading ? 'Отправка...' : 'Создать аккаунт'}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => nav.navigate('Login')} style={{ marginTop: Spacing.l }}>
<Text style={styles.link}>У меня уже есть аккаунт</Text>
</TouchableOpacity>
</ScrollView>
);
}
function Field({ label, ...props }: any) {
return (
<View style={{ marginBottom: Spacing.m }}>
<Text style={styles.label}>{label}</Text>
<TextInput
style={styles.input}
placeholderTextColor={Colors.textDim}
{...props}
/>
</View>
);
}
function ConsentRow({ label, value, onChange }: { label: string; value: boolean; onChange: (v: boolean) => void }) {
return (
<View style={styles.consent}>
<Switch
value={value}
onValueChange={onChange}
trackColor={{ true: Colors.primary }}
thumbColor={value ? Colors.primaryDark : '#f4f3f4'}
/>
<Text style={styles.consentText}>{label}</Text>
</View>
);
}
const styles = StyleSheet.create({
root: { flex: 1, backgroundColor: Colors.bg },
content: { padding: Spacing.l, paddingTop: Spacing.xxl },
title: { fontSize: FontSizes.xxl, fontWeight: '700', color: Colors.text },
subtitle: { fontSize: FontSizes.m, color: Colors.textDim, marginBottom: Spacing.l },
label: { fontSize: FontSizes.s, color: Colors.textDim, marginBottom: Spacing.xs },
input: {
borderWidth: 1, borderColor: Colors.border, borderRadius: Radius.m,
paddingHorizontal: Spacing.m, paddingVertical: Spacing.s,
fontSize: FontSizes.m, color: Colors.text, backgroundColor: Colors.bg,
},
row: { flexDirection: 'row', gap: Spacing.s as any },
chip: {
paddingVertical: Spacing.s, paddingHorizontal: Spacing.m,
borderRadius: Radius.full, borderWidth: 1, borderColor: Colors.border,
marginRight: Spacing.s,
},
chipActive: { backgroundColor: Colors.primary, borderColor: Colors.primary },
chipText: { color: Colors.text, fontSize: FontSizes.s },
chipTextActive: { color: Colors.textInverse, fontWeight: '600' },
consent: { flexDirection: 'row', alignItems: 'center', marginBottom: Spacing.s, gap: Spacing.s as any },
consentText: { flex: 1, fontSize: FontSizes.s, color: Colors.text },
btn: {
backgroundColor: Colors.primary, paddingVertical: Spacing.m, borderRadius: Radius.m,
alignItems: 'center', marginTop: Spacing.l,
},
btnDisabled: { opacity: 0.5 },
btnText: { color: Colors.textInverse, fontSize: FontSizes.l, fontWeight: '600' },
link: { color: Colors.secondary, textAlign: 'center', fontSize: FontSizes.m },
error: { color: Colors.danger, fontSize: FontSizes.s, marginTop: Spacing.s },
});