@symbiote-native/location 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +4 -0
- package/build/core/index.d.ts +5 -0
- package/build/core/index.js +5 -0
- package/build/core/location.d.ts +39 -0
- package/build/core/location.js +204 -0
- package/build/core/native-module.d.ts +31 -0
- package/build/core/native-module.js +3 -0
- package/build/core/subscribers.d.ts +23 -0
- package/build/core/subscribers.js +65 -0
- package/build/core/types.d.ts +160 -0
- package/build/core/types.js +44 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +5 -0
- package/build-ngc/core/index.js +6 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/location.d.ts +39 -0
- package/build-ngc/core/location.js +205 -0
- package/build-ngc/core/location.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +31 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/subscribers.d.ts +23 -0
- package/build-ngc/core/subscribers.js +66 -0
- package/build-ngc/core/subscribers.js.map +1 -0
- package/build-ngc/core/types.d.ts +160 -0
- package/build-ngc/core/types.js +45 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +40 -0
- package/package.json +151 -0
- package/src/angular/index.ts +4 -0
- package/src/core/index.ts +17 -0
- package/src/core/location.ts +313 -0
- package/src/core/native-module.ts +62 -0
- package/src/core/subscribers.ts +101 -0
- package/src/core/types.ts +191 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 A. Prokopenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @symbiote-native/location
|
|
2
|
+
|
|
3
|
+
A wrapper package for [SymbioteNative](../../README.md) that makes
|
|
4
|
+
[`expo-location`](https://github.com/expo/expo/tree/main/packages/expo-location) usable from
|
|
5
|
+
**every** adapter — React, Vue, Svelte, Solid, and Angular. Every export is a plain async
|
|
6
|
+
function or a `usePermissions`-style hook built by `expo-modules-core`'s `createPermissionHook`,
|
|
7
|
+
so there is no adapter-specific state to wrap: the React, Vue, Svelte, Solid, and Angular entry
|
|
8
|
+
points are plain re-exports of the same `core`.
|
|
9
|
+
|
|
10
|
+
Foreground position, heading, geocoding, and motion activity, plus background location updates
|
|
11
|
+
and geofencing — registered as tasks through [`@symbiote-native/task-manager`](../task-manager).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
**New app:**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @symbiote-native/cli new my-app --location
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Existing SymbioteNative app:**
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @symbiote-native/cli add --location
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Either way: installs `@symbiote-native/location`, wires the native autolinking automatically, and — since
|
|
28
|
+
this package has an optional, policy-sensitive Android bundle — asks at the end whether to grant
|
|
29
|
+
background location too (see the note below the manual-install block). See
|
|
30
|
+
[`@symbiote-native/cli`](../cli).
|
|
31
|
+
|
|
32
|
+
<details>
|
|
33
|
+
<summary>Manual install (no CLI — installing and wiring native autolinking by hand)</summary>
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @symbiote-native/location
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`expo-location` and `expo-modules-core` come along as regular dependencies, pinned to exact
|
|
40
|
+
versions — never install them yourself, and never add the `expo` meta-package to your project.
|
|
41
|
+
|
|
42
|
+
## Required one-time step: native autolinking wiring
|
|
43
|
+
|
|
44
|
+
Same one-time step as every other `expo-modules-core` package this project ships — see
|
|
45
|
+
[`@symbiote-native/local-auth`'s README](../local-auth/README.md#required-one-time-step-native-autolinking-wiring)
|
|
46
|
+
and the `symbiote-expo-native-module` project skill.
|
|
47
|
+
|
|
48
|
+
`native-link.json` declares four iOS `Info.plist` usage-description keys
|
|
49
|
+
(`NSLocationAlwaysAndWhenInUseUsageDescription`, `NSLocationAlwaysUsageDescription`,
|
|
50
|
+
`NSLocationWhenInUseUsageDescription`, `NSMotionUsageDescription`) with generic default text —
|
|
51
|
+
override any of them by setting the same key yourself before or after install
|
|
52
|
+
(`@symbiote-native/expo-modules-link`'s patcher is additive-only). Android's two base permissions
|
|
53
|
+
(`ACCESS_COARSE_LOCATION`/`ACCESS_FINE_LOCATION`) already ship in `expo-location`'s own
|
|
54
|
+
`AndroidManifest.xml` and merge automatically.
|
|
55
|
+
|
|
56
|
+
`native-link.json` also declares `ios.infoPlistArrayKeys.UIBackgroundModes: ["location"]` —
|
|
57
|
+
upstream's own `withLocation.ts` config plugin always adds it, since without it iOS silently
|
|
58
|
+
stops delivering `startLocationUpdatesAsync` updates once the app backgrounds. Wired
|
|
59
|
+
automatically by the same postinstall step (see `@symbiote-native/expo-modules-link`), merged
|
|
60
|
+
into the same array `@symbiote-native/audio`/`@symbiote-native/task-manager` may also populate.
|
|
61
|
+
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
**Background location — opt-in, asked for you.** `ACCESS_BACKGROUND_LOCATION`,
|
|
65
|
+
`FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_LOCATION`, and `ACTIVITY_RECOGNITION` (+ its Play
|
|
66
|
+
Services counterpart) are Android permissions opt-in by default, matching upstream — requesting
|
|
67
|
+
background location triggers Play Console policy review, so it's a deliberate choice, never a
|
|
68
|
+
package side effect. `new --location`/`add --location` above already ask, interactively, whether
|
|
69
|
+
to grant them; say yes and the trio (minus `ACTIVITY_RECOGNITION`, still a hand-edit) lands in
|
|
70
|
+
your `AndroidManifest.xml` for you, along with the `startLocationUpdatesAsync` `foregroundService`
|
|
71
|
+
option reminder. Said no, or ran non-interactively (CI, piped stdin)? Run it any time after:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @symbiote-native/cli grant location
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Idempotent — safe to run again even if already granted.
|
|
78
|
+
|
|
79
|
+
## Shape
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src/core/ location.ts (every function + the two useXPermissions hooks), subscribers.ts (the
|
|
83
|
+
watchId-keyed event-subscriber machinery shared by position/heading/motion-activity
|
|
84
|
+
watches), native-module.ts (requireNativeModule resolution), types.ts.
|
|
85
|
+
src/angular/ @symbiote-native/location/angular — export * from '../core'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`./react`, `./vue`, `./svelte`, and `./solid` are `exports`-map aliases straight onto
|
|
89
|
+
`src/core/`. `./angular` stays a physical file/subpath since Angular ships through a separate
|
|
90
|
+
`ngc`/AOT build (`build-ngc/`).
|
|
91
|
+
|
|
92
|
+
## Use it
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import {
|
|
96
|
+
getCurrentPositionAsync,
|
|
97
|
+
requestForegroundPermissionsAsync,
|
|
98
|
+
watchPositionAsync,
|
|
99
|
+
} from '@symbiote-native/location';
|
|
100
|
+
|
|
101
|
+
const { granted } = await requestForegroundPermissionsAsync();
|
|
102
|
+
if (granted) {
|
|
103
|
+
const position = await getCurrentPositionAsync();
|
|
104
|
+
const subscription = await watchPositionAsync(
|
|
105
|
+
{ distanceInterval: 10 },
|
|
106
|
+
location => {
|
|
107
|
+
console.log(location.coords);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
// later: subscription.remove();
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Background updates and geofencing register a task defined with
|
|
115
|
+
[`@symbiote-native/task-manager`](../task-manager)'s `defineTask` — the task itself must be
|
|
116
|
+
defined at module scope, since native can relaunch the JS bundle headlessly to run it:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// index.ts, alongside AppRegistry.registerComponent
|
|
120
|
+
import { defineTask } from '@symbiote-native/task-manager';
|
|
121
|
+
|
|
122
|
+
const SYNC_TASK = 'background-location-sync';
|
|
123
|
+
|
|
124
|
+
defineTask(SYNC_TASK, async ({ data, error }) => {
|
|
125
|
+
if (error) return;
|
|
126
|
+
const { locations } = data as { locations: unknown[] };
|
|
127
|
+
await syncLocations(locations);
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
requestBackgroundPermissionsAsync,
|
|
134
|
+
startLocationUpdatesAsync,
|
|
135
|
+
} from '@symbiote-native/location';
|
|
136
|
+
|
|
137
|
+
const { granted } = await requestBackgroundPermissionsAsync();
|
|
138
|
+
if (granted) {
|
|
139
|
+
await startLocationUpdatesAsync(SYNC_TASK, { distanceInterval: 100 });
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Identical import surface on every adapter — `@symbiote-native/location/react`, `/vue`, `/svelte`,
|
|
144
|
+
`/solid`, `/angular` all re-export the same functions.
|
|
145
|
+
|
|
146
|
+
## API
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
getProviderStatusAsync(): Promise<ILocationProviderStatus>
|
|
150
|
+
enableNetworkProviderAsync(): Promise<void> // android
|
|
151
|
+
getCurrentPositionAsync(options?): Promise<ILocationObject>
|
|
152
|
+
getLastKnownPositionAsync(options?): Promise<ILocationObject | null>
|
|
153
|
+
watchPositionAsync(options, callback, errorHandler?): Promise<ILocationSubscription>
|
|
154
|
+
getHeadingAsync(): Promise<ILocationHeadingObject>
|
|
155
|
+
watchHeadingAsync(callback, errorHandler?): Promise<ILocationSubscription>
|
|
156
|
+
geocodeAsync(address: string): Promise<ILocationGeocodedLocation[]>
|
|
157
|
+
reverseGeocodeAsync(location): Promise<ILocationGeocodedAddress[]>
|
|
158
|
+
getForegroundPermissionsAsync() / requestForegroundPermissionsAsync(): Promise<ILocationPermissionResponse>
|
|
159
|
+
useForegroundPermissions(options?)
|
|
160
|
+
getBackgroundPermissionsAsync() / requestBackgroundPermissionsAsync(): Promise<PermissionResponse>
|
|
161
|
+
useBackgroundPermissions(options?)
|
|
162
|
+
hasServicesEnabledAsync(): Promise<boolean>
|
|
163
|
+
getMotionActivityPermissionsAsync() / requestMotionActivityPermissionsAsync(): Promise<PermissionResponse>
|
|
164
|
+
useMotionActivityPermissions(options?)
|
|
165
|
+
getMotionActivityAsync(): Promise<IMotionActivityObject>
|
|
166
|
+
watchMotionActivityAsync(callback, errorHandler?): Promise<ILocationSubscription> // foreground only
|
|
167
|
+
isBackgroundLocationAvailableAsync(): Promise<boolean>
|
|
168
|
+
startLocationUpdatesAsync(taskName, options?) / stopLocationUpdatesAsync(taskName) / hasStartedLocationUpdatesAsync(taskName)
|
|
169
|
+
startGeofencingAsync(taskName, regions) / stopGeofencingAsync(taskName) / hasStartedGeofencingAsync(taskName)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Plus the `Accuracy`/`ActivityType`/`GeofencingEventType`/`GeofencingRegionState` enum aliases and
|
|
173
|
+
every `ILocation*`/`IMotionActivity*` type, ported from upstream's `Location.ts`/`Location.types.ts`
|
|
174
|
+
with this repo's `I`-prefix convention for exported types (`ts-js-best-practices`).
|
|
175
|
+
|
|
176
|
+
## Notes
|
|
177
|
+
|
|
178
|
+
- **Motion activity needs no location permission at all** — it reads Play Services activity
|
|
179
|
+
recognition / the iOS motion coprocessor directly, gated only by its own
|
|
180
|
+
`getMotionActivityPermissionsAsync`/`requestMotionActivityPermissionsAsync`.
|
|
181
|
+
- **`watchMotionActivityAsync` is foreground-only** — updates pause while the app is backgrounded
|
|
182
|
+
and resume when it returns, per upstream's own contract.
|
|
183
|
+
- **No Expo Go warning.** Upstream logs a one-time console warning about background-location
|
|
184
|
+
limits when running inside Expo Go; this project never runs under Expo Go
|
|
185
|
+
(`<examples_vs_dot_examples>` in root CLAUDE.md), so the check and the warning are dropped
|
|
186
|
+
rather than ported.
|
|
187
|
+
|
|
188
|
+
## Test it
|
|
189
|
+
|
|
190
|
+
No Fabric/Descriptor angle at all — every function here is a pure async-function surface plus a
|
|
191
|
+
handful of `watchId`-keyed event subscriptions, never a view or per-instance state. Tests inject a
|
|
192
|
+
fake native-module object in place of the real `requireNativeModule` resolution and fire the wired
|
|
193
|
+
listeners directly (`src/core/location.test.ts`) — no `installFabric()`, no ViewConfig.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { PermissionStatus, type PermissionHookOptions, type PermissionExpiration, } from 'expo-modules-core';
|
|
2
|
+
export { getCurrentWatchId } from './subscribers';
|
|
3
|
+
export * from './location';
|
|
4
|
+
export * from './types';
|
|
5
|
+
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType, LocationGeofencingEventType as GeofencingEventType, LocationGeofencingRegionState as GeofencingRegionState, } from './types';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { PermissionStatus, } from 'expo-modules-core';
|
|
2
|
+
export { getCurrentWatchId } from './subscribers.js';
|
|
3
|
+
export * from './location.js';
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType, LocationGeofencingEventType as GeofencingEventType, LocationGeofencingRegionState as GeofencingRegionState, } from './types.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ILocationCallback, ILocationErrorCallback, ILocationGeocodedAddress, ILocationGeocodedLocation, ILocationHeadingCallback, ILocationHeadingObject, ILocationLastKnownOptions, ILocationObject, ILocationOptions, ILocationPermissionResponse, ILocationProviderStatus, ILocationRegion, ILocationSubscription, ILocationTaskOptions, IMotionActivityCallback, IMotionActivityObject, PermissionResponse } from './types';
|
|
2
|
+
export declare function getProviderStatusAsync(): Promise<ILocationProviderStatus>;
|
|
3
|
+
/** @platform android */
|
|
4
|
+
export declare function enableNetworkProviderAsync(): Promise<void>;
|
|
5
|
+
export declare function getCurrentPositionAsync(options?: ILocationOptions): Promise<ILocationObject>;
|
|
6
|
+
export declare function getLastKnownPositionAsync(options?: ILocationLastKnownOptions): Promise<ILocationObject | null>;
|
|
7
|
+
export declare function watchPositionAsync(options: ILocationOptions, callback: ILocationCallback, errorHandler?: ILocationErrorCallback): Promise<ILocationSubscription>;
|
|
8
|
+
export declare function watchHeadingAsync(callback: ILocationHeadingCallback, errorHandler?: ILocationErrorCallback): Promise<ILocationSubscription>;
|
|
9
|
+
/** Waits for a couple of heading updates and returns the first accurate-enough one. */
|
|
10
|
+
export declare function getHeadingAsync(): Promise<ILocationHeadingObject>;
|
|
11
|
+
export declare function geocodeAsync(address: string): Promise<ILocationGeocodedLocation[]>;
|
|
12
|
+
export declare function reverseGeocodeAsync(location: Pick<ILocationGeocodedLocation, 'latitude' | 'longitude'>): Promise<ILocationGeocodedAddress[]>;
|
|
13
|
+
export declare function getForegroundPermissionsAsync(): Promise<ILocationPermissionResponse>;
|
|
14
|
+
export declare function requestForegroundPermissionsAsync(): Promise<ILocationPermissionResponse>;
|
|
15
|
+
export declare const useForegroundPermissions: (options?: import("expo-modules-core").PermissionHookOptions<object> | undefined) => [ILocationPermissionResponse | null, () => Promise<ILocationPermissionResponse>, () => Promise<ILocationPermissionResponse>];
|
|
16
|
+
export declare function getBackgroundPermissionsAsync(): Promise<PermissionResponse>;
|
|
17
|
+
export declare function requestBackgroundPermissionsAsync(): Promise<PermissionResponse>;
|
|
18
|
+
export declare const useBackgroundPermissions: (options?: import("expo-modules-core").PermissionHookOptions<object> | undefined) => [PermissionResponse | null, () => Promise<PermissionResponse>, () => Promise<PermissionResponse>];
|
|
19
|
+
export declare function hasServicesEnabledAsync(): Promise<boolean>;
|
|
20
|
+
export declare function getMotionActivityPermissionsAsync(): Promise<PermissionResponse>;
|
|
21
|
+
export declare function requestMotionActivityPermissionsAsync(): Promise<PermissionResponse>;
|
|
22
|
+
export declare const useMotionActivityPermissions: (options?: import("expo-modules-core").PermissionHookOptions<object> | undefined) => [PermissionResponse | null, () => Promise<PermissionResponse>, () => Promise<PermissionResponse>];
|
|
23
|
+
/** No location permission required — uses Play Services activity recognition / the iOS motion
|
|
24
|
+
* coprocessor directly. */
|
|
25
|
+
export declare function getMotionActivityAsync(): Promise<IMotionActivityObject>;
|
|
26
|
+
/** Foreground-only — updates pause while the app is backgrounded. */
|
|
27
|
+
export declare function watchMotionActivityAsync(callback: IMotionActivityCallback, errorHandler?: ILocationErrorCallback): Promise<ILocationSubscription>;
|
|
28
|
+
export declare function isBackgroundLocationAvailableAsync(): Promise<boolean>;
|
|
29
|
+
/** Register a task (defined with `@symbiote-native/task-manager`'s `defineTask`) to keep
|
|
30
|
+
* receiving location updates while the app is backgrounded. */
|
|
31
|
+
export declare function startLocationUpdatesAsync(taskName: string, options?: ILocationTaskOptions): Promise<void>;
|
|
32
|
+
export declare function stopLocationUpdatesAsync(taskName: string): Promise<void>;
|
|
33
|
+
export declare function hasStartedLocationUpdatesAsync(taskName: string): Promise<boolean>;
|
|
34
|
+
/** Register a task (defined with `@symbiote-native/task-manager`'s `defineTask`) to receive
|
|
35
|
+
* enter/exit events for the given regions, including while the app is backgrounded. Calling
|
|
36
|
+
* this again with a new region array replaces the regions already being watched. */
|
|
37
|
+
export declare function startGeofencingAsync(taskName: string, regions?: ILocationRegion[]): Promise<void>;
|
|
38
|
+
export declare function stopGeofencingAsync(taskName: string): Promise<void>;
|
|
39
|
+
export declare function hasStartedGeofencingAsync(taskName: string): Promise<boolean>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createPermissionHook, Platform } from 'expo-modules-core';
|
|
2
|
+
import { expoLocation } from './native-module.js';
|
|
3
|
+
import { headingSubscriber, locationErrorSubscriber, locationSubscriber, motionActivitySubscriber, } from './subscribers.js';
|
|
4
|
+
import { LocationAccuracy } from './types.js';
|
|
5
|
+
export async function getProviderStatusAsync() {
|
|
6
|
+
return expoLocation.getProviderStatusAsync();
|
|
7
|
+
}
|
|
8
|
+
/** @platform android */
|
|
9
|
+
export async function enableNetworkProviderAsync() {
|
|
10
|
+
if (Platform.OS === 'android') {
|
|
11
|
+
return expoLocation.enableNetworkProviderAsync();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function getCurrentPositionAsync(options = {}) {
|
|
15
|
+
return expoLocation.getCurrentPositionAsync(options);
|
|
16
|
+
}
|
|
17
|
+
export async function getLastKnownPositionAsync(options = {}) {
|
|
18
|
+
return expoLocation.getLastKnownPositionAsync(options);
|
|
19
|
+
}
|
|
20
|
+
export async function watchPositionAsync(options, callback, errorHandler) {
|
|
21
|
+
const watchId = locationSubscriber.registerCallback(callback);
|
|
22
|
+
if (errorHandler)
|
|
23
|
+
locationErrorSubscriber.registerCallbackForId(watchId, errorHandler);
|
|
24
|
+
await expoLocation.watchPositionImplAsync(watchId, options);
|
|
25
|
+
return {
|
|
26
|
+
remove() {
|
|
27
|
+
locationSubscriber.unregisterCallback(watchId);
|
|
28
|
+
if (errorHandler)
|
|
29
|
+
locationErrorSubscriber.unregisterCallback(watchId);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function watchHeadingAsync(callback, errorHandler) {
|
|
34
|
+
const watchId = headingSubscriber.registerCallback(callback);
|
|
35
|
+
if (errorHandler)
|
|
36
|
+
locationErrorSubscriber.registerCallbackForId(watchId, errorHandler);
|
|
37
|
+
await expoLocation.watchDeviceHeading(watchId);
|
|
38
|
+
return {
|
|
39
|
+
remove() {
|
|
40
|
+
headingSubscriber.unregisterCallback(watchId);
|
|
41
|
+
if (errorHandler)
|
|
42
|
+
locationErrorSubscriber.unregisterCallback(watchId);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Waits for a couple of heading updates and returns the first accurate-enough one. */
|
|
47
|
+
export async function getHeadingAsync() {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
let tries = 0;
|
|
50
|
+
let subscriber;
|
|
51
|
+
watchHeadingAsync(heading => {
|
|
52
|
+
if (heading.accuracy > 1 || tries > 5) {
|
|
53
|
+
subscriber?.remove();
|
|
54
|
+
resolve(heading);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
tries += 1;
|
|
58
|
+
}
|
|
59
|
+
}, reason => {
|
|
60
|
+
subscriber?.remove();
|
|
61
|
+
reject(new Error(reason));
|
|
62
|
+
})
|
|
63
|
+
.then(sub => {
|
|
64
|
+
subscriber = sub;
|
|
65
|
+
})
|
|
66
|
+
.catch(reject);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function geocodeAsync(address) {
|
|
70
|
+
if (typeof address !== 'string') {
|
|
71
|
+
throw new TypeError(`Address to geocode must be a string. Got ${address} instead.`);
|
|
72
|
+
}
|
|
73
|
+
return expoLocation.geocodeAsync(address);
|
|
74
|
+
}
|
|
75
|
+
export async function reverseGeocodeAsync(location) {
|
|
76
|
+
if (typeof location.latitude !== 'number' ||
|
|
77
|
+
typeof location.longitude !== 'number') {
|
|
78
|
+
throw new TypeError('Location to reverse-geocode must be an object with number properties `latitude` and `longitude`.');
|
|
79
|
+
}
|
|
80
|
+
return expoLocation.reverseGeocodeAsync(location);
|
|
81
|
+
}
|
|
82
|
+
export async function getForegroundPermissionsAsync() {
|
|
83
|
+
return expoLocation.getForegroundPermissionsAsync();
|
|
84
|
+
}
|
|
85
|
+
export async function requestForegroundPermissionsAsync() {
|
|
86
|
+
return expoLocation.requestForegroundPermissionsAsync();
|
|
87
|
+
}
|
|
88
|
+
export const useForegroundPermissions = createPermissionHook({
|
|
89
|
+
getMethod: getForegroundPermissionsAsync,
|
|
90
|
+
requestMethod: requestForegroundPermissionsAsync,
|
|
91
|
+
});
|
|
92
|
+
export async function getBackgroundPermissionsAsync() {
|
|
93
|
+
return expoLocation.getBackgroundPermissionsAsync();
|
|
94
|
+
}
|
|
95
|
+
export async function requestBackgroundPermissionsAsync() {
|
|
96
|
+
return expoLocation.requestBackgroundPermissionsAsync();
|
|
97
|
+
}
|
|
98
|
+
export const useBackgroundPermissions = createPermissionHook({
|
|
99
|
+
getMethod: getBackgroundPermissionsAsync,
|
|
100
|
+
requestMethod: requestBackgroundPermissionsAsync,
|
|
101
|
+
});
|
|
102
|
+
export async function hasServicesEnabledAsync() {
|
|
103
|
+
return expoLocation.hasServicesEnabledAsync();
|
|
104
|
+
}
|
|
105
|
+
export async function getMotionActivityPermissionsAsync() {
|
|
106
|
+
return expoLocation.getMotionActivityPermissionsAsync();
|
|
107
|
+
}
|
|
108
|
+
export async function requestMotionActivityPermissionsAsync() {
|
|
109
|
+
return expoLocation.requestMotionActivityPermissionsAsync();
|
|
110
|
+
}
|
|
111
|
+
export const useMotionActivityPermissions = createPermissionHook({
|
|
112
|
+
getMethod: getMotionActivityPermissionsAsync,
|
|
113
|
+
requestMethod: requestMotionActivityPermissionsAsync,
|
|
114
|
+
});
|
|
115
|
+
/** No location permission required — uses Play Services activity recognition / the iOS motion
|
|
116
|
+
* coprocessor directly. */
|
|
117
|
+
export async function getMotionActivityAsync() {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
let subscriber;
|
|
120
|
+
watchMotionActivityAsync(activity => {
|
|
121
|
+
subscriber?.remove();
|
|
122
|
+
resolve(activity);
|
|
123
|
+
}, reason => {
|
|
124
|
+
subscriber?.remove();
|
|
125
|
+
reject(new Error(reason));
|
|
126
|
+
})
|
|
127
|
+
.then(sub => {
|
|
128
|
+
subscriber = sub;
|
|
129
|
+
})
|
|
130
|
+
.catch(reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** Foreground-only — updates pause while the app is backgrounded. */
|
|
134
|
+
export async function watchMotionActivityAsync(callback, errorHandler) {
|
|
135
|
+
const watchId = motionActivitySubscriber.registerCallback(callback);
|
|
136
|
+
if (errorHandler)
|
|
137
|
+
locationErrorSubscriber.registerCallbackForId(watchId, errorHandler);
|
|
138
|
+
await expoLocation.watchMotionActivityImplAsync(watchId);
|
|
139
|
+
return {
|
|
140
|
+
remove() {
|
|
141
|
+
motionActivitySubscriber.unregisterCallback(watchId);
|
|
142
|
+
// forgetCallback, not unregisterCallback: the native watch was already torn down above by
|
|
143
|
+
// motionActivitySubscriber; re-issuing removeWatchAsync for the same id would hit this
|
|
144
|
+
// native module's location-permission guard and throw for a motion-activity-only caller.
|
|
145
|
+
if (errorHandler)
|
|
146
|
+
locationErrorSubscriber.forgetCallback(watchId);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function assertValidTaskName(taskName) {
|
|
151
|
+
if (!taskName || typeof taskName !== 'string') {
|
|
152
|
+
throw new Error(`\`taskName\` must be a non-empty string. Got ${taskName} instead.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function assertValidRegions(regions) {
|
|
156
|
+
if (!regions || regions.length === 0) {
|
|
157
|
+
throw new Error('Regions array cannot be empty. Use `stopGeofencingAsync` if you want to stop geofencing all regions');
|
|
158
|
+
}
|
|
159
|
+
for (const region of regions) {
|
|
160
|
+
if (typeof region.latitude !== 'number') {
|
|
161
|
+
throw new TypeError(`Region's latitude must be a number. Got '${region.latitude}' instead.`);
|
|
162
|
+
}
|
|
163
|
+
if (typeof region.longitude !== 'number') {
|
|
164
|
+
throw new TypeError(`Region's longitude must be a number. Got '${region.longitude}' instead.`);
|
|
165
|
+
}
|
|
166
|
+
if (typeof region.radius !== 'number') {
|
|
167
|
+
throw new TypeError(`Region's radius must be a number. Got '${region.radius}' instead.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export async function isBackgroundLocationAvailableAsync() {
|
|
172
|
+
const providerStatus = await getProviderStatusAsync();
|
|
173
|
+
return providerStatus.backgroundModeEnabled;
|
|
174
|
+
}
|
|
175
|
+
/** Register a task (defined with `@symbiote-native/task-manager`'s `defineTask`) to keep
|
|
176
|
+
* receiving location updates while the app is backgrounded. */
|
|
177
|
+
export async function startLocationUpdatesAsync(taskName, options = { accuracy: LocationAccuracy.Balanced }) {
|
|
178
|
+
assertValidTaskName(taskName);
|
|
179
|
+
await expoLocation.startLocationUpdatesAsync(taskName, options);
|
|
180
|
+
}
|
|
181
|
+
export async function stopLocationUpdatesAsync(taskName) {
|
|
182
|
+
assertValidTaskName(taskName);
|
|
183
|
+
await expoLocation.stopLocationUpdatesAsync(taskName);
|
|
184
|
+
}
|
|
185
|
+
export async function hasStartedLocationUpdatesAsync(taskName) {
|
|
186
|
+
assertValidTaskName(taskName);
|
|
187
|
+
return expoLocation.hasStartedLocationUpdatesAsync(taskName);
|
|
188
|
+
}
|
|
189
|
+
/** Register a task (defined with `@symbiote-native/task-manager`'s `defineTask`) to receive
|
|
190
|
+
* enter/exit events for the given regions, including while the app is backgrounded. Calling
|
|
191
|
+
* this again with a new region array replaces the regions already being watched. */
|
|
192
|
+
export async function startGeofencingAsync(taskName, regions = []) {
|
|
193
|
+
assertValidTaskName(taskName);
|
|
194
|
+
assertValidRegions(regions);
|
|
195
|
+
await expoLocation.startGeofencingAsync(taskName, { regions });
|
|
196
|
+
}
|
|
197
|
+
export async function stopGeofencingAsync(taskName) {
|
|
198
|
+
assertValidTaskName(taskName);
|
|
199
|
+
await expoLocation.stopGeofencingAsync(taskName);
|
|
200
|
+
}
|
|
201
|
+
export async function hasStartedGeofencingAsync(taskName) {
|
|
202
|
+
assertValidTaskName(taskName);
|
|
203
|
+
return expoLocation.hasStartedGeofencingAsync(taskName);
|
|
204
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import type { ILocationGeocodedAddress, ILocationGeocodedLocation, ILocationLastKnownOptions, ILocationObject, ILocationOptions, ILocationPermissionResponse, ILocationProviderStatus, ILocationRegion, ILocationTaskOptions, PermissionResponse } from './types';
|
|
3
|
+
export type INativeLocationModule = {
|
|
4
|
+
addListener(eventName: string, listener: (event: unknown) => void): EventSubscription;
|
|
5
|
+
getProviderStatusAsync(): Promise<ILocationProviderStatus>;
|
|
6
|
+
enableNetworkProviderAsync(): Promise<void>;
|
|
7
|
+
getCurrentPositionAsync(options: ILocationOptions): Promise<ILocationObject>;
|
|
8
|
+
getLastKnownPositionAsync(options: ILocationLastKnownOptions): Promise<ILocationObject | null>;
|
|
9
|
+
watchPositionImplAsync(watchId: number, options: ILocationOptions): Promise<void>;
|
|
10
|
+
watchDeviceHeading(watchId: number): Promise<void>;
|
|
11
|
+
removeWatchAsync(watchId: number): Promise<void>;
|
|
12
|
+
geocodeAsync(address: string): Promise<ILocationGeocodedLocation[]>;
|
|
13
|
+
reverseGeocodeAsync(location: Pick<ILocationGeocodedLocation, 'latitude' | 'longitude'>): Promise<ILocationGeocodedAddress[]>;
|
|
14
|
+
getForegroundPermissionsAsync(): Promise<ILocationPermissionResponse>;
|
|
15
|
+
requestForegroundPermissionsAsync(): Promise<ILocationPermissionResponse>;
|
|
16
|
+
getBackgroundPermissionsAsync(): Promise<PermissionResponse>;
|
|
17
|
+
requestBackgroundPermissionsAsync(): Promise<PermissionResponse>;
|
|
18
|
+
hasServicesEnabledAsync(): Promise<boolean>;
|
|
19
|
+
getMotionActivityPermissionsAsync(): Promise<PermissionResponse>;
|
|
20
|
+
requestMotionActivityPermissionsAsync(): Promise<PermissionResponse>;
|
|
21
|
+
watchMotionActivityImplAsync(watchId: number): Promise<void>;
|
|
22
|
+
startLocationUpdatesAsync(taskName: string, options: ILocationTaskOptions): Promise<void>;
|
|
23
|
+
stopLocationUpdatesAsync(taskName: string): Promise<void>;
|
|
24
|
+
hasStartedLocationUpdatesAsync(taskName: string): Promise<boolean>;
|
|
25
|
+
startGeofencingAsync(taskName: string, options: {
|
|
26
|
+
regions: ILocationRegion[];
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
stopGeofencingAsync(taskName: string): Promise<void>;
|
|
29
|
+
hasStartedGeofencingAsync(taskName: string): Promise<boolean>;
|
|
30
|
+
};
|
|
31
|
+
export declare const expoLocation: INativeLocationModule;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ILocationCallback, ILocationErrorCallback, ILocationHeadingCallback, IMotionActivityCallback } from './types';
|
|
2
|
+
declare class Subscriber<TCallback extends ILocationCallback | ILocationHeadingCallback | ILocationErrorCallback | IMotionActivityCallback> {
|
|
3
|
+
private eventName;
|
|
4
|
+
private eventDataField;
|
|
5
|
+
private callbacks;
|
|
6
|
+
private eventSubscription;
|
|
7
|
+
constructor(eventName: string, eventDataField: string);
|
|
8
|
+
private maybeInitializeSubscription;
|
|
9
|
+
registerCallback(callback: TCallback): number;
|
|
10
|
+
registerCallbackForId(watchId: number, callback: TCallback): number;
|
|
11
|
+
unregisterCallback(id: number): void;
|
|
12
|
+
/** Drops the local callback without calling native removeWatchAsync — used when another
|
|
13
|
+
* subscriber already tore down the same watchId natively. */
|
|
14
|
+
forgetCallback(id: number): void;
|
|
15
|
+
private releaseSubscriptionIfIdle;
|
|
16
|
+
private trigger;
|
|
17
|
+
}
|
|
18
|
+
export declare const locationSubscriber: Subscriber<ILocationCallback>;
|
|
19
|
+
export declare const headingSubscriber: Subscriber<ILocationHeadingCallback>;
|
|
20
|
+
export declare const locationErrorSubscriber: Subscriber<ILocationErrorCallback>;
|
|
21
|
+
export declare const motionActivitySubscriber: Subscriber<IMotionActivityCallback>;
|
|
22
|
+
export declare function getCurrentWatchId(): number;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { expoLocation } from './native-module.js';
|
|
2
|
+
let nextWatchId = 0;
|
|
3
|
+
class Subscriber {
|
|
4
|
+
eventName;
|
|
5
|
+
eventDataField;
|
|
6
|
+
callbacks = {};
|
|
7
|
+
eventSubscription = null;
|
|
8
|
+
constructor(eventName, eventDataField) {
|
|
9
|
+
this.eventName = eventName;
|
|
10
|
+
this.eventDataField = eventDataField;
|
|
11
|
+
}
|
|
12
|
+
maybeInitializeSubscription() {
|
|
13
|
+
if (this.eventSubscription)
|
|
14
|
+
return;
|
|
15
|
+
this.eventSubscription = expoLocation.addListener(this.eventName, (event) => this.trigger(event));
|
|
16
|
+
}
|
|
17
|
+
registerCallback(callback) {
|
|
18
|
+
this.maybeInitializeSubscription();
|
|
19
|
+
const id = ++nextWatchId;
|
|
20
|
+
this.callbacks[id] = callback;
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
registerCallbackForId(watchId, callback) {
|
|
24
|
+
this.maybeInitializeSubscription();
|
|
25
|
+
this.callbacks[watchId] = callback;
|
|
26
|
+
return watchId;
|
|
27
|
+
}
|
|
28
|
+
unregisterCallback(id) {
|
|
29
|
+
if (!this.callbacks[id])
|
|
30
|
+
return;
|
|
31
|
+
delete this.callbacks[id];
|
|
32
|
+
void expoLocation.removeWatchAsync(id);
|
|
33
|
+
this.releaseSubscriptionIfIdle();
|
|
34
|
+
}
|
|
35
|
+
/** Drops the local callback without calling native removeWatchAsync — used when another
|
|
36
|
+
* subscriber already tore down the same watchId natively. */
|
|
37
|
+
forgetCallback(id) {
|
|
38
|
+
if (!this.callbacks[id])
|
|
39
|
+
return;
|
|
40
|
+
delete this.callbacks[id];
|
|
41
|
+
this.releaseSubscriptionIfIdle();
|
|
42
|
+
}
|
|
43
|
+
releaseSubscriptionIfIdle() {
|
|
44
|
+
if (Object.keys(this.callbacks).length === 0 && this.eventSubscription) {
|
|
45
|
+
this.eventSubscription.remove();
|
|
46
|
+
this.eventSubscription = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
trigger(event) {
|
|
50
|
+
const callback = this.callbacks[event.watchId];
|
|
51
|
+
if (callback) {
|
|
52
|
+
callback(event[this.eventDataField]);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
void expoLocation.removeWatchAsync(event.watchId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const locationSubscriber = new Subscriber('Expo.locationChanged', 'location');
|
|
60
|
+
export const headingSubscriber = new Subscriber('Expo.headingChanged', 'heading');
|
|
61
|
+
export const locationErrorSubscriber = new Subscriber('Expo.locationError', 'reason');
|
|
62
|
+
export const motionActivitySubscriber = new Subscriber('Expo.motionActivityChanged', 'activity');
|
|
63
|
+
export function getCurrentWatchId() {
|
|
64
|
+
return nextWatchId;
|
|
65
|
+
}
|