181 lines
4.4 KiB
TypeScript
181 lines
4.4 KiB
TypeScript
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,
|
|
}
|
|
}
|
|
}
|