Files
info-target-mobile/screens/auth/register/RegisterForm.tsx
Samandar Turgunboyev fee9213c59 get discrit token
2026-03-19 14:04:41 +05:00

824 lines
24 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 AuthHeader from '@/components/ui/AuthHeader';
import { decryptToken } from '@/constants/crypto';
import { formatPhone, normalizeDigits } from '@/constants/formatPhone';
import { formatText, formatTextToLatin } from '@/constants/formatText';
import { products_api } from '@/screens/home/lib/api';
import BottomSheet, { BottomSheetBackdrop, BottomSheetFlatList, BottomSheetTextInput } from '@gorhom/bottom-sheet';
import { useMutation, useQuery } from '@tanstack/react-query';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import {
CheckIcon,
ChevronDown,
Globe,
Hash,
Search,
UserPlus
} from 'lucide-react-native';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Keyboard,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { SafeAreaView } from 'react-native-safe-area-context';
import { auth_api, GetInfo } from '../login/lib/api';
import PhonePrefix from '../login/ui/PhonePrefix';
import { UseLoginForm } from '../login/ui/UseLoginForm';
interface CoordsData {
lat: number;
lon: number;
polygon: [number, number][][];
}
export default function RegisterFormScreen() {
const router = useRouter();
const { t } = useTranslation();
const { phone, setPhone } = UseLoginForm();
const countrySheetRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ['60%', '90%'], []);
const [selectedCountry, setSelectedCountry] = useState<string>('UZ');
const [countrySearch, setCountrySearch] = useState<string>('');
const [stir, setStir] = useState('');
const [info, setInfo] = useState<GetInfo | null>(null);
const [loading, setLoading] = useState(false);
const [referal, setReferal] = useState('');
const [error, setError] = useState<string | null>(null)
const [district, setDistrict] = useState<string | null>(null)
const [region, setRegion] = useState<string | null>(null)
const [token, setTokens] = useState<{ name: string, value: string } | null>(null)
const [directorTinInput, setDirectorTinInput] = useState('');
const { data } = useQuery({
queryKey: ["tokens"],
queryFn: async () => auth_api.get_tokens(),
select(data) {
return data.data.data.results
},
})
useEffect(() => {
if (data?.length) {
const token = data[0]
const tokenValue = decryptToken(token.value)
if (tokenValue) {
setTokens({ name: token.key, value: tokenValue })
}
}
}, [data])
const { mutate } = useMutation({
mutationFn: (stir: string) => auth_api.get_info({ value: stir, token: token?.value || "", tokenName: token?.name || "" }),
onSuccess: (res) => {
setInfo(res.data);
setLoading(false);
setError(null)
setDistrict(res.data.address)
},
onError: () => {
setInfo(null);
setLoading(false);
setError("Foydalanuvchi topilmadi")
},
});
const { data: districts } = useQuery({
queryKey: ["discrit"],
queryFn: async () => auth_api.get_district({ token: token?.value || "", tokenName: token?.name || "" }),
enabled: !!token,
})
const { data: regions } = useQuery({
queryKey: ["regions"],
queryFn: async () => auth_api.get_region({ token: token?.value || "", tokenName: token?.name || "" }),
enabled: !!token,
})
const { data: countryResponse, isLoading: countryLoading } = useQuery({
queryKey: ['country-detail'],
queryFn: async () => products_api.getStates(),
select: (res) => res.data?.data || [],
});
const getRegionDistrictFromAddress = async (address: string) => {
try {
const encoded = encodeURIComponent(address + ", Uzbekistan");
const res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${encoded}&format=json&addressdetails=1&limit=1`,
{ headers: { 'Accept-Language': 'uz', 'User-Agent': 'MyApp/1.0 (turgunboyevsamandar4@gamil.com)' } }
);
const data = await res.json();
if (data.length > 0) {
const addr = data[0].address;
return {
district: addr.county || addr.district || addr.suburb,
region: addr.state,
};
}
} catch (e) { }
return null;
};
useEffect(() => {
if (district) {
const dis = formatText(district)?.split(" ")[0].toLocaleUpperCase()
let reg = null
if (dis) {
reg = districts?.data.find((item) => item.name.includes(dis))
};
if (reg) {
const region = regions?.data.find((item) => item.regionId == reg.regionId)
setRegion(region?.name || "")
}
}
}, [district])
const [districtId, setDistrictId] = useState<number | null>(null);
const [regionId, setRegionId] = useState<number | null>(null);
useEffect(() => {
if (!district || !countryResponse?.length) return;
const resolve = async () => {
const geo = await getRegionDistrictFromAddress(district.split(" ")[0]);
const searchRegion = geo?.region || region;
const searchDistrict = geo?.district || district
for (const country of countryResponse) {
const regionName = formatTextToLatin(searchRegion)?.split(" ")[0];
let foundRegion = null
if (regionName) {
foundRegion = country.region.find((r: any) =>
formatTextToLatin(r.name)?.includes(regionName)
);
}
if (foundRegion) {
setRegionId(foundRegion.id);
setDistrictId(null);
return;
}
for (const reg of country.region || []) {
const dis = formatTextToLatin(searchDistrict)?.split(" ")[0].toUpperCase();
let foundDistrict = null
if (dis) {
foundDistrict = reg.districts.find((d: any) => {
return formatTextToLatin(d.name)?.toUpperCase().includes(dis.slice(0, 4))
}
);
};
if (foundDistrict) {
setDistrictId(foundDistrict.id);
setRegionId(null);
return;
}
}
}
};
resolve();
}, [district, countryResponse]);
useEffect(() => {
if (info === null || (stir.length === 9 && info.name && info.fullName)) {
setError(null)
} else if (info?.name === null || info?.fullName === null) {
setError("Sizning shaxsiy ma'lumotlaringiz topilmadi")
} else if (!info?.selfEmployment && !info?.isItd) {
setError("Siz o'zini o'zi band qilgan yoki yakka tartibdagi tadbirkorlik bo'lishingiz kerak")
}
}, [info])
const hasDirectorTin = info?.directorPinfl && String(info.directorPinfl).length > 0;
const isDirectorTinValid = !hasDirectorTin || directorTinInput === String(info.directorPinfl);
const hasValidName = Boolean(info?.name || info?.fullName);
const filteredCountries = useMemo(() => {
if (!countrySearch.trim()) return countryResponse || [];
const q = countrySearch.toLowerCase().trim();
return (countryResponse || []).filter((c: any) => c.name?.toLowerCase().includes(q));
}, [countryResponse, countrySearch]);
const openCountrySheet = useCallback(() => {
Keyboard.dismiss();
setTimeout(() => {
countrySheetRef.current?.snapToIndex(0);
}, 100);
}, []);
const selectedCountryName = useMemo(() => {
if (!selectedCountry) return t('Tanlang');
return (
countryResponse?.find((c: any) => c.flag?.toUpperCase() === selectedCountry)?.name ||
t('Tanlang')
);
}, [selectedCountry, countryResponse, t]);
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop
{...props}
appearsOnIndex={0}
disappearsOnIndex={-1}
opacity={0.6}
pressBehavior="close"
/>
),
[]
);
const closeCountrySheet = useCallback(() => {
countrySheetRef.current?.close();
setTimeout(() => setCountrySearch(''), 300);
}, []);
const valid =
phone.length === 9 &&
(stir.length === 9 || stir.length === 14) &&
info &&
hasValidName &&
isDirectorTinValid &&
error === null;
return (
<>
<KeyboardAwareScrollView
enableOnAndroid
enableAutomaticScroll
extraScrollHeight={120}
style={styles.keyboardScroll}
>
<View
style={styles.container}
onStartShouldSetResponder={() => {
Keyboard.dismiss();
return false;
}}
>
<LinearGradient
colors={['#0f172a', '#1e293b', '#334155']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<View style={styles.decorCircle1} />
<View style={styles.decorCircle2} />
<AuthHeader />
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
<View style={styles.scrollContent}>
<View style={styles.header}>
<View style={styles.iconContainer}>
<LinearGradient
colors={['#10b981', '#059669']}
style={styles.iconGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<UserPlus size={32} color="#fff" />
</LinearGradient>
</View>
<Text style={styles.title}>{t('Ro\'yxatdan o\'tish')}</Text>
</View>
<View style={styles.card}>
<View style={styles.formGap}>
<View>
<View>
<Text style={styles.label}>{t('Davlat')}</Text>
<TouchableOpacity
style={[
styles.input,
styles.inputDisabled,
]}
onPress={openCountrySheet}
activeOpacity={0.7}
testID="country-select"
disabled
>
{countryLoading ? (
<ActivityIndicator size="small" color="#3b82f6" style={{ flex: 1 }} />
) : (
<>
<Image
source={{ uri: `https://flagcdn.com/w320/${selectedCountry.toLowerCase()}.png` }}
style={{ width: 30, height: 15 }}
resizeMode="cover"
/>
<Text
style={[
styles.textInput,
{ color: '#94a3b8' },
]}
numberOfLines={1}
>
{selectedCountryName}
</Text>
</>
)}
<ChevronDown size={18} color={'#cbd5e1'} />
</TouchableOpacity>
</View>
</View>
<View>
<Text style={styles.label}>{t('STIR')}</Text>
<View style={styles.input}>
<Hash size={18} color="#94a3b8" />
<TextInput
value={stir}
keyboardType="numeric"
placeholder={t('STIR')}
placeholderTextColor="#94a3b8"
style={{ flex: 1, color: "black" }}
onChangeText={(text) => {
const v = normalizeDigits(text).slice(0, 14);
setStir(v);
if (v.length === 9 || v.length === 14) {
setLoading(true);
mutate(v);
setRegionId(null)
setDistrictId(null)
setRegion(null)
setDistrict(null)
}
}}
/>
{loading && <ActivityIndicator size="small" />}
</View>
</View>
<View>
<Text style={styles.label}>{t('Referal')}</Text>
<View style={styles.input}>
<Hash size={18} color="#94a3b8" />
<TextInput
value={referal}
placeholder={t('Referal kodi')}
placeholderTextColor="#94a3b8"
style={styles.textInput}
onChangeText={setReferal}
maxLength={9}
testID="referal-input"
/>
</View>
</View>
<View>
<Text style={styles.label}>{t('Telefon raqami')}</Text>
<View style={styles.input}>
<PhonePrefix focused={false} />
<TextInput
value={formatPhone(phone)}
placeholder="90 123 45 67"
placeholderTextColor="#94a3b8"
keyboardType="phone-pad"
style={{ flex: 1 }}
onChangeText={(t) => setPhone(normalizeDigits(t))}
/>
</View>
</View>
{hasDirectorTin && (
<View>
<Text style={styles.label}>{t('Direktor STIR')}</Text>
<View style={[styles.input, { backgroundColor: isDirectorTinValid ? '#f0fdf4' : '#f8fafc' }]}>
<Hash size={18} color="#94a3b8" />
<TextInput
value={directorTinInput}
keyboardType="numeric"
placeholder={t('Direktor STIR')}
placeholderTextColor="#94a3b8"
style={{ flex: 1 }}
maxLength={14}
onChangeText={(t) => setDirectorTinInput(normalizeDigits(t))}
/>
</View>
{directorTinInput.length === 14 && !isDirectorTinValid && (
<Text style={styles.error}>{t('Direktor STIR notogri')}</Text>
)}
</View>
)}
{error !== null ?
<Text style={styles.notFound}>{t(error)}</Text>
: info && hasValidName &&
<Text style={styles.info}>{info.fullName || info.name}</Text>
}
<TouchableOpacity
disabled={!valid}
style={[styles.btn, !valid && styles.disabled]}
onPress={() => {
if (error === null) {
router.push({
pathname: '/(auth)/select-category',
params: {
phone,
first_name: stir.length === 9 ? info?.director : info?.fullName,
last_name: stir.length === 9 ? info?.director : info?.fullName,
company_name: info?.name,
district: districtId !== null ? districtId : regionId,
address: districtId !== null ? districtId : regionId,
director_full_name: stir.length === 9 ? info?.director : info?.fullName,
stir,
referal,
person_type: stir.length === 9 ? 'legal_entity' : info?.selfEmployment ? 'band' : info?.isItd ? 'ytt' : 'ytt',
},
})
}
}}
>
<Text style={styles.btnText}>{t('Davom etish')}</Text>
</TouchableOpacity>
</View>
</View>
</View>
</SafeAreaView>
</View >
</KeyboardAwareScrollView >
<BottomSheet
ref={countrySheetRef}
index={-1}
snapPoints={snapPoints}
enablePanDownToClose={true}
enableDynamicSizing={false}
enableOverDrag={false}
backdropComponent={renderBackdrop}
backgroundStyle={styles.bottomSheetBg}
handleIndicatorStyle={styles.handleIndicator}
android_keyboardInputMode="adjustResize"
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"
>
<View style={styles.sheetHeader}>
<Text style={styles.sheetTitle}>{t('Davlat')}</Text>
</View>
<View style={styles.searchContainer}>
<Search size={16} color="#94a3b8" />
<BottomSheetTextInput
value={countrySearch}
onChangeText={setCountrySearch}
placeholder={t('Qidirish...')}
placeholderTextColor="#94a3b8"
style={styles.searchInput}
clearButtonMode="while-editing"
autoCorrect={false}
/>
</View>
<BottomSheetFlatList
data={filteredCountries}
keyExtractor={(item: any) => item.id?.toString()}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View style={styles.emptyList}>
<Text style={styles.emptyListText}>{t('Natija topilmadi')}</Text>
</View>
}
renderItem={({ item }: { item: any }) => {
const isSelected = item.flag?.toUpperCase() === selectedCountry;
const flagCode = item.flag ? item.flag.toLowerCase() : '';
return (
<TouchableOpacity
style={[styles.listItem, isSelected && styles.selectedListItem]}
onPress={() => {
setSelectedCountry(item.flag?.toUpperCase());
closeCountrySheet();
}}
activeOpacity={0.7}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, flex: 1 }}>
{flagCode ? (
<Image
source={{ uri: `https://flagcdn.com/w320/${flagCode}.png` }}
style={{ width: 34, height: 22, borderRadius: 2 }}
/>
) : (
<Globe size={20} color={isSelected ? '#2563eb' : '#94a3b8'} />
)}
<Text style={[styles.listItemText, isSelected && styles.selectedListItemText]}>
{item.name}
</Text>
</View>
{isSelected && (
<View style={styles.checkmark}>
<CheckIcon color="#3b82f6" strokeWidth={2.5} size={16} />
</View>
)}
</TouchableOpacity>
);
}}
/>
</BottomSheet >
</>
);
}
const styles = StyleSheet.create({
keyboardScroll: {
flex: 1,
backgroundColor: '#0f172a',
},
container: {
flex: 1,
backgroundColor: '#0f172a',
minHeight: '100%',
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: 24,
paddingBottom: 40,
paddingTop: 10,
},
header: {
alignItems: 'center',
marginBottom: 28,
},
iconContainer: {
marginBottom: 16,
},
iconGradient: {
width: 72,
height: 72,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#10b981',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 8,
},
title: {
fontSize: 26,
fontWeight: '800' as const,
color: '#ffffff',
marginBottom: 8,
letterSpacing: 0.5,
},
subtitle: {
fontSize: 14,
color: '#94a3b8',
textAlign: 'center',
lineHeight: 20,
paddingHorizontal: 10,
},
card: {
backgroundColor: '#ffffff',
borderRadius: 28,
padding: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3,
shadowRadius: 20,
elevation: 10,
},
formGap: {
gap: 16,
},
label: {
fontWeight: '700' as const,
color: '#475569',
marginBottom: 6,
fontSize: 14,
},
input: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f8fafc',
borderRadius: 14,
paddingHorizontal: 12,
height: 52,
borderWidth: 1,
borderColor: '#e2e8f0',
gap: 8,
},
bottomSheetBg: {
backgroundColor: '#ffffff',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
shadowColor: '#000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 5,
},
handleIndicator: {
width: 40,
height: 4,
borderRadius: 2,
backgroundColor: '#cbd5e1',
},
sheetHeader: {
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
sheetTitle: {
fontSize: 18,
fontWeight: '700',
color: '#0f172a',
},
listContainer: {
padding: 16,
paddingBottom: 40,
},
listItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 14,
borderRadius: 12,
marginBottom: 8,
borderWidth: 1,
backgroundColor: '#ffffff',
borderColor: '#e2e8f0',
},
selectedListItem: {
backgroundColor: '#eff6ff',
borderColor: '#3b82f6',
},
listItemText: {
fontSize: 15,
fontWeight: '500',
color: '#1e293b',
},
selectedListItemText: {
color: '#2563eb',
fontWeight: '600',
},
checkmark: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: '#dbeafe',
justifyContent: 'center',
alignItems: 'center',
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#f1f5f9',
borderRadius: 12,
marginHorizontal: 16,
marginVertical: 12,
paddingHorizontal: 12,
gap: 8,
height: 44,
borderWidth: 1,
borderColor: '#e2e8f0',
},
searchInput: {
flex: 1,
fontSize: 15,
color: '#1e293b',
paddingVertical: 0,
},
emptyList: {
alignItems: 'center',
paddingVertical: 32,
},
emptyListText: {
fontSize: 14,
color: '#94a3b8',
fontWeight: '500',
},
textInput: {
flex: 1,
fontSize: 15,
color: '#1e293b',
},
passportRow: {
flexDirection: 'row',
gap: 10,
},
passportSeries: {
width: 100,
flex: undefined,
},
passportNumber: {
flex: 1,
},
infoBox: {
backgroundColor: '#f0fdf4',
padding: 14,
marginTop: 10,
borderRadius: 14,
borderWidth: 1,
borderColor: '#bbf7d0',
},
infoLabel: {
fontSize: 11,
fontWeight: '600' as const,
color: '#059669',
marginBottom: 2,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
infoText: {
fontWeight: '700' as const,
color: '#166534',
fontSize: 14,
},
errorBox: {
backgroundColor: '#fef2f2',
padding: 14,
borderRadius: 14,
borderWidth: 1,
borderColor: '#fecaca',
},
errorText: {
fontWeight: '700' as const,
color: '#dc2626',
fontSize: 14,
},
btn: {
height: 54,
backgroundColor: '#2563eb',
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#2563eb',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6,
},
disabled: {
opacity: 0.45,
},
btnText: {
color: '#fff',
fontWeight: '800' as const,
fontSize: 16,
},
decorCircle1: {
position: 'absolute',
top: -150,
right: -100,
width: 400,
height: 400,
borderRadius: 200,
backgroundColor: 'rgba(59, 130, 246, 0.1)',
},
decorCircle2: {
position: 'absolute',
bottom: -100,
left: -150,
width: 350,
height: 350,
borderRadius: 175,
backgroundColor: 'rgba(16, 185, 129, 0.08)',
},
inputDisabled: {
backgroundColor: '#f1f5f9',
borderColor: '#e2e8f0',
},
info: {
padding: 12,
borderRadius: 12,
fontWeight: '700',
backgroundColor: '#f0fdf4',
},
error: {
color: '#dc2626',
fontSize: 12,
marginTop: 4,
fontWeight: '600',
},
notFound: {
backgroundColor: '#fef2f2',
padding: 12,
borderRadius: 12,
fontWeight: '700',
color: '#dc2626',
borderWidth: 1,
borderColor: '#fecaca',
},
});