about summary refs log tree commit diff
path: root/src/state/geolocation/util.ts
blob: c92b42b13384d5db564b4b7748f9bb3ea4f5f467 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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,
    }
  }
}