Add Map screen with location permissions handling and integrate current location feature

This commit is contained in:
2025-12-24 23:49:57 +05:30
parent 48c266c252
commit 741ab60fce
4 changed files with 365 additions and 271 deletions

427
App.tsx
View File

@@ -1,14 +1,14 @@
import { import {
StatusBar, StatusBar,
useColorScheme, useColorScheme,
View, View,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity, TouchableOpacity,
Alert, Alert,
NativeModules, NativeModules,
Image, Image,
ScrollView, ScrollView,
} from 'react-native'; } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
@@ -18,241 +18,258 @@ import Register from './src/components/Register';
import { authAPI } from './src/services/authAPI'; import { authAPI } from './src/services/authAPI';
import { networkService } from './src/services/networkService'; import { networkService } from './src/services/networkService';
import { ScannerScreen } from './src/screens/ScannerScreen'; import { ScannerScreen } from './src/screens/ScannerScreen';
import Map from './src/screens/Map';
type Screen = 'login' | 'register' | 'home' | 'scanner'; type Screen = 'login' | 'register' | 'home' | 'scanner' | 'map';
const { MyNativeModule } = NativeModules; const { MyNativeModule } = NativeModules;
function App() { function App() {
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
const [currentScreen, setCurrentScreen] = useState<Screen>('login'); const [currentScreen, setCurrentScreen] = useState<Screen>('login');
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const [isOnline, setIsOnline] = useState(true); const [isOnline, setIsOnline] = useState(true);
const [currentUser, setCurrentUser] = useState<any>(null); const [currentUser, setCurrentUser] = useState<any>(null);
const [qrCode, setQrCode] = useState<string | null>(null); const [qrCode, setQrCode] = useState<string | null>(null);
const [isGeneratingQR, setIsGeneratingQR] = useState(false); const [isGeneratingQR, setIsGeneratingQR] = useState(false);
const [scannedCodes, setScannedCodes] = useState<any[]>([]); const [scannedCodes, setScannedCodes] = useState<any[]>([]);
/* -------------------- INIT -------------------- */ /* -------------------- INIT -------------------- */
useEffect(() => { useEffect(() => {
initializeApp(); initializeApp();
console.log('MyNativeModule:', MyNativeModule); console.log('MyNativeModule:', MyNativeModule);
MyNativeModule?.greet?.('John').then((msg: any) => { MyNativeModule?.greet?.('John').then((msg: any) => {
console.log(msg); console.log(msg);
}); });
const unsubscribe = networkService.addListener(networkState => { const unsubscribe = networkService.addListener(networkState => {
setIsOnline(networkState.isConnected); setIsOnline(networkState.isConnected);
}); });
return unsubscribe; return unsubscribe;
}, []); }, []);
const initializeApp = async () => { const initializeApp = async () => {
try { try {
await authAPI.initialize(); await authAPI.initialize();
const isLoggedIn = await authAPI.isLoggedIn(); const isLoggedIn = await authAPI.isLoggedIn();
if (isLoggedIn) { if (isLoggedIn) {
const user = await authAPI.getCurrentUser(); const user = await authAPI.getCurrentUser();
setCurrentUser(user); setCurrentUser(user);
setCurrentScreen('home'); setCurrentScreen('home');
} }
} catch (error) { } catch (error) {
console.error('Initialization error:', error); console.error('Initialization error:', error);
} finally { } finally {
setIsInitialized(true); setIsInitialized(true);
} }
}; };
/* -------------------- NAVIGATION -------------------- */ /* -------------------- NAVIGATION -------------------- */
const navigateToLogin = () => { const navigateToLogin = () => {
setCurrentUser(null); setCurrentUser(null);
setQrCode(null); setQrCode(null);
setCurrentScreen('login'); setCurrentScreen('login');
}; };
const navigateToRegister = () => { const navigateToRegister = () => {
setCurrentScreen('register'); setCurrentScreen('register');
}; };
const navigateToHome = async () => { const navigateToHome = async () => {
const user = await authAPI.getCurrentUser(); const user = await authAPI.getCurrentUser();
setCurrentUser(user); setCurrentUser(user);
setQrCode(null); setQrCode(null);
setCurrentScreen('home'); setCurrentScreen('home');
}; };
const openScanner = () => { const openScanner = () => {
setCurrentScreen('scanner'); setCurrentScreen('scanner');
}; };
/* -------------------- ACTIONS -------------------- */ const openMap = () => {
const handleLogout = async () => { setCurrentScreen('map');
try { };
await authAPI.logout();
setQrCode(null);
navigateToLogin();
Alert.alert('Success', 'Logged out successfully');
} catch {
Alert.alert('Error', 'Failed to logout');
}
};
const showAppStatus = async () => { /* -------------------- ACTIONS -------------------- */
try { const handleLogout = async () => {
const status = await authAPI.getAppStatus(); try {
Alert.alert( await authAPI.logout();
'App Status', setQrCode(null);
`Network: ${status.network.isOnline ? 'Online' : 'Offline'} navigateToLogin();
Alert.alert('Success', 'Logged out successfully');
} catch {
Alert.alert('Error', 'Failed to logout');
}
};
const showAppStatus = async () => {
try {
const status = await authAPI.getAppStatus();
Alert.alert(
'App Status',
`Network: ${status.network.isOnline ? 'Online' : 'Offline'}
Users: ${status.storage.totalUsers} Users: ${status.storage.totalUsers}
Current User: ${status.authentication.currentUser || 'None'} Current User: ${status.authentication.currentUser || 'None'}
Logged In: ${status.authentication.isLoggedIn ? 'Yes' : 'No'}` Logged In: ${status.authentication.isLoggedIn ? 'Yes' : 'No'}`
); );
} catch { } catch {
Alert.alert('Error', 'Failed to get app status'); Alert.alert('Error', 'Failed to get app status');
} }
}; };
const generateQRCode = async () => { const generateQRCode = async () => {
if (!currentUser?.email) { if (!currentUser?.email) {
Alert.alert('Error', 'No user email available'); Alert.alert('Error', 'No user email available');
return; return;
} }
setIsGeneratingQR(true); setIsGeneratingQR(true);
try { try {
const qrData = `User: ${currentUser.fullName}\nEmail: ${currentUser.email}`; const qrData = `User: ${currentUser.fullName}\nEmail: ${currentUser.email}`;
const base64Image = await MyNativeModule.generateQRCode(qrData, 300, 300); const base64Image = await MyNativeModule.generateQRCode(qrData, 300, 300);
setQrCode(base64Image); setQrCode(base64Image);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
Alert.alert('Error', 'Failed to generate QR code'); Alert.alert('Error', 'Failed to generate QR code');
} finally { } finally {
setIsGeneratingQR(false); setIsGeneratingQR(false);
} }
}; };
const handleScanResult = (result: any) => { const handleScanResult = (result: any) => {
setScannedCodes(prev => [ setScannedCodes(prev => [
{ {
code: result.code, code: result.code,
format: result.format, format: result.format,
timestamp: new Date().toLocaleTimeString(), timestamp: new Date().toLocaleTimeString(),
}, },
...prev, ...prev,
]); ]);
Alert.alert( Alert.alert(
'Scanned Successfully', 'Scanned Successfully',
`Code: ${result.code}\nFormat: ${result.format}`, `Code: ${result.code}\nFormat: ${result.format}`,
[{ text: 'OK', onPress: () => setCurrentScreen('home') }] [{ text: 'OK', onPress: () => setCurrentScreen('home') }]
); );
}; };
/* -------------------- RENDER -------------------- */ /* -------------------- RENDER -------------------- */
const renderScreen = () => { const renderScreen = () => {
switch (currentScreen) { switch (currentScreen) {
case 'login': case 'login':
return ( return (
<Login <Login
onNavigateToRegister= { navigateToRegister } onNavigateToRegister={navigateToRegister}
onLoginSuccess = { navigateToHome } onLoginSuccess={navigateToHome}
/> />
); );
case 'register': case 'register':
return ( return (
<Register <Register
onNavigateToLogin= { navigateToLogin } onNavigateToLogin={navigateToLogin}
onRegisterSuccess = { navigateToHome } onRegisterSuccess={navigateToHome}
/> />
); );
case 'scanner': case 'scanner':
return ( return (
<ScannerScreen <ScannerScreen
presignedUrl= "https://your-backend-presigned-url.com" presignedUrl="https://your-backend-presigned-url.com"
onScan = { handleScanResult } onScan={handleScanResult}
/> />
); );
case 'home': case 'home':
return ( return (
<ScrollView style= { styles.homeContainer } contentContainerStyle = { styles.scrollContent } > <ScrollView style={styles.homeContainer} contentContainerStyle={styles.scrollContent} >
<Text style={ styles.welcomeText }> <Text style={styles.welcomeText}>
Welcome, { currentUser?.fullName || 'User' Welcome, {currentUser?.fullName || 'User'
} }
</Text> </Text>
{ {
qrCode && ( qrCode && (
<Image source={ { uri: qrCode } } style = { styles.qrImage } /> <Image source={{ uri: qrCode }} style={styles.qrImage} />
) )
} }
<TouchableOpacity style={ styles.scannerButton } onPress = { openScanner } > <TouchableOpacity style={styles.scannerButton} onPress={openScanner} >
<Text style={ styles.buttonText }> Scan QR / Barcode </Text> <Text style={styles.buttonText}> Scan QR / Barcode </Text>
</TouchableOpacity> </TouchableOpacity>
< TouchableOpacity < TouchableOpacity
style = { styles.qrButton } style={styles.qrButton}
onPress = { generateQRCode } onPress={generateQRCode}
disabled = { isGeneratingQR } disabled={isGeneratingQR}
> >
<Text style={ styles.buttonText }> <Text style={styles.buttonText}>
{ isGeneratingQR? 'Generating...': 'Generate QR Code' } {isGeneratingQR ? 'Generating...' : 'Generate QR Code'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
< TouchableOpacity style = { styles.statusButton } onPress = { showAppStatus } > < TouchableOpacity style={styles.statusButton} onPress={showAppStatus} >
<Text style={ styles.buttonText }> App Status </Text> <Text style={styles.buttonText}> App Status </Text>
</TouchableOpacity> </TouchableOpacity>
< TouchableOpacity style = { styles.logoutButton } onPress = { handleLogout } > < TouchableOpacity style={styles.mapButton} onPress={openMap} >
<Text style={ styles.buttonText }> Logout </Text> <Text style={styles.buttonText}> Open Map </Text>
</TouchableOpacity> </TouchableOpacity>
</ScrollView>
);
default: < TouchableOpacity style={styles.logoutButton} onPress={handleLogout} >
return null; <Text style={styles.buttonText}> Logout </Text>
} </TouchableOpacity>
}; </ScrollView>
);
/* -------------------- LOADING -------------------- */ case 'map':
if (!isInitialized) { return (
return ( <View style={{ flex: 1 }}>
<SafeAreaProvider> <Map onClose={() => setCurrentScreen('home')} />
<View style= { styles.loadingContainer } > </View>
<Text style={ styles.loadingText }> Initializing...</Text> );
</View>
</SafeAreaProvider>
);
}
return ( default:
<SafeAreaProvider> return null;
<StatusBar barStyle= { isDarkMode? 'light-content': 'dark-content' } /> }
{ renderScreen() } };
</SafeAreaProvider>
); /* -------------------- LOADING -------------------- */
if (!isInitialized) {
return (
<SafeAreaProvider>
<View style={styles.loadingContainer} >
<Text style={styles.loadingText}> Initializing...</Text>
</View>
</SafeAreaProvider>
);
}
return (
<SafeAreaProvider>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
{renderScreen()}
</SafeAreaProvider>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
loadingText: { fontSize: 18, fontWeight: '600' }, loadingText: { fontSize: 18, fontWeight: '600' },
homeContainer: { flex: 1 }, homeContainer: { flex: 1 },
scrollContent: { alignItems: 'center', padding: 20 }, scrollContent: { alignItems: 'center', padding: 20 },
welcomeText: { fontSize: 24, fontWeight: '700', marginBottom: 20 }, welcomeText: { fontSize: 24, fontWeight: '700', marginBottom: 20 },
qrImage: { width: 300, height: 300, marginBottom: 20 }, qrImage: { width: 300, height: 300, marginBottom: 20 },
scannerButton: { backgroundColor: '#9333ea', padding: 14, borderRadius: 20, marginBottom: 10 }, scannerButton: { backgroundColor: '#9333ea', padding: 14, borderRadius: 20, marginBottom: 10 },
qrButton: { backgroundColor: '#10b981', padding: 14, borderRadius: 20, marginBottom: 10 }, qrButton: { backgroundColor: '#10b981', padding: 14, borderRadius: 20, marginBottom: 10 },
statusButton: { backgroundColor: '#3bb6d8', padding: 14, borderRadius: 20, marginBottom: 10 }, statusButton: { backgroundColor: '#3bb6d8', padding: 14, borderRadius: 20, marginBottom: 10 },
logoutButton: { backgroundColor: '#ff6b6b', padding: 14, borderRadius: 20 }, mapButton: { backgroundColor: '#2563eb', padding: 14, borderRadius: 20, marginBottom: 10 },
buttonText: { color: '#fff', fontWeight: '600' }, logoutButton: { backgroundColor: '#ff6b6b', padding: 14, borderRadius: 20 },
buttonText: { color: '#fff', fontWeight: '600' },
}); });
export default App; export default App;

View File

@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"

View File

@@ -34,7 +34,7 @@
<true/> <true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string>Used to show your current location on the map.</string>
<key>RCTNewArchEnabled</key> <key>RCTNewArchEnabled</key>
<true/> <true/>
<key>UIAppFonts</key> <key>UIAppFonts</key>

View File

@@ -1,72 +1,147 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { View, Text, Platform, Alert, Linking } from 'react-native'; import { View, Text, Platform, Alert, Linking, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions'; import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
import MapView, { Marker, PROVIDER_GOOGLE, Region } from 'react-native-maps';
import Geolocation from '@react-native-community/geolocation';
export default function Map() { type Props = { onClose?: () => void };
useEffect(() => { export default function Map({ onClose }: Props) {
const init = async () => { const [region, setRegion] = useState<Region | null>(null);
const hasPermission = await requestLocationPermission(); const [loading, setLoading] = useState(true);
if (hasPermission) { const [permissionStatus, setPermissionStatus] = useState<string>('loading');
console.log('Permission granted');
} else {
console.log('Permission denied');
}
};
init();
}, []);
async function requestLocationPermission(): Promise<boolean> { useEffect(() => {
try { const init = async () => {
const permission = Platform.select({ setLoading(true);
const status = await requestLocationPermission();
android: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION, if (status === 'granted') {
ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE, getCurrentLocation();
default: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION, } else {
}); setLoading(false);
}
const status = await check(permission); };
if (status === RESULTS.GRANTED) { init();
console.log('Location permission already granted'); }, []);
return true;
} const getCurrentLocation = () => {
setLoading(true);
if (status === RESULTS.UNAVAILABLE) { Geolocation.getCurrentPosition(
console.log('Location permission not available on this device'); (pos) => {
return false; const { latitude, longitude } = pos.coords;
} setRegion({ latitude, longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 });
setLoading(false);
const result = await request(permission); },
if (result === RESULTS.GRANTED) { (err) => {
console.log('Location permission granted'); console.error(err);
return true; Alert.alert('Error', 'Failed to get current location');
} else if (result === RESULTS.DENIED) { setLoading(false);
console.log('Location permission denied'); },
return false; { enableHighAccuracy: true, timeout: 15000, maximumAge: 10000 }
} else if (result === RESULTS.BLOCKED) { );
console.log('Location permission permanently denied'); };
Alert.alert(
'Location Permission Blocked', async function requestLocationPermission(): Promise<'granted' | 'denied' | 'blocked' | 'unavailable'> {
'Location permission is required to use this feature.', try {
[ const permission = Platform.select({
{ text: 'Cancel', style: 'cancel' }, android: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
{ text: 'Open Settings', onPress: () => Linking.openSettings() }, ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
], default: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
{ cancelable: true }, });
);
return false; const status = await check(permission);
} if (status === RESULTS.GRANTED) {
setPermissionStatus('granted');
return false; return 'granted';
} catch (error) {
console.error('Location permission error:', error);
return false;
}
} }
return ( if (status === RESULTS.UNAVAILABLE) {
<View> setPermissionStatus('unavailable');
<Text>Map</Text> return 'unavailable';
}
const result = await request(permission);
if (result === RESULTS.GRANTED) {
setPermissionStatus('granted');
return 'granted';
} else if (result === RESULTS.DENIED) {
setPermissionStatus('denied');
return 'denied';
} else if (result === RESULTS.BLOCKED) {
setPermissionStatus('blocked');
Alert.alert(
'Location Permission Blocked',
'Location permission is required to use this feature.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
],
{ cancelable: true },
);
return 'blocked';
}
setPermissionStatus('unavailable');
return 'unavailable';
} catch (error) {
console.error('Location permission error:', error);
setPermissionStatus('unavailable');
return 'unavailable';
}
}
const openSettings = () => Linking.openSettings();
const retryPermission = async () => {
setLoading(true);
const res = await requestLocationPermission();
if (res === 'granted') getCurrentLocation();
else setLoading(false);
};
const retryGetLocation = () => {
if (permissionStatus === 'granted') getCurrentLocation();
else retryPermission();
};
return (
<View style={styles.container}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<Text style={styles.closeText}>Back</Text>
</TouchableOpacity>
{loading ? (
<ActivityIndicator size="large" />
) : permissionStatus === 'blocked' ? (
<View style={styles.center}>
<Text style={{ marginBottom: 12 }}>Location permission is blocked. Open settings to enable it.</Text>
<TouchableOpacity style={styles.actionButton} onPress={openSettings}><Text style={styles.actionText}>Open Settings</Text></TouchableOpacity>
<TouchableOpacity style={styles.actionButton} onPress={retryPermission}><Text style={styles.actionText}>Retry</Text></TouchableOpacity>
</View> </View>
); ) : permissionStatus === 'denied' || permissionStatus === 'unavailable' ? (
} <View style={styles.center}>
<Text style={{ marginBottom: 12 }}>Location permission is not available. Please allow location access.</Text>
<TouchableOpacity style={styles.actionButton} onPress={retryPermission}><Text style={styles.actionText}>Request Permission</Text></TouchableOpacity>
</View>
) : region ? (
<MapView style={styles.map} provider={PROVIDER_GOOGLE} region={region} showsUserLocation={true} showsMyLocationButton={true}>
<Marker coordinate={{ latitude: region.latitude, longitude: region.longitude }} title="You are here" />
</MapView>
) : (
<View style={styles.center}>
<Text style={{ marginBottom: 12 }}>No location available</Text>
<TouchableOpacity style={styles.actionButton} onPress={retryGetLocation}><Text style={styles.actionText}>Retry Location</Text></TouchableOpacity>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
closeButton: { position: 'absolute', top: 16, left: 16, zIndex: 10, padding: 8, backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: 8 },
closeText: { color: '#fff' },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
actionButton: { backgroundColor: '#2563eb', padding: 12, borderRadius: 8, marginTop: 8 },
actionText: { color: '#fff', fontWeight: '600' },
});