You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
11 KiB
441 lines
11 KiB
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, |
|
}, |
|
});
|