8 changed files with 1289 additions and 33 deletions
@ -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", |
||||||
|
}, |
||||||
|
});
|
@ -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", |
||||||
|
}, |
||||||
|
});
|
@ -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…
Reference in new issue