Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 68 additions & 1 deletion __tests__/modules/location/locationManager.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NativeModules } from 'react-native';

import LocationManager, {
LocationModuleEventEmitter,
getLocationModuleEventEmitter,
} from '../../../src/modules/location/locationManager';

const MapboxGL = NativeModules.RNMBXModule;
Expand Down Expand Up @@ -148,6 +148,7 @@ describe('LocationManager', () => {
});

describe('#start', () => {
const LocationModuleEventEmitter = getLocationModuleEventEmitter();
jest.spyOn(MapboxGLLocationManager, 'start');
jest.spyOn(LocationModuleEventEmitter, 'addListener');

Expand Down Expand Up @@ -275,3 +276,69 @@ describe('LocationManager', () => {
});
});
});

describe('native location module', () => {
let restore;

afterEach(() => {
restore();
});

const loadPackage = (OS) => {
const reads = jest.fn();
let Mapbox;
let onAppStateChange;
jest.isolateModules(() => {
const {
AppState,
NativeModules: nativeModules,
Platform,
} = require('react-native');
const nativeLocationModule = nativeModules.RNMBXLocationModule;
const originalOS = Platform.OS;
restore = () => {
Platform.OS = originalOS;
Object.defineProperty(nativeModules, 'RNMBXLocationModule', {
configurable: true,
enumerable: true,
writable: true,
value: nativeLocationModule,
});
};
Platform.OS = OS;
Object.defineProperty(nativeModules, 'RNMBXLocationModule', {
configurable: true,
get() {
reads();
return nativeLocationModule;
},
});
AppState.addEventListener.mockClear();
Mapbox = require('../../../src').default;
[, onAppStateChange] = AppState.addEventListener.mock.calls.find(
([type]) => type === 'change',
);
});
return { reads, locationManager: Mapbox.locationManager, onAppStateChange };
};

describe.each(['ios', 'android'])('on %s', (OS) => {
test('is not read when the package loads', () => {
const { reads } = loadPackage(OS);
expect(reads).not.toHaveBeenCalled();
});

test('is not read when the app goes to the background and back', () => {
const { reads, onAppStateChange } = loadPackage(OS);
onAppStateChange('background');
onAppStateChange('active');
expect(reads).not.toHaveBeenCalled();
});

test('is read on first use', async () => {
const { reads, locationManager } = loadPackage(OS);
await locationManager.getLastKnownLocation();
expect(reads).toHaveBeenCalled();
});
});
});
67 changes: 44 additions & 23 deletions src/modules/location/locationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,44 @@ import {
type AppStateStatus,
Platform,
type EventSubscription,
TurboModuleRegistry,
} from 'react-native';

import NativeRNMBXLocationModule from '../../specs/NativeRNMBXLocationModule';
import type { Spec as NativeRNMBXLocationModuleSpec } from '../../specs/NativeRNMBXLocationModule';

const Mapbox = NativeModules.RNMBXModule;
const MapboxLocationManager: typeof NativeRNMBXLocationModule = Platform.select(
{
ios: NativeModules.RNMBXLocationModule,
android: NativeRNMBXLocationModule,
},
);

const IsTurbo: boolean =
typeof MapboxLocationManager.onLocationUpdate === 'function';
// Read the native module on first use, not when the package loads: on iOS the first
// read creates it on the main thread, and its init reads
// CLLocationManager.accuracyAuthorization, which can block for seconds.
// stop() runs on every background transition, so it must not be the first read.
let mapboxLocationManager: NativeRNMBXLocationModuleSpec | undefined;

function getMapboxLocationManager(): NativeRNMBXLocationModuleSpec {
if (mapboxLocationManager === undefined) {
mapboxLocationManager =
Platform.OS === 'android'
? TurboModuleRegistry.getEnforcing<NativeRNMBXLocationModuleSpec>(
'RNMBXLocationModule',
)
: (NativeModules.RNMBXLocationModule as NativeRNMBXLocationModuleSpec);
}
return mapboxLocationManager;
}

let locationModuleEventEmitter: NativeEventEmitter | null | undefined;

export const LocationModuleEventEmitter =
Platform.OS === 'ios' || (Platform.OS === 'android' && !IsTurbo)
? new NativeEventEmitter(MapboxLocationManager as any)
: null;
export function getLocationModuleEventEmitter(): NativeEventEmitter | null {
if (locationModuleEventEmitter === undefined) {
const isTurbo =
typeof getMapboxLocationManager().onLocationUpdate === 'function';
locationModuleEventEmitter =
Platform.OS === 'ios' || (Platform.OS === 'android' && !isTurbo)
? new NativeEventEmitter(getMapboxLocationManager() as any)
: null;
}
return locationModuleEventEmitter;
}

/**
* Location sent by locationManager
Expand Down Expand Up @@ -112,7 +131,8 @@ export class LocationManager {
// let's silently catch it and simply log out
// instead of throwing an exception
try {
lastKnownLocation = await MapboxLocationManager.getLastKnownLocation();
lastKnownLocation =
await getMapboxLocationManager().getLastKnownLocation();
} catch (error) {
console.warn('locationManager Error: ', error);
}
Expand Down Expand Up @@ -175,17 +195,18 @@ export class LocationManager {
}

if (!this._isListening) {
MapboxLocationManager.start(validDisplacement);
getMapboxLocationManager().start(validDisplacement);
//Determine if TurboModules (new architecture) are available.

if (LocationModuleEventEmitter) {
const eventEmitter = getLocationModuleEventEmitter();
if (eventEmitter) {
// Cast to match NativeEventEmitter's strict signature - runtime behavior is correct
this.subscription = LocationModuleEventEmitter.addListener(
this.subscription = eventEmitter.addListener(
Mapbox.LocationCallbackName.Update,
this._onUpdate as (...args: readonly Object[]) => unknown,
);
} else {
this.subscription = MapboxLocationManager.onLocationUpdate(
this.subscription = getMapboxLocationManager().onLocationUpdate(
(location: any) => {
this._onUpdate(location.payload);
},
Expand All @@ -197,7 +218,7 @@ export class LocationManager {
}

stop() {
MapboxLocationManager.stop();
mapboxLocationManager?.stop();

if (this._isListening && this.subscription) {
this.subscription.remove();
Expand All @@ -208,11 +229,11 @@ export class LocationManager {

setMinDisplacement(minDisplacement: number) {
this._minDisplacement = minDisplacement;
MapboxLocationManager.setMinDisplacement(minDisplacement);
getMapboxLocationManager().setMinDisplacement(minDisplacement);
}

setRequestsAlwaysUse(requestsAlwaysUse: boolean) {
MapboxLocationManager.setRequestsAlwaysUse(requestsAlwaysUse);
getMapboxLocationManager().setRequestsAlwaysUse(requestsAlwaysUse);
this._requestsAlwaysUse = requestsAlwaysUse;
}

Expand All @@ -226,7 +247,7 @@ export class LocationManager {
* simulates location updates, experimental [V10, iOS only]
*/
_simulateHeading(changesPerSecond: number, increment: number) {
MapboxLocationManager.simulateHeading(changesPerSecond, increment);
getMapboxLocationManager().simulateHeading(changesPerSecond, increment);
}

/**
Expand All @@ -240,7 +261,7 @@ export class LocationManager {
* @return {void}
*/
setLocationEventThrottle(throttleValue: number) {
MapboxLocationManager.setLocationEventThrottle(throttleValue);
getMapboxLocationManager().setLocationEventThrottle(throttleValue);
}
}

Expand Down
Loading