about summary refs log tree commit diff
path: root/src/state/geolocation/index.tsx
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-09-04 11:07:12 -0500
committerGitHub <noreply@github.com>2025-09-04 11:07:12 -0500
commitf8ae0540a062e6346baf9fbf0481f769fb23a120 (patch)
treefc888e55258169e8e7246a1c099aab78fc7d9c99 /src/state/geolocation/index.tsx
parent625b4e61dbf11c1d485bf8e8265df4d5af0c9657 (diff)
downloadvoidsky-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.tsx153
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)
+}