added reset-password
This commit is contained in:
@@ -21,6 +21,7 @@ const NavigationString = {
|
|||||||
LiveTenderDetail:'LiveTenderDetail',
|
LiveTenderDetail:'LiveTenderDetail',
|
||||||
AuctionDetail: 'AuctionDetail',
|
AuctionDetail: 'AuctionDetail',
|
||||||
Profile:'Profile',
|
Profile:'Profile',
|
||||||
ViewTenderDetail:'ViewTenderDetail'
|
ViewTenderDetail:'ViewTenderDetail',
|
||||||
|
ResetPassword:'ResetPassword'
|
||||||
}
|
}
|
||||||
export default NavigationString
|
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.Registration} component={Screens.Registration} options={option} />
|
||||||
<Stack.Screen name={NavigationString.Partnership} component={Screens.Partnership} 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.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>
|
</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 NavigationString from '../../Constant/NavigationString';
|
||||||
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
import ReactNativeHapticFeedback from 'react-native-haptic-feedback';
|
||||||
import { Loader } from '../../Component/Index';
|
import { Loader } from '../../Component/Index';
|
||||||
|
import ForgotModal from './ForgotPassword';
|
||||||
|
|
||||||
const hapticOptions = {
|
const hapticOptions = {
|
||||||
enableVibrateFallback: true,
|
enableVibrateFallback: true,
|
||||||
@@ -38,6 +39,7 @@ const Login = () => {
|
|||||||
const { mutate: auctionMutate } = useAuctionLogin();
|
const { mutate: auctionMutate } = useAuctionLogin();
|
||||||
const { data } = useGetDesignation();
|
const { data } = useGetDesignation();
|
||||||
const { mutate: registrationStateMutate } = useRagistrationState();
|
const { mutate: registrationStateMutate } = useRagistrationState();
|
||||||
|
const [forgotPassword, setForgotPassword] = useState(false)
|
||||||
|
|
||||||
const { data: MasterData } = usefetchMasterData()
|
const { data: MasterData } = usefetchMasterData()
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ const Login = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setAuctionLoading(false)
|
||||||
} finally {
|
} finally {
|
||||||
setAuctionLoading(false);
|
setAuctionLoading(false);
|
||||||
}
|
}
|
||||||
@@ -249,7 +251,7 @@ const Login = () => {
|
|||||||
</View>
|
</View>
|
||||||
{errors.password && touched.password && <Text style={styles.errorText}>{errors.password}</Text>}
|
{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>
|
<Text style={styles.forgotPassText}>Forgot Password?</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
@@ -293,6 +295,7 @@ const Login = () => {
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
{forgotPassword && <ForgotModal visible={forgotPassword} onClose={()=>setForgotPassword(false)}/>}
|
||||||
</View>
|
</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 StartReverseBidding from "./Bidding/StartReverseBidding";
|
||||||
import Home from "./Home/Home";
|
import Home from "./Home/Home";
|
||||||
import Login from "./Login/Login";
|
import Login from "./Login/Login";
|
||||||
|
import ResetPassword from "./Login/ResetPassword";
|
||||||
import Onboarding from "./Onboarding/Onboarding";
|
import Onboarding from "./Onboarding/Onboarding";
|
||||||
import ProfileTab from "./Profile/ProfileTab";
|
import ProfileTab from "./Profile/ProfileTab";
|
||||||
import Partnership from "./Ragistration/PartnerShip";
|
import Partnership from "./Ragistration/PartnerShip";
|
||||||
@@ -42,6 +43,7 @@ const Screens = {
|
|||||||
Partnership,
|
Partnership,
|
||||||
LiveTenderDetail,
|
LiveTenderDetail,
|
||||||
ProfileTab,
|
ProfileTab,
|
||||||
ViewTenderDetail
|
ViewTenderDetail,
|
||||||
|
ResetPassword
|
||||||
}
|
}
|
||||||
export default Screens
|
export default Screens
|
||||||
Reference in New Issue
Block a user