From f8ae0540a062e6346baf9fbf0481f769fb23a120 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 4 Sep 2025 11:07:12 -0500 Subject: Provide geo-gated users optional GPS fallback for precise location data (#8973) --- src/state/geolocation/index.tsx | 153 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/state/geolocation/index.tsx (limited to 'src/state/geolocation/index.tsx') 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({ + deviceGeolocation: undefined, +}) +DeviceGeolocationContext.displayName = 'DeviceGeolocationContext' + +const DeviceGeolocationAPIContext = + React.createContext({ + setDeviceGeolocation: () => {}, + }) +DeviceGeolocationAPIContext.displayName = 'DeviceGeolocationAPIContext' + +const GeolocationConfigContext = React.createContext({ + config: DEFAULT_GEOLOCATION_CONFIG, +}) +GeolocationConfigContext.displayName = 'GeolocationConfigContext' + +const GeolocationStatusContext = React.createContext({ + 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 ( + + + {children} + + + ) +} + +/** + * 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 ( + ({setDeviceGeolocation: handleSetDeviceGeolocation}), + [handleSetDeviceGeolocation], + )}> + ({deviceGeolocation}), [deviceGeolocation])}> + {children} + + + ) +} + +export function useDeviceGeolocationApi() { + return React.useContext(DeviceGeolocationAPIContext) +} + +export function useGeolocationConfig() { + return React.useContext(GeolocationConfigContext) +} + +export function useGeolocationStatus() { + return React.useContext(GeolocationStatusContext) +} -- cgit 1.4.1