Bluesky app fork with some witchin' additions 馃挮
at main 163 lines 5.0 kB view raw
1import {useCallback, useEffect, useRef} from 'react' 2import {Platform} from 'react-native' 3import * as Location from 'expo-location' 4import {createPermissionHook} from 'expo-modules-core' 5 6import {IS_NATIVE} from '#/env' 7import * as debug from '#/geolocation/debug' 8import {logger} from '#/geolocation/logger' 9import {type Geolocation} from '#/geolocation/types' 10import {normalizeDeviceLocation} from '#/geolocation/util' 11import {device} from '#/storage' 12 13/** 14 * Location.useForegroundPermissions on web just errors if the 15 * navigator.permissions API is not available. We need to catch and ignore it, 16 * since it's effectively denied. 17 * 18 * @see https://github.com/expo/expo/blob/72f1562ed9cce5ff6dfe04aa415b71632a3d4b87/packages/expo-location/src/Location.ts#L290-L293 19 */ 20const useForegroundPermissions = createPermissionHook({ 21 getMethod: () => 22 Location.getForegroundPermissionsAsync().catch(error => { 23 logger.debug( 24 'useForegroundPermission: error getting location permissions', 25 {safeMessage: error}, 26 ) 27 return { 28 status: Location.PermissionStatus.DENIED, 29 granted: false, 30 canAskAgain: false, 31 expires: 0, 32 } 33 }), 34 requestMethod: () => 35 Location.requestForegroundPermissionsAsync().catch(error => { 36 logger.debug( 37 'useForegroundPermission: error requesting location permissions', 38 {safeMessage: error}, 39 ) 40 return { 41 status: Location.PermissionStatus.DENIED, 42 granted: false, 43 canAskAgain: false, 44 expires: 0, 45 } 46 }), 47}) 48 49export async function getDeviceGeolocation(): Promise<Geolocation> { 50 if (debug.enabled && debug.deviceGeolocation) 51 return debug.resolve(debug.deviceGeolocation) 52 53 try { 54 const geocode = await Location.getCurrentPositionAsync() 55 const locations = await Location.reverseGeocodeAsync({ 56 latitude: geocode.coords.latitude, 57 longitude: geocode.coords.longitude, 58 }) 59 const location = locations.at(0) 60 const normalized = location ? normalizeDeviceLocation(location) : undefined 61 if (normalized?.regionCode && normalized.regionCode.length > 5) { 62 /* 63 * We want short codes only, and we're still seeing some full names here. 64 * 5 is just a heuristic for a region that is probably not formatted as a 65 * short code. 66 */ 67 logger.error('getDeviceGeolocation: invalid regionCode', { 68 os: Platform.OS, 69 version: Platform.Version, 70 regionCode: normalized.regionCode, 71 }) 72 } 73 return { 74 countryCode: normalized?.countryCode ?? undefined, 75 regionCode: normalized?.regionCode ?? undefined, 76 } 77 } catch (e) { 78 logger.error('getDeviceGeolocation: failed', {safeMessage: e}) 79 return { 80 countryCode: undefined, 81 regionCode: undefined, 82 } 83 } 84} 85 86export function useRequestDeviceGeolocation(): () => Promise< 87 | { 88 granted: true 89 location: Geolocation | undefined 90 } 91 | { 92 granted: false 93 } 94> { 95 return useCallback(async () => { 96 const status = await Location.requestForegroundPermissionsAsync() 97 if (status.granted) { 98 return { 99 granted: true, 100 location: await getDeviceGeolocation(), 101 } 102 } else { 103 return { 104 granted: false, 105 } 106 } 107 }, []) 108} 109 110/** 111 * Hook to get and sync the device geolocation from the device GPS and store it 112 * using device storage. If permissions are not granted, it will clear any cached 113 * storage value. 114 */ 115export function useSyncDeviceGeolocationOnStartup( 116 sync: (location: Geolocation | undefined) => void, 117) { 118 const synced = useRef(false) 119 const [status] = useForegroundPermissions() 120 useEffect(() => { 121 if (!IS_NATIVE) return 122 123 async function get() { 124 // no need to set this more than once per session 125 if (synced.current) return 126 logger.debug('useSyncDeviceGeolocationOnStartup: checking perms') 127 if (status?.granted) { 128 const location = await getDeviceGeolocation() 129 if (location) { 130 logger.debug('useSyncDeviceGeolocationOnStartup: got location') 131 sync(location) 132 synced.current = true 133 } 134 } else { 135 const hasCachedValue = device.get(['deviceGeolocation']) !== undefined 136 /** 137 * If we have a cached value, but user has revoked permissions, 138 * quietly (will take effect lazily) clear this out. 139 */ 140 if (hasCachedValue) { 141 logger.debug( 142 'useSyncDeviceGeolocationOnStartup: clearing cached location, perms revoked', 143 ) 144 device.set(['deviceGeolocation'], undefined) 145 } 146 } 147 } 148 149 get().catch(e => { 150 logger.error( 151 'useSyncDeviceGeolocationOnStartup: failed to get location', 152 { 153 safeMessage: e, 154 }, 155 ) 156 }) 157 }, [status, sync]) 158} 159 160export function useIsDeviceGeolocationGranted() { 161 const [status] = useForegroundPermissions() 162 return status?.granted === true 163}