about summary refs log tree commit diff
path: root/src/state/geolocation/config.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/geolocation/config.ts')
-rw-r--r--src/state/geolocation/config.ts143
1 files changed, 143 insertions, 0 deletions
diff --git a/src/state/geolocation/config.ts b/src/state/geolocation/config.ts
new file mode 100644
index 000000000..1f7f2daf2
--- /dev/null
+++ b/src/state/geolocation/config.ts
@@ -0,0 +1,143 @@
+import {networkRetry} from '#/lib/async/retry'
+import {
+  DEFAULT_GEOLOCATION_CONFIG,
+  GEOLOCATION_CONFIG_URL,
+} from '#/state/geolocation/const'
+import {emitGeolocationConfigUpdate} from '#/state/geolocation/events'
+import {logger} from '#/state/geolocation/logger'
+import {BAPP_CONFIG_DEV_BYPASS_SECRET, IS_DEV} from '#/env'
+import {type Device, device} from '#/storage'
+
+async function getGeolocationConfig(
+  url: string,
+): Promise<Device['geolocation']> {
+  const res = await fetch(url, {
+    headers: IS_DEV
+      ? {
+          'x-dev-bypass-secret': BAPP_CONFIG_DEV_BYPASS_SECRET,
+        }
+      : undefined,
+  })
+
+  if (!res.ok) {
+    throw new Error(`geolocation config: fetch failed ${res.status}`)
+  }
+
+  const json = await res.json()
+
+  if (json.countryCode) {
+    /**
+     * Only construct known values here, ignore any extras.
+     */
+    const config: Device['geolocation'] = {
+      countryCode: json.countryCode,
+      regionCode: json.regionCode ?? undefined,
+      ageRestrictedGeos: json.ageRestrictedGeos ?? [],
+      ageBlockedGeos: json.ageBlockedGeos ?? [],
+    }
+    logger.debug(`geolocation config: success`)
+    return config
+  } else {
+    return undefined
+  }
+}
+
+/**
+ * Local promise used within this file only.
+ */
+let geolocationConfigResolution: Promise<{success: boolean}> | undefined
+
+/**
+ * Begin the process of resolving geolocation config. This should be called
+ * once at app start.
+ *
+ * THIS METHOD SHOULD NEVER THROW.
+ *
+ * This method is otherwise not used for any purpose. To ensure geolocation
+ * config is resolved, use {@link ensureGeolocationConfigIsResolved}
+ */
+export function beginResolveGeolocationConfig() {
+  /**
+   * Here for debug purposes. Uncomment to prevent hitting the remote geo service, and apply whatever data you require for testing.
+   */
+  // if (__DEV__) {
+  //   geolocationConfigResolution = new Promise(y => y({success: true}))
+  //   device.set(['deviceGeolocation'], undefined) // clears GPS data
+  //   device.set(['geolocation'], DEFAULT_GEOLOCATION_CONFIG) // clears bapp-config data
+  //   return
+  // }
+
+  geolocationConfigResolution = new Promise(async resolve => {
+    let success = true
+
+    try {
+      // Try once, fail fast
+      const config = await getGeolocationConfig(GEOLOCATION_CONFIG_URL)
+      if (config) {
+        device.set(['geolocation'], config)
+        emitGeolocationConfigUpdate(config)
+      } else {
+        // endpoint should throw on all failures, this is insurance
+        throw new Error(
+          `geolocation config: nothing returned from initial request`,
+        )
+      }
+    } catch (e: any) {
+      success = false
+
+      logger.debug(`geolocation config: failed initial request`, {
+        safeMessage: e.message,
+      })
+
+      // set to default
+      device.set(['geolocation'], DEFAULT_GEOLOCATION_CONFIG)
+
+      // retry 3 times, but don't await, proceed with default
+      networkRetry(3, () => getGeolocationConfig(GEOLOCATION_CONFIG_URL))
+        .then(config => {
+          if (config) {
+            device.set(['geolocation'], config)
+            emitGeolocationConfigUpdate(config)
+            success = true
+          } else {
+            // endpoint should throw on all failures, this is insurance
+            throw new Error(`geolocation config: nothing returned from retries`)
+          }
+        })
+        .catch((e: any) => {
+          // complete fail closed
+          logger.debug(`geolocation config: failed retries`, {
+            safeMessage: e.message,
+          })
+        })
+    } finally {
+      resolve({success})
+    }
+  })
+}
+
+/**
+ * Ensure that geolocation config has been resolved, or at the very least attempted
+ * once. Subsequent retries will not be captured by this `await`. Those will be
+ * reported via {@link emitGeolocationConfigUpdate}.
+ */
+export async function ensureGeolocationConfigIsResolved() {
+  if (!geolocationConfigResolution) {
+    throw new Error(
+      `geolocation config: beginResolveGeolocationConfig not called yet`,
+    )
+  }
+
+  const cached = device.get(['geolocation'])
+  if (cached) {
+    logger.debug(`geolocation config: using cache`)
+  } else {
+    logger.debug(`geolocation config: no cache`)
+    const {success} = await geolocationConfigResolution
+    if (success) {
+      logger.debug(`geolocation config: resolved`)
+    } else {
+      logger.info(`geolocation config: failed to resolve`)
+    }
+  }
+}