Initial commit

This commit is contained in:
Samandar Turgunboyev
2025-08-26 16:26:59 +05:00
commit fd95422447
318 changed files with 38301 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
// form.ts
import { z } from 'zod';
export const loginSchema = z.object({
phone: z.string().min(12, 'Xato raqam kiritildi'),
passportSeriya: z.string().length(2, '2 ta harf kerak'),
passportNumber: z.string().length(7, '7 ta raqam kerak'),
branchId: z.number().min(1, 'Filialni tanlang'),
});
export type LoginFormType = z.infer<typeof loginSchema>;

View File

@@ -0,0 +1,32 @@
import { create } from 'zustand';
interface UserState {
firstName: string;
lastName: string;
phoneNumber: string;
expireTime: number;
setExpireTime: (time: number) => void;
setUser: (user: Partial<UserState>) => void;
clearUser: () => void;
}
export const useUserStore = create<UserState>(set => ({
firstName: '',
lastName: '',
phoneNumber: '',
expireTime: 0,
setExpireTime: time => set({ expireTime: time }),
setUser: user =>
set(state => ({
...state,
...user,
})),
clearUser: () =>
set({
firstName: '',
lastName: '',
phoneNumber: '',
}),
}));

View File

@@ -0,0 +1,321 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import { otpPayload, resendPayload } from 'api/auth/type';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
ImageBackground,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
import LanguageSelector from 'screens/auth/select-language/SelectLang';
import { RootStackParamList } from 'types/types';
import { useUserStore } from '../lib/userstore';
import { Loginstyle } from './styled';
type VerificationCodeScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'Confirm'
>;
const OTP_LENGTH = 4;
const Confirm = () => {
const navigation = useNavigation<VerificationCodeScreenNavigationProp>();
const { t } = useTranslation();
const [code, setCode] = useState<string[]>(new Array(OTP_LENGTH).fill(''));
const [timer, setTimer] = useState(60);
const [errorConfirm, setErrorConfirm] = useState<string | null>(null);
const [canResend, setCanResend] = useState(false);
const inputRefs = useRef<Array<TextInput | null>>([]);
const { phoneNumber } = useUserStore(state => state);
const { mutate, isPending } = useMutation({
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
onSuccess: async res => {
await AsyncStorage.setItem('token', res.data.accessToken);
navigation.navigate('Home');
setErrorConfirm(null);
},
onError: (err: any) => {
setErrorConfirm(err?.response.data.message);
},
});
const { mutate: resendMutate } = useMutation({
mutationFn: (payload: resendPayload) => authApi.resendOtp(payload),
onSuccess: async res => {
setTimer(60);
setCanResend(false);
setCode(new Array(OTP_LENGTH).fill(''));
inputRefs.current[0]?.focus();
setErrorConfirm(null);
},
onError: (err: any) => {
setErrorConfirm(err?.response.data.message);
},
});
const openModal = useModalStore(state => state.openModal);
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (timer > 0) {
interval = setInterval(() => {
setTimer(prevTimer => prevTimer - 1);
}, 1000);
} else {
setCanResend(true);
if (interval) clearInterval(interval);
}
return () => {
if (interval) clearInterval(interval);
};
}, [timer]);
const handleCodeChange = (text: string, index: number) => {
const newCode = [...code];
newCode[index] = text;
setCode(newCode);
if (text.length > 0 && index < OTP_LENGTH - 1) {
inputRefs.current[index + 1]?.focus();
}
if (text.length === 0 && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
if (nativeEvent.key === 'Backspace' && code[index] === '' && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handleResendCode = () => {
resendMutate({
phoneNumber: phoneNumber,
otpType: 'LOGIN',
});
};
const handleVerifyCode = () => {
const enteredCode = code.join('');
mutate({
phoneNumber: phoneNumber,
otp: String(enteredCode),
otpType: 'LOGIN',
});
// navigation.navigate('Home');
};
return (
<ImageBackground
source={Logo}
style={Loginstyle.background}
resizeMode="contain"
imageStyle={{
opacity: 0.3,
height: '100%',
width: '100%',
transform: [{ scale: 1.5 }],
}}
>
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.langContainer}>
<TouchableOpacity onPress={() => navigation.navigate('Login')}>
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
</TouchableOpacity>
<LanguageSelector />
</View>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<Text style={styles.title}>{t('Tasdiqlash kodini kiriting')}</Text>
<Text style={styles.message}>
{phoneNumber} {t('raqamiga yuborilgan')} {OTP_LENGTH}{' '}
{t('xonali kodni kiriting.')}
</Text>
<View style={styles.otpContainer}>
{code.map((digit, index) => (
<TextInput
key={index}
ref={ref => {
inputRefs.current[index] = ref;
}}
style={styles.otpInput}
keyboardType="number-pad"
maxLength={1}
onChangeText={text => handleCodeChange(text, index)}
onKeyPress={e => handleKeyPress(e, index)}
value={digit}
autoFocus={index === 0}
/>
))}
</View>
{errorConfirm !== null && (
<Text style={styles.errorText}>{errorConfirm}</Text>
)}
<View style={styles.resendContainer}>
{canResend ? (
<TouchableOpacity
onPress={handleResendCode}
style={styles.resendButton}
>
<Text style={styles.resendButtonText}>
{t('Kodni qayta yuborish')}
</Text>
</TouchableOpacity>
) : (
<Text style={styles.timerText}>
{t('Kodni qayta yuborish vaqti')} ({timer}s)
</Text>
)}
</View>
<TouchableOpacity
style={styles.verifyButton}
onPress={handleVerifyCode}
>
{isPending ? (
<ActivityIndicator color="#fff" />
) : (
<Text
style={[
Loginstyle.btnText,
isPending && styles.buttonTextDisabled,
]}
>
{t('Tasdiqlash')}
</Text>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</ImageBackground>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
errorText: {
color: 'red',
fontSize: 14,
fontWeight: '500',
marginTop: 10,
textAlign: 'center',
},
buttonTextDisabled: {
color: 'white',
},
langContainer: {
width: '100%',
marginTop: 10,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
headerTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
content: {
width: '100%',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
textAlign: 'center',
},
message: {
fontSize: 15,
color: '#666',
textAlign: 'center',
marginBottom: 30,
lineHeight: 22,
},
otpContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 10,
},
otpInput: {
width: 45,
height: 55,
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
textAlign: 'center',
fontSize: 22,
fontWeight: 'bold',
color: '#333',
backgroundColor: '#fff',
},
resendContainer: {
marginBottom: 30,
marginTop: 20,
},
timerText: {
fontSize: 15,
color: '#999',
},
resendButton: {
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 8,
},
resendButtonText: {
fontSize: 15,
color: '#007bff',
fontWeight: 'bold',
},
verifyButton: {
backgroundColor: '#007bff',
paddingVertical: 15,
paddingHorizontal: 40,
borderRadius: 8,
width: '100%',
alignItems: 'center',
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
verifyButtonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
});
export default Confirm;

View File

@@ -0,0 +1,327 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation, useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import { loginPayload } from 'api/auth/type';
import { Branch, branchApi } from 'api/branch';
import formatPhone from 'helpers/formatPhone';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import { LoginFormType, loginSchema } from 'screens/auth/login/lib/form';
import LanguageSelector from 'screens/auth/select-language/SelectLang';
import { useUserStore } from '../lib/userstore';
import { Loginstyle } from './styled';
const Filial = [
{ label: 'Toshkent shahar' },
{ label: 'Andijon viloyati' },
{ label: 'Samarqand viloyati' },
{ label: 'Toshkent viloyati' },
{ label: 'Xorazm viloyati' },
];
const Login = () => {
const { t } = useTranslation();
const passportNumberRef = useRef<TextInput>(null);
const [filialDropdownVisible, setFilialDropdownVisible] = useState(false);
const navigation = useNavigation<NativeStackNavigationProp<any>>();
const { setUser, setExpireTime } = useUserStore(state => state);
const [error, setError] = useState<string>();
const [rawPhone, setRawPhone] = useState('+998');
const { data: branchList } = useQuery({
queryKey: ['branchList'],
queryFn: branchApi.branchList,
});
const { mutate, isPending } = useMutation({
mutationFn: (payload: loginPayload) => authApi.login(payload),
onSuccess: res => {
navigation.navigate('Login-Confirm');
setExpireTime(res.data.expireTime);
},
onError: err => {
setError('Xatolik yuz berdi');
console.dir(err);
},
});
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<LoginFormType>({
resolver: zodResolver(loginSchema),
defaultValues: {
phone: '',
passportSeriya: '',
passportNumber: '',
},
});
const onSubmit = (data: LoginFormType) => {
mutate({
branchId: data.branchId,
phoneNumber: data.phone,
passportSerial: `${data.passportSeriya.toUpperCase()}${
data.passportNumber
}`,
});
// navigation.navigate('Login-Confirm');
setUser({
phoneNumber: data.phone,
});
};
const handleBackNavigation = useCallback(() => {
navigation.navigate('select-auth');
}, [navigation]);
const handlePhoneChange = useCallback((text: string) => {
const digits = text.replace(/\D/g, '');
const full = digits.startsWith('998') ? digits : `998${digits}`;
setRawPhone(`+${full}`);
setValue('phone', full, { shouldValidate: true });
}, []);
const keyboardBehavior = useMemo(
() => (Platform.OS === 'ios' ? 'padding' : 'height'),
[],
);
return (
<ImageBackground
source={Logo}
style={Loginstyle.background}
resizeMode="contain"
imageStyle={{
opacity: 0.3,
height: '100%',
width: '100%',
transform: [{ scale: 1.5 }],
}}
>
<SafeAreaView style={{ flex: 1 }}>
<View style={Loginstyle.langContainer}>
<TouchableOpacity onPress={handleBackNavigation}>
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
</TouchableOpacity>
<LanguageSelector />
</View>
<KeyboardAvoidingView
style={Loginstyle.container}
behavior={keyboardBehavior}
>
<ScrollView style={{ flex: 1 }}>
<View style={Loginstyle.scrollContainer}>
<View style={Loginstyle.loginContainer}>
<Text style={Loginstyle.title}>{t('Tizimga kirish')}</Text>
<Controller
control={control}
name="phone"
render={({ field: { onChange } }) => {
const formatted = formatPhone(rawPhone);
return (
<View>
<Text style={Loginstyle.label}>
{t('Telefon raqami')}
</Text>
<TextInput
keyboardType="numeric"
placeholder="+998 90 123-45-67"
value={formatted}
onChangeText={handlePhoneChange}
style={Loginstyle.input}
placeholderTextColor="#D8DADC"
maxLength={19} // +998 90 123-45-67 bo'lishi uchun
/>
{errors.phone && (
<Text style={Loginstyle.errorText}>
{t(errors.phone.message || '')}
</Text>
)}
</View>
);
}}
/>
<View>
<Text style={Loginstyle.label}>
{t('Passport seriya raqami')}
</Text>
<View style={{ flexDirection: 'row' }}>
<Controller
control={control}
name="passportSeriya"
render={({ field: { onChange, value } }) => (
<TextInput
style={[Loginstyle.input, Loginstyle.seriyaInput]}
placeholder="AA"
maxLength={2}
autoCapitalize="characters"
value={value}
onChangeText={text => {
onChange(text);
if (text.length === 2) {
passportNumberRef.current?.focus();
}
}}
placeholderTextColor="#D8DADC"
/>
)}
/>
<Controller
control={control}
name="passportNumber"
render={({ field: { onChange, value } }) => (
<TextInput
ref={passportNumberRef}
style={[Loginstyle.input, Loginstyle.raqamInput]}
placeholder="1234567"
maxLength={7}
keyboardType="numeric"
value={value}
onChangeText={text => {
const onlyNumbers = text.replace(/[^0-9]/g, '');
onChange(onlyNumbers);
}}
placeholderTextColor="#D8DADC"
/>
)}
/>
</View>
{(errors.passportSeriya || errors.passportNumber) && (
<Text style={Loginstyle.errorText}>
{t(errors.passportSeriya?.message || '') ||
t(errors.passportNumber?.message || '')}
</Text>
)}
</View>
<Controller
control={control}
name="branchId"
render={({ field: { value } }) => (
<View style={{ position: 'relative' }}>
<Text style={Loginstyle.label}>{t('Filial')}</Text>
<View style={Loginstyle.input}>
<TouchableOpacity
style={Loginstyle.selector}
onPress={() =>
setFilialDropdownVisible(prev => !prev)
}
>
<Text
style={
value
? { color: '#000' }
: Loginstyle.selectedText
}
>
{branchList?.find(e => e.id === value)?.name ||
t('Filialni tanlang...')}
</Text>
<SimpleLineIcons
name={
filialDropdownVisible ? 'arrow-up' : 'arrow-down'
}
color="#000"
size={14}
/>
</TouchableOpacity>
</View>
{filialDropdownVisible && (
<View style={[Loginstyle.dropdown, { maxHeight: 200 }]}>
<ScrollView nestedScrollEnabled>
{branchList &&
branchList.map((item: Branch) => (
<TouchableOpacity
key={item.id}
style={Loginstyle.dropdownItem}
onPress={() => {
setValue('branchId', item.id);
setFilialDropdownVisible(false);
}}
>
<Text style={Loginstyle.dropdownItemText}>
{item.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
{errors.branchId && (
<Text style={Loginstyle.errorText}>
{t(errors.branchId.message || '')}
</Text>
)}
{error && (
<Text style={[Loginstyle.errorText]}>{t(error)}</Text>
)}
</View>
)}
/>
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
style={Loginstyle.button}
>
{isPending ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={Loginstyle.btnText}>
{t('Tizimga kirish')}
</Text>
)}
</TouchableOpacity>
<View
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Text style={{ fontSize: 14, fontWeight: '500' }}>
{t('ID va kabinet yoqmi?')}
</Text>
<TouchableOpacity
style={Loginstyle.dropdownItem}
onPress={() => navigation.navigate('Register')}
>
<Text
style={{
color: '#28A7E8',
fontSize: 14,
fontWeight: '500',
}}
>
{t('Royxatdan otish')}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</ImageBackground>
);
};
export default Login;

View File

@@ -0,0 +1,126 @@
import { StyleSheet } from 'react-native';
export const Loginstyle = StyleSheet.create({
langContainer: {
width: '100%',
marginTop: 10,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255, 255, 255, 0.7)',
},
container: {
flex: 1,
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
width: '100%',
},
loginContainer: {
borderRadius: 20,
padding: 30,
display: 'flex',
gap: 20,
width: '100%',
position: 'relative',
},
label: {
fontSize: 12,
fontWeight: '500',
color: '#28A7E8',
marginBottom: 5,
},
title: {
fontSize: 24,
fontWeight: '600',
textAlign: 'center',
color: '#28A7E8',
marginBottom: 20,
},
input: {
borderWidth: 1,
borderColor: '#D8DADC',
borderRadius: 12,
padding: 15,
fontSize: 16,
fontWeight: '400',
backgroundColor: '#FFFFFF',
color: '#000',
},
seriyaInput: {
width: 60,
fontSize: 14,
textTransform: 'uppercase',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
raqamInput: {
flex: 1,
fontSize: 16,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
selector: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
},
selectedText: {
fontSize: 14,
color: '#D8DADC',
},
dropdown: {
position: 'absolute',
top: 80,
left: 0,
right: 0,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
zIndex: 10,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
maxHeight: 150,
},
dropdownItem: {
paddingVertical: 10,
paddingHorizontal: 12,
},
dropdownItemText: {
fontSize: 14,
color: '#333',
},
button: {
backgroundColor: '#28A7E8',
height: 56,
borderRadius: 8,
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
},
btnText: {
color: '#fff',
fontSize: 20,
textAlign: 'center',
},
errorText: {
color: 'red',
fontSize: 12,
},
});

View File

@@ -0,0 +1,20 @@
// form.ts
import { z } from 'zod';
export const FirstStepSchema = z.object({
firstName: z.string().min(3, "Eng kamida 3ta belgi bo'lishi kerak"),
lastName: z.string().min(3, "Eng kamida 3ta belgi bo'lishi kerak"),
phoneNumber: z.string().min(12, 'Xato raqam kiritildi'),
branchId: z.number().min(1, 'Filialni tanlang'),
recommend: z.string().min(1, 'Majburiy maydon'),
});
export const SecondStepSchema = z.object({
passportSeriya: z.string().length(2, '2 ta harf kerak'),
birthDate: z.string().min(8, 'Majburiy maydon'),
passportNumber: z.string().length(7, '7 ta raqam kerak'),
jshshir: z.string().length(14, '14 ta raqam kerak'),
});
export type FirstStepFormType = z.infer<typeof FirstStepSchema>;
export type SecondStepFormType = z.infer<typeof SecondStepSchema>;

View File

@@ -0,0 +1,42 @@
import { create } from 'zustand';
interface ModalState {
isVisible: boolean;
title: string;
message: string;
type: 'success' | 'error' | 'info';
onConfirm: () => void;
onCancel?: () => void;
openModal: (
title: string,
message: string,
type?: 'success' | 'error' | 'info',
onConfirm?: () => void,
onCancel?: () => void
) => void;
closeModal: () => void;
}
export const useModalStore = create<ModalState>((set) => ({
isVisible: false,
title: '',
message: '',
type: 'info',
onConfirm: () => { },
onCancel: undefined,
openModal: (title, message, type = 'info', onConfirm, onCancel) =>
set({
isVisible: true,
title,
message,
type,
onConfirm: () => {
if (onConfirm) onConfirm();
set({ isVisible: false });
},
onCancel: onCancel || undefined,
}),
closeModal: () => set({ isVisible: false }),
}));

View File

@@ -0,0 +1,28 @@
import { create } from 'zustand';
interface UserState {
firstName: string;
lastName: string;
phoneNumber: string;
setUser: (user: Partial<UserState>) => void;
clearUser: () => void;
}
export const useUserStore = create<UserState>(set => ({
firstName: '',
lastName: '',
phoneNumber: '',
setUser: user =>
set(state => ({
...state,
...user,
})),
clearUser: () =>
set({
firstName: '',
lastName: '',
phoneNumber: '',
}),
}));

View File

@@ -0,0 +1,320 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import { otpPayload, resendPayload } from 'api/auth/type';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
ImageBackground,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import { useModalStore } from 'screens/auth/registeration/lib/modalStore';
import LanguageSelector from 'screens/auth/select-language/SelectLang';
import { RootStackParamList } from 'types/types';
import { useUserStore } from '../lib/userstore';
import { RegisterStyle } from './styled';
type VerificationCodeScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'Confirm'
>;
const OTP_LENGTH = 4;
const Confirm = ({
setStep,
}: {
setStep: React.Dispatch<React.SetStateAction<number>>;
}) => {
const navigation = useNavigation<VerificationCodeScreenNavigationProp>();
const { t } = useTranslation();
const [code, setCode] = useState<string[]>(new Array(OTP_LENGTH).fill(''));
const [timer, setTimer] = useState(60);
const [canResend, setCanResend] = useState(false);
const [errorConfirm, setErrorConfirm] = useState<string | null>(null);
const inputRefs = useRef<Array<TextInput | null>>([]);
const { phoneNumber } = useUserStore(state => state);
const { mutate, isPending } = useMutation({
mutationFn: (payload: otpPayload) => authApi.verifyOtp(payload),
onSuccess: async res => {
await AsyncStorage.setItem('token', res.data.accessToken);
navigation.navigate('Confirm');
setErrorConfirm(null);
},
onError: (err: any) => {
setErrorConfirm(err?.response.data.message);
},
});
const { mutate: resendMutate } = useMutation({
mutationFn: (payload: resendPayload) => authApi.resendOtp(payload),
onSuccess: async res => {
setTimer(60);
setCanResend(false);
setCode(new Array(OTP_LENGTH).fill(''));
inputRefs.current[0]?.focus();
setErrorConfirm(null);
},
onError: (err: any) => {
setErrorConfirm(err?.response.data.message);
},
});
const openModal = useModalStore(state => state.openModal);
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (timer > 0) {
interval = setInterval(() => {
setTimer(prevTimer => prevTimer - 1);
}, 1000);
} else {
setCanResend(true);
if (interval) clearInterval(interval);
}
return () => {
if (interval) clearInterval(interval);
};
}, [timer]);
const handleCodeChange = (text: string, index: number) => {
const newCode = [...code];
newCode[index] = text;
setCode(newCode);
if (text.length > 0 && index < OTP_LENGTH - 1) {
inputRefs.current[index + 1]?.focus();
}
if (text.length === 0 && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handleKeyPress = ({ nativeEvent }: any, index: number) => {
if (nativeEvent.key === 'Backspace' && code[index] === '' && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};
const handleResendCode = () => {
resendMutate({
phoneNumber: phoneNumber,
otpType: 'REGISTRATION',
});
};
const handleVerifyCode = () => {
const enteredCode = code.join('');
mutate({
phoneNumber: phoneNumber,
otp: String(enteredCode),
otpType: 'REGISTRATION',
});
};
return (
<ImageBackground
source={Logo}
style={RegisterStyle.background}
resizeMode="contain"
imageStyle={{
opacity: 0.3,
height: '100%',
width: '100%',
transform: [{ scale: 1.5 }],
}}
>
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.langContainer}>
<TouchableOpacity onPress={() => setStep(1)}>
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
</TouchableOpacity>
<LanguageSelector />
</View>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.content}>
<Text style={styles.title}>{t('Tasdiqlash kodini kiriting')}</Text>
<Text style={styles.message}>
{phoneNumber} {t('raqamiga yuborilgan')} {OTP_LENGTH}{' '}
{t('xonali kodni kiriting.')}
</Text>
<View style={styles.otpContainer}>
{code.map((digit, index) => (
<TextInput
key={index}
ref={ref => {
inputRefs.current[index] = ref;
}}
style={styles.otpInput}
keyboardType="number-pad"
maxLength={1}
onChangeText={text => handleCodeChange(text, index)}
onKeyPress={e => handleKeyPress(e, index)}
value={digit}
autoFocus={index === 0}
/>
))}
</View>
{errorConfirm !== null && (
<Text style={styles.errorText}>{errorConfirm}</Text>
)}
<View style={styles.resendContainer}>
{canResend ? (
<TouchableOpacity
onPress={handleResendCode}
style={styles.resendButton}
>
<Text style={styles.resendButtonText}>
{t('Kodni qayta yuborish')}
</Text>
</TouchableOpacity>
) : (
<Text style={styles.timerText}>
{t('Kodni qayta yuborish vaqti')} ({timer}s)
</Text>
)}
</View>
<TouchableOpacity
style={styles.verifyButton}
onPress={handleVerifyCode}
>
{isPending ? (
<ActivityIndicator color="#fff" />
) : (
<Text
style={[
RegisterStyle.btnText,
isPending && RegisterStyle.buttonTextDisabled,
]}
>
{t('Tasdiqlash')}
</Text>
)}
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</ImageBackground>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#f8f9fa',
},
errorText: {
color: 'red',
fontSize: 14,
fontWeight: '500',
marginTop: 10,
textAlign: 'center',
},
langContainer: {
width: '100%',
marginTop: 10,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
headerTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
},
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
content: {
width: '100%',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
textAlign: 'center',
},
message: {
fontSize: 15,
color: '#666',
textAlign: 'center',
marginBottom: 30,
lineHeight: 22,
},
otpContainer: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
gap: 10,
},
otpInput: {
width: 45,
height: 55,
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
textAlign: 'center',
fontSize: 22,
fontWeight: 'bold',
color: '#333',
backgroundColor: '#fff',
},
resendContainer: {
marginBottom: 30,
},
timerText: {
fontSize: 15,
color: '#999',
},
resendButton: {
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 8,
},
resendButtonText: {
fontSize: 15,
color: '#007bff',
fontWeight: 'bold',
},
verifyButton: {
backgroundColor: '#007bff',
paddingVertical: 15,
paddingHorizontal: 40,
borderRadius: 8,
width: '100%',
alignItems: 'center',
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
},
verifyButtonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
});
export default Confirm;

View File

@@ -0,0 +1,468 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import {
type RouteProp,
useNavigation,
useRoute,
} from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation, useQuery } from '@tanstack/react-query';
import { authApi } from 'api/auth';
import { registerPayload } from 'api/auth/type';
import { Branch, branchApi } from 'api/branch';
import formatPhone from 'helpers/formatPhone';
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Animated,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import AntDesign from 'react-native-vector-icons/AntDesign';
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import {
FirstStepFormType,
FirstStepSchema,
} from 'screens/auth/registeration/lib/form';
import LanguageSelector from 'screens/auth/select-language/SelectLang';
import { RootStackParamList } from 'types/types';
import { useUserStore } from '../lib/userstore';
import { RegisterStyle } from './styled';
type LoginScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'Login'
>;
const recommended = [
{ label: 'Tanishim orqali', value: 'FRIEND' },
{ label: 'Telegram orqali', value: 'TELEGRAM' },
{ label: 'Instagram orqali', value: 'INSTAGRAM' },
{ label: 'Facebook orqali', value: 'FACEBOOK' },
];
const FirstStep = ({ onNext }: { onNext: () => void }) => {
const { t } = useTranslation();
const [filialDropdownVisible, setFilialDropdownVisible] = useState(false);
const [error, setError] = useState<string>();
const { setUser } = useUserStore(state => state);
const { data: branchList } = useQuery({
queryKey: ['branchList'],
queryFn: branchApi.branchList,
});
const { mutate, isPending } = useMutation({
mutationFn: (payload: registerPayload) => authApi.register(payload),
onSuccess: res => {
onNext();
},
onError: err => {
console.dir(err);
setError('Xatolik yuz berdi');
},
});
const [recommendedDropdownVisible, setRecommendedDropdownVisible] =
useState(false);
const [termsAccepted, setTermsAccepted] = useState(false);
const [checkboxAnimation] = useState(new Animated.Value(0));
const navigation = useNavigation<LoginScreenNavigationProp>();
const [rawPhone, setRawPhone] = useState('+998');
const route = useRoute<RouteProp<RootStackParamList, 'Register'>>();
const {
control,
handleSubmit,
setValue,
formState: { errors },
getValues,
} = useForm<FirstStepFormType>({
resolver: zodResolver(FirstStepSchema),
defaultValues: {
firstName: '',
lastName: '',
recommend: '',
},
});
const onSubmit = (data: FirstStepFormType) => {
setUser({
firstName: data.firstName,
lastName: data.lastName,
phoneNumber: data.phoneNumber,
});
mutate(data);
};
useEffect(() => {
if (route.params?.termsAccepted) {
setTermsAccepted(true);
Animated.spring(checkboxAnimation, {
toValue: 1,
useNativeDriver: false,
tension: 100,
friction: 8,
}).start();
}
}, [route.params]);
const navigateToTerms = () => {
navigation.navigate('TermsAndConditions');
setTermsAccepted(true);
Animated.spring(checkboxAnimation, {
toValue: 1,
useNativeDriver: false,
tension: 100,
friction: 8,
}).start();
};
const toggleCheckbox = () => {
if (!termsAccepted) {
navigateToTerms();
} else {
setTermsAccepted(false);
Animated.spring(checkboxAnimation, {
toValue: 0,
useNativeDriver: false,
tension: 100,
friction: 8,
}).start();
}
};
return (
<ImageBackground
source={Logo}
style={RegisterStyle.background}
resizeMode="contain"
imageStyle={{
opacity: 0.3,
height: '100%',
width: '100%',
transform: [{ scale: 1.5 }],
}}
>
<SafeAreaView style={{ flex: 1 }}>
<View style={RegisterStyle.langContainer}>
<TouchableOpacity onPress={() => navigation.navigate('select-auth')}>
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
</TouchableOpacity>
<LanguageSelector />
</View>
<KeyboardAvoidingView
style={RegisterStyle.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
showsVerticalScrollIndicator={false}
style={RegisterStyle.content}
>
<View style={RegisterStyle.scrollContainer}>
<View style={RegisterStyle.loginContainer}>
<Text style={RegisterStyle.title}>
{t("Ro'yxatdan o'tish")}
</Text>
<Controller
control={control}
name="firstName"
render={({ field: { onChange, value } }) => (
<View>
<Text style={RegisterStyle.label}>{t('Ism')}</Text>
<TextInput
style={RegisterStyle.input}
placeholder={t('Ismingiz')}
onChangeText={onChange}
value={value}
placeholderTextColor={'#D8DADC'}
/>
{errors.firstName && (
<Text style={RegisterStyle.errorText}>
{t(errors.firstName.message || '')}
</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="lastName"
render={({ field: { onChange, value } }) => (
<View>
<Text style={RegisterStyle.label}>{t('Familiya')}</Text>
<TextInput
style={RegisterStyle.input}
placeholder={t('Familiyangiz')}
placeholderTextColor={'#D8DADC'}
onChangeText={onChange}
value={value}
/>
{errors.lastName && (
<Text style={RegisterStyle.errorText}>
{t(errors.lastName.message || '')}
</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="phoneNumber"
render={({ field: { onChange } }) => {
const formatted = formatPhone(rawPhone);
return (
<View>
<Text style={RegisterStyle.label}>
{t('Telefon raqami')}
</Text>
<TextInput
keyboardType="numeric"
placeholder="+998 __ ___-__-__"
value={formatted}
onChangeText={text => {
const digits = text.replace(/\D/g, '').slice(0, 12);
const full = digits.startsWith('998')
? digits
: `998${digits}`;
setRawPhone(full);
onChange(full);
}}
style={RegisterStyle.input}
placeholderTextColor="#D8DADC"
maxLength={17}
/>
{errors.phoneNumber && (
<Text style={RegisterStyle.errorText}>
{t(errors.phoneNumber.message || '')}
</Text>
)}
</View>
);
}}
/>
<Controller
control={control}
name="branchId"
render={({ field: { value } }) => (
<View style={{ position: 'relative' }}>
<Text style={RegisterStyle.label}>{t('Filial')}</Text>
<View style={RegisterStyle.input}>
<TouchableOpacity
style={RegisterStyle.selector}
onPress={() =>
setFilialDropdownVisible(prev => !prev)
}
>
<Text
style={
value
? { color: '#000' }
: RegisterStyle.selectedText
}
>
{branchList?.find(e => e.id === value)?.name ||
t('Filialni tanlang...')}
</Text>
<SimpleLineIcons
name={
filialDropdownVisible ? 'arrow-up' : 'arrow-down'
}
color="#000"
size={14}
/>
</TouchableOpacity>
</View>
{filialDropdownVisible && (
<View
style={[RegisterStyle.dropdown, { maxHeight: 200 }]}
>
<ScrollView nestedScrollEnabled>
{branchList &&
branchList.map((item: Branch) => (
<TouchableOpacity
key={item.id}
style={RegisterStyle.dropdownItem}
onPress={() => {
setValue('branchId', item.id);
setFilialDropdownVisible(false);
}}
>
<Text style={RegisterStyle.dropdownItemText}>
{item.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
{errors.branchId && (
<Text style={RegisterStyle.errorText}>
{t(errors.branchId.message || '')}
</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="recommend"
render={({ field: { value } }) => (
<View style={{ position: 'relative' }}>
<Text style={RegisterStyle.label}>
{t('Bizni qaerdan topdingiz?')}
</Text>
<View style={RegisterStyle.input}>
<TouchableOpacity
style={RegisterStyle.selector}
onPress={() =>
setRecommendedDropdownVisible(prev => !prev)
}
>
<Text
style={
value
? { color: '#000' }
: RegisterStyle.selectedText
}
>
{t(
recommended.find(e => e.value === value)?.label ||
'Bizni kim tavsiya qildi...',
)}
</Text>
<SimpleLineIcons
name={
recommendedDropdownVisible
? 'arrow-up'
: 'arrow-down'
}
color="#000"
size={14}
/>
</TouchableOpacity>
</View>
{recommendedDropdownVisible && (
<View
style={[RegisterStyle.dropdown, { maxHeight: 200 }]}
>
<ScrollView nestedScrollEnabled>
{recommended.map((item, index) => (
<TouchableOpacity
key={index}
style={RegisterStyle.dropdownItem}
onPress={() => {
setValue('recommend', item.value);
setRecommendedDropdownVisible(false);
}}
>
<Text style={RegisterStyle.dropdownItemText}>
{t(item.label)}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
)}
{errors.recommend && (
<Text style={RegisterStyle.errorText}>
{t(errors.recommend.message || '')}
</Text>
)}
{error && (
<Text style={[RegisterStyle.errorText]}>
{t(error)}
</Text>
)}
</View>
)}
/>
<View style={RegisterStyle.termsContainer}>
<TouchableOpacity
style={RegisterStyle.checkboxContainer}
onPress={toggleCheckbox}
activeOpacity={0.7}
>
<Animated.View
style={[
RegisterStyle.checkbox,
termsAccepted && RegisterStyle.checkboxChecked,
{
transform: [
{
scale: checkboxAnimation.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [1, 1.1, 1],
}),
},
],
},
]}
>
{termsAccepted && (
<Animated.View
style={{
opacity: checkboxAnimation,
transform: [
{
scale: checkboxAnimation,
},
],
}}
>
<AntDesign name="check" color="#fff" size={20} />
</Animated.View>
)}
</Animated.View>
<View style={RegisterStyle.termsTextContainer}>
<Text style={RegisterStyle.termsText}>
<Text>{t('Foydalanish shartlari')}</Text>
<Text> {t('bilan tanishib chiqdim!')}</Text>
</Text>
</View>
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
style={[
RegisterStyle.button,
(!termsAccepted || isPending) &&
RegisterStyle.buttonDisabled,
]}
disabled={!termsAccepted || isPending}
>
{isPending ? (
<ActivityIndicator color="#fff" />
) : (
<Text
style={[
RegisterStyle.btnText,
(!termsAccepted || isPending) &&
RegisterStyle.buttonTextDisabled,
]}
>
{t('Davom etish')}
</Text>
)}
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</ImageBackground>
);
};
export default FirstStep;

View File

@@ -0,0 +1,427 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import {
useNavigation,
useRoute,
type RouteProp,
} from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useMutation } from '@tanstack/react-query';
import passportApi, { sendPassportPayload } from 'api/passport';
import DatePickerInput from 'components/DatePicker';
import SingleFileDrop from 'components/FileDrop';
import { useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Animated,
Dimensions,
Image,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import AntDesign from 'react-native-vector-icons/AntDesign';
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import LanguageSelector from 'screens/auth/select-language/SelectLang';
import { RootStackParamList } from 'types/types';
import { SecondStepFormType, SecondStepSchema } from '../lib/form';
import { RegisterStyle } from './styled';
interface FileData {
uri: string;
name: string;
type: string;
base64: string;
}
const SecondStep = () => {
const { t } = useTranslation();
const windowWidth = Dimensions.get('window').width;
const [frontImage, setFrontImage] = useState<FileData | null>(null);
const [backImage, setBackImage] = useState<FileData | null>(null);
const isSmallScreen = windowWidth < 200;
const [termsAccepted, setTermsAccepted] = useState(true);
const passportNumberRef = useRef<TextInput>(null);
const [checkboxAnimation] = useState(new Animated.Value(1));
const [inputValue, setInputValue] = useState('');
const navigation =
useNavigation<NativeStackNavigationProp<RootStackParamList, 'Login'>>();
const route = useRoute<RouteProp<RootStackParamList, 'Register'>>();
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const formatDate = (date: Date) => {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
};
const { mutate, isPending } = useMutation({
mutationFn: (payload: sendPassportPayload) =>
passportApi.sendPassport(payload),
onSuccess: res => {
navigation.navigate('Home');
},
onError: err => {
console.dir(err);
},
});
useEffect(() => {
if (route.params?.termsAccepted) {
setTermsAccepted(true);
Animated.spring(checkboxAnimation, {
toValue: 1,
useNativeDriver: false,
tension: 100,
friction: 8,
}).start();
}
}, [route.params]);
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<SecondStepFormType>({
resolver: zodResolver(SecondStepSchema),
defaultValues: {
passportNumber: '',
passportSeriya: '',
birthDate: '',
jshshir: '',
},
});
const onSubmit = async (data: SecondStepFormType) => {
const [d, m, y] = data.birthDate.split('/');
const isoBirthDate = `${y}-${m.padStart(2, '0')}-${d.padStart(2, '0')}`;
mutate({
fullName: data.passportSeriya.toUpperCase(),
birthDate: isoBirthDate,
passportSerial: `${data.passportSeriya.toUpperCase()}${
data.passportNumber
}`,
passportPin: data.jshshir,
passportFrontImage: frontImage ? `${frontImage.base64}` : '',
passportBackImage: backImage ? `${backImage.base64}` : '',
});
};
return (
<ImageBackground
source={Logo}
style={RegisterStyle.background}
resizeMode="contain"
imageStyle={{
opacity: 0.3,
height: '100%',
width: '100%',
transform: [{ scale: 1.5 }],
}}
>
<SafeAreaView style={{ flex: 1 }}>
<View style={RegisterStyle.langContainer}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
</TouchableOpacity>
<LanguageSelector />
</View>
<KeyboardAvoidingView
style={RegisterStyle.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView style={RegisterStyle.content}>
<View style={RegisterStyle.scrollContainer}>
<View style={RegisterStyle.loginContainer}>
<Text style={RegisterStyle.title}>
{t('Shaxsiy maʼlumotlar')}
</Text>
<Image
source={require('screens/../../assets/bootsplash/passport-sample.jpg')}
style={{ width: '100%', height: 300, borderRadius: 8 }}
resizeMode="contain"
/>
<View>
{/* PASSPORT */}
<Text style={RegisterStyle.label}>
{t('Passport seriya raqami')}
</Text>
<View style={{ flexDirection: 'row' }}>
<Controller
control={control}
name="passportSeriya"
render={({ field: { onChange, value } }) => (
<TextInput
style={[
RegisterStyle.input,
RegisterStyle.seriyaInput,
]}
placeholder="AA"
maxLength={2}
autoCapitalize="characters"
value={value}
onChangeText={text => {
onChange(text);
if (text.length === 2)
passportNumberRef.current?.focus();
}}
placeholderTextColor="#D8DADC"
/>
)}
/>
<Controller
control={control}
name="passportNumber"
render={({ field: { onChange, value } }) => (
<TextInput
ref={passportNumberRef}
style={[
RegisterStyle.input,
RegisterStyle.raqamInput,
]}
placeholder="1234567"
maxLength={7}
keyboardType="numeric"
value={value}
onChangeText={text => {
const onlyNumbers = text.replace(/[^0-9]/g, '');
onChange(onlyNumbers);
}}
placeholderTextColor="#D8DADC"
/>
)}
/>
</View>
{(errors.passportSeriya || errors.passportNumber) && (
<Text style={RegisterStyle.errorText}>
{t(errors.passportSeriya?.message || '') ||
t(errors.passportNumber?.message || '')}
</Text>
)}
</View>
{/* JSHSHIR */}
<Controller
control={control}
name="jshshir"
render={({ field: { onChange, value } }) => (
<View>
<Text style={RegisterStyle.label}>{t('JSHSHIR')}</Text>
<TextInput
style={RegisterStyle.input}
placeholder="12345678901234"
placeholderTextColor="#D8DADC"
keyboardType="numeric"
maxLength={14}
value={value}
onChangeText={text =>
onChange(text.replace(/[^0-9]/g, ''))
}
/>
{errors.jshshir && (
<Text style={RegisterStyle.errorText}>
{t(errors.jshshir.message || '')}
</Text>
)}
</View>
)}
/>
{/* BIRTH DATE */}
<Controller
control={control}
name="birthDate"
render={({ field: { onChange, value } }) => (
<View style={{ marginBottom: 10 }}>
<Text style={RegisterStyle.label}>
{t("Tug'ilgan sana")}
</Text>
<View
style={[
RegisterStyle.inputContainer,
{ paddingHorizontal: 0 },
]}
>
<TextInput
style={[
RegisterStyle.input,
{ flex: 1, borderWidth: 0 },
]}
placeholder="dd/mm/yyyy"
placeholderTextColor="#D8DADC"
keyboardType="numeric"
value={value}
onChangeText={text => {
let cleaned = text
.replace(/[^\d]/g, '')
.slice(0, 8);
let formatted = '';
// Early validation for day and month before formatting
if (cleaned.length >= 1) {
const firstDigit = cleaned[0];
if (firstDigit > '3') return; // Day can't start with 4-9
}
if (cleaned.length >= 2) {
const day = parseInt(cleaned.slice(0, 2), 10);
if (day > 31 || day === 0) return;
}
if (cleaned.length >= 3) {
const monthFirstDigit = cleaned[2];
if (monthFirstDigit > '1') return; // Month can't start with 2-9
}
if (cleaned.length >= 4) {
const month = parseInt(cleaned.slice(2, 4), 10);
if (month > 12 || month === 0) return;
}
// Now format it after initial checks
if (cleaned.length <= 2) {
formatted = cleaned;
} else if (cleaned.length <= 4) {
formatted = `${cleaned.slice(
0,
2,
)}/${cleaned.slice(2)}`;
} else {
formatted = `${cleaned.slice(
0,
2,
)}/${cleaned.slice(2, 4)}/${cleaned.slice(4)}`;
}
// Validate full date not in future
if (formatted.length === 10) {
const [d, m, y] = formatted.split('/');
const inputDate = new Date(+y, +m - 1, +d);
const today = new Date();
inputDate.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
if (inputDate > today) return;
}
setValue('birthDate', formatted);
}}
/>
<TouchableOpacity
onPress={() => setDatePickerVisibility(true)}
style={{ right: 15 }}
>
<AntDesign
name="calendar"
color="#D8DADC"
size={25}
/>
</TouchableOpacity>
</View>
{errors.birthDate && (
<Text style={RegisterStyle.errorText}>
{t(errors.birthDate?.message || '')}
</Text>
)}
</View>
)}
/>
<DatePickerInput
showPicker={isDatePickerVisible}
setShowPicker={setDatePickerVisibility}
value={selectedDate || new Date()}
onChange={date => {
if (date) {
const formattedDate = formatDate(date);
setSelectedDate(date);
setInputValue(formattedDate);
setValue('birthDate', formattedDate);
}
}}
maximumDate={new Date()}
/>
{/* FILE UPLOAD */}
<Text style={RegisterStyle.mainTitle}>
{t('Passport/ID karta rasmi yoki faylni yuklang')}
</Text>
<View
style={[
RegisterStyle.sectionsContainer,
{ flexDirection: isSmallScreen ? 'column' : 'row' },
]}
>
<View
style={{
width: isSmallScreen ? '100%' : '48%',
marginBottom: 10,
}}
>
<SingleFileDrop
title={t('Old tomon')}
onFileSelected={setFrontImage}
/>
</View>
<View
style={{
width: isSmallScreen ? '100%' : '48%',
marginBottom: 10,
}}
>
<SingleFileDrop
title={t('Orqa tomon')}
onFileSelected={setBackImage}
/>
</View>
</View>
{/* BUTTON */}
<TouchableOpacity
onPress={handleSubmit(onSubmit)}
style={[
RegisterStyle.button,
!termsAccepted && RegisterStyle.buttonDisabled,
]}
disabled={!termsAccepted}
>
{isPending ? (
<ActivityIndicator color="#fff" />
) : (
<Text
style={[
RegisterStyle.btnText,
!termsAccepted && RegisterStyle.buttonTextDisabled,
]}
>
{t('Tasdiqlash')}
</Text>
)}
</TouchableOpacity>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</ImageBackground>
);
};
export default SecondStep;

View File

@@ -0,0 +1,155 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import SimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons';
import { RootStackParamList } from 'types/types';
type TermsScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'TermsAndConditions'
>;
const TermsAndConditions = () => {
const navigation = useNavigation<TermsScreenNavigationProp>();
const { t } = useTranslation();
const handleAgree = () => {
navigation.goBack();
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => navigation.goBack()}>
<SimpleLineIcons name="arrow-left" color="#000" size={20} />
</TouchableOpacity>
<Text style={styles.headerTitle}>{t('Foydalanish shartlari')}</Text>
<View style={{ width: 20 }} />
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
<Text style={styles.title}>
{t('foydalanish_shartlari_va_qoidalari')}
</Text>
<Text style={styles.sectionTitle}>{t('umumiy_qoidalar')}</Text>
<Text style={styles.text}>{t('umumiy_qoidalar_text')}</Text>
<Text style={styles.sectionTitle}>
{t('foydalanuvchi_majburiyatlari')}
</Text>
<Text style={styles.text}>
{t('foydalanuvchi_majburiyatlari_text')}
</Text>
<Text style={styles.sectionTitle}>{t('maxfiylik_siyosati')}</Text>
<Text style={styles.text}>{t('maxfiylik_siyosati_text')}</Text>
<Text style={styles.sectionTitle}>{t('javobgarlik')}</Text>
<Text style={styles.text}>{t('javobgarlik_text')}</Text>
<Text style={styles.sectionTitle}>{t('shartlarni_ozgartirish')}</Text>
<Text style={styles.text}>{t('shartlarni_ozgartirish_text')}</Text>
<Text style={styles.sectionTitle}>{t('aloqa')}</Text>
<Text style={styles.text}>{t('aloqa_text')}</Text>
<View style={styles.footer}>
<Text style={styles.footerText}>
{t('oxirgi_yangilanish')} {new Date().toLocaleDateString('uz-UZ')}
</Text>
</View>
</ScrollView>
<View style={styles.bottomContainer}>
<TouchableOpacity style={styles.agreeButton} onPress={handleAgree}>
<Text style={styles.agreeButtonText}>{t('roziman')}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 15,
borderBottomWidth: 1,
borderBottomColor: '#E5E5E5',
},
headerTitle: {
fontSize: 18,
fontWeight: '600',
color: '#000',
},
content: {
flex: 1,
paddingHorizontal: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#000',
marginVertical: 20,
textAlign: 'center',
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: '#000',
marginTop: 20,
marginBottom: 10,
},
text: {
fontSize: 14,
lineHeight: 22,
color: '#333',
marginBottom: 15,
textAlign: 'justify',
},
footer: {
marginTop: 30,
marginBottom: 20,
alignItems: 'center',
},
footerText: {
fontSize: 12,
color: '#666',
fontStyle: 'italic',
},
bottomContainer: {
paddingHorizontal: 20,
paddingVertical: 15,
borderTopWidth: 1,
borderTopColor: '#E5E5E5',
},
agreeButton: {
backgroundColor: '#28A7E8',
paddingVertical: 15,
borderRadius: 8,
alignItems: 'center',
},
agreeButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
export default TermsAndConditions;

View File

@@ -0,0 +1,24 @@
'use client';
import { useState } from 'react';
import Confirm from './Confirm';
import FirstStep from './FirstStep';
const Register = () => {
const [step, setStep] = useState<number>(1);
const nextStep = () => {
if (step === 1) {
setStep(2);
}
};
return (
<>
{step === 1 && <FirstStep onNext={nextStep} />}
{step === 2 && <Confirm setStep={setStep} />}
</>
);
};
export default Register;

View File

@@ -0,0 +1,210 @@
import { StyleSheet } from 'react-native';
export const RegisterStyle = StyleSheet.create({
langContainer: {
width: '100%',
marginTop: 10,
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
},
container: {
flex: 1,
},
background: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
width: '100%',
},
loginContainer: {
borderRadius: 20,
padding: 30,
display: 'flex',
gap: 20,
width: '100%',
position: 'relative',
},
sectionsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
gap: 15,
},
mainTitle: {
fontSize: 16,
fontWeight: '500',
color: '#28A7E8',
marginBottom: 10,
},
label: {
fontSize: 12,
fontWeight: '500',
color: '#28A7E8',
marginBottom: 5,
},
title: {
fontSize: 24,
fontWeight: '600',
textAlign: 'center',
color: '#28A7E8',
marginBottom: 20,
},
errorText: {
color: 'red',
fontSize: 12,
},
input: {
borderWidth: 1,
borderColor: '#D8DADC',
borderRadius: 12,
padding: 15,
fontSize: 16,
fontWeight: '400',
backgroundColor: '#FFFFFF',
color: '#000',
},
seriyaInput: {
width: 60,
fontSize: 14,
textTransform: 'uppercase',
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
raqamInput: {
flex: 1,
fontSize: 16,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
selector: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
},
selectedText: {
fontSize: 14,
color: '#D8DADC',
},
dropdown: {
position: 'absolute',
top: 80,
left: 0,
right: 0,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
zIndex: 10,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
maxHeight: 150,
},
dropdownItem: {
paddingVertical: 10,
paddingHorizontal: 12,
},
dropdownItemText: {
fontSize: 14,
color: '#333',
},
button: {
backgroundColor: '#28A7E8',
height: 56,
borderRadius: 8,
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
},
btnText: {
color: '#fff',
fontSize: 20,
textAlign: 'center',
},
termsContainer: {
marginVertical: 15,
paddingHorizontal: 5,
},
checkboxContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
paddingVertical: 5,
},
checkbox: {
width: 24,
height: 24,
borderWidth: 2.5,
borderColor: '#E0E4E7',
borderRadius: 6,
marginRight: 12,
justifyContent: 'center',
alignItems: 'center',
marginTop: 1,
backgroundColor: '#FFFFFF',
elevation: 2,
},
checkboxChecked: {
backgroundColor: '#28A7E8',
borderColor: '#28A7E8',
},
termsTextContainer: {
flex: 1,
paddingTop: 2,
},
termsText: {
fontSize: 15,
color: '#2C3E50',
lineHeight: 22,
fontWeight: '400',
},
termsLink: {
color: '#28A7E8',
fontWeight: '600',
textDecorationLine: 'underline',
fontSize: 15,
},
buttonDisabled: {
backgroundColor: '#28A7E8',
opacity: 0.7,
},
buttonTextDisabled: {
color: 'white',
},
btnRegister: {
color: '#28A7E8',
fontSize: 14,
fontWeight: '500',
},
content: {
flex: 1,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#D8DADC',
borderRadius: 12,
paddingHorizontal: 15,
height: 56,
backgroundColor: '#FFFFFF',
},
iconButton: {
position: 'absolute',
right: 12,
top: '50%',
transform: [{ translateY: -12 }],
justifyContent: 'center',
alignItems: 'center',
height: 24,
width: 24,
},
});

View File

@@ -0,0 +1,146 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useTranslation } from 'react-i18next';
import {
Image,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
useWindowDimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import { RootStackParamList } from 'types/types';
type LoginScreenNavigationProp = NativeStackNavigationProp<
RootStackParamList,
'Login'
>;
const SelectAuth = () => {
const { t } = useTranslation();
const navigation = useNavigation<LoginScreenNavigationProp>();
const { width } = useWindowDimensions();
const isSmallScreen = width < 360;
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={[styles.innerContainer, { maxWidth: 500 }]}>
<View style={styles.logoWrapper}>
<Image
source={Logo}
style={[
styles.logoImage,
{
width: isSmallScreen ? 120 : 250,
height: isSmallScreen ? 120 : 200,
borderRadius: 20000,
},
]}
/>
<Text
style={[styles.logoText, { fontSize: isSmallScreen ? 22 : 50 }]}
>
CPOST
</Text>
</View>
<Text style={[styles.title, { fontSize: isSmallScreen ? 20 : 24 }]}>
{t('Royxatdan otganmisz')}
</Text>
<View style={styles.btnContainer}>
<View style={{ gap: 4 }}>
<Text style={styles.helperText}>
{t("Botdan ro'yxatdan otganmisiz")}
</Text>
<TouchableOpacity
onPress={() => navigation.navigate('Login')}
style={styles.button}
>
<Text style={styles.btnText}>{t('Tizimga kirish')}</Text>
</TouchableOpacity>
</View>
<View style={{ gap: 4 }}>
<Text style={styles.helperText}>
{t("Yangi royxatdan o'tmoqchimisiz")}
</Text>
<TouchableOpacity
onPress={() => navigation.navigate('Register')}
style={styles.button}
>
<Text style={styles.btnText}>{t('Royxatdan otish')}</Text>
</TouchableOpacity>
</View>
</View>
</View>
</ScrollView>
</View>
</SafeAreaView>
);
};
export default SelectAuth;
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
margin: 5,
borderRadius: 12,
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
},
innerContainer: {
width: '100%',
paddingHorizontal: 10,
},
logoWrapper: {
alignItems: 'center',
marginBottom: 40,
},
logoImage: {
resizeMode: 'stretch',
},
logoText: {
fontWeight: '700',
color: '#28A7E8',
},
title: {
fontWeight: '600',
textAlign: 'center',
color: '#28A7E8',
marginBottom: 20,
},
helperText: {
color: '#28A7E8',
fontSize: 16,
},
button: {
backgroundColor: '#28A7E8',
height: 56,
borderRadius: 6,
justifyContent: 'center',
},
btnText: {
color: '#fff',
fontSize: 18,
textAlign: 'center',
},
btnContainer: {
gap: 16,
},
});

View File

@@ -0,0 +1,120 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import RU from 'screens/../../assets/bootsplash/RU.png';
import UZ from 'screens/../../assets/bootsplash/UZ.png';
import { changeLanguage } from 'utils/changeLanguage';
const languages = [
{ code: 'uz', label: 'Uzb', Icon: UZ },
{ code: 'ru', label: 'Ru', Icon: RU },
];
const LanguageSelector = () => {
const { i18n } = useTranslation();
const [dropdownVisible, setDropdownVisible] = useState(false);
const selectedLang = languages.find(l => l.code === i18n.language);
const handleLanguageChange = async (lang: string) => {
await changeLanguage(lang);
setDropdownVisible(false);
};
return (
<View style={styles.wrapper}>
<TouchableOpacity
style={styles.selector}
onPress={() => setDropdownVisible(prev => !prev)}
>
<View
style={{
display: 'flex',
flexDirection: 'row',
gap: 10,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Image
source={selectedLang?.Icon}
style={{ width: 20, height: 20, objectFit: 'contain' }}
/>
<Text style={styles.selectedText}>{selectedLang?.label}</Text>
</View>
<Icon
name={dropdownVisible ? 'chevron-up' : 'chevron-down'}
size={20}
color="#555"
/>
</TouchableOpacity>
{dropdownVisible && (
<View style={styles.dropdown}>
{languages.map(item => (
<TouchableOpacity
key={item.code}
style={styles.dropdownItem}
onPress={() => handleLanguageChange(item.code)}
>
<Image
source={item.Icon}
style={{ width: 25, height: 25, objectFit: 'contain' }}
/>
<Text style={styles.dropdownItemText}>{item.label}</Text>
</TouchableOpacity>
))}
</View>
)}
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
alignSelf: 'flex-end',
margin: 10,
width: 160,
},
selector: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 10,
backgroundColor: '#fff',
flexDirection: 'row',
gap: 10,
alignItems: 'center',
justifyContent: 'space-between',
alignSelf: 'flex-end',
},
selectedText: {
fontSize: 14,
color: '#333',
},
dropdown: {
position: 'absolute',
top: 45,
right: 0,
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 6,
backgroundColor: '#fff',
zIndex: 999,
width: 110,
},
dropdownItem: {
paddingVertical: 5,
paddingHorizontal: 5,
flexDirection: 'row',
alignItems: 'center',
gap: 5,
},
dropdownItemText: {
fontSize: 14,
color: '#333',
},
});
export default LanguageSelector;

View File

@@ -0,0 +1,151 @@
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import React from 'react';
import {
Image,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
useWindowDimensions,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import RU from 'screens/../../assets/bootsplash/RU.png';
import UZ from 'screens/../../assets/bootsplash/UZ.png';
import Logo from 'screens/../../assets/bootsplash/logo_512.png';
import { RootStackParamList } from 'types/types';
import { changeLanguage } from 'utils/changeLanguage';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
const SelectLangPage = () => {
const navigation = useNavigation<NavigationProp>();
const { width } = useWindowDimensions();
const isSmallScreen = width < 380;
const selectLanguage = async (lang: 'uz' | 'ru') => {
await changeLanguage(lang);
navigation.navigate('select-auth');
};
return (
<SafeAreaView style={{ flex: 1 }}>
<View style={styles.container}>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={[styles.innerContainer, { maxWidth: 500 }]}>
<View style={styles.logoWrapper}>
<Image
source={Logo}
style={[
styles.logoImage,
{
width: isSmallScreen ? 120 : 250,
height: isSmallScreen ? 120 : 200,
borderRadius: 20000,
},
]}
/>
<Text
style={[styles.logoText, { fontSize: isSmallScreen ? 24 : 40 }]}
>
CPOST
</Text>
</View>
<Text style={[styles.title, { fontSize: isSmallScreen ? 18 : 24 }]}>
Tilni tanlang{' '}
<Text
style={[styles.title, { fontSize: isSmallScreen ? 14 : 18 }]}
>
(Выберите язык)
</Text>
</Text>
<View style={styles.btnContainer}>
<TouchableOpacity
onPress={() => selectLanguage('uz')}
style={styles.button}
>
<Image source={UZ} style={styles.flag} />
<Text style={styles.btnText}>O'zbek tili</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => selectLanguage('ru')}
style={styles.button}
>
<Image source={RU} style={styles.flag} />
<Text style={styles.btnText}>Русский язык</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</View>
</SafeAreaView>
);
};
export default SelectLangPage;
const styles = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 20,
margin: 5,
borderRadius: 12,
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
},
innerContainer: {
width: '100%',
paddingHorizontal: 10,
},
logoWrapper: {
alignItems: 'center',
marginBottom: 30,
},
logoImage: {
resizeMode: 'stretch',
marginBottom: 8,
},
logoText: {
fontWeight: '700',
color: '#28A7E8',
},
title: {
fontWeight: '600',
textAlign: 'center',
color: '#28A7E8',
marginBottom: 24,
},
button: {
backgroundColor: '#28A7E8',
height: 56,
borderRadius: 6,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingHorizontal: 16,
},
btnText: {
color: '#fff',
fontSize: 18,
},
btnContainer: {
gap: 16,
},
flag: {
width: 30,
height: 30,
resizeMode: 'cover',
},
});