Monitor circular regions for enter and exit events
One.Location monitors circular regions for arrivals and departures:
alert when the user reaches the office, silence notifications at home, or
log a store visit. Each region is an identifier, a center, and a radius
in meters, with entry and exit notifications that default on. For
one-shot positions and address lookup, see location.
On iOS regions run on CLLocationManager with CLCircularRegion,
keeping Core Location’s names, inputs, and composition. On Android the
same calls drive software regions over the platform location providers,
with no Play Services dependency. On web there is no region monitoring:
submits resolve without effect, the list stays empty, state reads
unknown, listeners never fire, and the always grant reads denied.
import { One } from 'one'
const permission = await One.Location.requestAlwaysAuthorization()if (permission === 'always' || permission === 'whenInUse') { await One.Location.startMonitoring({ identifier: 'office', center: { latitude: 37.7749, longitude: -122.4194 }, radius: 100, }) const entered = One.Location.addDidEnterRegionListener((region) => { console.log('entered', region.identifier) }) const exited = One.Location.addDidExitRegionListener((region) => { console.log('exited', region.identifier) })}The listeners stand in for the location manager delegate, each receiving the region only. Remove them when the screen no longer needs events, and stop each region promptly when tracking ends to spare the battery.
Request the always grant while the app is active, usually after a tap.
It resolves with the resulting status: not determined, restricted,
denied, when in use, or always. An existing decision returns without
another prompt. On Android 11 and newer the background grant is
requested second; without location.background in the manifest the
status reaches when-in-use at best. The request rejects when the usage
text is missing or the app is inactive.
Add the prompt strings and background flag to the manifest:
import { one } from 'one/vite'
export default { plugins: [ one({ native: { app: { name: 'MyApp', location: { whenInUse: 'Show places near you.', alwaysAndWhenInUse: 'Alert when you arrive or leave saved places.', background: true, }, }, }, }), ],}whenInUse stamps the in-use description and foreground Android
permissions; alwaysAndWhenInUse stamps the always description, which
the always request requires on iOS. background: true adds location
to the background modes and stamps the background location permission;
background watches and region monitoring then run under a
location-type foreground service with a persistent notification while
registered.
Each startMonitoring call adds one region: up to 20 on iOS, 100 on
Android, beyond which it rejects. Starting an unchanged region resolves
without restarting it; stopping an unmonitored identifier resolves
without effect. Invalid regions throw a TypeError at the JavaScript
boundary before reaching native. Starting without permission rejects,
and unavailable hardware rejects too: the iOS simulator has no
region-monitoring hardware, so delivery needs a device. Monitored
regions report a state of unknown, inside, or outside. No event fires
for the initial determination; call requestState, which rejects for
unmonitored identifiers. Regions with entry or exit notifications off
never emit that event, but their state still tracks.
On iOS the system can wake a terminated app on a new region event. On Android a terminated app does not restart for region events, so start monitoring again after a fresh launch.
| Methods | Type | Description |
|---|---|---|
requestAlwaysAuthorization() => Promise<LocationPermissionStatus>Ask for the always grant. Resolves with the result. | () => Promise<LocationPermissionStatus> | Ask for the always grant. Resolves with the result. |
startMonitoring(region: LocationRegion) => Promise<void>Watch one circular region. | (region: LocationRegion) => Promise<void> | Watch one circular region. |
stopMonitoring(identifier: string) => Promise<void>Stop watching one region. | (identifier: string) => Promise<void> | Stop watching one region. |
monitoredRegions() => LocationRegion[]The regions watched right now. | () => LocationRegion[] | The regions watched right now. |
requestState(identifier: string) => Promise<RegionState>Determine inside or outside once. | (identifier: string) => Promise<RegionState> | Determine inside or outside once. |
addDidEnterRegionListener(listener: (region: LocationRegion) => void) => RegionSubscriptionHear arrivals. | (listener: (region: LocationRegion) => void) => RegionSubscription | Hear arrivals. |
addDidExitRegionListener(listener: (region: LocationRegion) => void) => RegionSubscriptionHear departures. | (listener: (region: LocationRegion) => void) => RegionSubscription | Hear departures. |
| Region | Type | Description |
|---|---|---|
identifierstringRequired, non-empty. Unique per region. | string | Required, non-empty. Unique per region. |
centerRegionCenterLatitude and longitude of the middle. | RegionCenter | Latitude and longitude of the middle. |
radiusnumberRadius in meters. | number | Radius in meters. |
notifyOnEntrybooleanEmit enter events. Default true. | boolean | Emit enter events. Default true. |
notifyOnExitbooleanEmit exit events. Default true. | boolean | Emit exit events. Default true. |
stateRegionStateUnknown, inside, or outside on reads. | RegionState | Unknown, inside, or outside on reads. |
| Center | Type | Description |
|---|---|---|
latitudenumberDegrees north of the equator. | number | Degrees north of the equator. |
longitudenumberDegrees east of Greenwich. | number | Degrees east of Greenwich. |
| iOS | Android | Web | |
|---|---|---|---|
| Engine | Location hub | Software fences | None |
| Limit | 20 regions | 100 regions | Empty |
| Terminated | Wakes app | Stays dead | Never |
| Service | Background mode | Foreground note | None |
requestState for the current inside or outside.import { useEffect } from 'react'import { One } from 'one'
const OFFICE = { identifier: 'office', center: { latitude: 37.7749, longitude: -122.4194 }, radius: 100,}
function useOfficeArrival(onArrive: (at: number) => void) { useEffect(() => { async function monitor() { const permission = await One.Location.requestAlwaysAuthorization() if (permission !== 'always' && permission !== 'whenInUse') return await One.Location.startMonitoring(OFFICE) const state = await One.Location.requestState(OFFICE.identifier) if (state === 'inside') onArrive(Date.now()) return One.Location.addDidEnterRegionListener(() => onArrive(Date.now())) } const subscription = monitor() return () => { void subscription.then((sub) => sub?.remove()) void One.Location.stopMonitoring(OFFICE.identifier) } }, [onArrive])}The initial state check catches a user who opens the app already inside, since no enter event fires for them. Cleanup removes the listener and the region together.
import { useEffect, useState } from 'react'import { One } from 'one'
const HOME = { identifier: 'home', center: { latitude: 37.3382, longitude: -121.8863 }, radius: 150, notifyOnEntry: true, notifyOnExit: true,}
function useAtHome() { const [atHome, setAtHome] = useState(false)
useEffect(() => { async function monitor() { const permission = await One.Location.requestAlwaysAuthorization() if (permission !== 'always' && permission !== 'whenInUse') return await One.Location.startMonitoring(HOME) setAtHome((await One.Location.requestState(HOME.identifier)) === 'inside') const entered = One.Location.addDidEnterRegionListener(() => setAtHome(true)) const exited = One.Location.addDidExitRegionListener(() => setAtHome(false)) return () => { entered.remove() exited.remove() } } const cleanup = monitor() return () => { void cleanup.then((remove) => remove?.()) void One.Location.stopMonitoring(HOME.identifier) } }, [])
return atHome}Both crossings drive one boolean, seeded by the current state so the first render is already right. Screens read the boolean and mute themselves without touching regions at all.
Edit this page on GitHub.