Browse Source

重置密码

main
Your Name 3 weeks ago
parent
commit
ec97ba90c0
  1. 44
      app/locales/en/translation.json
  2. 44
      app/locales/fr/translation.json
  3. 47
      app/screens/loginList/EmailLoginModal.tsx
  4. 377
      app/screens/loginList/ForgotEmailPassword.tsx
  5. 120
      app/screens/loginList/ForgotPhonePassword.tsx
  6. 1
      app/screens/loginList/PhoneLoginModal.tsx
  7. 248
      app/screens/loginList/ResetPasswordModal.tsx
  8. 441
      app/screens/loginList/VerificationCodeInput.tsx

44
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"
}

44
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é"
}

47
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<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 防止重复关闭
const isClosing = useRef(false);
@ -146,22 +150,27 @@ const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => {
</TouchableOpacity>
);
// 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 && (
<>
<Text style={styles.passwordErrorText}>
{t("passwordIncorrect")}
{error || t("passwordIncorrect")}
</Text>
<TouchableOpacity
@ -326,19 +338,30 @@ const EmailLoginModal = ({ visible, onClose }: EmailLoginModalProps) => {
<TouchableOpacity
style={[
styles.emailContinueButton,
(!isValidEmail(email) || !emailPassword) &&
(!isValidEmail(email) || !emailPassword || loading) &&
styles.emailDisabledButton,
]}
onPress={handleEmailContinue}
disabled={!isValidEmail(email) || !emailPassword}
disabled={!isValidEmail(email) || !emailPassword || loading}
activeOpacity={0.7}
>
<Text style={styles.emailContinueButtonText}>
{t("continue")}
</Text>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.emailContinueButtonText}>
{t("continue")}
</Text>
)}
</TouchableOpacity>
</View>
</View>
{/* Add ForgotEmailPassword modal */}
<ForgotEmailPassword
visible={showForgotPasswordModal}
onClose={() => setShowForgotPasswordModal(false)}
email={email}
/>
</View>
</Modal>
);

377
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<string | null>(null);
const [showVerificationModal, setShowVerificationModal] = useState(false);
// Refs
const emailInputRef = useRef<TextInput>(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<boolean> => {
// 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<void> => {
// 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<boolean> => {
// 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 (
<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("login.forgotPassword.title")}</Text>
</View>
<View style={styles.forgotPasswordContent}>
<Text style={styles.forgotPasswordDescription}>
{t("login.forgotPassword.emailDescription")}
</Text>
<View style={styles.emailInputContainer}>
<TextInput
ref={emailInputRef}
style={styles.emailInput}
placeholder={t("email")}
value={userEmail}
onChangeText={handleEmailChange}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoFocus={!email}
/>
{userEmail.length > 0 && (
<TouchableOpacity
style={styles.emailClearButton}
onPress={() => {
setUserEmail("");
setEmailError(false);
setError(null);
}}
activeOpacity={0.7}
>
<Text style={styles.emailClearButtonText}></Text>
</TouchableOpacity>
)}
</View>
{emailError && (
<Text style={styles.emailErrorText}>
{t("login.forgotPassword.invalidEmail")}
</Text>
)}
{error && (
<Text style={styles.errorText}>
{error}
</Text>
)}
<TouchableOpacity
style={[
styles.submitButton,
(!userEmail.trim() || emailError) && styles.disabledButton,
]}
onPress={handleSubmit}
disabled={!userEmail.trim() || emailError || loading}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.submitButtonText}>
{t("login.forgotPassword.submit")}
</Text>
)}
</TouchableOpacity>
</View>
</View>
{/* Verification Code Modal */}
<VerificationCodeInput
visible={showVerificationModal}
onClose={() => setShowVerificationModal(false)}
phoneNumber={userEmail} // We're using phoneNumber prop for email too
onVerify={handleVerifyCode}
onResend={handleResendCode}
onResetPassword={handleResetPassword}
/>
</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,
},
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",
},
});

120
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<string | null>(null);
@ -38,6 +41,7 @@ export const ForgotPhonePassword = ({
const [showCountryModal, setShowCountryModal] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [filteredCountryList, setFilteredCountryList] = useState<CountryList[]>([]);
const [showVerificationModal, setShowVerificationModal] = useState(false);
// Refs
const phoneInputRef = useRef<TextInput>(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<boolean> => {
// 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<void> => {
// 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<boolean> => {
// 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 (
<Modal
visible={visible}
@ -228,12 +300,12 @@ export const ForgotPhonePassword = ({
>
<Text style={styles.forgotPasswordCloseButtonText}></Text>
</TouchableOpacity>
<Text style={styles.forgotPasswordTitle}>{t("forgotPassword")}</Text>
<Text style={styles.forgotPasswordTitle}>{t("login.forgotPassword.title")}</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.
{t("login.forgotPassword.phoneDescription")}
</Text>
<View style={styles.phoneInputContainer}>
@ -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 && (
<TouchableOpacity
style={styles.phoneClearButton}
onPress={() => {
setPhoneNumber("");
setPhoneNum("");
setPhoneNumberError(false);
setError(null);
}}
@ -277,9 +349,9 @@ export const ForgotPhonePassword = ({
{phoneNumberError && (
<Text style={styles.phoneNumberErrorText}>
{t("invalidPhoneNumber")}
{t("login.forgotPassword.invalidPhone")}
{currentCountry?.valid_digits &&
`(${t("requiresDigits")}: ${currentCountry.valid_digits.join(', ')})`}
`(${t("login.forgotPassword.requiresDigits")}: ${currentCountry.valid_digits.join(', ')})`}
</Text>
)}
@ -292,17 +364,17 @@ export const ForgotPhonePassword = ({
<TouchableOpacity
style={[
styles.submitButton,
(!phoneNumber.trim() || phoneNumberError) && styles.disabledButton,
(!phoneNum.trim() || phoneNumberError) && styles.disabledButton,
]}
onPress={handleSubmit}
disabled={!phoneNumber.trim() || phoneNumberError || loading}
disabled={!phoneNum.trim() || phoneNumberError || loading}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.submitButtonText}>
{t("submit")}
{t("login.forgotPassword.submit")}
</Text>
)}
</TouchableOpacity>
@ -391,6 +463,16 @@ export const ForgotPhonePassword = ({
</View>
</View>
</Modal>
{/* Verification Code Modal */}
<VerificationCodeInput
visible={showVerificationModal}
onClose={() => setShowVerificationModal(false)}
phoneNumber={phoneNum}
onVerify={handleVerifyCode}
onResend={handleResendCode}
onResetPassword={handleResetPassword}
/>
</View>
</Modal>
);

1
app/screens/loginList/PhoneLoginModal.tsx

@ -483,6 +483,7 @@ const PhoneLoginModal = ({ visible, onClose }: PhoneLoginModalProps) => {
visible={showForgotPasswordModal}
onClose={() => setShowForgotPasswordModal(false)}
selectedCountry={selectedCountry}
phoneNumber={phoneNumber}
/>
</View>
</Modal>

248
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<boolean>;
};
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 (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
statusBarTranslucent={true}
presentationStyle="overFullScreen"
>
<View style={styles.modalContainer}>
<View style={styles.passwordContainer}>
<View style={styles.passwordHeader}>
<TouchableOpacity
style={styles.closeButton}
onPress={onClose}
activeOpacity={0.7}
>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>{t("login.resetPassword.title")}</Text>
</View>
<View style={styles.passwordContent}>
{/* Password input */}
<View style={styles.inputContainer}>
<TextInput
style={[styles.input, passwordError ? styles.inputError : null]}
placeholder={t("login.resetPassword.enterPassword")}
value={password}
onChangeText={(text) => {
setPassword(text);
if (passwordError) setPasswordError("");
}}
secureTextEntry={true}
autoCapitalize="none"
/>
{passwordError ? (
<Text style={styles.errorText}>{passwordError}</Text>
) : null}
</View>
{/* Confirm password input */}
<View style={styles.inputContainer}>
<TextInput
style={[styles.input, confirmError ? styles.inputError : null]}
placeholder={t("login.resetPassword.confirmPassword")}
value={confirmPassword}
onChangeText={(text) => {
setConfirmPassword(text);
if (confirmError) setConfirmError("");
}}
secureTextEntry={true}
autoCapitalize="none"
/>
{confirmError ? (
<Text style={styles.errorText}>{confirmError}</Text>
) : null}
</View>
{/* Submit button */}
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit}
disabled={loading}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.submitButtonText}>{t("login.resetPassword.submit")}</Text>
)}
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
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",
},
});

441
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<boolean>;
onResend: () => Promise<void>;
onResetPassword?: (password: string) => Promise<boolean>;
};
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<TextInput>(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 (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
statusBarTranslucent={true}
presentationStyle="overFullScreen"
>
<View style={styles.modalContainer}>
<View style={styles.verificationContainer}>
<View style={styles.verificationHeader}>
<TouchableOpacity
style={styles.closeButton}
onPress={onClose}
activeOpacity={0.7}
>
<Text style={styles.closeButtonText}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}>{t("login.verification.title")}</Text>
</View>
<View style={styles.verificationContent}>
<Text style={styles.description}>
{t("login.verification.description")} {phoneNumber}. {t("login.verification.expiration")}
</Text>
{/* Hidden input that captures all key presses */}
<TextInput
ref={hiddenInputRef}
value={verificationCode}
onChangeText={handleCodeChange}
keyboardType="number-pad"
maxLength={6}
style={styles.hiddenInput}
caretHidden={true}
autoFocus={visible}
/>
{/* Touchable area to focus input when touched */}
<TouchableWithoutFeedback onPress={focusInput}>
<View style={styles.codeInputContainer}>
{codeArray.map((digit, index) => (
<View
key={`code-box-${index}`}
style={[
styles.codeInput,
isCodeError && styles.codeInputError,
digit ? styles.codeInputFilled : {}
]}
>
<Text style={styles.codeInputText}>{digit}</Text>
</View>
))}
</View>
</TouchableWithoutFeedback>
{/* Error message */}
{isCodeError && (
<View style={styles.errorContainer}>
<View style={styles.errorIconContainer}>
<Text style={styles.errorIcon}>!</Text>
</View>
<Text style={styles.errorText}>{t("login.verification.incorrect")}</Text>
</View>
)}
{/* Resend button */}
<View style={styles.resendContainer}>
{isResending ? (
<ActivityIndicator size="small" color="#0066FF" />
) : (
<TouchableOpacity
onPress={handleResend}
disabled={!resendActive}
style={[
styles.resendButton,
!resendActive && styles.resendButtonDisabled
]}
>
<Text style={[
styles.resendText,
!resendActive && styles.resendTextDisabled
]}>
{resendActive
? t("login.verification.resend")
: `${t("login.verification.resend")} (${countdown}s)`}
</Text>
</TouchableOpacity>
)}
</View>
{/* Help section */}
<View style={styles.helpContainer}>
<Text style={styles.helpTitle}>{t("login.verification.didntReceive")}</Text>
<View style={styles.helpItem}>
<View style={styles.bulletPoint} />
<Text style={styles.helpText}>
{t("login.verification.helpPoint1")}
</Text>
</View>
<View style={styles.helpItem}>
<View style={styles.bulletPoint} />
<Text style={styles.helpText}>
{t("login.verification.helpPoint2")}
</Text>
</View>
</View>
</View>
</View>
{/* Password Reset Modal */}
<ResetPasswordModal
visible={showPasswordReset}
onClose={() => setShowPasswordReset(false)}
onSubmit={handlePasswordReset}
/>
</View>
</Modal>
);
};
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,
},
});
Loading…
Cancel
Save