Add ErrorBoundary for Map component and enhance location permission handling

This commit is contained in:
2025-12-25 23:17:11 +05:30
parent 741ab60fce
commit 9998688103

View File

@@ -1,15 +1,45 @@
import React, { useEffect, useState } from 'react';
import { View, Text, Platform, Alert, Linking, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
import MapView, { Marker, PROVIDER_GOOGLE, Region } from 'react-native-maps';
import { View, Text, Platform, Alert, Linking, StyleSheet, TouchableOpacity, ActivityIndicator, UIManager, NativeModules } from 'react-native';
import { check, PERMISSIONS, request, requestMultiple, RESULTS } from 'react-native-permissions';
import MapView, { Marker, UrlTile, Region } from 'react-native-maps';
import Geolocation from '@react-native-community/geolocation';
type Props = { onClose?: () => void };
class ErrorBoundary extends React.Component<any, { error: any }> {
constructor(props: any) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error: any) {
return { error };
}
componentDidCatch(error: any, info: any) {
if (this.props.onError) this.props.onError(error);
console.error('ErrorBoundary caught:', error, info);
}
render() {
if (this.state.error) {
return (
<View style={styles.center}>
<Text style={{ marginBottom: 12 }}>Map failed to load: {String(this.state.error)}</Text>
<TouchableOpacity style={styles.actionButton} onPress={() => this.setState({ error: null })}>
<Text style={styles.actionText}>Reset</Text>
</TouchableOpacity>
</View>
);
}
return this.props.children;
}
}
export default function Map({ onClose }: Props) {
const [region, setRegion] = useState<Region | null>(null);
const [loading, setLoading] = useState(true);
const [permissionStatus, setPermissionStatus] = useState<string>('loading');
const [nativeMapAvailable, setNativeMapAvailable] = useState<boolean | null>(null);
const [mapAttempt, setMapAttempt] = useState(false);
const [mapError, setMapError] = useState<any>(null);
useEffect(() => {
const init = async () => {
@@ -24,8 +54,22 @@ export default function Map({ onClose }: Props) {
init();
}, []);
useEffect(() => {
// Check whether the native map view manager is available to avoid immediate native crash
try {
const hasAirMap = !!(UIManager.getViewManagerConfig && (UIManager.getViewManagerConfig('AIRMap') || UIManager.getViewManagerConfig('AIRGoogleMap') || UIManager.getViewManagerConfig('AIRMapMarker')));
setNativeMapAvailable(Boolean(hasAirMap));
} catch (e) {
console.warn('Native map availability check failed', e);
setNativeMapAvailable(false);
}
}, []);
const getCurrentLocation = () => {
setLoading(true);
if (Platform.OS === 'ios' && Geolocation.requestAuthorization) {
Geolocation.requestAuthorization();
}
Geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude } = pos.coords;
@@ -33,7 +77,7 @@ export default function Map({ onClose }: Props) {
setLoading(false);
},
(err) => {
console.error(err);
console.error('Geolocation error:', err);
Alert.alert('Error', 'Failed to get current location');
setLoading(false);
},
@@ -43,46 +87,70 @@ export default function Map({ onClose }: Props) {
async function requestLocationPermission(): Promise<'granted' | 'denied' | 'blocked' | 'unavailable'> {
try {
const permission = Platform.select({
android: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
default: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
});
if (Platform.OS === 'android') {
const statuses = await requestMultiple([
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
PERMISSIONS.ANDROID.ACCESS_COARSE_LOCATION,
]);
const fineStatus = statuses[PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION];
if (fineStatus === RESULTS.GRANTED) {
setPermissionStatus('granted');
return 'granted';
}
if (fineStatus === 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('denied');
return 'denied';
} else {
const permission = PERMISSIONS.IOS.LOCATION_WHEN_IN_USE;
const status = await check(permission);
if (status === RESULTS.GRANTED) {
setPermissionStatus('granted');
if (Geolocation.requestAuthorization) Geolocation.requestAuthorization();
return 'granted';
}
const status = await check(permission);
if (status === RESULTS.GRANTED) {
setPermissionStatus('granted');
return 'granted';
}
if (status === RESULTS.UNAVAILABLE) {
setPermissionStatus('unavailable');
return 'unavailable';
}
const result = await request(permission);
if (result === RESULTS.GRANTED) {
setPermissionStatus('granted');
if (Geolocation.requestAuthorization) Geolocation.requestAuthorization();
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';
}
if (status === RESULTS.UNAVAILABLE) {
setPermissionStatus('unavailable');
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');
@@ -102,7 +170,18 @@ export default function Map({ onClose }: Props) {
if (permissionStatus === 'granted') getCurrentLocation();
else retryPermission();
};
return (
<MapView
style={{ flex: 1 }}
initialRegion={{
latitude: 22.3039,
longitude: 70.8022,
latitudeDelta: 0.05,
longitudeDelta: 0.05,
}}
/>
)
return (
<View style={styles.container}>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
@@ -123,9 +202,41 @@ export default function Map({ onClose }: Props) {
<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>
// Provide a safe UX that does NOT auto-mount the native map until the user taps Try Map.
nativeMapAvailable === false && !mapAttempt ? (
<View style={styles.center}>
<Text style={{ marginBottom: 12 }}>Native map module not available map disabled.</Text>
<TouchableOpacity style={styles.actionButton} onPress={() => setMapAttempt(true)}>
<Text style={styles.actionText}>Try Map</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionButton} onPress={() => {
Alert.alert('Debug', 'UIManager keys: ' + JSON.stringify(Object.keys(UIManager), null, 2));
}}>
<Text style={styles.actionText}>Show UIManager keys</Text>
</TouchableOpacity>
</View>
) : mapAttempt ? (
<ErrorBoundary onError={(e: any) => { setMapError(e); console.error('Map render error:', e); }}>
<MapView style={styles.map} initialRegion={region as Region} showsUserLocation={true} showsMyLocationButton={true}>
<UrlTile
urlTemplate="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"
maximumZ={19}
flipY={false}
tileSize={256}
/>
<Marker coordinate={{ latitude: region.latitude, longitude: region.longitude }} title="You are here" />
</MapView>
<TouchableOpacity style={styles.osmAttribution} onPress={() => Linking.openURL('https://www.openstreetmap.org/copyright')}>
<Text style={styles.osmAttributionText}>© OpenStreetMap</Text>
</TouchableOpacity>
</ErrorBoundary>
) : (
<View style={styles.center}>
<Text style={{ marginBottom: 12 }}>Map is ready. Tap "Try Map" to open it.</Text>
<TouchableOpacity style={styles.actionButton} onPress={() => setMapAttempt(true)}><Text style={styles.actionText}>Try Map</Text></TouchableOpacity>
{mapError ? <Text style={{ marginTop: 12, color: 'red' }}>Map error: {String(mapError)}</Text> : null}
</View>
)
) : (
<View style={styles.center}>
<Text style={{ marginBottom: 12 }}>No location available</Text>
@@ -144,4 +255,6 @@ const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
actionButton: { backgroundColor: '#2563eb', padding: 12, borderRadius: 8, marginTop: 8 },
actionText: { color: '#fff', fontWeight: '600' },
osmAttribution: { position: 'absolute', bottom: 16, right: 8, backgroundColor: 'rgba(255,255,255,0.9)', padding: 6, borderRadius: 6 },
osmAttributionText: { fontSize: 10, color: '#333' },
});