1import { useFocusEffect } from '@react-navigation/native'; 2import { BlurView } from 'expo-blur'; 3import * as Location from 'expo-location'; 4import * as Notifications from 'expo-notifications'; 5import * as TaskManager from 'expo-task-manager'; 6import React from 'react'; 7import { NativeSyntheticEvent, StyleSheet, Text, View } from 'react-native'; 8import MapView from 'react-native-maps'; 9 10import Button from '../../components/Button'; 11 12const GEOFENCING_TASK = 'geofencing'; 13 14interface GeofencingRegion { 15 identifier: string; 16 latitude: number; 17 longitude: number; 18 radius: number; 19} 20 21export interface MapEvent<T = object> 22 extends NativeSyntheticEvent< 23 T & { 24 coordinate: { 25 latitude: number; 26 longitude: number; 27 }; 28 position: { 29 x: number; 30 y: number; 31 }; 32 } 33 > {} 34 35type Region = { 36 latitude: number; 37 longitude: number; 38 latitudeDelta: number; 39 longitudeDelta: number; 40}; 41 42type State = { 43 isGeofencing: boolean; 44 geofencingRegions: GeofencingRegion[]; 45 initialRegion: Region | null; 46}; 47 48const initialState: State = { isGeofencing: false, geofencingRegions: [], initialRegion: null }; 49 50function reducer( 51 state: State, 52 action: 53 | { type: 'toggle' } 54 | { type: 'clearRegions' } 55 | ({ type: 'update' } & State) 56 | ({ type: 'updateRegions' } & Pick<State, 'geofencingRegions'>) 57): State { 58 switch (action.type) { 59 case 'update': 60 return { 61 isGeofencing: action.isGeofencing, 62 geofencingRegions: action.geofencingRegions, 63 initialRegion: action.initialRegion, 64 }; 65 case 'updateRegions': 66 return { 67 ...state, 68 geofencingRegions: action.geofencingRegions, 69 }; 70 case 'clearRegions': 71 return { ...state, geofencingRegions: [] }; 72 case 'toggle': 73 return { ...state, isGeofencing: !state.isGeofencing }; 74 } 75} 76 77export default function GeofencingScreen() { 78 const mapViewRef = React.useRef<MapView>(null); 79 const [state, dispatch] = React.useReducer(reducer, initialState); 80 81 const onFocus = React.useCallback(() => { 82 let isActive = true; 83 84 (async () => { 85 await Location.requestForegroundPermissionsAsync(); 86 87 const { coords } = await Location.getCurrentPositionAsync(); 88 const isGeofencing = await Location.hasStartedGeofencingAsync(GEOFENCING_TASK); 89 const geofencingRegions = await getSavedRegions(); 90 91 if (isActive) { 92 dispatch({ 93 type: 'update', 94 isGeofencing, 95 geofencingRegions, 96 initialRegion: { 97 latitude: coords.latitude, 98 longitude: coords.longitude, 99 latitudeDelta: 0.004, 100 longitudeDelta: 0.002, 101 }, 102 }); 103 } 104 })(); 105 return () => (isActive = false); 106 }, []); 107 108 useFocusEffect(onFocus); 109 110 const toggleGeofencing = React.useCallback(async () => { 111 if (state.isGeofencing) { 112 await Location.stopGeofencingAsync(GEOFENCING_TASK); 113 dispatch({ type: 'clearRegions' }); 114 } else { 115 await Location.startGeofencingAsync(GEOFENCING_TASK, state.geofencingRegions); 116 } 117 dispatch({ type: 'toggle' }); 118 }, [state.isGeofencing, state.geofencingRegions]); 119 120 const centerMap = React.useCallback(async () => { 121 const { coords } = await Location.getCurrentPositionAsync(); 122 const mapView = mapViewRef.current; 123 124 if (mapView) { 125 mapView.animateToRegion({ 126 latitude: coords.latitude, 127 longitude: coords.longitude, 128 latitudeDelta: 0.004, 129 longitudeDelta: 0.002, 130 }); 131 } 132 }, []); 133 134 const onMapPress = async ({ nativeEvent: { coordinate } }: MapEvent) => { 135 const next = [...state.geofencingRegions]; 136 next.push({ 137 identifier: `${coordinate.latitude},${coordinate.longitude}`, 138 latitude: coordinate.latitude, 139 longitude: coordinate.longitude, 140 radius: 50, 141 }); 142 dispatch({ type: 'updateRegions', geofencingRegions: next }); 143 144 if (await Location.hasStartedGeofencingAsync(GEOFENCING_TASK)) { 145 // update existing geofencing task 146 await Location.startGeofencingAsync(GEOFENCING_TASK, next); 147 } 148 }; 149 150 const renderRegions = React.useCallback(() => { 151 return state.geofencingRegions.map(region => { 152 return ( 153 // @ts-ignore 154 <MapView.Circle 155 key={region.identifier} 156 center={region} 157 radius={region.radius} 158 strokeColor="rgba(78,155,222,0.8)" 159 fillColor="rgba(78,155,222,0.2)" 160 /> 161 ); 162 }); 163 }, [state.geofencingRegions]); 164 165 if (!state.initialRegion) { 166 return null; 167 } 168 169 return ( 170 <View style={styles.screen}> 171 <View style={styles.heading}> 172 <BlurView tint="light" intensity={70} style={styles.blurView}> 173 <Text style={styles.headingText}> 174 {state.isGeofencing 175 ? 'You will be receiving notifications when the device enters or exits from selected regions.' 176 : // tslint:disable-next-line: max-line-length 177 'Click `Start geofencing` to start getting geofencing notifications. Tap on the map to select geofencing regions.'} 178 </Text> 179 </BlurView> 180 </View> 181 182 <MapView 183 ref={mapViewRef} 184 style={styles.mapView} 185 initialRegion={state.initialRegion} 186 onPress={onMapPress} 187 showsUserLocation> 188 {renderRegions()} 189 </MapView> 190 <View style={styles.buttons}> 191 <View style={styles.leftButtons}> 192 <Button 193 disabled={!state.isGeofencing && state.geofencingRegions.length === 0} 194 buttonStyle={styles.button} 195 title={state.isGeofencing ? 'Stop geofencing' : 'Start geofencing'} 196 onPress={toggleGeofencing} 197 /> 198 </View> 199 <Button buttonStyle={styles.button} title="Center" onPress={centerMap} /> 200 </View> 201 </View> 202 ); 203} 204 205GeofencingScreen.navigationOptions = { 206 title: 'Geofencing Map', 207}; 208 209async function getSavedRegions(): Promise<GeofencingRegion[]> { 210 const tasks = await TaskManager.getRegisteredTasksAsync(); 211 const task = tasks.find(({ taskName }) => taskName === GEOFENCING_TASK); 212 return task ? task.options.regions : []; 213} 214 215TaskManager.defineTask(GEOFENCING_TASK, async ({ data: { region } }: { data: any }) => { 216 const stateString = Location.GeofencingRegionState[region.state].toLowerCase(); 217 218 // tslint:disable-next-line no-console 219 console.log(`${stateString} region ${region.identifier}`); 220 221 await Notifications.scheduleNotificationAsync({ 222 content: { 223 title: 'Expo Geofencing', 224 body: `You're ${stateString} a region ${region.identifier}`, 225 data: region, 226 }, 227 trigger: null, 228 }); 229}); 230 231const styles = StyleSheet.create({ 232 screen: { 233 flex: 1, 234 }, 235 heading: { 236 backgroundColor: 'rgba(255, 255, 0, 0.1)', 237 position: 'absolute', 238 top: 0, 239 right: 0, 240 left: 0, 241 zIndex: 2, 242 }, 243 blurView: { 244 flex: 1, 245 padding: 5, 246 }, 247 headingText: { 248 textAlign: 'center', 249 }, 250 mapView: { 251 flex: 1, 252 }, 253 buttons: { 254 flexDirection: 'row', 255 justifyContent: 'space-between', 256 alignItems: 'flex-end', 257 padding: 10, 258 position: 'absolute', 259 right: 0, 260 bottom: 0, 261 left: 0, 262 }, 263 leftButtons: { 264 alignItems: 'flex-start', 265 }, 266 button: { 267 padding: 10, 268 marginVertical: 5, 269 }, 270}); 271