From f8ae0540a062e6346baf9fbf0481f769fb23a120 Mon Sep 17 00:00:00 2001
From: Eric Bailey <git@esb.lol>
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<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)
+}
-- 
cgit 1.4.1

