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 && debug.deviceGeolocation)
50 return debug.resolve(debug.deviceGeolocation)
51
52 try {
53 const geocode = await Location.getCurrentPositionAsync()
54 const locations = await Location.reverseGeocodeAsync({
55 latitude: geocode.coords.latitude,
56 longitude: geocode.coords.longitude,
57 })
58 const location = locations.at(0)
59 const normalized = location ? normalizeDeviceLocation(location) : undefined
60 return {
61 countryCode: normalized?.countryCode ?? undefined,
62 regionCode: normalized?.regionCode ?? undefined,
63 }
64 } catch (e) {
65 logger.error('getDeviceGeolocation: failed', {safeMessage: e})
66 return {
67 countryCode: undefined,
68 regionCode: undefined,
69 }
70 }
71}
72
73export function useRequestDeviceGeolocation(): () => Promise<
74 | {
75 granted: true
76 location: Geolocation | undefined
77 }
78 | {
79 granted: false
80 }
81> {
82 return useCallback(async () => {
83 const status = await Location.requestForegroundPermissionsAsync()
84 if (status.granted) {
85 return {
86 granted: true,
87 location: await getDeviceGeolocation(),
88 }
89 } else {
90 return {
91 granted: false,
92 }
93 }
94 }, [])
95}
96
97/**
98 * Hook to get and sync the device geolocation from the device GPS and store it
99 * using device storage. If permissions are not granted, it will clear any cached
100 * storage value.
101 */
102export function useSyncDeviceGeolocationOnStartup(
103 sync: (location: Geolocation | undefined) => void,
104) {
105 const synced = useRef(false)
106 const [status] = useForegroundPermissions()
107 useEffect(() => {
108 if (!isNative) return
109
110 async function get() {
111 // no need to set this more than once per session
112 if (synced.current) return
113 logger.debug('useSyncDeviceGeolocationOnStartup: checking perms')
114 if (status?.granted) {
115 const location = await getDeviceGeolocation()
116 if (location) {
117 logger.debug('useSyncDeviceGeolocationOnStartup: got location')
118 sync(location)
119 synced.current = true
120 }
121 } else {
122 const hasCachedValue = device.get(['deviceGeolocation']) !== undefined
123 /**
124 * If we have a cached value, but user has revoked permissions,
125 * quietly (will take effect lazily) clear this out.
126 */
127 if (hasCachedValue) {
128 logger.debug(
129 'useSyncDeviceGeolocationOnStartup: clearing cached location, perms revoked',
130 )
131 device.set(['deviceGeolocation'], undefined)
132 }
133 }
134 }
135
136 get().catch(e => {
137 logger.error(
138 'useSyncDeviceGeolocationOnStartup: failed to get location',
139 {
140 safeMessage: e,
141 },
142 )
143 })
144 }, [status, sync])
145}
146
147export function useIsDeviceGeolocationGranted() {
148 const [status] = useForegroundPermissions()
149 return status?.granted === true
150}