From 9998688103b039d534a8e828ee527a7fa72ed2b6 Mon Sep 17 00:00:00 2001 From: Mansi Date: Thu, 25 Dec 2025 23:17:11 +0530 Subject: [PATCH] Add ErrorBoundary for Map component and enhance location permission handling --- src/screens/Map.tsx | 197 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 155 insertions(+), 42 deletions(-) diff --git a/src/screens/Map.tsx b/src/screens/Map.tsx index e94bbaf..6b29d3e 100644 --- a/src/screens/Map.tsx +++ b/src/screens/Map.tsx @@ -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 { + 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 ( + + Map failed to load: {String(this.state.error)} + this.setState({ error: null })}> + Reset + + + ); + } + return this.props.children; + } +} + export default function Map({ onClose }: Props) { const [region, setRegion] = useState(null); const [loading, setLoading] = useState(true); const [permissionStatus, setPermissionStatus] = useState('loading'); + const [nativeMapAvailable, setNativeMapAvailable] = useState(null); + const [mapAttempt, setMapAttempt] = useState(false); + const [mapError, setMapError] = useState(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 ( + + ) return ( @@ -123,9 +202,41 @@ export default function Map({ onClose }: Props) { Request Permission ) : region ? ( - - - + // Provide a safe UX that does NOT auto-mount the native map until the user taps Try Map. + nativeMapAvailable === false && !mapAttempt ? ( + + Native map module not available — map disabled. + setMapAttempt(true)}> + Try Map + + { + Alert.alert('Debug', 'UIManager keys: ' + JSON.stringify(Object.keys(UIManager), null, 2)); + }}> + Show UIManager keys + + + ) : mapAttempt ? ( + { setMapError(e); console.error('Map render error:', e); }}> + + + + + Linking.openURL('https://www.openstreetmap.org/copyright')}> + © OpenStreetMap + + + ) : ( + + Map is ready. Tap "Try Map" to open it. + setMapAttempt(true)}>Try Map + {mapError ? Map error: {String(mapError)} : null} + + ) ) : ( No location available @@ -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' }, }); \ No newline at end of file