fork
Configure Feed
Select the types of activity you want to include in your feed.
An ATproto social media client -- with an independent Appview.
fork
Configure Feed
Select the types of activity you want to include in your feed.
1import {
2 getCurrentPositionAsync,
3 type LocationGeocodedAddress,
4 reverseGeocodeAsync,
5} from 'expo-location'
6
7import {logger} from '#/state/geolocation/logger'
8import {type DeviceLocation} from '#/state/geolocation/types'
9import {type Device} from '#/storage'
10
11/**
12 * Maps full US region names to their short codes.
13 *
14 * Context: in some cases, like on Android, we get the full region name instead
15 * of the short code. We may need to expand this in the future to other
16 * countries, hence the prefix.
17 */
18export const USRegionNameToRegionCode: {
19 [regionName: string]: string
20} = {
21 Alabama: 'AL',
22 Alaska: 'AK',
23 Arizona: 'AZ',
24 Arkansas: 'AR',
25 California: 'CA',
26 Colorado: 'CO',
27 Connecticut: 'CT',
28 Delaware: 'DE',
29 Florida: 'FL',
30 Georgia: 'GA',
31 Hawaii: 'HI',
32 Idaho: 'ID',
33 Illinois: 'IL',
34 Indiana: 'IN',
35 Iowa: 'IA',
36 Kansas: 'KS',
37 Kentucky: 'KY',
38 Louisiana: 'LA',
39 Maine: 'ME',
40 Maryland: 'MD',
41 Massachusetts: 'MA',
42 Michigan: 'MI',
43 Minnesota: 'MN',
44 Mississippi: 'MS',
45 Missouri: 'MO',
46 Montana: 'MT',
47 Nebraska: 'NE',
48 Nevada: 'NV',
49 ['New Hampshire']: 'NH',
50 ['New Jersey']: 'NJ',
51 ['New Mexico']: 'NM',
52 ['New York']: 'NY',
53 ['North Carolina']: 'NC',
54 ['North Dakota']: 'ND',
55 Ohio: 'OH',
56 Oklahoma: 'OK',
57 Oregon: 'OR',
58 Pennsylvania: 'PA',
59 ['Rhode Island']: 'RI',
60 ['South Carolina']: 'SC',
61 ['South Dakota']: 'SD',
62 Tennessee: 'TN',
63 Texas: 'TX',
64 Utah: 'UT',
65 Vermont: 'VT',
66 Virginia: 'VA',
67 Washington: 'WA',
68 ['West Virginia']: 'WV',
69 Wisconsin: 'WI',
70 Wyoming: 'WY',
71}
72
73/**
74 * Normalizes a `LocationGeocodedAddress` into a `DeviceLocation`.
75 *
76 * We don't want or care about the full location data, so we trim it down and
77 * normalize certain fields, like region, into the format we need.
78 */
79export function normalizeDeviceLocation(
80 location: LocationGeocodedAddress,
81): DeviceLocation {
82 let {isoCountryCode, region} = location
83
84 if (region) {
85 if (isoCountryCode === 'US') {
86 region = USRegionNameToRegionCode[region] ?? region
87 }
88 }
89
90 return {
91 countryCode: isoCountryCode ?? undefined,
92 regionCode: region ?? undefined,
93 }
94}
95
96/**
97 * Combines precise location data with the geolocation config fetched from the
98 * IP service, with preference to the precise data.
99 */
100export function mergeGeolocation(
101 location?: DeviceLocation,
102 config?: Device['geolocation'],
103): DeviceLocation {
104 if (location?.countryCode) return location
105 return {
106 countryCode: config?.countryCode,
107 regionCode: config?.regionCode,
108 }
109}
110
111/**
112 * Computes the geolocation status (age-restricted, age-blocked) based on the
113 * given location and geolocation config. `location` here should be merged with
114 * `mergeGeolocation()` ahead of time if needed.
115 */
116export function computeGeolocationStatus(
117 location: DeviceLocation,
118 config: Device['geolocation'],
119) {
120 /**
121 * We can't do anything if we don't have this data.
122 */
123 if (!location.countryCode) {
124 return {
125 ...location,
126 isAgeRestrictedGeo: false,
127 isAgeBlockedGeo: false,
128 }
129 }
130
131 const isAgeRestrictedGeo = config?.ageRestrictedGeos?.some(rule => {
132 if (rule.countryCode === location.countryCode) {
133 if (!rule.regionCode) {
134 return true // whole country is blocked
135 } else if (rule.regionCode === location.regionCode) {
136 return true
137 }
138 }
139 })
140
141 const isAgeBlockedGeo = config?.ageBlockedGeos?.some(rule => {
142 if (rule.countryCode === location.countryCode) {
143 if (!rule.regionCode) {
144 return true // whole country is blocked
145 } else if (rule.regionCode === location.regionCode) {
146 return true
147 }
148 }
149 })
150
151 return {
152 ...location,
153 isAgeRestrictedGeo: !!isAgeRestrictedGeo,
154 isAgeBlockedGeo: !!isAgeBlockedGeo,
155 }
156}
157
158export async function getDeviceGeolocation(): Promise<DeviceLocation> {
159 try {
160 const geocode = await getCurrentPositionAsync()
161 const locations = await reverseGeocodeAsync({
162 latitude: geocode.coords.latitude,
163 longitude: geocode.coords.longitude,
164 })
165 const location = locations.at(0)
166 const normalized = location ? normalizeDeviceLocation(location) : undefined
167 return {
168 countryCode: normalized?.countryCode ?? undefined,
169 regionCode: normalized?.regionCode ?? undefined,
170 }
171 } catch (e) {
172 logger.error('getDeviceGeolocation: failed', {
173 safeMessage: e,
174 })
175 return {
176 countryCode: undefined,
177 regionCode: undefined,
178 }
179 }
180}