11import { useState , useEffect , useMemo , useRef } from 'react' ;
22import {
3+ ActivityIndicator ,
34 Alert ,
45 AlertButton ,
6+ Linking ,
57 Modal ,
8+ PermissionsAndroid ,
69 Text ,
710 View ,
811 Pressable ,
@@ -11,7 +14,10 @@ import {
1114 Image ,
1215 Platform ,
1316} from 'react-native' ;
14- import Geolocation , { GeolocationResponse } from '@react-native-community/geolocation' ;
17+ import Geolocation , {
18+ GeolocationError ,
19+ GeolocationResponse ,
20+ } from '@react-native-community/geolocation' ;
1521import {
1622 useChatContext ,
1723 useMessageComposer ,
@@ -26,12 +32,18 @@ type LiveLocationCreateModalProps = {
2632} ;
2733
2834const endedAtDurations = [ 60000 , 600000 , 3600000 ] ; // 1 min, 10 mins, 1 hour
35+ const LOCATION_PERMISSION_ERROR = 'Location permission is required.' ;
36+ const LOCATION_SETTINGS_ERROR = 'Location permission is blocked. Enable it in Settings.' ;
2937
3038export const LiveLocationCreateModal = ( {
3139 visible,
3240 onRequestClose,
3341} : LiveLocationCreateModalProps ) => {
3442 const [ location , setLocation ] = useState < GeolocationResponse > ( ) ;
43+ const [ locationError , setLocationError ] = useState < string | null > ( null ) ;
44+ const [ locationPermissionIssue , setLocationPermissionIssue ] = useState ( false ) ;
45+ const [ permissionBlocked , setPermissionBlocked ] = useState ( false ) ;
46+ const [ locationSetupAttempt , setLocationSetupAttempt ] = useState ( 0 ) ;
3547 const messageComposer = useMessageComposer ( ) ;
3648 const { width, height } = useWindowDimensions ( ) ;
3749 const { client } = useChatContext ( ) ;
@@ -43,6 +55,7 @@ export const LiveLocationCreateModal = ({
4355 const { t } = useTranslationContext ( ) ;
4456 const mapRef = useRef < MapView | null > ( null ) ;
4557 const markerRef = useRef < MapMarker | null > ( null ) ;
58+ const watchIdRef = useRef < number | null > ( null ) ;
4659
4760 const aspect_ratio = width / height ;
4861
@@ -59,10 +72,75 @@ export const LiveLocationCreateModal = ({
5972 }
6073 } , [ aspect_ratio , location ] ) ;
6174
75+ const ensureAndroidLocationPermission = async ( ) : Promise < {
76+ blocked : boolean ;
77+ granted : boolean ;
78+ } > => {
79+ if ( Platform . OS !== 'android' ) {
80+ return { blocked : false , granted : true } ;
81+ }
82+
83+ const finePermission = PermissionsAndroid . PERMISSIONS . ACCESS_FINE_LOCATION ;
84+ const coarsePermission = PermissionsAndroid . PERMISSIONS . ACCESS_COARSE_LOCATION ;
85+ const [ hasFinePermission , hasCoarsePermission ] = await Promise . all ( [
86+ PermissionsAndroid . check ( finePermission ) ,
87+ PermissionsAndroid . check ( coarsePermission ) ,
88+ ] ) ;
89+
90+ if ( hasFinePermission || hasCoarsePermission ) {
91+ setPermissionBlocked ( false ) ;
92+ return { blocked : false , granted : true } ;
93+ }
94+
95+ const result = await PermissionsAndroid . requestMultiple ( [ finePermission , coarsePermission ] ) ;
96+ const fineResult = result [ finePermission ] ;
97+ const coarseResult = result [ coarsePermission ] ;
98+ const granted =
99+ fineResult === PermissionsAndroid . RESULTS . GRANTED ||
100+ coarseResult === PermissionsAndroid . RESULTS . GRANTED ;
101+
102+ if ( granted ) {
103+ setPermissionBlocked ( false ) ;
104+ return { blocked : false , granted : true } ;
105+ }
106+
107+ const blocked =
108+ fineResult === PermissionsAndroid . RESULTS . NEVER_ASK_AGAIN ||
109+ coarseResult === PermissionsAndroid . RESULTS . NEVER_ASK_AGAIN ;
110+ setPermissionBlocked ( blocked ) ;
111+ return { blocked, granted : false } ;
112+ } ;
113+
114+ const isPermissionError = ( error : GeolocationError ) =>
115+ error . code === error . PERMISSION_DENIED || error . code === 1 ;
116+
117+ const retryLocationSetup = ( ) => {
118+ setLocation ( undefined ) ;
119+ setPermissionBlocked ( false ) ;
120+ setLocationPermissionIssue ( false ) ;
121+ setLocationError ( null ) ;
122+ setLocationSetupAttempt ( ( current ) => current + 1 ) ;
123+ } ;
124+
125+ const openSettings = async ( ) => {
126+ try {
127+ await Linking . openSettings ( ) ;
128+ } catch ( error ) {
129+ console . error ( 'openSettings' , error ) ;
130+ }
131+ } ;
132+
62133 useEffect ( ( ) => {
63- let watchId : number | null = null ;
64- const watchLocationHandler = async ( ) => {
65- watchId = await Geolocation . watchPosition (
134+ if ( ! visible ) {
135+ return ;
136+ }
137+
138+ let isMounted = true ;
139+ const startWatchingLocation = ( ) => {
140+ setLocationError ( null ) ;
141+ setLocationPermissionIssue ( false ) ;
142+
143+ watchIdRef . current = Geolocation . watchPosition (
66144 ( position ) => {
67145 setLocation ( position ) ;
68146 const newPosition = {
@@ -74,12 +152,16 @@ export const LiveLocationCreateModal = ({
74152 if ( mapRef . current ?. animateToRegion ) {
75153 mapRef . current . animateToRegion ( newPosition , 500 ) ;
76154 }
77- // This is android only
78155 if ( Platform . OS === 'android' && markerRef . current ?. animateMarkerToCoordinate ) {
79156 markerRef . current . animateMarkerToCoordinate ( newPosition , 500 ) ;
80157 }
81158 } ,
82159 ( error ) => {
160+ if ( ! isMounted ) {
161+ return ;
162+ }
163+ setLocationPermissionIssue ( isPermissionError ( error ) ) ;
164+ setLocationError ( error . message || 'Unable to fetch your current location.' ) ;
83165 console . error ( 'watchPosition' , error ) ;
84166 } ,
85167 {
@@ -90,13 +172,41 @@ export const LiveLocationCreateModal = ({
90172 } ,
91173 ) ;
92174 } ;
175+
176+ const watchLocationHandler = async ( ) => {
177+ const { blocked, granted } = await ensureAndroidLocationPermission ( ) ;
178+ if ( ! granted ) {
179+ if ( isMounted ) {
180+ setLocationPermissionIssue ( true ) ;
181+ setLocationError ( blocked ? LOCATION_SETTINGS_ERROR : LOCATION_PERMISSION_ERROR ) ;
182+ }
183+ return ;
184+ }
185+
186+ if ( Platform . OS === 'android' ) {
187+ startWatchingLocation ( ) ;
188+ return ;
189+ }
190+
191+ Geolocation . requestAuthorization ( undefined , ( error ) => {
192+ if ( ! isMounted ) {
193+ return ;
194+ }
195+ setLocationPermissionIssue ( isPermissionError ( error ) ) ;
196+ setLocationError ( error . message || 'Location permission is required.' ) ;
197+ console . error ( 'requestAuthorization' , error ) ;
198+ } ) ;
199+ startWatchingLocation ( ) ;
200+ } ;
93201 watchLocationHandler ( ) ;
94202 return ( ) => {
95- if ( watchId ) {
96- Geolocation . clearWatch ( watchId ) ;
203+ isMounted = false ;
204+ if ( watchIdRef . current !== null ) {
205+ Geolocation . clearWatch ( watchIdRef . current ) ;
206+ watchIdRef . current = null ;
97207 }
98208 } ;
99- } , [ aspect_ratio ] ) ;
209+ } , [ aspect_ratio , locationSetupAttempt , visible ] ) ;
100210
101211 const buttons = [
102212 {
@@ -132,24 +242,34 @@ export const LiveLocationCreateModal = ({
132242 text : 'Share Current Location' ,
133243 description : 'Share your current location once' ,
134244 onPress : async ( ) => {
135- Geolocation . getCurrentPosition ( async ( position ) => {
136- if ( position . coords ) {
137- await messageComposer . locationComposer . setData ( {
138- latitude : position . coords . latitude ,
139- longitude : position . coords . longitude ,
140- } ) ;
141- await messageComposer . sendLocation ( ) ;
142- onRequestClose ( ) ;
143- }
144- } ) ;
245+ const { blocked, granted } = await ensureAndroidLocationPermission ( ) ;
246+ if ( ! granted ) {
247+ setLocationPermissionIssue ( true ) ;
248+ setLocationError ( blocked ? LOCATION_SETTINGS_ERROR : LOCATION_PERMISSION_ERROR ) ;
249+ return ;
250+ }
251+
252+ setLocationPermissionIssue ( false ) ;
253+ Geolocation . getCurrentPosition (
254+ async ( position ) => {
255+ if ( position . coords ) {
256+ await messageComposer . locationComposer . setData ( {
257+ latitude : position . coords . latitude ,
258+ longitude : position . coords . longitude ,
259+ } ) ;
260+ await messageComposer . sendLocation ( ) ;
261+ onRequestClose ( ) ;
262+ }
263+ } ,
264+ ( error ) => {
265+ setLocationPermissionIssue ( isPermissionError ( error ) ) ;
266+ setLocationError ( error . message || 'Unable to fetch your current location.' ) ;
267+ } ,
268+ ) ;
145269 } ,
146270 } ,
147271 ] ;
148272
149- if ( ! location && client ) {
150- return null ;
151- }
152-
153273 return (
154274 < Modal
155275 animationType = 'slide'
@@ -165,13 +285,13 @@ export const LiveLocationCreateModal = ({
165285 < View style = { styles . rightContent } />
166286 </ View >
167287
168- < MapView
169- cameraZoomRange = { { maxCenterCoordinateDistance : 2000 } }
170- initialRegion = { region }
171- ref = { mapRef }
172- style = { styles . mapView }
173- >
174- { location && (
288+ { location && region ? (
289+ < MapView
290+ cameraZoomRange = { { maxCenterCoordinateDistance : 2000 } }
291+ initialRegion = { region }
292+ ref = { mapRef }
293+ style = { styles . mapView }
294+ >
175295 < Marker
176296 coordinate = { {
177297 latitude : location . coords . latitude ,
@@ -183,13 +303,45 @@ export const LiveLocationCreateModal = ({
183303 >
184304 < View style = { styles . markerWrapper } >
185305 < Image
186- source = { { uri : client . user ?. image || '' } }
306+ source = { { uri : client ? .user ?. image || '' } }
187307 style = { [ styles . markerImage , { borderColor : accent_blue } ] }
188308 />
189309 </ View >
190310 </ Marker >
191- ) }
192- </ MapView >
311+ </ MapView >
312+ ) : (
313+ < View style = { styles . loadingContainer } >
314+ < ActivityIndicator color = { accent_blue } />
315+ < Text style = { [ styles . loadingText , { color : grey } ] } >
316+ { locationError || t ( 'Fetching your current location...' ) }
317+ </ Text >
318+ { permissionBlocked ? (
319+ < Pressable
320+ onPress = { openSettings }
321+ style = { ( { pressed } ) => [
322+ styles . settingsButton ,
323+ { borderColor : pressed ? accent_blue : grey_whisper } ,
324+ ] }
325+ >
326+ < Text style = { [ styles . settingsButtonText , { color : accent_blue } ] } >
327+ { t ( 'Open Settings' ) }
328+ </ Text >
329+ </ Pressable >
330+ ) : locationPermissionIssue && Platform . OS === 'android' ? (
331+ < Pressable
332+ onPress = { retryLocationSetup }
333+ style = { ( { pressed } ) => [
334+ styles . settingsButton ,
335+ { borderColor : pressed ? accent_blue : grey_whisper } ,
336+ ] }
337+ >
338+ < Text style = { [ styles . settingsButtonText , { color : accent_blue } ] } >
339+ { t ( 'Allow Location' ) }
340+ </ Text >
341+ </ Pressable >
342+ ) : null }
343+ </ View >
344+ ) }
193345 < View style = { styles . buttons } >
194346 { buttons . map ( ( button , index ) => (
195347 < Pressable
@@ -218,6 +370,26 @@ const styles = StyleSheet.create({
218370 width : 'auto' ,
219371 flex : 3 ,
220372 } ,
373+ loadingContainer : {
374+ flex : 3 ,
375+ alignItems : 'center' ,
376+ justifyContent : 'center' ,
377+ gap : 12 ,
378+ } ,
379+ loadingText : {
380+ fontSize : 14 ,
381+ } ,
382+ settingsButton : {
383+ borderWidth : 1 ,
384+ borderRadius : 8 ,
385+ marginTop : 8 ,
386+ paddingHorizontal : 12 ,
387+ paddingVertical : 10 ,
388+ } ,
389+ settingsButtonText : {
390+ fontSize : 14 ,
391+ fontWeight : '600' ,
392+ } ,
221393 textStyle : {
222394 fontSize : 12 ,
223395 color : 'gray' ,
0 commit comments