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