feat: Add user registration component and authentication services
- Implemented Register component for user sign-up with form validation and network status handling. - Created authAPI service for handling user login and registration with online/offline support. - Developed localStorage service for managing user data and sessions offline. - Introduced networkService for detecting online/offline status and managing connectivity. - Defined authentication types for requests and responses. - Added TypeScript configuration for the project.
This commit is contained in:
BIN
src/assets/img/eye.png
Normal file
BIN
src/assets/img/eye.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/img/images.jpeg
Normal file
BIN
src/assets/img/images.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/img/images.jpg
Normal file
BIN
src/assets/img/images.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
305
src/components/Login.tsx
Normal file
305
src/components/Login.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
ImageBackground,
|
||||
Keyboard,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { authAPI } from '../services/authAPI';
|
||||
import { ApiError } from '../types/auth';
|
||||
import { networkService } from '../services/networkService';
|
||||
|
||||
interface LoginProps {
|
||||
onNavigateToRegister: () => void;
|
||||
onLoginSuccess: () => void;
|
||||
}
|
||||
|
||||
const Login: React.FC<LoginProps> = ({ onNavigateToRegister, onLoginSuccess }) => {
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
|
||||
// Listen for network changes
|
||||
React.useEffect(() => {
|
||||
setIsOnline(networkService.isOnline());
|
||||
|
||||
const unsubscribe = networkService.addListener((networkState) => {
|
||||
setIsOnline(networkState.isConnected);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Error', 'Please fill in all fields');
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
Alert.alert('Error', 'Please enter a valid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await authAPI.login({ email, password });
|
||||
|
||||
Alert.alert('Success', response.message, [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
console.log('User logged in:', response.user);
|
||||
console.log('Token:', response.token);
|
||||
onLoginSuccess();
|
||||
}
|
||||
}
|
||||
]);
|
||||
} catch (error: any) {
|
||||
const apiError = error as ApiError;
|
||||
Alert.alert(
|
||||
'Login Failed',
|
||||
apiError.error || apiError.message || 'An unexpected error occurred'
|
||||
);
|
||||
console.error('Login error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
Alert.alert('Forgot Password', 'Password reset functionality would be implemented here');
|
||||
};
|
||||
|
||||
const handleUseTestCredentials = () => {
|
||||
const testCreds = authAPI.getTestCredentials();
|
||||
setEmail(testCreds.email);
|
||||
setPassword(testCreds.password);
|
||||
Alert.alert('Test Credentials', `Using ReqRes API test credentials:\n${testCreds.email} / ${testCreds.password}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={require('../assets/img/images.jpg')}
|
||||
style={styles.container}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.containerMain}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.containerMain}>
|
||||
<Text style={styles.text}>Login</Text>
|
||||
|
||||
{/* Status Info */}
|
||||
<View style={styles.testInfo}>
|
||||
<Text style={styles.testInfoText}>
|
||||
{isOnline ? '🟢 Online Mode' : '🔴 Offline Mode'}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={handleUseTestCredentials} style={styles.testCredentialsButton}>
|
||||
<Text style={styles.testCredentialsText}>Use Test Credentials</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoComplete="email"
|
||||
returnKeyType="next"
|
||||
onSubmitEditing={() => Keyboard.dismiss()}
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry={secureTextEntry}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleLogin}
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setSecureTextEntry(!secureTextEntry)}
|
||||
>
|
||||
<Image
|
||||
source={require('../assets/img/eye.png')}
|
||||
style={[
|
||||
styles.Icon,
|
||||
!secureTextEntry ? styles.eyeIconActive : styles.eyeIconInactive
|
||||
]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* <TouchableOpacity style={styles.forgotPassword} onPress={handleForgotPassword}>
|
||||
<Text style={styles.forgotPasswordText}>Forgot Password?</Text>
|
||||
</TouchableOpacity> */}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isLoading && styles.buttonDisabled]}
|
||||
onPress={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="white" size="small" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Login</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.buttonSignup} onPress={onNavigateToRegister}>
|
||||
<Text style={styles.buttonSignupText}>Not a member? Sign Up</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
containerMain: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
marginHorizontal: 10,
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
color: 'black',
|
||||
fontSize: 30,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
testInfo: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 15,
|
||||
padding: 10,
|
||||
backgroundColor: '#ffffff10',
|
||||
borderRadius: 10,
|
||||
},
|
||||
testInfoText: {
|
||||
color: '#3bb6d8',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginBottom: 5,
|
||||
},
|
||||
testCredentialsButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
backgroundColor: '#3bb6d8',
|
||||
borderRadius: 15,
|
||||
},
|
||||
testCredentialsText: {
|
||||
color: 'white',
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 10,
|
||||
height: 40,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'white',
|
||||
borderRadius: 100,
|
||||
backgroundColor: '#ffffff20',
|
||||
paddingHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
color: 'black',
|
||||
},
|
||||
eyeIcon: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
Icon: {
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
},
|
||||
eyeIconActive: {
|
||||
tintColor: '#3bb6d8',
|
||||
},
|
||||
eyeIconInactive: {
|
||||
tintColor: '#ffffff',
|
||||
},
|
||||
forgotPassword: {
|
||||
alignSelf: 'flex-end',
|
||||
marginBottom: 10,
|
||||
},
|
||||
forgotPasswordText: {
|
||||
color: '#3bb6d8',
|
||||
fontSize: 14,
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
button: {
|
||||
marginTop: 20,
|
||||
backgroundColor: '#3bb6d8',
|
||||
padding: 8,
|
||||
borderRadius: 100,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'white',
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#a0a0a0',
|
||||
borderColor: '#cccccc',
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
buttonSignup: {
|
||||
marginTop: 10,
|
||||
backgroundColor: '#ffffff80',
|
||||
padding: 8,
|
||||
borderRadius: 100,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#3bb6d8',
|
||||
},
|
||||
buttonSignupText: {
|
||||
color: 'black',
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default Login;
|
||||
386
src/components/Register.tsx
Normal file
386
src/components/Register.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
ImageBackground,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
Alert,
|
||||
ScrollView,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { authAPI } from '../services/authAPI';
|
||||
import { ApiError } from '../types/auth';
|
||||
import { networkService } from '../services/networkService';
|
||||
|
||||
interface RegisterProps {
|
||||
onNavigateToLogin: () => void;
|
||||
onRegisterSuccess: () => void;
|
||||
}
|
||||
|
||||
const Register: React.FC<RegisterProps> = ({ onNavigateToLogin, onRegisterSuccess }) => {
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [confirmSecureTextEntry, setConfirmSecureTextEntry] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOnline, setIsOnline] = useState(true);
|
||||
|
||||
// Listen for network changes
|
||||
React.useEffect(() => {
|
||||
setIsOnline(networkService.isOnline());
|
||||
|
||||
const unsubscribe = networkService.addListener((networkState) => {
|
||||
setIsOnline(networkState.isConnected);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
fullName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const { fullName, email, phone, password, confirmPassword } = formData;
|
||||
|
||||
if (!fullName || !email || !phone || !password || !confirmPassword) {
|
||||
Alert.alert('Error', 'Please fill in all fields');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
Alert.alert('Error', 'Passwords do not match');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
Alert.alert('Error', 'Password must be at least 6 characters long');
|
||||
return false;
|
||||
}
|
||||
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
Alert.alert('Error', 'Please enter a valid email address');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await authAPI.register({
|
||||
fullName: formData.fullName,
|
||||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
password: formData.password,
|
||||
});
|
||||
|
||||
Alert.alert('Success', response.message, [
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => {
|
||||
console.log('User registered:', response.user);
|
||||
console.log('Token:', response.token);
|
||||
onRegisterSuccess();
|
||||
}
|
||||
}
|
||||
]);
|
||||
} catch (error: any) {
|
||||
const apiError = error as ApiError;
|
||||
Alert.alert(
|
||||
'Registration Failed',
|
||||
apiError.error || apiError.message || 'An unexpected error occurred'
|
||||
);
|
||||
console.error('Registration error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
source={require('../assets/img/images.jpg')}
|
||||
style={styles.container}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.containerMain}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.containerMain}>
|
||||
<Text style={styles.text}>Sign Up</Text>
|
||||
|
||||
{/* Status Info */}
|
||||
<View style={styles.apiInfo}>
|
||||
<Text style={styles.apiInfoText}>
|
||||
{isOnline ? '🟢 Online Mode' : '🔴 Offline Mode'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Full Name"
|
||||
value={formData.fullName}
|
||||
onChangeText={(value) => handleInputChange('fullName', value)}
|
||||
autoCapitalize="words"
|
||||
autoCorrect={false}
|
||||
returnKeyType="next"
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Email"
|
||||
value={formData.email}
|
||||
onChangeText={(value) => handleInputChange('email', value)}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoComplete="email"
|
||||
returnKeyType="next"
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Phone Number"
|
||||
value={formData.phone}
|
||||
onChangeText={(value) => handleInputChange('phone', value)}
|
||||
keyboardType="phone-pad"
|
||||
autoComplete="tel"
|
||||
returnKeyType="next"
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChangeText={(value) => handleInputChange('password', value)}
|
||||
secureTextEntry={secureTextEntry}
|
||||
returnKeyType="next"
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setSecureTextEntry(!secureTextEntry)}
|
||||
>
|
||||
<Image
|
||||
source={require('../assets/img/eye.png')}
|
||||
style={[
|
||||
styles.Icon,
|
||||
!secureTextEntry ? styles.eyeIconActive : styles.eyeIconInactive
|
||||
]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Confirm Password"
|
||||
value={formData.confirmPassword}
|
||||
onChangeText={(value) => handleInputChange('confirmPassword', value)}
|
||||
secureTextEntry={confirmSecureTextEntry}
|
||||
returnKeyType="done"
|
||||
onSubmitEditing={handleRegister}
|
||||
placeholderTextColor="black"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.eyeIcon}
|
||||
onPress={() => setConfirmSecureTextEntry(!confirmSecureTextEntry)}
|
||||
>
|
||||
<Image
|
||||
source={require('../assets/img/eye.png')}
|
||||
style={[
|
||||
styles.Icon,
|
||||
!confirmSecureTextEntry ? styles.eyeIconActive : styles.eyeIconInactive
|
||||
]}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.termsContainer}>
|
||||
<Text style={styles.termsText}>
|
||||
By signing up, you agree to our{' '}
|
||||
<Text style={styles.linkText}>Terms of Service</Text>
|
||||
{' '}and{' '}
|
||||
<Text style={styles.linkText}>Privacy Policy</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isLoading && styles.buttonDisabled]}
|
||||
onPress={handleRegister}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<ActivityIndicator color="white" size="small" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Sign Up</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.buttonSignup} onPress={onNavigateToLogin}>
|
||||
<Text style={styles.buttonSignupText}>Already have an account? Login</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
</ImageBackground>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContainer: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
containerMain: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
marginHorizontal: 10,
|
||||
paddingVertical: 20,
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
color: 'black',
|
||||
fontSize: 30,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 10,
|
||||
height: 40,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'white',
|
||||
borderRadius: 100,
|
||||
backgroundColor: '#ffffff20',
|
||||
paddingHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
color: 'black',
|
||||
},
|
||||
eyeIcon: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
Icon: {
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
},
|
||||
eyeIconActive: {
|
||||
tintColor: '#3bb6d8',
|
||||
},
|
||||
eyeIconInactive: {
|
||||
tintColor: '#ffffff',
|
||||
},
|
||||
termsContainer: {
|
||||
marginVertical: 15,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
termsText: {
|
||||
color: 'black',
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
lineHeight: 18,
|
||||
},
|
||||
linkText: {
|
||||
color: '#3bb6d8',
|
||||
textDecorationLine: 'underline',
|
||||
fontWeight: '500',
|
||||
},
|
||||
apiInfo: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 15,
|
||||
padding: 8,
|
||||
backgroundColor: '#ffffff10',
|
||||
borderRadius: 10,
|
||||
},
|
||||
apiInfoText: {
|
||||
color: '#3bb6d8',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
button: {
|
||||
marginTop: 10,
|
||||
backgroundColor: '#3bb6d8',
|
||||
padding: 8,
|
||||
borderRadius: 100,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'white',
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#a0a0a0',
|
||||
borderColor: '#cccccc',
|
||||
},
|
||||
buttonText: {
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
buttonSignup: {
|
||||
marginTop: 10,
|
||||
backgroundColor: '#ffffff80',
|
||||
padding: 8,
|
||||
borderRadius: 100,
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#3bb6d8',
|
||||
},
|
||||
buttonSignupText: {
|
||||
color: 'black',
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default Register;
|
||||
446
src/services/authAPI.ts
Normal file
446
src/services/authAPI.ts
Normal file
@@ -0,0 +1,446 @@
|
||||
// Simple Open Authentication API using ReqRes
|
||||
// ReqRes is a free, open API for testing - no setup required!
|
||||
// Website: https://reqres.in
|
||||
|
||||
import { LoginRequest, RegisterRequest, AuthResponse, ApiError } from '../types/auth';
|
||||
import { localStorageService } from './localStorage';
|
||||
import { networkService } from './networkService';
|
||||
|
||||
// ReqRes API Configuration (Free testing API)
|
||||
const API_BASE_URL = 'https://reqres.in/api';
|
||||
|
||||
// API request helper
|
||||
async function apiRequest(endpoint: string, options: RequestInit = {}): Promise<any> {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
try {
|
||||
console.log('Making API request to:', url);
|
||||
console.log('Request options:', options);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response ok:', response.ok);
|
||||
|
||||
let data;
|
||||
try {
|
||||
const responseText = await response.text();
|
||||
console.log('Response text:', responseText);
|
||||
|
||||
if (responseText && responseText.trim()) {
|
||||
// Try to parse as JSON
|
||||
try {
|
||||
data = JSON.parse(responseText);
|
||||
} catch {
|
||||
console.error('JSON parsing failed, response was:', responseText);
|
||||
// If JSON parsing fails, treat as a server error and use fallback
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Server returned invalid response',
|
||||
error: 'API_PARSE_ERROR',
|
||||
isParseError: true,
|
||||
} as ApiError & { isParseError: boolean };
|
||||
}
|
||||
} else {
|
||||
console.log('Empty response, treating as error');
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Server returned empty response',
|
||||
error: 'API_EMPTY_RESPONSE',
|
||||
isParseError: true,
|
||||
} as ApiError & { isParseError: boolean };
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.isParseError) {
|
||||
throw error;
|
||||
}
|
||||
console.error('Response reading error:', error);
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Failed to read server response',
|
||||
error: 'API_READ_ERROR',
|
||||
isParseError: true,
|
||||
} as ApiError & { isParseError: boolean };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('API error response:', data);
|
||||
throw {
|
||||
success: false,
|
||||
message: data.error || data.message || 'Request failed',
|
||||
error: `HTTP ${response.status}: ${data.error || 'Unknown error'}`,
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
console.log('API success response:', data);
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
console.error('API request error:', error);
|
||||
|
||||
if (error.success === false) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle network errors
|
||||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Network connection failed',
|
||||
error: 'Please check your internet connection',
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Network error',
|
||||
error: error.message || 'Unable to connect to server',
|
||||
} as ApiError;
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication API
|
||||
export const authAPI = {
|
||||
// Initialize local storage with default users
|
||||
async initialize(): Promise<void> {
|
||||
await localStorageService.initializeDefaultUsers();
|
||||
},
|
||||
|
||||
// Login with online/offline support
|
||||
async login(credentials: LoginRequest): Promise<AuthResponse> {
|
||||
console.log('Attempting login with credentials:', { email: credentials.email });
|
||||
console.log('Network status:', networkService.getNetworkInfo());
|
||||
|
||||
// Always try local authentication first for better performance
|
||||
const localResult = await this.localLogin(credentials);
|
||||
if (localResult.success) {
|
||||
// If we're online, also try to sync with API in background
|
||||
if (networkService.isOnline()) {
|
||||
this.backgroundSync(credentials).catch(error => {
|
||||
console.log('Background sync failed:', error);
|
||||
});
|
||||
}
|
||||
return localResult;
|
||||
}
|
||||
|
||||
// If local login failed and we're online, try API
|
||||
if (networkService.isOnline()) {
|
||||
try {
|
||||
console.log('Local login failed, trying API login');
|
||||
const response = await apiRequest('/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.token) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Login failed',
|
||||
error: 'No authentication token received',
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
// Save user locally for future offline use
|
||||
try {
|
||||
await localStorageService.saveUser({
|
||||
fullName: 'API User',
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
});
|
||||
} catch (saveError) {
|
||||
console.log('User already exists locally:', saveError);
|
||||
}
|
||||
|
||||
// Create local session
|
||||
const user = await localStorageService.findUserByEmail(credentials.email);
|
||||
if (user) {
|
||||
const token = await localStorageService.createSession(user);
|
||||
return {
|
||||
success: true,
|
||||
message: 'Login successful! (Online)',
|
||||
user: {
|
||||
id: user.id,
|
||||
fullName: user.fullName,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
},
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback response
|
||||
return {
|
||||
success: true,
|
||||
message: 'Login successful!',
|
||||
user: {
|
||||
id: '1',
|
||||
fullName: 'Test User',
|
||||
email: credentials.email,
|
||||
},
|
||||
token: response.token,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('API login error:', error);
|
||||
// If API fails, fall back to local authentication
|
||||
return await this.localLogin(credentials, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Offline and local login failed
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Login failed',
|
||||
error: 'Invalid credentials. Please check your email and password.',
|
||||
} as ApiError;
|
||||
},
|
||||
|
||||
// Register with online/offline support
|
||||
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
||||
try {
|
||||
console.log('Attempting registration with data:', { email: userData.email });
|
||||
console.log('Network status:', networkService.getNetworkInfo());
|
||||
|
||||
// Always save user locally first
|
||||
const localUser = await localStorageService.saveUser({
|
||||
fullName: userData.fullName,
|
||||
email: userData.email,
|
||||
phone: userData.phone,
|
||||
password: userData.password,
|
||||
});
|
||||
|
||||
// Create local session
|
||||
const token = await localStorageService.createSession(localUser);
|
||||
|
||||
// If online, try to sync with API in background
|
||||
if (networkService.isOnline()) {
|
||||
this.backgroundRegisterSync(userData).catch(error => {
|
||||
console.log('Background registration sync failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
const message = networkService.isOnline()
|
||||
? 'Registration successful!'
|
||||
: 'Registration successful! (Offline mode)';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
user: {
|
||||
id: localUser.id,
|
||||
fullName: localUser.fullName,
|
||||
email: localUser.email,
|
||||
phone: localUser.phone,
|
||||
},
|
||||
token,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('Registration error:', error);
|
||||
|
||||
if (error.message && error.message.includes('User already exists')) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Registration failed',
|
||||
error: 'An account with this email already exists',
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Registration failed',
|
||||
error: error.message || 'An unexpected error occurred',
|
||||
} as ApiError;
|
||||
}
|
||||
},
|
||||
|
||||
// Background registration sync with API
|
||||
async backgroundRegisterSync(userData: RegisterRequest): Promise<void> {
|
||||
try {
|
||||
console.log('Background registration sync with API');
|
||||
await apiRequest('/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: userData.email,
|
||||
password: userData.password,
|
||||
}),
|
||||
});
|
||||
console.log('Background registration sync successful');
|
||||
} catch (error) {
|
||||
console.log('Background registration sync failed (non-critical):', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Get test credentials for easy testing
|
||||
getTestCredentials() {
|
||||
return {
|
||||
email: 'eve.holt@reqres.in',
|
||||
password: 'cityslicka',
|
||||
};
|
||||
},
|
||||
|
||||
// Check if credentials are test credentials
|
||||
isTestCredentials(email: string, password: string) {
|
||||
const testCreds = this.getTestCredentials();
|
||||
return email === testCreds.email && password === testCreds.password;
|
||||
},
|
||||
|
||||
// Test API connectivity
|
||||
async testAPI(): Promise<boolean> {
|
||||
try {
|
||||
console.log('Testing API connectivity...');
|
||||
const response = await fetch(`${API_BASE_URL}/users/1`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const isWorking = response.ok;
|
||||
console.log('API test result:', isWorking ? 'Working' : 'Failed');
|
||||
return isWorking;
|
||||
} catch (error) {
|
||||
console.log('API test failed:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Get current user (from local storage)
|
||||
async getCurrentUser() {
|
||||
return await localStorageService.getCurrentUser();
|
||||
},
|
||||
|
||||
// Check if user is logged in
|
||||
async isLoggedIn(): Promise<boolean> {
|
||||
const token = await localStorageService.getCurrentToken();
|
||||
if (!token) return false;
|
||||
|
||||
return await localStorageService.isValidSession(token);
|
||||
},
|
||||
|
||||
// Logout user
|
||||
async logout(): Promise<void> {
|
||||
await localStorageService.logout();
|
||||
},
|
||||
|
||||
// Get app status
|
||||
async getAppStatus() {
|
||||
const storageInfo = await localStorageService.getStorageInfo();
|
||||
const networkState = networkService.getNetworkState();
|
||||
const isLoggedIn = await this.isLoggedIn();
|
||||
|
||||
return {
|
||||
network: {
|
||||
isOnline: networkService.isOnline(),
|
||||
...networkState,
|
||||
},
|
||||
storage: storageInfo,
|
||||
authentication: {
|
||||
isLoggedIn,
|
||||
currentUser: storageInfo.currentUser,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// Clear all local data (for debugging)
|
||||
async clearAllData(): Promise<void> {
|
||||
await localStorageService.clearAllData();
|
||||
},
|
||||
|
||||
// Local authentication (works offline)
|
||||
async localLogin(credentials: LoginRequest, showOfflineMessage = false): Promise<AuthResponse> {
|
||||
try {
|
||||
console.log('Attempting local login');
|
||||
|
||||
// Validate user locally
|
||||
const user = await localStorageService.validateUser(credentials.email, credentials.password);
|
||||
|
||||
if (user) {
|
||||
// Create local session
|
||||
const token = await localStorageService.createSession(user);
|
||||
|
||||
const message = showOfflineMessage
|
||||
? 'Login successful! (Offline mode)'
|
||||
: 'Login successful! (Local authentication)';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message,
|
||||
user: {
|
||||
id: user.id,
|
||||
fullName: user.fullName,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
},
|
||||
token,
|
||||
};
|
||||
}
|
||||
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Invalid credentials',
|
||||
error: 'User not found or invalid password',
|
||||
} as ApiError;
|
||||
} catch (error) {
|
||||
console.error('Local login error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Background sync with API (non-blocking)
|
||||
async backgroundSync(credentials: LoginRequest): Promise<void> {
|
||||
try {
|
||||
console.log('Background sync with API');
|
||||
await apiRequest('/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
}),
|
||||
});
|
||||
console.log('Background sync successful');
|
||||
} catch (error) {
|
||||
console.log('Background sync failed (non-critical):', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Fallback login for when API is not working (legacy)
|
||||
async fallbackLogin(credentials: LoginRequest): Promise<AuthResponse> {
|
||||
return await this.localLogin(credentials, true);
|
||||
},
|
||||
|
||||
// Fallback register for when API is not working
|
||||
async fallbackRegister(userData: RegisterRequest): Promise<AuthResponse> {
|
||||
console.log('Using fallback registration');
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise<void>(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Simple validation
|
||||
if (!userData.email || !userData.password) {
|
||||
throw {
|
||||
success: false,
|
||||
message: 'Registration failed',
|
||||
error: 'Email and password are required',
|
||||
} as ApiError;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Registration successful! (Using local authentication)',
|
||||
user: {
|
||||
id: '2',
|
||||
fullName: userData.fullName,
|
||||
email: userData.email,
|
||||
phone: userData.phone,
|
||||
},
|
||||
token: 'fallback-register-token-67890',
|
||||
};
|
||||
},
|
||||
};
|
||||
296
src/services/localStorage.ts
Normal file
296
src/services/localStorage.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
// Local Storage Service for Offline Data Management
|
||||
// Works without internet connection
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { User } from '../types/auth';
|
||||
|
||||
// Storage keys
|
||||
const STORAGE_KEYS = {
|
||||
USERS: '@local_users',
|
||||
CURRENT_USER: '@current_user',
|
||||
AUTH_TOKEN: '@auth_token',
|
||||
USER_SESSIONS: '@user_sessions',
|
||||
APP_SETTINGS: '@app_settings',
|
||||
};
|
||||
|
||||
// Local user data structure
|
||||
interface LocalUser {
|
||||
id: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
password: string; // Hashed in real app
|
||||
createdAt: string;
|
||||
lastLogin?: string;
|
||||
}
|
||||
|
||||
interface UserSession {
|
||||
userId: string;
|
||||
token: string;
|
||||
loginTime: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
// Local Storage Service
|
||||
export const localStorageService = {
|
||||
// User Management
|
||||
async saveUser(userData: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
password: string;
|
||||
}): Promise<LocalUser> {
|
||||
try {
|
||||
const users = await this.getAllUsers();
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = users.find(u => u.email === userData.email);
|
||||
if (existingUser) {
|
||||
throw new Error('User already exists with this email');
|
||||
}
|
||||
|
||||
// Create new user
|
||||
const newUser: LocalUser = {
|
||||
id: Date.now().toString(),
|
||||
fullName: userData.fullName,
|
||||
email: userData.email,
|
||||
phone: userData.phone,
|
||||
password: userData.password, // In real app, hash this
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Add to users list
|
||||
users.push(newUser);
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.USERS, JSON.stringify(users));
|
||||
|
||||
console.log('User saved locally:', { email: newUser.email, id: newUser.id });
|
||||
return newUser;
|
||||
} catch (error) {
|
||||
console.error('Error saving user:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getAllUsers(): Promise<LocalUser[]> {
|
||||
try {
|
||||
const usersJson = await AsyncStorage.getItem(STORAGE_KEYS.USERS);
|
||||
return usersJson ? JSON.parse(usersJson) : [];
|
||||
} catch (error) {
|
||||
console.error('Error getting users:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async findUserByEmail(email: string): Promise<LocalUser | null> {
|
||||
try {
|
||||
const users = await this.getAllUsers();
|
||||
return users.find(u => u.email === email) || null;
|
||||
} catch (error) {
|
||||
console.error('Error finding user:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async validateUser(email: string, password: string): Promise<LocalUser | null> {
|
||||
try {
|
||||
const user = await this.findUserByEmail(email);
|
||||
if (user && user.password === password) {
|
||||
// Update last login
|
||||
user.lastLogin = new Date().toISOString();
|
||||
await this.updateUser(user);
|
||||
return user;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error validating user:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async updateUser(userData: LocalUser): Promise<void> {
|
||||
try {
|
||||
const users = await this.getAllUsers();
|
||||
const userIndex = users.findIndex(u => u.id === userData.id);
|
||||
|
||||
if (userIndex !== -1) {
|
||||
users[userIndex] = userData;
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.USERS, JSON.stringify(users));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Session Management
|
||||
async createSession(user: LocalUser): Promise<string> {
|
||||
try {
|
||||
const token = `local_token_${user.id}_${Date.now()}`;
|
||||
const session: UserSession = {
|
||||
userId: user.id,
|
||||
token,
|
||||
loginTime: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
|
||||
};
|
||||
|
||||
// Save current session
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.AUTH_TOKEN, token);
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.CURRENT_USER, JSON.stringify(user));
|
||||
|
||||
// Save to sessions history
|
||||
const sessions = await this.getAllSessions();
|
||||
sessions.push(session);
|
||||
await AsyncStorage.setItem(STORAGE_KEYS.USER_SESSIONS, JSON.stringify(sessions));
|
||||
|
||||
console.log('Session created:', { userId: user.id, token });
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error('Error creating session:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getCurrentUser(): Promise<User | null> {
|
||||
try {
|
||||
const userJson = await AsyncStorage.getItem(STORAGE_KEYS.CURRENT_USER);
|
||||
if (userJson) {
|
||||
const localUser: LocalUser = JSON.parse(userJson);
|
||||
// Convert to User format (without password)
|
||||
return {
|
||||
id: localUser.id,
|
||||
fullName: localUser.fullName,
|
||||
email: localUser.email,
|
||||
phone: localUser.phone,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error getting current user:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async getCurrentToken(): Promise<string | null> {
|
||||
try {
|
||||
return await AsyncStorage.getItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
} catch (error) {
|
||||
console.error('Error getting current token:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async getAllSessions(): Promise<UserSession[]> {
|
||||
try {
|
||||
const sessionsJson = await AsyncStorage.getItem(STORAGE_KEYS.USER_SESSIONS);
|
||||
return sessionsJson ? JSON.parse(sessionsJson) : [];
|
||||
} catch (error) {
|
||||
console.error('Error getting sessions:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
async isValidSession(token: string): Promise<boolean> {
|
||||
try {
|
||||
const sessions = await this.getAllSessions();
|
||||
const session = sessions.find(s => s.token === token);
|
||||
|
||||
if (!session) return false;
|
||||
|
||||
// Check if session is expired
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(session.expiresAt);
|
||||
|
||||
return now < expiresAt;
|
||||
} catch (error) {
|
||||
console.error('Error validating session:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Logout
|
||||
async logout(): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN);
|
||||
await AsyncStorage.removeItem(STORAGE_KEYS.CURRENT_USER);
|
||||
console.log('User logged out locally');
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Data Management
|
||||
async clearAllData(): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.multiRemove([
|
||||
STORAGE_KEYS.USERS,
|
||||
STORAGE_KEYS.CURRENT_USER,
|
||||
STORAGE_KEYS.AUTH_TOKEN,
|
||||
STORAGE_KEYS.USER_SESSIONS,
|
||||
]);
|
||||
console.log('All local data cleared');
|
||||
} catch (error) {
|
||||
console.error('Error clearing data:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async getStorageInfo(): Promise<{
|
||||
totalUsers: number;
|
||||
currentUser: string | null;
|
||||
activeSessions: number;
|
||||
hasValidSession: boolean;
|
||||
}> {
|
||||
try {
|
||||
const users = await this.getAllUsers();
|
||||
const currentUser = await this.getCurrentUser();
|
||||
const sessions = await this.getAllSessions();
|
||||
const token = await this.getCurrentToken();
|
||||
const hasValidSession = token ? await this.isValidSession(token) : false;
|
||||
|
||||
return {
|
||||
totalUsers: users.length,
|
||||
currentUser: currentUser?.email || null,
|
||||
activeSessions: sessions.length,
|
||||
hasValidSession,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting storage info:', error);
|
||||
return {
|
||||
totalUsers: 0,
|
||||
currentUser: null,
|
||||
activeSessions: 0,
|
||||
hasValidSession: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Initialize with default test user
|
||||
async initializeDefaultUsers(): Promise<void> {
|
||||
try {
|
||||
const users = await this.getAllUsers();
|
||||
|
||||
// Add default test user if no users exist
|
||||
if (users.length === 0) {
|
||||
await this.saveUser({
|
||||
fullName: 'Test User',
|
||||
email: 'test@example.com',
|
||||
phone: '+1234567890',
|
||||
password: 'password123',
|
||||
});
|
||||
|
||||
// Also add the ReqRes test user
|
||||
await this.saveUser({
|
||||
fullName: 'Eve Holt',
|
||||
email: 'eve.holt@reqres.in',
|
||||
phone: '+1987654321',
|
||||
password: 'cityslicka',
|
||||
});
|
||||
|
||||
console.log('Default test users created');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing default users:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
136
src/services/networkService.ts
Normal file
136
src/services/networkService.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// Network Detection Service
|
||||
// Detects online/offline status and manages connectivity
|
||||
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
|
||||
interface NetworkState {
|
||||
isConnected: boolean;
|
||||
isInternetReachable: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class NetworkService {
|
||||
private static instance: NetworkService;
|
||||
private networkState: NetworkState = {
|
||||
isConnected: false,
|
||||
isInternetReachable: false,
|
||||
type: 'unknown',
|
||||
};
|
||||
private listeners: ((state: NetworkState) => void)[] = [];
|
||||
|
||||
static getInstance(): NetworkService {
|
||||
if (!NetworkService.instance) {
|
||||
NetworkService.instance = new NetworkService();
|
||||
}
|
||||
return NetworkService.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
try {
|
||||
// Get initial network state
|
||||
const state = await NetInfo.fetch();
|
||||
this.updateNetworkState(state);
|
||||
|
||||
// Listen for network changes
|
||||
NetInfo.addEventListener(state => {
|
||||
this.updateNetworkState(state);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing network service:', error);
|
||||
// Fallback to assuming offline
|
||||
this.networkState = {
|
||||
isConnected: false,
|
||||
isInternetReachable: false,
|
||||
type: 'unknown',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private updateNetworkState(state: any) {
|
||||
const newState: NetworkState = {
|
||||
isConnected: state.isConnected ?? false,
|
||||
isInternetReachable: state.isInternetReachable ?? false,
|
||||
type: state.type || 'unknown',
|
||||
};
|
||||
|
||||
const wasOnline = this.networkState.isConnected;
|
||||
const isNowOnline = newState.isConnected;
|
||||
|
||||
this.networkState = newState;
|
||||
|
||||
// Log network changes
|
||||
if (wasOnline !== isNowOnline) {
|
||||
console.log(`Network status changed: ${isNowOnline ? 'ONLINE' : 'OFFLINE'}`);
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
this.listeners.forEach(listener => {
|
||||
try {
|
||||
listener(newState);
|
||||
} catch (error) {
|
||||
console.error('Error in network listener:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get current network state
|
||||
getNetworkState(): NetworkState {
|
||||
return { ...this.networkState };
|
||||
}
|
||||
|
||||
// Check if device is online
|
||||
isOnline(): boolean {
|
||||
return this.networkState.isConnected && this.networkState.isInternetReachable !== false;
|
||||
}
|
||||
|
||||
// Check if device is offline
|
||||
isOffline(): boolean {
|
||||
return !this.isOnline();
|
||||
}
|
||||
|
||||
// Add network state listener
|
||||
addListener(listener: (state: NetworkState) => void): () => void {
|
||||
this.listeners.push(listener);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index > -1) {
|
||||
this.listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Test internet connectivity
|
||||
async testConnectivity(): Promise<boolean> {
|
||||
try {
|
||||
// Try to fetch a small resource
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const response = await fetch('https://httpbin.org/status/200', {
|
||||
method: 'HEAD',
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.log('Connectivity test failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get network info for debugging
|
||||
getNetworkInfo(): string {
|
||||
const state = this.networkState;
|
||||
return `Connected: ${state.isConnected}, Internet: ${state.isInternetReachable}, Type: ${state.type}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const networkService = NetworkService.getInstance();
|
||||
34
src/types/auth.ts
Normal file
34
src/types/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Simple authentication types for open API
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterRequest {
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
user?: User;
|
||||
token?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
success: false;
|
||||
message: string;
|
||||
error: string;
|
||||
}
|
||||
Reference in New Issue
Block a user