Add ErrorBoundary for Map component and enhance location permission handling
This commit is contained in:
@@ -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' },
|
||||
});
|
||||
Reference in New Issue
Block a user