You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

678 lines
18 KiB

import React, { useState, useRef, useEffect, useCallback } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
Modal,
ActivityIndicator,
Platform,
FlatList,
} from "react-native";
import { useTranslation } from "react-i18next";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { CountryList } from "../../constants/countries";
import { settingApi } from "../../services/api/setting";
type ForgotPhonePasswordProps = {
visible?: boolean;
onClose?: () => void;
selectedCountry?: CountryList;
};
export const ForgotPhonePassword = ({
visible = true,
onClose = () => {},
selectedCountry
}: ForgotPhonePasswordProps) => {
const { t } = useTranslation();
// States
const [phoneNumber, setPhoneNumber] = useState("");
const [phoneNumberError, setPhoneNumberError] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [countryList, setCountryList] = useState<CountryList[]>([]);
const [currentCountry, setCurrentCountry] = useState<CountryList | undefined>(selectedCountry);
const [showCountryModal, setShowCountryModal] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [filteredCountryList, setFilteredCountryList] = useState<CountryList[]>([]);
// Refs
const phoneInputRef = useRef<TextInput>(null);
const searchInputRef = useRef<TextInput>(null);
// Load country data if not provided
useEffect(() => {
if (visible && !currentCountry) {
loadCountryData();
}
if (visible && selectedCountry) {
setCurrentCountry(selectedCountry);
}
}, [visible, selectedCountry]);
// Focus phone input when modal opens
useEffect(() => {
if (visible) {
const timer = setTimeout(() => {
if (phoneInputRef.current) {
phoneInputRef.current.focus();
}
}, 300);
return () => clearTimeout(timer);
}
}, [visible]);
// Load country data
const loadCountryData = async () => {
try {
const res = await settingApi.getSendSmsCountryList();
setCountryList(res);
setFilteredCountryList(res);
const savedCountry = await AsyncStorage.getItem("@selected_country");
if (savedCountry) {
try {
const parsedCountry = JSON.parse(savedCountry);
const item = res.find(item => item.country === parsedCountry.country);
if (item) {
setCurrentCountry(item);
} else if (res.length > 0) {
setCurrentCountry(res[0]);
}
} catch (e) {
console.error("Error parsing stored country", e);
if (res.length > 0) {
setCurrentCountry(res[0]);
}
}
} else if (res.length > 0) {
setCurrentCountry(res[0]);
}
} catch (error) {
console.error("Failed to load country data", error);
}
};
// Filter countries based on search query
useEffect(() => {
if (searchQuery.trim() === "") {
setFilteredCountryList(countryList);
} else {
const query = searchQuery.toLowerCase();
const filtered = countryList.filter(
country =>
country.name_en.toLowerCase().includes(query) ||
country.country.toString().includes(query)
);
setFilteredCountryList(filtered);
}
}, [searchQuery, countryList]);
// Focus search input when country modal opens
useEffect(() => {
if (showCountryModal && searchInputRef.current) {
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 300);
return () => clearTimeout(timer);
}
}, [showCountryModal]);
// Clear search when modal closes
useEffect(() => {
if (!showCountryModal) {
setSearchQuery("");
}
}, [showCountryModal]);
// Handle country selection
const handleCountrySelect = (country: CountryList) => {
setCurrentCountry(country);
setShowCountryModal(false);
// Save selected country to AsyncStorage
AsyncStorage.setItem("@selected_country", JSON.stringify(country));
// Reset validation errors when country changes
if (phoneNumber) {
setPhoneNumberError(!validatePhoneNumber(phoneNumber, country));
}
};
// Render country list item - with performance optimization
const renderCountryItem = useCallback(
({ item }: { item: CountryList }) => (
<TouchableOpacity
style={styles.countryItem}
onPress={() => handleCountrySelect(item)}
activeOpacity={0.7}
>
<View style={styles.countryItemContent}>
<Text style={styles.countryCode}>+{item.country}</Text>
<Text style={[styles.countryName]}>{item.name_en}</Text>
</View>
{/* Add checkmark for selected country */}
{currentCountry && currentCountry.country === item.country && (
<Text style={styles.checkmark}></Text>
)}
</TouchableOpacity>
),
[currentCountry]
);
// Validate phone number
const validatePhoneNumber = (phoneNum: string, country = currentCountry) => {
if (!country || !country.valid_digits || country.valid_digits.length === 0) {
return true; // No validation if no valid_digits available
}
return country.valid_digits.includes(phoneNum.length);
};
// Handle phone number change
const handlePhoneNumberChange = (text: string) => {
setPhoneNumber(text);
if (text.length > 0) {
setPhoneNumberError(!validatePhoneNumber(text));
} else {
setPhoneNumberError(false);
}
setError(null);
};
// Handle submit
const handleSubmit = async () => {
if (!validatePhoneNumber(phoneNumber)) {
setPhoneNumberError(true);
return;
}
try {
setLoading(true);
// TODO: Replace with actual API call to send reset code
// For example: await userApi.sendPhonePasswordResetCode({ phone: phoneNumber, country: currentCountry?.country });
// Simulate API call success
setTimeout(() => {
setLoading(false);
onClose();
// You could navigate to a verification code screen here if needed
}, 1500);
} catch (error) {
setLoading(false);
setError('Failed to send reset code. Please try again.');
}
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
statusBarTranslucent={true}
>
<View style={styles.modalContainer}>
<View style={styles.forgotPasswordContainer}>
<View style={styles.forgotPasswordHeader}>
<TouchableOpacity
style={styles.forgotPasswordCloseButton}
onPress={onClose}
activeOpacity={0.7}
>
<Text style={styles.forgotPasswordCloseButtonText}></Text>
</TouchableOpacity>
<Text style={styles.forgotPasswordTitle}>{t("forgotPassword")}</Text>
</View>
<View style={styles.forgotPasswordContent}>
<Text style={styles.forgotPasswordDescription}>
Enter your phone number below, and we'll send you a 6-digit password reset code.
</Text>
<View style={styles.phoneInputContainer}>
<TouchableOpacity
style={styles.countryCodeButton}
onPress={() => setShowCountryModal(true)}
>
<Text style={styles.countryCodeText}>
+{currentCountry?.country || ''}
</Text>
<Text style={styles.countryCodeArrow}>▼</Text>
</TouchableOpacity>
<View style={styles.phoneInputDivider} />
<TextInput
ref={phoneInputRef}
style={styles.phoneInput}
placeholder={t("phoneNumber")}
value={phoneNumber}
onChangeText={handlePhoneNumberChange}
keyboardType="phone-pad"
autoFocus
maxLength={15}
/>
{phoneNumber.length > 0 && (
<TouchableOpacity
style={styles.phoneClearButton}
onPress={() => {
setPhoneNumber("");
setPhoneNumberError(false);
setError(null);
}}
activeOpacity={0.7}
>
<Text style={styles.phoneClearButtonText}>✕</Text>
</TouchableOpacity>
)}
</View>
{phoneNumberError && (
<Text style={styles.phoneNumberErrorText}>
{t("invalidPhoneNumber")}
{currentCountry?.valid_digits &&
`(${t("requiresDigits")}: ${currentCountry.valid_digits.join(', ')})`}
</Text>
)}
{error && (
<Text style={styles.errorText}>
{error}
</Text>
)}
<TouchableOpacity
style={[
styles.submitButton,
(!phoneNumber.trim() || phoneNumberError) && styles.disabledButton,
]}
onPress={handleSubmit}
disabled={!phoneNumber.trim() || phoneNumberError || loading}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.submitButtonText}>
{t("submit")}
</Text>
)}
</TouchableOpacity>
</View>
</View>
{/* Country selection modal */}
<Modal
visible={showCountryModal}
animationType="slide"
transparent={true}
onRequestClose={() => setShowCountryModal(false)}
hardwareAccelerated={true}
statusBarTranslucent={true}
presentationStyle="overFullScreen"
>
<View style={styles.countryModalContainer}>
<TouchableOpacity
style={styles.countryModalOverlay}
activeOpacity={1}
onPress={() => setShowCountryModal(false)}
/>
<View style={styles.countryModalContent}>
<View style={styles.modalHandleContainer}>
<View style={styles.modalHandle} />
</View>
<View style={styles.countryModalHeader}>
<TouchableOpacity
style={styles.countryModalCloseButton}
onPress={() => setShowCountryModal(false)}
activeOpacity={0.7}
>
<Text style={styles.countryModalCloseButtonText}>✕</Text>
</TouchableOpacity>
<Text style={styles.countryModalTitle}>
{t("selectCountry")}
</Text>
</View>
{/* Country search input */}
<View style={styles.searchContainer}>
<View style={styles.searchInputContainer}>
<Text style={styles.searchIcon}>🔍</Text>
<TextInput
ref={searchInputRef}
style={styles.searchInput}
placeholder={t("searchCountry")}
value={searchQuery}
onChangeText={setSearchQuery}
clearButtonMode="while-editing"
autoCapitalize="none"
/>
{searchQuery.length > 0 && (
<TouchableOpacity
style={styles.searchClearButton}
onPress={() => setSearchQuery("")}
activeOpacity={0.7}
>
<Text style={styles.searchClearButtonText}>✕</Text>
</TouchableOpacity>
)}
</View>
</View>
<FlatList
data={filteredCountryList}
renderItem={renderCountryItem}
keyExtractor={(item) => item.country.toString()}
style={styles.countryList}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={10}
getItemLayout={(data, index) => ({
length: 69,
offset: 69 * index,
index,
})}
ListEmptyComponent={() => (
<View style={styles.emptyResultContainer}>
<Text style={styles.emptyResultText}>{t("noCountriesFound")}</Text>
</View>
)}
/>
</View>
</View>
</Modal>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "flex-end",
zIndex: 9999,
},
forgotPasswordContainer: {
backgroundColor: "#fff",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
height: "80%",
shadowColor: "#000",
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 5,
elevation: 5,
},
forgotPasswordHeader: {
flexDirection: "row",
alignItems: "center",
paddingTop: 20,
paddingHorizontal: 16,
paddingBottom: 15,
borderBottomWidth: 1,
borderBottomColor: "#f0f0f0",
},
forgotPasswordCloseButton: {
padding: 8,
width: 36,
height: 36,
justifyContent: "center",
alignItems: "center",
},
forgotPasswordCloseButtonText: {
fontSize: 18,
color: "#000",
},
forgotPasswordTitle: {
flex: 1,
fontSize: 18,
fontWeight: "600",
color: "#000",
textAlign: "center",
marginRight: 36,
},
forgotPasswordContent: {
padding: 20,
paddingBottom: Platform.OS === "ios" ? 50 : 30,
},
forgotPasswordDescription: {
fontSize: 14,
color: "#333",
marginBottom: 20,
lineHeight: 20,
},
phoneInputContainer: {
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderColor: "#E1E1E1",
borderRadius: 25,
height: 50,
marginBottom: 20,
position: "relative",
},
countryCodeButton: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
height: "100%",
minWidth: 80,
justifyContent: "space-between",
},
countryCodeText: {
fontSize: 15,
color: "#333",
},
countryCodeArrow: {
fontSize: 10,
color: "#666",
marginLeft: 4,
},
phoneInputDivider: {
width: 1,
height: "60%",
backgroundColor: "#E1E1E1",
},
phoneInput: {
flex: 1,
height: "100%",
paddingLeft: 10,
paddingRight: 36,
fontSize: 16,
},
phoneClearButton: {
position: "absolute",
right: 12,
top: "50%",
transform: [{ translateY: -12 }],
height: 24,
width: 24,
justifyContent: "center",
alignItems: "center",
},
phoneClearButtonText: {
fontSize: 16,
color: "#999",
fontWeight: "500",
textAlign: "center",
},
phoneNumberErrorText: {
color: "#FF3B30",
fontSize: 14,
marginTop: -12,
marginBottom: 16,
paddingHorizontal: 5,
},
errorText: {
color: "#FF3B30",
fontSize: 14,
marginTop: -12,
marginBottom: 16,
paddingHorizontal: 5,
},
submitButton: {
height: 50,
backgroundColor: "#0039CB",
borderRadius: 25,
justifyContent: "center",
alignItems: "center",
marginTop: 20,
},
disabledButton: {
backgroundColor: "#CCCCCC",
},
submitButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
// Country modal styles
countryModalContainer: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "flex-end",
zIndex: 999,
},
countryModalOverlay: {
flex: 1,
backgroundColor: "transparent",
},
countryModalContent: {
backgroundColor: "#fff",
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
height: "80%",
maxHeight: "80%",
shadowColor: "#000",
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 5,
elevation: 5,
},
modalHandleContainer: {
width: "100%",
alignItems: "center",
paddingTop: 12,
paddingBottom: 8,
},
modalHandle: {
width: 40,
height: 4,
backgroundColor: "#E0E0E0",
borderRadius: 2,
},
countryModalHeader: {
flexDirection: "row",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#E5E5E5",
},
countryModalCloseButton: {
padding: 4,
},
countryModalCloseButtonText: {
fontSize: 18,
color: "#999",
},
countryModalTitle: {
flex: 1,
fontSize: 18,
fontWeight: "600",
textAlign: "center",
marginRight: 24,
},
countryList: {
padding: 8,
},
countryItem: {
flexDirection: "row",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: "#F0F0F0",
},
countryItemContent: {
flex: 1,
flexDirection: "row",
alignItems: "center",
},
countryName: {
fontSize: 16,
color: "#333",
marginLeft: 10,
},
countryCode: {
fontSize: 15,
color: "#666",
width: 40,
textAlign: "center",
},
checkmark: {
fontSize: 20,
color: "#0066FF",
fontWeight: "bold",
marginRight: 10,
},
// Search styles
searchContainer: {
paddingHorizontal: 16,
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: "#F0F0F0",
},
searchInputContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#F5F5F5",
borderRadius: 20,
paddingHorizontal: 12,
height: 40,
position: "relative",
},
searchIcon: {
fontSize: 16,
marginRight: 8,
color: "#999",
},
searchInput: {
flex: 1,
height: "100%",
fontSize: 15,
color: "#333",
paddingRight: 30,
},
searchClearButton: {
position: "absolute",
right: 12,
height: 20,
width: 20,
justifyContent: "center",
alignItems: "center",
},
searchClearButtonText: {
fontSize: 14,
color: "#999",
fontWeight: "500",
},
emptyResultContainer: {
padding: 20,
alignItems: "center",
},
emptyResultText: {
fontSize: 16,
color: "#999",
textAlign: "center",
},
});