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