forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}