added reset-password
This commit is contained in:
@@ -21,6 +21,7 @@ const NavigationString = {
|
||||
LiveTenderDetail:'LiveTenderDetail',
|
||||
AuctionDetail: 'AuctionDetail',
|
||||
Profile:'Profile',
|
||||
ViewTenderDetail:'ViewTenderDetail'
|
||||
ViewTenderDetail:'ViewTenderDetail',
|
||||
ResetPassword:'ResetPassword'
|
||||
}
|
||||
export default NavigationString
|
||||
@@ -46,7 +46,7 @@ export default function MainStack() {
|
||||
<Stack.Screen name={NavigationString.Registration} component={Screens.Registration} options={option} />
|
||||
<Stack.Screen name={NavigationString.Partnership} component={Screens.Partnership} options={option} />
|
||||
<Stack.Screen name={NavigationString.LiveTenderDetail} component={Screens.LiveTenderDetail} options={option} />
|
||||
<Stack.Screen name={NavigationString.ViewTenderDetail} component={Screens.ViewTenderDetail} options={option} />
|
||||
<Stack.Screen name={NavigationString.ResetPassword} component={Screens.ResetPassword} options={option} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Modal,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
TouchableWithoutFeedback,
|
||||
Keyboard,
|
||||
ActivityIndicator
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import colors from '../../Constant/Color';
|
||||
|
||||
interface ForgotModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export type Navigation = {
|
||||
navigate(route: string, params?: object): unknown;
|
||||
};
|
||||
const ForgotModal: React.FC<ForgotModalProps> = ({
|
||||
visible,
|
||||
onClose
|
||||
}) => {
|
||||
const navigation = useNavigation() as Navigation;
|
||||
const [email, setEmail] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
// Reset error
|
||||
setError('');
|
||||
|
||||
// Validate email
|
||||
if (!email) {
|
||||
setError('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
setError('Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Make API call to send OTP for forgot password
|
||||
const response = await fetch(
|
||||
`https://procurelinc.in/EProcurementSB/eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'OTP has been sent to your email address. Please check your inbox.',
|
||||
[
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
onClose();
|
||||
navigation.navigate('ResetPassword', { email });
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
setEmail('');
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
console.error('API Error:', errorData);
|
||||
setError('Failed to send OTP. Please try again.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Network Error:', err);
|
||||
setError('Network error. Please check your connection and try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setEmail('');
|
||||
setError('');
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={styles.centeredView}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardAvoidingView}
|
||||
>
|
||||
<View style={styles.modalView}>
|
||||
<Text style={styles.title}>Reset Password</Text>
|
||||
|
||||
<Text style={styles.subtitle}>
|
||||
Enter your email address and we'll send you an OTP to reset your password.
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email address"
|
||||
placeholderTextColor="#999"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
editable={!loading}
|
||||
/>
|
||||
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.cancelButton]}
|
||||
onPress={handleClose}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.resetButton, loading && styles.disabledButton]}
|
||||
onPress={handleResetPassword}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.resetButtonText}>Send OTP</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centeredView: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalView: {
|
||||
width: '85%',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 12,
|
||||
color: '#333',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
marginBottom: 24,
|
||||
color: '#666',
|
||||
lineHeight: 20,
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: '#ff3b30',
|
||||
fontSize: 14,
|
||||
marginBottom: 16,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
button: {
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
minWidth: '45%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
cancelButton: {
|
||||
backgroundColor: '#f1f1f1',
|
||||
},
|
||||
resetButton: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
cancelButtonText: {
|
||||
color: '#666',
|
||||
fontWeight: '600',
|
||||
},
|
||||
resetButtonText: {
|
||||
color: 'white',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
export default ForgotModal;
|
||||
@@ -15,6 +15,7 @@ import { useNavigation, CommonActions } from '@react-navigation/native';
|
||||
import NavigationString from '../../Constant/NavigationString';
|
||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||
import { Loader } from '../../Component/Index';
|
||||
import ForgotModal from './ForgotPassword';
|
||||
|
||||
const hapticOptions = {
|
||||
enableVibrateFallback: true,
|
||||
@@ -38,6 +39,7 @@ const Login = () => {
|
||||
const { mutate: auctionMutate } = useAuctionLogin();
|
||||
const { data } = useGetDesignation();
|
||||
const { mutate: registrationStateMutate } = useRagistrationState();
|
||||
const [forgotPassword, setForgotPassword] = useState(false)
|
||||
|
||||
const { data: MasterData } = usefetchMasterData()
|
||||
|
||||
@@ -155,7 +157,7 @@ const Login = () => {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
setAuctionLoading(false)
|
||||
} finally {
|
||||
setAuctionLoading(false);
|
||||
}
|
||||
@@ -249,7 +251,7 @@ const Login = () => {
|
||||
</View>
|
||||
{errors.password && touched.password && <Text style={styles.errorText}>{errors.password}</Text>}
|
||||
|
||||
<TouchableOpacity style={styles.forgotPass}>
|
||||
<TouchableOpacity style={styles.forgotPass} onPress={()=>setForgotPassword(true)} >
|
||||
<Text style={styles.forgotPassText}>Forgot Password?</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -293,6 +295,7 @@ const Login = () => {
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
{forgotPassword && <ForgotModal visible={forgotPassword} onClose={()=>setForgotPassword(false)}/>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,619 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import axios from 'axios';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
ScrollView,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
NativeSyntheticEvent,
|
||||
TextInputKeyPressEventData,
|
||||
ToastAndroid,
|
||||
} from 'react-native';
|
||||
import { BASE_URL } from '../../Services/BBPS/ApiUrls';
|
||||
import colors from '../../Constant/Color';
|
||||
interface ResetPasswordProps {
|
||||
route?: any;
|
||||
navigation?: any;
|
||||
}
|
||||
|
||||
const ResetPassword: React.FC<ResetPasswordProps> = ({ route, navigation }) => {
|
||||
const [email, setEmail] = useState<string>(route?.params?.email || '');
|
||||
const [otp, setOtp] = useState<string[]>(['', '', '', '', '', '']);
|
||||
const [newPassword, setNewPassword] = useState<string>('');
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [otpLoading, setOtpLoading] = useState<boolean>(false);
|
||||
const [resendLoading, setResendLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [otpError, setOtpError] = useState<string>('');
|
||||
const [passwordError, setPasswordError] = useState<string>('');
|
||||
const [confirmPasswordError, setConfirmPasswordError] = useState<string>('');
|
||||
const [isOtpVerified, setIsOtpVerified] = useState<boolean>(false);
|
||||
const [resendCooldown, setResendCooldown] = useState<number>(0);
|
||||
const [newPasswordVisible, setNewPasswordVisible] = useState<boolean>(false);
|
||||
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState<boolean>(false);
|
||||
|
||||
const otpInputs = useRef<Array<TextInput | null>>([]);
|
||||
const newPasswordRef = useRef<TextInput>(null);
|
||||
const confirmPasswordRef = useRef<TextInput>(null);
|
||||
const resendTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Initialize resend cooldown from storage or set to 0
|
||||
useEffect(() => {
|
||||
checkResendCooldown();
|
||||
}, []);
|
||||
|
||||
// Clean up timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (resendTimerRef.current) {
|
||||
clearInterval(resendTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update timer every second
|
||||
useEffect(() => {
|
||||
if (resendCooldown > 0) {
|
||||
resendTimerRef.current = setInterval(() => {
|
||||
setResendCooldown(prev => {
|
||||
if (prev <= 1) {
|
||||
if (resendTimerRef.current) {
|
||||
clearInterval(resendTimerRef.current);
|
||||
}
|
||||
AsyncStorage.removeItem('otpResendTime');
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (resendTimerRef.current) {
|
||||
clearInterval(resendTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [resendCooldown]);
|
||||
|
||||
const checkResendCooldown = async () => {
|
||||
try {
|
||||
const storedTime = await AsyncStorage.getItem('otpResendTime');
|
||||
if (storedTime) {
|
||||
const remainingTime = Math.max(0, Math.floor((300000 - (Date.now() - parseInt(storedTime))) / 1000));
|
||||
if (remainingTime > 0) {
|
||||
setResendCooldown(remainingTime);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking resend cooldown:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const startResendCooldown = async () => {
|
||||
const cooldownTime = 300; // 5 minutes in seconds
|
||||
setResendCooldown(cooldownTime);
|
||||
await AsyncStorage.setItem('otpResendTime', Date.now().toString());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Auto-submit OTP when all fields are filled
|
||||
const fullOtp = otp.join('');
|
||||
if (fullOtp.length === 6 && !isOtpVerified) {
|
||||
handleSubmitOtp();
|
||||
}
|
||||
}, [otp]);
|
||||
|
||||
const handleOtpChange = (value: string, index: number) => {
|
||||
if (!/^\d*$/.test(value)) return; // Only allow numbers
|
||||
|
||||
const newOtp = [...otp];
|
||||
newOtp[index] = value;
|
||||
setOtp(newOtp);
|
||||
setOtpError('');
|
||||
|
||||
// Auto-focus to next input
|
||||
if (value && index < 5) {
|
||||
otpInputs.current[index + 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpKeyPress = (e: NativeSyntheticEvent<TextInputKeyPressEventData>, index: number) => {
|
||||
if (e.nativeEvent.key === 'Backspace' && !otp[index] && index > 0) {
|
||||
// Move focus to previous input on backspace
|
||||
otpInputs.current[index - 1]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitOtp = async () => {
|
||||
const fullOtp = otp.join('');
|
||||
|
||||
if (fullOtp.length !== 6) {
|
||||
setOtpError('Please enter a valid 6-digit OTP');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
setOtpError('Email is required for OTP verification');
|
||||
return;
|
||||
}
|
||||
|
||||
setOtpLoading(true);
|
||||
setOtpError('');
|
||||
|
||||
try {
|
||||
// Make the API request to verify OTP
|
||||
const res = await axios.post(
|
||||
`${BASE_URL}eproc/verify-otp?email=${encodeURIComponent(email)}&otp=${fullOtp}`,
|
||||
null,
|
||||
{
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('API Response OTP verification:', res.data);
|
||||
|
||||
if (res.data?.statusCode === 200) {
|
||||
ToastAndroid.show('OTP verified successfully!', ToastAndroid.SHORT);
|
||||
setIsOtpVerified(true);
|
||||
|
||||
// Focus on password field after successful OTP verification
|
||||
setTimeout(() => {
|
||||
newPasswordRef.current?.focus();
|
||||
}, 100);
|
||||
} else {
|
||||
throw new Error(res.data?.message || 'OTP verification failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verification Error:', error);
|
||||
|
||||
const errorMessage = axios.isAxiosError(error)
|
||||
? error.response?.data?.message || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'OTP verification failed';
|
||||
|
||||
setOtpError(errorMessage);
|
||||
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
||||
} finally {
|
||||
setOtpLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
let isValid = true;
|
||||
|
||||
// Validate OTP
|
||||
if (!isOtpVerified) {
|
||||
setOtpError('Please verify OTP first');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if (newPassword.length < 8) {
|
||||
setPasswordError('Password must be at least 8 characters long');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate confirm password
|
||||
if (newPassword !== confirmPassword) {
|
||||
setConfirmPasswordError('Passwords do not match');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
setError('');
|
||||
setOtpError('');
|
||||
setPasswordError('');
|
||||
setConfirmPasswordError('');
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Make API call to reset password according to curl
|
||||
const res = await axios.post(
|
||||
`${BASE_URL}eproc/reset-password?email=${encodeURIComponent(email)}&otp=${otp.join('')}&newPassword=${encodeURIComponent(newPassword)}&confirmPassword=${encodeURIComponent(confirmPassword)}`,
|
||||
null,
|
||||
{
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('API Response password reset:', res.data);
|
||||
|
||||
if (res.data?.statusCode === 200) {
|
||||
Alert.alert(
|
||||
'Success',
|
||||
'Your password has been reset successfully.',
|
||||
[{
|
||||
text: 'OK', onPress: () => {
|
||||
// Navigate to login screen
|
||||
navigation.navigate('Login');
|
||||
}
|
||||
}]
|
||||
);
|
||||
} else {
|
||||
throw new Error(res.data?.message || 'Password reset failed');
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = axios.isAxiosError(err)
|
||||
? err.response?.data?.message || err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: 'Failed to reset password. Please try again.';
|
||||
|
||||
setError(errorMessage);
|
||||
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResendOtp = async () => {
|
||||
if (resendCooldown > 0) return;
|
||||
|
||||
setResendLoading(true);
|
||||
|
||||
try {
|
||||
// Make API call to resend OTP
|
||||
const res = await axios.post(
|
||||
`${BASE_URL}eproc/send-otp-forgot-password?email=${encodeURIComponent(email)}`,
|
||||
null,
|
||||
{
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.data?.statusCode === 200) {
|
||||
ToastAndroid.show('OTP sent successfully', ToastAndroid.SHORT);
|
||||
// Clear OTP fields
|
||||
setOtp(['', '', '', '', '', '']);
|
||||
setIsOtpVerified(false);
|
||||
setOtpError('');
|
||||
// Start the cooldown timer
|
||||
startResendCooldown();
|
||||
} else {
|
||||
throw new Error(res.data?.message || 'Failed to resend OTP');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = axios.isAxiosError(error)
|
||||
? error.response?.data?.message || error.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to resend OTP';
|
||||
|
||||
ToastAndroid.show(errorMessage, ToastAndroid.SHORT);
|
||||
} finally {
|
||||
setResendLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number): string => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||
<Text style={styles.title}>Reset Password</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Enter the OTP sent to {email} and your new password
|
||||
</Text>
|
||||
|
||||
{/* OTP Input Section */}
|
||||
<View style={styles.otpSection}>
|
||||
<Text style={styles.sectionLabel}>OTP Code</Text>
|
||||
<View style={styles.otpContainer}>
|
||||
{otp.map((digit, index) => (
|
||||
<TextInput
|
||||
key={index}
|
||||
ref={(ref) => { otpInputs.current[index] = ref; }}
|
||||
style={[styles.otpInput, otpError ? styles.inputError : null, isOtpVerified && styles.verifiedInput]}
|
||||
value={digit}
|
||||
onChangeText={(value) => handleOtpChange(value, index)}
|
||||
onKeyPress={(e) => handleOtpKeyPress(e, index)}
|
||||
keyboardType="number-pad"
|
||||
maxLength={1}
|
||||
selectTextOnFocus
|
||||
editable={!isOtpVerified && !otpLoading}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
{otpError ? <Text style={styles.errorText}>{otpError}</Text> : null}
|
||||
|
||||
{isOtpVerified ? (
|
||||
<View style={styles.verifiedContainer}>
|
||||
<Text style={styles.verifiedText}>✓ OTP Verified</Text>
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={handleSubmitOtp}
|
||||
style={[styles.submitButton, otpLoading && styles.disabledButton]}
|
||||
disabled={otpLoading}
|
||||
>
|
||||
{otpLoading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.submitButtonText}>Verify OTP</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
</View>
|
||||
|
||||
{/* Password Input Section - Only enabled after OTP verification */}
|
||||
<View style={styles.passwordSection}>
|
||||
<Text style={styles.sectionLabel}>New Password</Text>
|
||||
<View style={styles.passwordInputContainer}>
|
||||
<TextInput
|
||||
ref={newPasswordRef}
|
||||
style={[styles.input, passwordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]}
|
||||
placeholder="Enter new password"
|
||||
placeholderTextColor={!isOtpVerified ? '#4f058bff' : '#000'}
|
||||
value={newPassword}
|
||||
onChangeText={(text) => {
|
||||
setNewPassword(text);
|
||||
setPasswordError('');
|
||||
}}
|
||||
secureTextEntry={!newPasswordVisible}
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => confirmPasswordRef.current?.focus()}
|
||||
editable={isOtpVerified}
|
||||
/>
|
||||
{isOtpVerified && (
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setNewPasswordVisible(!newPasswordVisible)}
|
||||
>
|
||||
<Text style={styles.eyeIconText}>
|
||||
{newPasswordVisible ? '🙈' : '👁️'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{passwordError ? <Text style={styles.errorText}>{passwordError}</Text> : null}
|
||||
|
||||
<Text style={styles.sectionLabel}>Confirm Password</Text>
|
||||
<View style={styles.passwordInputContainer}>
|
||||
<TextInput
|
||||
ref={confirmPasswordRef}
|
||||
style={[styles.input, confirmPasswordError ? styles.inputError : null, !isOtpVerified && styles.disabledInput]}
|
||||
placeholder="Confirm new password"
|
||||
placeholderTextColor={!isOtpVerified ? '#4f058bff' : '#000'}
|
||||
value={confirmPassword}
|
||||
onChangeText={(text) => {
|
||||
setConfirmPassword(text);
|
||||
setConfirmPasswordError('');
|
||||
}}
|
||||
secureTextEntry={!confirmPasswordVisible}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleResetPassword}
|
||||
editable={isOtpVerified}
|
||||
/>
|
||||
{isOtpVerified && (
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setConfirmPasswordVisible(!confirmPasswordVisible)}
|
||||
>
|
||||
<Text style={styles.eyeIconText}>
|
||||
{confirmPasswordVisible ? '🙈' : '👁️'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
{confirmPasswordError ? (
|
||||
<Text style={styles.errorText}>{confirmPasswordError}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{error ? <Text style={styles.errorText}>{error}</Text> : null}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.resetButton, (loading || !isOtpVerified) && styles.disabledButton]}
|
||||
onPress={handleResetPassword}
|
||||
disabled={loading || !isOtpVerified}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.resetButtonText}>Reset Password</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.resendButton, resendCooldown > 0 && styles.disabledResendButton]}
|
||||
onPress={handleResendOtp}
|
||||
disabled={resendLoading || resendCooldown > 0}
|
||||
>
|
||||
{resendLoading ? (
|
||||
<ActivityIndicator color="#007AFF" />
|
||||
) : resendCooldown > 0 ? (
|
||||
<Text style={styles.resendButtonText}>
|
||||
Resend OTP in {formatTime(resendCooldown)}
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={styles.resendButtonText}>Resend OTP</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
scrollContainer: {
|
||||
padding: 20,
|
||||
paddingTop: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 10,
|
||||
textAlign: 'center',
|
||||
color: '#333',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginBottom: 30,
|
||||
color: '#666',
|
||||
lineHeight: 22,
|
||||
},
|
||||
otpSection: {
|
||||
marginBottom: 25,
|
||||
alignItems: 'center',
|
||||
},
|
||||
passwordSection: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
sectionLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
marginBottom: 10,
|
||||
color: '#333',
|
||||
},
|
||||
otpContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 10,
|
||||
width: '100%',
|
||||
},
|
||||
otpInput: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 8,
|
||||
textAlign: 'center',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
verifiedInput: {
|
||||
borderColor: '#34C759',
|
||||
backgroundColor: '#f0fff4',
|
||||
},
|
||||
passwordInputContainer: {
|
||||
position: 'relative',
|
||||
marginBottom: 15,
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 15,
|
||||
fontSize: 16,
|
||||
paddingRight: 50,
|
||||
backgroundColor: '#fff',
|
||||
color: '#000',
|
||||
},
|
||||
disabledInput: {
|
||||
backgroundColor: '#f5f5f5',
|
||||
borderColor: '#eee',
|
||||
},
|
||||
inputError: {
|
||||
borderColor: '#ff3b30',
|
||||
},
|
||||
eyeIcon: {
|
||||
position: 'absolute',
|
||||
right: 15,
|
||||
top: 13,
|
||||
zIndex: 1,
|
||||
},
|
||||
eyeIconText: {
|
||||
fontSize: 20,
|
||||
},
|
||||
errorText: {
|
||||
color: '#ff3b30',
|
||||
fontSize: 14,
|
||||
marginBottom: 10,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: colors.primary,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginTop: 10,
|
||||
minWidth: 120,
|
||||
},
|
||||
submitButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
verifiedContainer: {
|
||||
backgroundColor: '#34C759',
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginTop: 10,
|
||||
},
|
||||
verifiedText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
resetButton: {
|
||||
backgroundColor: colors.primary,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
disabledButton: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
resetButtonText: {
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
resendButton: {
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
disabledResendButton: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
resendButtonText: {
|
||||
color: colors.primary,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
});
|
||||
|
||||
export default ResetPassword;
|
||||
@@ -6,6 +6,7 @@ import StartBidding from "./Bidding/StartBidding";
|
||||
import StartReverseBidding from "./Bidding/StartReverseBidding";
|
||||
import Home from "./Home/Home";
|
||||
import Login from "./Login/Login";
|
||||
import ResetPassword from "./Login/ResetPassword";
|
||||
import Onboarding from "./Onboarding/Onboarding";
|
||||
import ProfileTab from "./Profile/ProfileTab";
|
||||
import Partnership from "./Ragistration/PartnerShip";
|
||||
@@ -42,6 +43,7 @@ const Screens = {
|
||||
Partnership,
|
||||
LiveTenderDetail,
|
||||
ProfileTab,
|
||||
ViewTenderDetail
|
||||
ViewTenderDetail,
|
||||
ResetPassword
|
||||
}
|
||||
export default Screens
|
||||
Reference in New Issue
Block a user