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