diff options
author | Eric Bailey <git@esb.lol> | 2025-09-04 11:07:12 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-09-04 11:07:12 -0500 |
commit | f8ae0540a062e6346baf9fbf0481f769fb23a120 (patch) | |
tree | fc888e55258169e8e7246a1c099aab78fc7d9c99 /src/state/geolocation/index.tsx | |
parent | 625b4e61dbf11c1d485bf8e8265df4d5af0c9657 (diff) | |
download | voidsky-f8ae0540a062e6346baf9fbf0481f769fb23a120.tar.zst |
Provide geo-gated users optional GPS fallback for precise location data (#8973)
Diffstat (limited to 'src/state/geolocation/index.tsx')
-rw-r--r-- | src/state/geolocation/index.tsx | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/src/state/geolocation/index.tsx b/src/state/geolocation/index.tsx new file mode 100644 index 000000000..d01e3f262 --- /dev/null +++ b/src/state/geolocation/index.tsx @@ -0,0 +1,153 @@ +import React from 'react' + +import { + DEFAULT_GEOLOCATION_CONFIG, + DEFAULT_GEOLOCATION_STATUS, +} from '#/state/geolocation/const' +import {onGeolocationConfigUpdate} from '#/state/geolocation/events' +import {logger} from '#/state/geolocation/logger' +import { + type DeviceLocation, + type GeolocationStatus, +} from '#/state/geolocation/types' +import {useSyncedDeviceGeolocation} from '#/state/geolocation/useSyncedDeviceGeolocation' +import { + computeGeolocationStatus, + mergeGeolocation, +} from '#/state/geolocation/util' +import {type Device, device} from '#/storage' + +export * from '#/state/geolocation/config' +export * from '#/state/geolocation/types' +export * from '#/state/geolocation/util' + +type DeviceGeolocationContext = { + deviceGeolocation: DeviceLocation | undefined +} + +type DeviceGeolocationAPIContext = { + setDeviceGeolocation(deviceGeolocation: DeviceLocation): void +} + +type GeolocationConfigContext = { + config: Device['geolocation'] +} + +type GeolocationStatusContext = { + /** + * Merged geolocation from config and device GPS (if available). + */ + location: DeviceLocation + /** + * Computed geolocation status based on the merged location and config. + */ + status: GeolocationStatus +} + +const DeviceGeolocationContext = React.createContext<DeviceGeolocationContext>({ + deviceGeolocation: undefined, +}) +DeviceGeolocationContext.displayName = 'DeviceGeolocationContext' + +const DeviceGeolocationAPIContext = + React.createContext<DeviceGeolocationAPIContext>({ + setDeviceGeolocation: () => {}, + }) +DeviceGeolocationAPIContext.displayName = 'DeviceGeolocationAPIContext' + +const GeolocationConfigContext = React.createContext<GeolocationConfigContext>({ + config: DEFAULT_GEOLOCATION_CONFIG, +}) +GeolocationConfigContext.displayName = 'GeolocationConfigContext' + +const GeolocationStatusContext = React.createContext<GeolocationStatusContext>({ + location: { + countryCode: undefined, + regionCode: undefined, + }, + status: DEFAULT_GEOLOCATION_STATUS, +}) +GeolocationStatusContext.displayName = 'GeolocationStatusContext' + +/** + * Provider of geolocation config and computed geolocation status. + */ +export function GeolocationStatusProvider({ + children, +}: { + children: React.ReactNode +}) { + const {deviceGeolocation} = React.useContext(DeviceGeolocationContext) + const [config, setConfig] = React.useState(() => { + const initial = device.get(['geolocation']) || DEFAULT_GEOLOCATION_CONFIG + return initial + }) + + React.useEffect(() => { + return onGeolocationConfigUpdate(config => { + setConfig(config!) + }) + }, []) + + const configContext = React.useMemo(() => ({config}), [config]) + const statusContext = React.useMemo(() => { + if (deviceGeolocation) { + logger.debug('geolocation: has device geolocation available') + } + const geolocation = mergeGeolocation(deviceGeolocation, config) + const status = computeGeolocationStatus(geolocation, config) + return {location: geolocation, status} + }, [config, deviceGeolocation]) + + return ( + <GeolocationConfigContext.Provider value={configContext}> + <GeolocationStatusContext.Provider value={statusContext}> + {children} + </GeolocationStatusContext.Provider> + </GeolocationConfigContext.Provider> + ) +} + +/** + * Provider of providers. Provides device geolocation data to lower-level + * `GeolocationStatusProvider`, and device geolocation APIs to children. + */ +export function Provider({children}: {children: React.ReactNode}) { + const [deviceGeolocation, setDeviceGeolocation] = useSyncedDeviceGeolocation() + + const handleSetDeviceGeolocation = React.useCallback( + (location: DeviceLocation) => { + logger.debug('geolocation: setting device geolocation') + setDeviceGeolocation({ + countryCode: location.countryCode ?? undefined, + regionCode: location.regionCode ?? undefined, + }) + }, + [setDeviceGeolocation], + ) + + return ( + <DeviceGeolocationAPIContext.Provider + value={React.useMemo( + () => ({setDeviceGeolocation: handleSetDeviceGeolocation}), + [handleSetDeviceGeolocation], + )}> + <DeviceGeolocationContext.Provider + value={React.useMemo(() => ({deviceGeolocation}), [deviceGeolocation])}> + <GeolocationStatusProvider>{children}</GeolocationStatusProvider> + </DeviceGeolocationContext.Provider> + </DeviceGeolocationAPIContext.Provider> + ) +} + +export function useDeviceGeolocationApi() { + return React.useContext(DeviceGeolocationAPIContext) +} + +export function useGeolocationConfig() { + return React.useContext(GeolocationConfigContext) +} + +export function useGeolocationStatus() { + return React.useContext(GeolocationStatusContext) +} |