diff options
Diffstat (limited to 'src/state/geolocation/util.ts')
-rw-r--r-- | src/state/geolocation/util.ts | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/src/state/geolocation/util.ts b/src/state/geolocation/util.ts new file mode 100644 index 000000000..c92b42b13 --- /dev/null +++ b/src/state/geolocation/util.ts @@ -0,0 +1,180 @@ +import { + getCurrentPositionAsync, + type LocationGeocodedAddress, + reverseGeocodeAsync, +} from 'expo-location' + +import {logger} from '#/state/geolocation/logger' +import {type DeviceLocation} from '#/state/geolocation/types' +import {type Device} from '#/storage' + +/** + * Maps full US region names to their short codes. + * + * Context: in some cases, like on Android, we get the full region name instead + * of the short code. We may need to expand this in the future to other + * countries, hence the prefix. + */ +export const USRegionNameToRegionCode: { + [regionName: string]: string +} = { + Alabama: 'AL', + Alaska: 'AK', + Arizona: 'AZ', + Arkansas: 'AR', + California: 'CA', + Colorado: 'CO', + Connecticut: 'CT', + Delaware: 'DE', + Florida: 'FL', + Georgia: 'GA', + Hawaii: 'HI', + Idaho: 'ID', + Illinois: 'IL', + Indiana: 'IN', + Iowa: 'IA', + Kansas: 'KS', + Kentucky: 'KY', + Louisiana: 'LA', + Maine: 'ME', + Maryland: 'MD', + Massachusetts: 'MA', + Michigan: 'MI', + Minnesota: 'MN', + Mississippi: 'MS', + Missouri: 'MO', + Montana: 'MT', + Nebraska: 'NE', + Nevada: 'NV', + ['New Hampshire']: 'NH', + ['New Jersey']: 'NJ', + ['New Mexico']: 'NM', + ['New York']: 'NY', + ['North Carolina']: 'NC', + ['North Dakota']: 'ND', + Ohio: 'OH', + Oklahoma: 'OK', + Oregon: 'OR', + Pennsylvania: 'PA', + ['Rhode Island']: 'RI', + ['South Carolina']: 'SC', + ['South Dakota']: 'SD', + Tennessee: 'TN', + Texas: 'TX', + Utah: 'UT', + Vermont: 'VT', + Virginia: 'VA', + Washington: 'WA', + ['West Virginia']: 'WV', + Wisconsin: 'WI', + Wyoming: 'WY', +} + +/** + * Normalizes a `LocationGeocodedAddress` into a `DeviceLocation`. + * + * We don't want or care about the full location data, so we trim it down and + * normalize certain fields, like region, into the format we need. + */ +export function normalizeDeviceLocation( + location: LocationGeocodedAddress, +): DeviceLocation { + let {isoCountryCode, region} = location + + if (region) { + if (isoCountryCode === 'US') { + region = USRegionNameToRegionCode[region] ?? region + } + } + + return { + countryCode: isoCountryCode ?? undefined, + regionCode: region ?? undefined, + } +} + +/** + * Combines precise location data with the geolocation config fetched from the + * IP service, with preference to the precise data. + */ +export function mergeGeolocation( + location?: DeviceLocation, + config?: Device['geolocation'], +): DeviceLocation { + if (location?.countryCode) return location + return { + countryCode: config?.countryCode, + regionCode: config?.regionCode, + } +} + +/** + * Computes the geolocation status (age-restricted, age-blocked) based on the + * given location and geolocation config. `location` here should be merged with + * `mergeGeolocation()` ahead of time if needed. + */ +export function computeGeolocationStatus( + location: DeviceLocation, + config: Device['geolocation'], +) { + /** + * We can't do anything if we don't have this data. + */ + if (!location.countryCode) { + return { + ...location, + isAgeRestrictedGeo: false, + isAgeBlockedGeo: false, + } + } + + const isAgeRestrictedGeo = config?.ageRestrictedGeos?.some(rule => { + if (rule.countryCode === location.countryCode) { + if (!rule.regionCode) { + return true // whole country is blocked + } else if (rule.regionCode === location.regionCode) { + return true + } + } + }) + + const isAgeBlockedGeo = config?.ageBlockedGeos?.some(rule => { + if (rule.countryCode === location.countryCode) { + if (!rule.regionCode) { + return true // whole country is blocked + } else if (rule.regionCode === location.regionCode) { + return true + } + } + }) + + return { + ...location, + isAgeRestrictedGeo: !!isAgeRestrictedGeo, + isAgeBlockedGeo: !!isAgeBlockedGeo, + } +} + +export async function getDeviceGeolocation(): Promise<DeviceLocation> { + try { + const geocode = await getCurrentPositionAsync() + const locations = await reverseGeocodeAsync({ + latitude: geocode.coords.latitude, + longitude: geocode.coords.longitude, + }) + const location = locations.at(0) + const normalized = location ? normalizeDeviceLocation(location) : undefined + return { + countryCode: normalized?.countryCode ?? undefined, + regionCode: normalized?.regionCode ?? undefined, + } + } catch (e) { + logger.error('getDeviceGeolocation: failed', { + safeMessage: e, + }) + return { + countryCode: undefined, + regionCode: undefined, + } + } +} |