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/config.ts | |
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/config.ts')
-rw-r--r-- | src/state/geolocation/config.ts | 143 |
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`) + } + } +} |