One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Geofencing

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.

Usage

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.

How it works

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.

API reference

MethodsTypeDescription
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) => RegionSubscriptionHear arrivals.
addDidExitRegionListener(listener: (region: LocationRegion) => void) => RegionSubscriptionHear departures.
(listener: (region: LocationRegion) => void) => RegionSubscriptionHear departures.
RegionTypeDescription
identifierstringRequired, non-empty. Unique per region.
stringRequired, non-empty. Unique per region.
centerRegionCenterLatitude and longitude of the middle.
RegionCenterLatitude and longitude of the middle.
radiusnumberRadius in meters.
numberRadius in meters.
notifyOnEntrybooleanEmit enter events. Default true.
booleanEmit enter events. Default true.
notifyOnExitbooleanEmit exit events. Default true.
booleanEmit exit events. Default true.
stateRegionStateUnknown, inside, or outside on reads.
RegionStateUnknown, inside, or outside on reads.
CenterTypeDescription
latitudenumberDegrees north of the equator.
numberDegrees north of the equator.
longitudenumberDegrees east of Greenwich.
numberDegrees east of Greenwich.

Platform differences

iOSAndroidWeb
EngineLocation hubSoftware fencesNone
Limit20 regions100 regionsEmpty
TerminatedWakes appStays deadNever
ServiceBackground modeForeground noteNone

Limits and gotchas

Examples

Alert on arriving at the office

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.

Silence notifications at home

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.