one
Version:
One is a React framework for web, iOS, and Android apps.
158 lines (157 loc) • 5.2 kB
text/typescript
import { Location as unavailable } from './unavailable'
import { validateCallback } from '../validateCallback'
import type {
LocationPermissionStatus,
LocationPosition,
LocationPlace,
LocationRegion,
RegionCenter,
RegionState,
} from '../specs/OneLocation.nitro'
import { assertIdentifier, assertRegion, assertRegionListener } from './validation'
export type {
LocationPermissionStatus,
LocationPosition,
LocationPlace,
LocationRegion,
RegionCenter,
RegionState,
}
export type LocationWatchError = Error & { code: string }
export interface RegionSubscription {
remove(): void
}
let permission: LocationPermissionStatus = 'notDetermined'
let observing: Promise<void> | undefined
function observe(): Promise<void> {
return (observing ??= (async () => {
if (!navigator.permissions) return
try {
const status = await navigator.permissions.query({ name: 'geolocation' })
const update = () => {
permission =
status.state === 'granted'
? 'whenInUse'
: status.state === 'denied'
? 'denied'
: 'notDetermined'
}
update()
status.addEventListener('change', update)
} catch {
/* permission queries are not supported by every browser. */
}
})())
}
function position(value: GeolocationPosition): LocationPosition {
permission = 'whenInUse'
const c = value.coords
return {
latitude: c.latitude,
longitude: c.longitude,
accuracy: c.accuracy,
altitude: c.altitude ?? 0,
altitudeAccuracy: c.altitudeAccuracy ?? -1,
course: c.heading ?? -1,
speed: c.speed ?? -1,
timestamp: value.timestamp,
}
}
function failure(error: GeolocationPositionError): LocationWatchError {
if (error.code === 1) permission = 'denied'
return Object.assign(new Error(error.message), {
code: error.code === 1 ? 'E_LOCATION_PERMISSION' : 'E_LOCATION_UNAVAILABLE',
})
}
async function current(): Promise<LocationPosition> {
if (typeof window === 'undefined' || !navigator.geolocation)
return unavailable.getCurrentPosition()
await observe()
return new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(
(value) => resolve(position(value)),
(error) => reject(failure(error)),
{ enableHighAccuracy: true, maximumAge: 30_000, timeout: 10_000 }
)
)
}
export const Location = Object.freeze({
getPermissionStatus: (): LocationPermissionStatus => {
if (typeof window === 'undefined' || !navigator.geolocation) return 'denied'
void observe()
return permission
},
requestWhenInUsePermission: async (): Promise<LocationPermissionStatus> => {
if (typeof window === 'undefined' || !navigator.geolocation) return 'denied'
try {
await current()
} catch (error) {
if ((error as LocationWatchError).code !== 'E_LOCATION_PERMISSION') throw error
}
return permission
},
getCurrentPosition: current,
watchPosition: (
onPosition: (value: LocationPosition) => void,
onError: (error: LocationWatchError) => void,
options: { background?: boolean } = {}
): (() => void) => {
if (typeof window === 'undefined')
return unavailable.watchPosition(onPosition, onError, options)
validateCallback(onPosition, 'Location.watchPosition: onPosition must be a function')
validateCallback(onError, 'Location.watchPosition: onError must be a function')
if (options.background || !navigator.geolocation) {
onError(
Object.assign(
new Error('Location.watchPosition: background location is unavailable on web'),
{ code: 'E_LOCATION_UNAVAILABLE' }
)
)
return () => {}
}
void observe()
let active = true
const id = navigator.geolocation.watchPosition(
(value) => {
if (active) onPosition(position(value))
},
(error) => {
if (active) onError(failure(error))
},
{ enableHighAccuracy: true, maximumAge: 30_000, timeout: 10_000 }
)
return () => {
active = false
navigator.geolocation.clearWatch(id)
}
},
geocodeAddress: unavailable.geocodeAddress,
reverseGeocode: unavailable.reverseGeocode,
requestAlwaysAuthorization: (): Promise<LocationPermissionStatus> =>
Promise.resolve('denied'),
startMonitoring: (region: LocationRegion): Promise<void> => {
assertRegion(region, 'startMonitoring')
return Promise.resolve()
},
stopMonitoring: (identifier: string): Promise<void> => {
assertIdentifier(identifier, 'stopMonitoring')
return Promise.resolve()
},
monitoredRegions: (): LocationRegion[] => [],
requestState: (identifier: string): Promise<RegionState> => {
assertIdentifier(identifier, 'requestState')
return Promise.resolve('unknown')
},
addDidEnterRegionListener: (
listener: (region: LocationRegion) => void
): RegionSubscription => {
assertRegionListener(listener, 'addDidEnterRegionListener')
return { remove: () => {} }
},
addDidExitRegionListener: (
listener: (region: LocationRegion) => void
): RegionSubscription => {
assertRegionListener(listener, 'addDidExitRegionListener')
return { remove: () => {} }
},
})