diff --git a/app/locales/en/translation.json b/app/locales/en/translation.json index b7d9765..715a0b8 100644 --- a/app/locales/en/translation.json +++ b/app/locales/en/translation.json @@ -321,5 +321,47 @@ "defaultResponse": "Thank you for your message. Our team will get back to you shortly.", "errorResponse": "Sorry, there was an error processing your request. Please try again later.", "loginRequired": "Login Required", - "pleaseLoginToChat": "Please login to continue with the chat service." + "pleaseLoginToChat": "Please login to continue with the chat service.", + "login": { + "logInOrSignUp": "Log in or sign up", + "phoneNumber": "Phone number", + "enterPassword": "Please re-enter your password", + "passwordIncorrect": "Password incorrect, please confirm your password.", + "verificationCodeInfo": "We will send a verification code on your number to confirm it's you.", + "continue": "Continue", + "pleaseEnterEmail": "Please enter your e-mail address", + "forgotPassword": { + "title": "Forget your password?", + "phoneDescription": "Enter your phone number below, and we'll send you a 6-digit password reset code.", + "emailDescription": "Enter your email address below, and we'll send you a 6-digit password reset code.", + "submit": "Submit", + "invalidPhone": "Invalid phone number", + "requiresDigits": "Requires digits", + "invalidEmail": "Invalid email address" + }, + "verification": { + "title": "Enter the verification code", + "description": "Please check your phone now! Enter the 6-digit password reset code sent to", + "expiration": "The code expires after 2 hours.", + "incorrect": "Incorrect verification code", + "resend": "Resend code", + "didntReceive": "Didn't receive the code?", + "helpPoint1": "1. Make sure your phone number is correct.", + "helpPoint2": "2. Please check if your phone can receive SMS messages." + }, + "resetPassword": { + "title": "Setting a password", + "enterPassword": "Please enter your password", + "confirmPassword": "Please reconfirm your password", + "required": "Password is required", + "minLength": "Password must be at least 6 characters", + "confirmRequired": "Please confirm your password", + "mismatch": "Passwords do not match", + "failed": "Failed to reset password. Please try again.", + "error": "An error occurred. Please try again.", + "submit": "Submit" + } + }, + "searchCountry": "Search country", + "noCountriesFound": "No countries found" } \ No newline at end of file diff --git a/app/locales/fr/translation.json b/app/locales/fr/translation.json index e2d4153..017dd57 100644 --- a/app/locales/fr/translation.json +++ b/app/locales/fr/translation.json @@ -316,5 +316,47 @@ "address_management": "Gestion des adresses", "set_default": "Définir comme adresse par défaut", "delete": "Supprimer" - } + }, + "login": { + "logInOrSignUp": "Se connecter ou s'inscrire", + "phoneNumber": "Numéro de téléphone", + "enterPassword": "Veuillez réentrer votre mot de passe", + "passwordIncorrect": "Mot de passe incorrect, veuillez confirmer votre mot de passe.", + "verificationCodeInfo": "Nous enverrons un code de vérification sur votre numéro pour confirmer que c'est vous.", + "continue": "Continuer", + "pleaseEnterEmail": "Veuillez entrer votre adresse e-mail", + "forgotPassword": { + "title": "Mot de passe oublié ?", + "phoneDescription": "Entrez votre numéro de téléphone ci-dessous, et nous vous enverrons un code de réinitialisation à 6 chiffres.", + "emailDescription": "Entrez votre adresse e-mail ci-dessous, et nous vous enverrons un code de réinitialisation à 6 chiffres.", + "submit": "Soumettre", + "invalidPhone": "Numéro de téléphone invalide", + "requiresDigits": "Nécessite des chiffres", + "invalidEmail": "Adresse e-mail invalide" + }, + "verification": { + "title": "Entrez le code de vérification", + "description": "Vérifiez votre téléphone maintenant ! Entrez le code de réinitialisation à 6 chiffres envoyé à", + "expiration": "Le code expire après 2 heures.", + "incorrect": "Code de vérification incorrect", + "resend": "Renvoyer le code", + "didntReceive": "Vous n'avez pas reçu le code ?", + "helpPoint1": "1. Assurez-vous que votre numéro de téléphone est correct.", + "helpPoint2": "2. Veuillez vérifier si votre téléphone peut recevoir des SMS." + }, + "resetPassword": { + "title": "Définition d'un mot de passe", + "enterPassword": "Veuillez entrer votre mot de passe", + "confirmPassword": "Veuillez confirmer votre mot de passe", + "required": "Le mot de passe est requis", + "minLength": "Le mot de passe doit comporter au moins 6 caractères", + "confirmRequired": "Veuillez confirmer votre mot de passe", + "mismatch": "Les mots de passe ne correspondent pas", + "failed": "Échec de la réinitialisation du mot de passe. Veuillez réessayer.", + "error": "Une erreur s'est produite. Veuillez réessayer.", + "submit": "Soumettre" + } + }, + "searchCountry": "Rechercher un pays", + "noCountriesFound": "Aucun pays trouvé" } \ No newline at end of file diff --git a/app/screens/loginList/EmailLoginModal.tsx b/app/screens/loginList/EmailLoginModal.tsx index a31dd98..e436f28 100644 --- a/app/screens/loginList/EmailLoginModal.tsx +++ b/app/screens/loginList/EmailLoginModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, useEffect } from "react"; import { View, Text, @@ -9,7 +9,8 @@ import { Keyboard, Alert, Platform, - Modal + Modal, + ActivityIndicator } from "react-native"; import { useTranslation } from "react-i18next"; import { useNavigation } from "@react-navigation/native"; @@ -18,6 +19,7 @@ import { userApi } from "../../services/api/userApi"; import { settingApi } from "../../services/api/setting"; import useUserStore from "../../store/user"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; +import { ForgotEmailPassword } from "./ForgotEmailPassword"; // Common email domains list const EMAIL_DOMAINS = [ @@ -57,6 +59,8 @@ const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => { const [emailPasswordError, setEmailPasswordError] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false); const [suggestions, setSuggestions] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); // 防止重复关闭 const isClosing = useRef(false); @@ -146,22 +150,27 @@ const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => { ); + // Add state for forgot password modal + const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); + // Handle forgot password const handleForgotPassword = () => { - // Handle forgot password logic + // Show forgot password modal + setShowForgotPasswordModal(true); }; // Handle email login const handleEmailContinue = async () => { const params = { grant_type: "password", - username: "lifei", - password: "123456", + username: email, + password: emailPassword, client_id: "2", client_secret: "", scope: "", }; try { + setLoading(true); const res = await userApi.login(params); if (res.access_token) { const token = res.token_type + " " + res.access_token; @@ -170,11 +179,14 @@ const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => { setSettings(data); const user = await userApi.getProfile(); setUser(user); + setLoading(false); navigation.navigate("MainTabs", { screen: "Home" }); onClose(); } } catch (error) { - Alert.alert(t("loginFailed")); + setLoading(false); + setError(t("passwordIncorrect")); + setEmailPasswordError(true); } }; @@ -308,7 +320,7 @@ const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => { {emailPasswordError && ( <> - {t("passwordIncorrect")} + {error || t("passwordIncorrect")} { - - {t("continue")} - + {loading ? ( + + ) : ( + + {t("continue")} + + )} + + {/* Add ForgotEmailPassword modal */} + setShowForgotPasswordModal(false)} + email={email} + /> ); diff --git a/app/screens/loginList/ForgotEmailPassword.tsx b/app/screens/loginList/ForgotEmailPassword.tsx new file mode 100644 index 0000000..e2ac531 --- /dev/null +++ b/app/screens/loginList/ForgotEmailPassword.tsx @@ -0,0 +1,377 @@ +import React, { useState, useRef, useEffect } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + TextInput, + Modal, + ActivityIndicator, + Platform, +} from "react-native"; +import { useTranslation } from "react-i18next"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { VerificationCodeInput } from "./VerificationCodeInput"; + +type ForgotEmailPasswordProps = { + visible?: boolean; + onClose?: () => void; + email?: string; +}; + +export const ForgotEmailPassword = ({ + visible = true, + onClose = () => {}, + email = "" +}: ForgotEmailPasswordProps) => { + const { t } = useTranslation(); + + // States + const [userEmail, setUserEmail] = useState(email); + const [emailError, setEmailError] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showVerificationModal, setShowVerificationModal] = useState(false); + + // Refs + const emailInputRef = useRef(null); + + // Focus email input when modal opens + useEffect(() => { + if (visible && !email) { + const timer = setTimeout(() => { + if (emailInputRef.current) { + emailInputRef.current.focus(); + } + }, 300); + + return () => clearTimeout(timer); + } + }, [visible, email]); + + // Set initial email value if provided + useEffect(() => { + if (email) { + setUserEmail(email); + } + }, [email]); + + // Validate email + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Handle email change + const handleEmailChange = (text: string) => { + setUserEmail(text); + if (text) { + setEmailError(!validateEmail(text)); + } else { + setEmailError(false); + } + setError(null); + }; + + // Handle submit + const handleSubmit = async () => { + if (!validateEmail(userEmail)) { + setEmailError(true); + return; + } + + try { + setLoading(true); + // TODO: Replace with actual API call to send reset code + // For example: await userApi.sendEmailPasswordResetCode({ email: userEmail }); + + // Log reset method + console.log("Password reset method: Email"); + try { + // Store reset method in AsyncStorage or other storage + await AsyncStorage.setItem("@password_reset_method", "email"); + } catch (storageError) { + console.error("Failed to store reset method:", storageError); + } + + // Simulate API call success + setTimeout(() => { + setLoading(false); + setShowVerificationModal(true); + }, 1500); + } catch (error) { + setLoading(false); + setError('Failed to send reset code. Please try again.'); + } + }; + + // Handle verification code submission + const handleVerifyCode = async (code: string): Promise => { + // TODO: Replace with actual API call to verify code + // For example: return await userApi.verifyEmailPasswordResetCode({ + // email: userEmail, + // code: code + // }); + + // Simulate verification for demo + return new Promise((resolve) => { + setTimeout(() => { + // For demo: code "123456" is valid, others are invalid + resolve(code === "123456"); + }, 1500); + }); + }; + + // Handle resend code + const handleResendCode = async (): Promise => { + // TODO: Replace with actual API call to resend code + // For example: await userApi.sendEmailPasswordResetCode({ + // email: userEmail + // }); + + // Simulate resend for demo + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1500); + }); + }; + + // Handle reset password + const handleResetPassword = async (password: string): Promise => { + // TODO: Replace with actual API call to reset password + // For example: return await userApi.resetEmailPassword({ + // email: userEmail, + // password: password + // }); + + // Simulate API call for demo + return new Promise((resolve) => { + setTimeout(() => { + // On success, close this modal too + if (onClose) onClose(); + resolve(true); // Always succeed for demo + }, 1500); + }); + }; + + return ( + + + + + + + + {t("login.forgotPassword.title")} + + + + + {t("login.forgotPassword.emailDescription")} + + + + + + {userEmail.length > 0 && ( + { + setUserEmail(""); + setEmailError(false); + setError(null); + }} + activeOpacity={0.7} + > + + + )} + + + {emailError && ( + + {t("login.forgotPassword.invalidEmail")} + + )} + + {error && ( + + {error} + + )} + + + {loading ? ( + + ) : ( + + {t("login.forgotPassword.submit")} + + )} + + + + + {/* Verification Code Modal */} + setShowVerificationModal(false)} + phoneNumber={userEmail} // We're using phoneNumber prop for email too + onVerify={handleVerifyCode} + onResend={handleResendCode} + onResetPassword={handleResetPassword} + /> + + + ); +}; + +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, + }, + emailInputContainer: { + flexDirection: "row", + alignItems: "center", + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 25, + height: 50, + marginBottom: 20, + position: "relative", + }, + emailInput: { + flex: 1, + height: "100%", + paddingHorizontal: 16, + fontSize: 16, + paddingRight: 36, + }, + emailClearButton: { + position: "absolute", + right: 12, + top: "50%", + transform: [{ translateY: -12 }], + height: 24, + width: 24, + justifyContent: "center", + alignItems: "center", + }, + emailClearButtonText: { + fontSize: 16, + color: "#999", + fontWeight: "500", + textAlign: "center", + }, + emailErrorText: { + 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", + }, +}); \ No newline at end of file diff --git a/app/screens/loginList/ForgotPhonePassword.tsx b/app/screens/loginList/ForgotPhonePassword.tsx index b5346b2..e5d01a6 100644 --- a/app/screens/loginList/ForgotPhonePassword.tsx +++ b/app/screens/loginList/ForgotPhonePassword.tsx @@ -14,22 +14,25 @@ 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"; +import { VerificationCodeInput } from "./VerificationCodeInput"; type ForgotPhonePasswordProps = { visible?: boolean; onClose?: () => void; selectedCountry?: CountryList; + phoneNumber?: string; }; export const ForgotPhonePassword = ({ visible = true, onClose = () => {}, - selectedCountry + selectedCountry, + phoneNumber = "" }: ForgotPhonePasswordProps) => { const { t } = useTranslation(); // States - const [phoneNumber, setPhoneNumber] = useState(""); + const [phoneNum, setPhoneNum] = useState(phoneNumber); const [phoneNumberError, setPhoneNumberError] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -38,6 +41,7 @@ export const ForgotPhonePassword = ({ const [showCountryModal, setShowCountryModal] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [filteredCountryList, setFilteredCountryList] = useState([]); + const [showVerificationModal, setShowVerificationModal] = useState(false); // Refs const phoneInputRef = useRef(null); @@ -131,6 +135,13 @@ export const ForgotPhonePassword = ({ } }, [showCountryModal]); + // Set initial phone number value if provided + useEffect(() => { + if (phoneNumber) { + setPhoneNum(phoneNumber); + } + }, [phoneNumber]); + // Handle country selection const handleCountrySelect = (country: CountryList) => { setCurrentCountry(country); @@ -140,8 +151,8 @@ export const ForgotPhonePassword = ({ AsyncStorage.setItem("@selected_country", JSON.stringify(country)); // Reset validation errors when country changes - if (phoneNumber) { - setPhoneNumberError(!validatePhoneNumber(phoneNumber, country)); + if (phoneNum) { + setPhoneNumberError(!validatePhoneNumber(phoneNum, country)); } }; @@ -177,7 +188,7 @@ export const ForgotPhonePassword = ({ // Handle phone number change const handlePhoneNumberChange = (text: string) => { - setPhoneNumber(text); + setPhoneNum(text); if (text.length > 0) { setPhoneNumberError(!validatePhoneNumber(text)); } else { @@ -188,7 +199,7 @@ export const ForgotPhonePassword = ({ // Handle submit const handleSubmit = async () => { - if (!validatePhoneNumber(phoneNumber)) { + if (!validatePhoneNumber(phoneNum)) { setPhoneNumberError(true); return; } @@ -196,13 +207,21 @@ export const ForgotPhonePassword = ({ try { setLoading(true); // TODO: Replace with actual API call to send reset code - // For example: await userApi.sendPhonePasswordResetCode({ phone: phoneNumber, country: currentCountry?.country }); + // For example: await userApi.sendPhonePasswordResetCode({ phone: phoneNum, country: currentCountry?.country }); + + // Log reset method + console.log("Password reset method: Phone"); + try { + // Store reset method in AsyncStorage or other storage + await AsyncStorage.setItem("@password_reset_method", "phone"); + } catch (storageError) { + console.error("Failed to store reset method:", storageError); + } // Simulate API call success setTimeout(() => { setLoading(false); - onClose(); - // You could navigate to a verification code screen here if needed + setShowVerificationModal(true); }, 1500); } catch (error) { setLoading(false); @@ -210,6 +229,59 @@ export const ForgotPhonePassword = ({ } }; + // Handle verification code submission + const handleVerifyCode = async (code: string): Promise => { + // TODO: Replace with actual API call to verify code + // For example: return await userApi.verifyPhonePasswordResetCode({ + // phone: phoneNum, + // country: currentCountry?.country, + // code: code + // }); + + // Simulate verification for demo + return new Promise((resolve) => { + setTimeout(() => { + // For demo: code "123456" is valid, others are invalid + resolve(code === "123456"); + }, 1500); + }); + }; + + // Handle resend code + const handleResendCode = async (): Promise => { + // TODO: Replace with actual API call to resend code + // For example: await userApi.sendPhonePasswordResetCode({ + // phone: phoneNum, + // country: currentCountry?.country + // }); + + // Simulate resend for demo + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1500); + }); + }; + + // Handle reset password + const handleResetPassword = async (password: string): Promise => { + // TODO: Replace with actual API call to reset password + // For example: return await userApi.resetPhonePassword({ + // phone: phoneNum, + // country: currentCountry?.country, + // password: password + // }); + + // Simulate API call for demo + return new Promise((resolve) => { + setTimeout(() => { + // On success, close this modal too + if (onClose) onClose(); + resolve(true); // Always succeed for demo + }, 1500); + }); + }; + return ( - {t("forgotPassword")} + {t("login.forgotPassword.title")} - Enter your phone number below, and we'll send you a 6-digit password reset code. + {t("login.forgotPassword.phoneDescription")} @@ -253,18 +325,18 @@ export const ForgotPhonePassword = ({ ref={phoneInputRef} style={styles.phoneInput} placeholder={t("phoneNumber")} - value={phoneNumber} + value={phoneNum} onChangeText={handlePhoneNumberChange} keyboardType="phone-pad" autoFocus maxLength={15} /> - {phoneNumber.length > 0 && ( + {phoneNum.length > 0 && ( { - setPhoneNumber(""); + setPhoneNum(""); setPhoneNumberError(false); setError(null); }} @@ -277,9 +349,9 @@ export const ForgotPhonePassword = ({ {phoneNumberError && ( - {t("invalidPhoneNumber")} + {t("login.forgotPassword.invalidPhone")} {currentCountry?.valid_digits && - `(${t("requiresDigits")}: ${currentCountry.valid_digits.join(', ')})`} + `(${t("login.forgotPassword.requiresDigits")}: ${currentCountry.valid_digits.join(', ')})`} )} @@ -292,17 +364,17 @@ export const ForgotPhonePassword = ({ {loading ? ( ) : ( - {t("submit")} + {t("login.forgotPassword.submit")} )} @@ -391,6 +463,16 @@ export const ForgotPhonePassword = ({ + + {/* Verification Code Modal */} + setShowVerificationModal(false)} + phoneNumber={phoneNum} + onVerify={handleVerifyCode} + onResend={handleResendCode} + onResetPassword={handleResetPassword} + /> ); diff --git a/app/screens/loginList/PhoneLoginModal.tsx b/app/screens/loginList/PhoneLoginModal.tsx index a95c5a8..7bbcb04 100644 --- a/app/screens/loginList/PhoneLoginModal.tsx +++ b/app/screens/loginList/PhoneLoginModal.tsx @@ -483,6 +483,7 @@ const PhoneLoginModal = ({ visible, onClose }: PhoneLoginModalProps) => { visible={showForgotPasswordModal} onClose={() => setShowForgotPasswordModal(false)} selectedCountry={selectedCountry} + phoneNumber={phoneNumber} /> diff --git a/app/screens/loginList/ResetPasswordModal.tsx b/app/screens/loginList/ResetPasswordModal.tsx new file mode 100644 index 0000000..d48aee9 --- /dev/null +++ b/app/screens/loginList/ResetPasswordModal.tsx @@ -0,0 +1,248 @@ +import React, { useState } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + TextInput, + Modal, + ActivityIndicator, + Platform, +} from "react-native"; +import { useTranslation } from "react-i18next"; + +type ResetPasswordModalProps = { + visible: boolean; + onClose: () => void; + onSubmit: (password: string) => Promise; +}; + +export const ResetPasswordModal = ({ + visible, + onClose, + onSubmit, +}: ResetPasswordModalProps) => { + const { t } = useTranslation(); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [passwordError, setPasswordError] = useState(""); + const [confirmError, setConfirmError] = useState(""); + const [loading, setLoading] = useState(false); + + const validatePassword = () => { + let isValid = true; + + // Reset errors + setPasswordError(""); + setConfirmError(""); + + // Validate password + if (!password) { + setPasswordError(t("login.resetPassword.required")); + isValid = false; + } else if (password.length < 6) { + setPasswordError(t("login.resetPassword.minLength")); + isValid = false; + } + + // Validate password confirmation + if (!confirmPassword) { + setConfirmError(t("login.resetPassword.confirmRequired")); + isValid = false; + } else if (password !== confirmPassword) { + setConfirmError(t("login.resetPassword.mismatch")); + isValid = false; + } + + return isValid; + }; + + const handleSubmit = async () => { + if (!validatePassword()) { + return; + } + + setLoading(true); + try { + const success = await onSubmit(password); + if (success) { + // Password was reset successfully + setPassword(""); + setConfirmPassword(""); + onClose(); + } else { + // Handle failed submission + setPasswordError(t("login.resetPassword.failed")); + } + } catch (error) { + setPasswordError(t("login.resetPassword.error")); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + + + {t("login.resetPassword.title")} + + + + {/* Password input */} + + { + setPassword(text); + if (passwordError) setPasswordError(""); + }} + secureTextEntry={true} + autoCapitalize="none" + /> + {passwordError ? ( + {passwordError} + ) : null} + + + {/* Confirm password input */} + + { + setConfirmPassword(text); + if (confirmError) setConfirmError(""); + }} + secureTextEntry={true} + autoCapitalize="none" + /> + {confirmError ? ( + {confirmError} + ) : null} + + + {/* Submit button */} + + {loading ? ( + + ) : ( + {t("login.resetPassword.submit")} + )} + + + + + + ); +}; + +const styles = StyleSheet.create({ + modalContainer: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "flex-end", + zIndex: 9999, + }, + passwordContainer: { + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + height: "80%", + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 5, + }, + passwordHeader: { + flexDirection: "row", + alignItems: "center", + paddingTop: 20, + paddingHorizontal: 16, + paddingBottom: 15, + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + closeButton: { + padding: 8, + width: 36, + height: 36, + justifyContent: "center", + alignItems: "center", + }, + closeButtonText: { + fontSize: 18, + color: "#000", + }, + headerTitle: { + flex: 1, + fontSize: 18, + fontWeight: "600", + color: "#000", + textAlign: "center", + marginRight: 36, + }, + passwordContent: { + padding: 20, + paddingTop: 40, + paddingBottom: Platform.OS === "ios" ? 50 : 30, + }, + inputContainer: { + marginBottom: 20, + }, + input: { + height: 50, + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 25, + paddingHorizontal: 16, + fontSize: 16, + }, + inputError: { + borderColor: "#FF3B30", + }, + errorText: { + color: "#FF3B30", + fontSize: 14, + marginTop: 5, + paddingHorizontal: 5, + }, + submitButton: { + height: 50, + backgroundColor: "#0039CB", + borderRadius: 25, + justifyContent: "center", + alignItems: "center", + marginTop: 20, + }, + submitButtonText: { + color: "#fff", + fontSize: 16, + fontWeight: "600", + }, +}); \ No newline at end of file diff --git a/app/screens/loginList/VerificationCodeInput.tsx b/app/screens/loginList/VerificationCodeInput.tsx new file mode 100644 index 0000000..7c024df --- /dev/null +++ b/app/screens/loginList/VerificationCodeInput.tsx @@ -0,0 +1,441 @@ +import React, { useState, useRef, useEffect } from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + TextInput, + Modal, + ActivityIndicator, + Platform, + Keyboard, + TouchableWithoutFeedback, +} from "react-native"; +import { useTranslation } from "react-i18next"; +import { ResetPasswordModal } from "./ResetPasswordModal"; + +type VerificationCodeInputProps = { + visible: boolean; + onClose: () => void; + phoneNumber?: string; + onVerify: (code: string) => Promise; + onResend: () => Promise; + onResetPassword?: (password: string) => Promise; +}; + +export const VerificationCodeInput = ({ + visible, + onClose, + phoneNumber = "", + onVerify, + onResend, + onResetPassword = async () => true, +}: VerificationCodeInputProps) => { + const { t } = useTranslation(); + + // Single string to store the entire verification code + const [verificationCode, setVerificationCode] = useState(""); + const [isCodeError, setIsCodeError] = useState(false); + const [loading, setLoading] = useState(false); + const [countdown, setCountdown] = useState(60); + const [isResending, setIsResending] = useState(false); + const [resendActive, setResendActive] = useState(false); + // New state for password reset modal + const [showPasswordReset, setShowPasswordReset] = useState(false); + + // Reference to the hidden input that will handle all keystrokes + const hiddenInputRef = useRef(null); + + // Focus the hidden input whenever the component is visible + useEffect(() => { + if (visible) { + startCountdown(); + setTimeout(() => { + hiddenInputRef.current?.focus(); + }, 300); + } + return () => { + // Reset states when component unmounts + setVerificationCode(""); + setIsCodeError(false); + setCountdown(60); + setResendActive(false); + setShowPasswordReset(false); + }; + }, [visible]); + + // Function to focus the hidden input + const focusInput = () => { + hiddenInputRef.current?.focus(); + }; + + // Function to start countdown + const startCountdown = () => { + setResendActive(false); + setCountdown(60); + + const timer = setInterval(() => { + setCountdown((prevCount) => { + if (prevCount <= 1) { + clearInterval(timer); + setResendActive(true); + return 0; + } + return prevCount - 1; + }); + }, 1000); + + return () => clearInterval(timer); + }; + + // Handle code input change + const handleCodeChange = (text: string) => { + if (isCodeError) setIsCodeError(false); + + // Only accept digits and limit to 6 characters + const cleanedText = text.replace(/[^0-9]/g, '').substring(0, 6); + setVerificationCode(cleanedText); + + // Auto-submit if all 6 digits are entered + if (cleanedText.length === 6) { + Keyboard.dismiss(); + handleVerifyCode(cleanedText); + } + }; + + // Handle verify code submission + const handleVerifyCode = async (code: string) => { + setLoading(true); + try { + const success = await onVerify(code); + if (!success) { + setIsCodeError(true); + } else { + // Instead of closing, show password reset modal + setShowPasswordReset(true); + } + } catch (error) { + setIsCodeError(true); + } finally { + setLoading(false); + } + }; + + // Handle resend code + const handleResend = async () => { + if (!resendActive) return; + + setIsResending(true); + try { + await onResend(); + startCountdown(); + } catch (error) { + console.error("Failed to resend code:", error); + } finally { + setIsResending(false); + } + }; + + // Handle password reset submission + const handlePasswordReset = async (password: string) => { + try { + const success = await onResetPassword(password); + if (success) { + // Password reset was successful + // The parent component already handles closing all modals + setShowPasswordReset(false); + // Note: Don't call onClose() here as the parent already does that + } + return success; + } catch (error) { + console.error("Failed to reset password:", error); + return false; + } + }; + + // Create an array representation of the code for display + const codeArray = verificationCode.split(''); + while (codeArray.length < 6) { + codeArray.push(''); + } + + return ( + + + + + + + + {t("login.verification.title")} + + + + + {t("login.verification.description")} {phoneNumber}. {t("login.verification.expiration")} + + + {/* Hidden input that captures all key presses */} + + + {/* Touchable area to focus input when touched */} + + + {codeArray.map((digit, index) => ( + + {digit} + + ))} + + + + {/* Error message */} + {isCodeError && ( + + + ! + + {t("login.verification.incorrect")} + + )} + + {/* Resend button */} + + {isResending ? ( + + ) : ( + + + {resendActive + ? t("login.verification.resend") + : `${t("login.verification.resend")} (${countdown}s)`} + + + )} + + + {/* Help section */} + + {t("login.verification.didntReceive")} + + + + {t("login.verification.helpPoint1")} + + + + + + {t("login.verification.helpPoint2")} + + + + + + + {/* Password Reset Modal */} + setShowPasswordReset(false)} + onSubmit={handlePasswordReset} + /> + + + ); +}; + +const styles = StyleSheet.create({ + modalContainer: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "flex-end", + zIndex: 9999, + }, + verificationContainer: { + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + height: "80%", + shadowColor: "#000", + shadowOffset: { width: 0, height: -2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 5, + }, + verificationHeader: { + flexDirection: "row", + alignItems: "center", + paddingTop: 20, + paddingHorizontal: 16, + paddingBottom: 15, + borderBottomWidth: 1, + borderBottomColor: "#f0f0f0", + }, + closeButton: { + padding: 8, + width: 36, + height: 36, + justifyContent: "center", + alignItems: "center", + }, + closeButtonText: { + fontSize: 18, + color: "#000", + }, + headerTitle: { + flex: 1, + fontSize: 18, + fontWeight: "600", + color: "#000", + textAlign: "center", + marginRight: 36, + }, + verificationContent: { + padding: 20, + paddingBottom: Platform.OS === "ios" ? 50 : 30, + }, + description: { + fontSize: 14, + color: "#333", + marginBottom: 20, + lineHeight: 20, + }, + hiddenInput: { + position: 'absolute', + opacity: 0, + height: 0, + width: 0, + }, + codeInputContainer: { + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 20, + width: "100%", + }, + codeInput: { + width: '14%', + height: 60, + borderWidth: 1, + borderColor: "#E1E1E1", + borderRadius: 5, + justifyContent: 'center', + alignItems: 'center', + }, + codeInputText: { + fontSize: 24, + fontWeight: "500", + }, + codeInputError: { + borderColor: "#FF3B30", + }, + codeInputFilled: { + borderColor: "#0066FF", + }, + errorContainer: { + flexDirection: "row", + alignItems: "center", + marginBottom: 15, + }, + errorIconContainer: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: "#FF3B30", + justifyContent: "center", + alignItems: "center", + marginRight: 8, + }, + errorIcon: { + color: "white", + fontSize: 14, + fontWeight: "bold", + }, + errorText: { + color: "#FF3B30", + fontSize: 14, + }, + resendContainer: { + alignItems: "flex-end", + marginBottom: 30, + }, + resendButton: { + padding: 5, + }, + resendButtonDisabled: { + opacity: 0.7, + }, + resendText: { + color: "#0066FF", + fontSize: 14, + fontWeight: "500", + }, + resendTextDisabled: { + color: "#999", + }, + helpContainer: { + backgroundColor: "#F8F8F8", + padding: 15, + borderRadius: 10, + marginTop: 20, + }, + helpTitle: { + fontSize: 16, + fontWeight: "600", + marginBottom: 10, + color: "#333", + }, + helpItem: { + flexDirection: "row", + alignItems: "center", + marginBottom: 8, + }, + bulletPoint: { + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: "#FF9500", + marginRight: 10, + }, + helpText: { + fontSize: 14, + color: "#333", + flex: 1, + }, +}); \ No newline at end of file