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 React, { useEffect, useState } from 'react';
|
||||||
import { View, Text, Platform, Alert, Linking, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
import { View, Text, Platform, Alert, Linking, StyleSheet, TouchableOpacity, ActivityIndicator, UIManager, NativeModules } from 'react-native';
|
||||||
import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions';
|
import { check, PERMISSIONS, request, requestMultiple, RESULTS } from 'react-native-permissions';
|
||||||
import MapView, { Marker, PROVIDER_GOOGLE, Region } from 'react-native-maps';
|
import MapView, { Marker, UrlTile, Region } from 'react-native-maps';
|
||||||
import Geolocation from '@react-native-community/geolocation';
|
import Geolocation from '@react-native-community/geolocation';
|
||||||
|
|
||||||
type Props = { onClose?: () => void };
|
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) {
|
export default function Map({ onClose }: Props) {
|
||||||
const [region, setRegion] = useState<Region | null>(null);
|
const [region, setRegion] = useState<Region | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [permissionStatus, setPermissionStatus] = useState<string>('loading');
|
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(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
@@ -24,8 +54,22 @@ export default function Map({ onClose }: Props) {
|
|||||||
init();
|
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 = () => {
|
const getCurrentLocation = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
if (Platform.OS === 'ios' && Geolocation.requestAuthorization) {
|
||||||
|
Geolocation.requestAuthorization();
|
||||||
|
}
|
||||||
Geolocation.getCurrentPosition(
|
Geolocation.getCurrentPosition(
|
||||||
(pos) => {
|
(pos) => {
|
||||||
const { latitude, longitude } = pos.coords;
|
const { latitude, longitude } = pos.coords;
|
||||||
@@ -33,7 +77,7 @@ export default function Map({ onClose }: Props) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error(err);
|
console.error('Geolocation error:', err);
|
||||||
Alert.alert('Error', 'Failed to get current location');
|
Alert.alert('Error', 'Failed to get current location');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
},
|
},
|
||||||
@@ -43,46 +87,70 @@ export default function Map({ onClose }: Props) {
|
|||||||
|
|
||||||
async function requestLocationPermission(): Promise<'granted' | 'denied' | 'blocked' | 'unavailable'> {
|
async function requestLocationPermission(): Promise<'granted' | 'denied' | 'blocked' | 'unavailable'> {
|
||||||
try {
|
try {
|
||||||
const permission = Platform.select({
|
if (Platform.OS === 'android') {
|
||||||
android: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
|
const statuses = await requestMultiple([
|
||||||
ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
|
PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION,
|
||||||
default: 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.UNAVAILABLE) {
|
||||||
if (status === RESULTS.GRANTED) {
|
setPermissionStatus('unavailable');
|
||||||
setPermissionStatus('granted');
|
return 'unavailable';
|
||||||
return 'granted';
|
}
|
||||||
}
|
|
||||||
|
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');
|
setPermissionStatus('unavailable');
|
||||||
return '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) {
|
} catch (error) {
|
||||||
console.error('Location permission error:', error);
|
console.error('Location permission error:', error);
|
||||||
setPermissionStatus('unavailable');
|
setPermissionStatus('unavailable');
|
||||||
@@ -102,7 +170,18 @@ export default function Map({ onClose }: Props) {
|
|||||||
if (permissionStatus === 'granted') getCurrentLocation();
|
if (permissionStatus === 'granted') getCurrentLocation();
|
||||||
else retryPermission();
|
else retryPermission();
|
||||||
};
|
};
|
||||||
|
return (
|
||||||
|
<MapView
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
initialRegion={{
|
||||||
|
latitude: 22.3039,
|
||||||
|
longitude: 70.8022,
|
||||||
|
latitudeDelta: 0.05,
|
||||||
|
longitudeDelta: 0.05,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
|
<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>
|
<TouchableOpacity style={styles.actionButton} onPress={retryPermission}><Text style={styles.actionText}>Request Permission</Text></TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : region ? (
|
) : region ? (
|
||||||
<MapView style={styles.map} provider={PROVIDER_GOOGLE} region={region} showsUserLocation={true} showsMyLocationButton={true}>
|
// Provide a safe UX that does NOT auto-mount the native map until the user taps Try Map.
|
||||||
<Marker coordinate={{ latitude: region.latitude, longitude: region.longitude }} title="You are here" />
|
nativeMapAvailable === false && !mapAttempt ? (
|
||||||
</MapView>
|
<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}>
|
<View style={styles.center}>
|
||||||
<Text style={{ marginBottom: 12 }}>No location available</Text>
|
<Text style={{ marginBottom: 12 }}>No location available</Text>
|
||||||
@@ -144,4 +255,6 @@ const styles = StyleSheet.create({
|
|||||||
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||||
actionButton: { backgroundColor: '#2563eb', padding: 12, borderRadius: 8, marginTop: 8 },
|
actionButton: { backgroundColor: '#2563eb', padding: 12, borderRadius: 8, marginTop: 8 },
|
||||||
actionText: { color: '#fff', fontWeight: '600' },
|
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