diff --git a/src/Constant/NavigationString.tsx b/src/Constant/NavigationString.tsx
index 6ae544d..5f25a0c 100644
--- a/src/Constant/NavigationString.tsx
+++ b/src/Constant/NavigationString.tsx
@@ -21,6 +21,7 @@ const NavigationString = {
LiveTenderDetail:'LiveTenderDetail',
AuctionDetail: 'AuctionDetail',
Profile:'Profile',
- ViewTenderDetail:'ViewTenderDetail'
+ ViewTenderDetail:'ViewTenderDetail',
+ ResetPassword:'ResetPassword'
}
export default NavigationString
\ No newline at end of file
diff --git a/src/Navigation/MainStack.jsx b/src/Navigation/MainStack.jsx
index e088e14..03aaccc 100644
--- a/src/Navigation/MainStack.jsx
+++ b/src/Navigation/MainStack.jsx
@@ -46,7 +46,7 @@ export default function MainStack() {
-
+
);
}
diff --git a/src/Screens/Login/ForgotPassword.tsx b/src/Screens/Login/ForgotPassword.tsx
new file mode 100644
index 0000000..88bc600
--- /dev/null
+++ b/src/Screens/Login/ForgotPassword.tsx
@@ -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 = ({
+ visible,
+ onClose
+}) => {
+ const navigation = useNavigation() as Navigation;
+ const [email, setEmail] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+
+ 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 (
+
+
+
+
+
+ Reset Password
+
+
+ Enter your email address and we'll send you an OTP to reset your password.
+
+
+
+
+ {error ? {error} : null}
+
+
+
+ Cancel
+
+
+
+ {loading ? (
+
+ ) : (
+ Send OTP
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/Screens/Login/Login.tsx b/src/Screens/Login/Login.tsx
index b8382f3..9d13edb 100644
--- a/src/Screens/Login/Login.tsx
+++ b/src/Screens/Login/Login.tsx
@@ -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 = () => {
{errors.password && touched.password && {errors.password}}
-
+ setForgotPassword(true)} >
Forgot Password?
@@ -293,6 +295,7 @@ const Login = () => {
+ {forgotPassword && setForgotPassword(false)}/>}
);
};
diff --git a/src/Screens/Login/ResetPassword.tsx b/src/Screens/Login/ResetPassword.tsx
new file mode 100644
index 0000000..c9b30af
--- /dev/null
+++ b/src/Screens/Login/ResetPassword.tsx
@@ -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 = ({ route, navigation }) => {
+ const [email, setEmail] = useState(route?.params?.email || '');
+ const [otp, setOtp] = useState(['', '', '', '', '', '']);
+ const [newPassword, setNewPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [otpLoading, setOtpLoading] = useState(false);
+ const [resendLoading, setResendLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [otpError, setOtpError] = useState('');
+ const [passwordError, setPasswordError] = useState('');
+ const [confirmPasswordError, setConfirmPasswordError] = useState('');
+ const [isOtpVerified, setIsOtpVerified] = useState(false);
+ const [resendCooldown, setResendCooldown] = useState(0);
+ const [newPasswordVisible, setNewPasswordVisible] = useState(false);
+ const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
+
+ const otpInputs = useRef>([]);
+ const newPasswordRef = useRef(null);
+ const confirmPasswordRef = useRef(null);
+ const resendTimerRef = useRef(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, 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 (
+
+
+ Reset Password
+
+ Enter the OTP sent to {email} and your new password
+
+
+ {/* OTP Input Section */}
+
+ OTP Code
+
+ {otp.map((digit, index) => (
+ { 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}
+ />
+ ))}
+
+ {otpError ? {otpError} : null}
+
+ {isOtpVerified ? (
+
+ ✓ OTP Verified
+
+ ) : (
+
+ {otpLoading ? (
+
+ ) : (
+ Verify OTP
+ )}
+
+ )}
+
+
+
+ {/* Password Input Section - Only enabled after OTP verification */}
+
+ New Password
+
+ {
+ setNewPassword(text);
+ setPasswordError('');
+ }}
+ secureTextEntry={!newPasswordVisible}
+ returnKeyType="next"
+ onSubmitEditing={() => confirmPasswordRef.current?.focus()}
+ editable={isOtpVerified}
+ />
+ {isOtpVerified && (
+ setNewPasswordVisible(!newPasswordVisible)}
+ >
+
+ {newPasswordVisible ? '🙈' : '👁️'}
+
+
+ )}
+
+ {passwordError ? {passwordError} : null}
+
+ Confirm Password
+
+ {
+ setConfirmPassword(text);
+ setConfirmPasswordError('');
+ }}
+ secureTextEntry={!confirmPasswordVisible}
+ returnKeyType="done"
+ onSubmitEditing={handleResetPassword}
+ editable={isOtpVerified}
+ />
+ {isOtpVerified && (
+ setConfirmPasswordVisible(!confirmPasswordVisible)}
+ >
+
+ {confirmPasswordVisible ? '🙈' : '👁️'}
+
+
+ )}
+
+ {confirmPasswordError ? (
+ {confirmPasswordError}
+ ) : null}
+
+
+ {error ? {error} : null}
+
+
+ {loading ? (
+
+ ) : (
+ Reset Password
+ )}
+
+
+ 0 && styles.disabledResendButton]}
+ onPress={handleResendOtp}
+ disabled={resendLoading || resendCooldown > 0}
+ >
+ {resendLoading ? (
+
+ ) : resendCooldown > 0 ? (
+
+ Resend OTP in {formatTime(resendCooldown)}
+
+ ) : (
+ Resend OTP
+ )}
+
+
+
+ );
+};
+
+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;
\ No newline at end of file
diff --git a/src/Screens/index.ts b/src/Screens/index.ts
index 6105bf0..51a6cae 100644
--- a/src/Screens/index.ts
+++ b/src/Screens/index.ts
@@ -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
\ No newline at end of file