@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +4 -0
  5. package/build/core/index.d.ts +5 -0
  6. package/build/core/index.js +5 -0
  7. package/build/core/location.d.ts +39 -0
  8. package/build/core/location.js +204 -0
  9. package/build/core/native-module.d.ts +31 -0
  10. package/build/core/native-module.js +3 -0
  11. package/build/core/subscribers.d.ts +23 -0
  12. package/build/core/subscribers.js +65 -0
  13. package/build/core/types.d.ts +160 -0
  14. package/build/core/types.js +44 -0
  15. package/build-ngc/angular/index.d.ts +1 -0
  16. package/build-ngc/angular/index.js +5 -0
  17. package/build-ngc/angular/index.js.map +1 -0
  18. package/build-ngc/core/index.d.ts +5 -0
  19. package/build-ngc/core/index.js +6 -0
  20. package/build-ngc/core/index.js.map +1 -0
  21. package/build-ngc/core/location.d.ts +39 -0
  22. package/build-ngc/core/location.js +205 -0
  23. package/build-ngc/core/location.js.map +1 -0
  24. package/build-ngc/core/native-module.d.ts +31 -0
  25. package/build-ngc/core/native-module.js +4 -0
  26. package/build-ngc/core/native-module.js.map +1 -0
  27. package/build-ngc/core/subscribers.d.ts +23 -0
  28. package/build-ngc/core/subscribers.js +66 -0
  29. package/build-ngc/core/subscribers.js.map +1 -0
  30. package/build-ngc/core/types.d.ts +160 -0
  31. package/build-ngc/core/types.js +45 -0
  32. package/build-ngc/core/types.js.map +1 -0
  33. package/native-link.json +40 -0
  34. package/package.json +151 -0
  35. package/src/angular/index.ts +4 -0
  36. package/src/core/index.ts +17 -0
  37. package/src/core/location.ts +313 -0
  38. package/src/core/native-module.ts +62 -0
  39. package/src/core/subscribers.ts +101 -0
  40. 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,4 @@
1
+ // @symbiote-native/location/angular: the Angular entry over the framework-agnostic core. Same
2
+ // reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
3
+ // service, so this is a plain re-export.
4
+ export * from '../core/index.js';
@@ -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,3 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ const EXPO_LOCATION_MODULE_NAME = 'ExpoLocation';
3
+ export const expoLocation = requireNativeModule(EXPO_LOCATION_MODULE_NAME);
@@ -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
+ }