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:
2025-12-11 23:33:43 +05:30
parent a7aa9e53a7
commit 801725edf3
75 changed files with 15734 additions and 0 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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
View 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
View 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
View 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',
};
},
};

View 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);
}
},
};

View 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
View 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;
}