Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3b700ac
fix(android): add null safety checks in various components to prevent…
ShahilMangroliya Mar 28, 2025
5956963
fix(android): set view tree lifecycle owner in RNMBXMapView to fix li…
ShahilMangroliya Mar 29, 2025
5cf6d01
fix(android): enhance null safety checks in image and style component…
ShahilMangroliya Mar 29, 2025
41ed721
fix(android): add logger import to ChangeLineOffsetsShapeAnimatorModu…
ShahilMangroliya Mar 31, 2025
fdc2a25
fix(android): improve null value handling in ReadableArray and Readab…
ShahilMangroliya Apr 11, 2025
5b40438
Added null handling in readableArray, added logging to RNMBXSHapeSour…
Magnus-V Apr 25, 2025
0680f71
reverse setViewTreeLifecycleOwner changes and fix errors
Magnus-V Apr 25, 2025
6cd1096
Merge remote-tracking branch 'new-upstream/main'
Magnus-V Apr 25, 2025
ff4b037
reinstate setViewTreeLifecycleOwner
Magnus-V Apr 25, 2025
7b8721b
update examples
Magnus-V May 5, 2025
cac1348
Merge pull request #1 from Magnus-V/update-examples
Magnus-V May 5, 2025
4498a65
Fix versions
Magnus-V May 6, 2025
c90295f
Updated, should work on iOS now
Magnus-V May 8, 2025
13b91b2
Generate to fox old arch ran
Magnus-V May 13, 2025
fec58c5
updated to avoid mismatching dependencies to fix type-errors
Magnus-V May 15, 2025
799b866
fix: unittests
mfazekas May 22, 2025
2ad974a
fix: support older android sdk as well
mfazekas May 25, 2025
e030889
lifecycle refactor
mfazekas May 25, 2025
98d5d43
use targetSdkVersion for checking lifecycle version
mfazekas May 25, 2025
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
24 changes: 14 additions & 10 deletions __tests__/__mocks__/react-native.mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => {
};
});


jest.mock('react-native/Libraries/Utilities/Platform', () => ({
OS: 'ios', // or 'android'
select: (x) => {
if (x.android) {
return x.android;
} else if (x.native) {
return x.native;
} else if (x.default) {
return x.default;
}
},
__esModule: true,
default: {
OS: 'ios', // or 'android'
select: (x) => {
if (x.android) {
return x.android;
} else if (x.native) {
return x.native;
} else if (x.default) {
return x.default;
}
},
}
}));

jest.mock('react-native/src/private/animated/NativeAnimatedHelper', () => ({
Expand Down
6 changes: 2 additions & 4 deletions __tests__/components/BackgroundLayer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import BackgroundLayer from '../../src/components/BackgroundLayer';

describe('BackgroundLayer', () => {
test('renders correctly with default props', () => {
const { UNSAFE_root: backgroundLayer } = render(
const { root: backgroundLayer } = render(
<BackgroundLayer id="requiredBackgroundLayerID" />,
);

const { props } = backgroundLayer;

expect(props.sourceID).toStrictEqual('DefaultSourceID');
expect(backgroundLayer).toHaveProp('sourceID', 'DefaultSourceID');
});

test('renders correctly with custom props', () => {
Expand Down
6 changes: 3 additions & 3 deletions __tests__/components/Callout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ describe('Callout', () => {
tipStyle: { height: 4 },
textStyle: { height: 5 },
};
const { UNSAFE_getByType, UNSAFE_getAllByType } = render(
const result = render(
<Callout {...testProps} />,
);

const { UNSAFE_getByType, UNSAFE_getAllByType } = result
const callout = UNSAFE_getByType('RNMBXCallout');
const views = UNSAFE_getAllByType(View);
const text = UNSAFE_getByType(Text);

const calloutWrapperTestStyle = callout.props.style[1].height;
const animatedViewTestStyle = views[0].props.style.height;
const animatedViewTestStyle = views[0].props.style[1].height;
const wrapperViewTestStyle = views[1].props.style[1].height;
const tipViewTestStyle = views[2].props.style[1].height;
const textTestStyle = text.props.style[1].height;
Expand Down
6 changes: 2 additions & 4 deletions __tests__/components/CircleLayer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import CircleLayer from '../../src/components/CircleLayer';

describe('CircleLayer', () => {
test('renders correctly with default props', () => {
const { UNSAFE_root: circleLayer } = render(
const { root: circleLayer } = render(
<CircleLayer id="requiredCircleLayerID" />,
);
const { props } = circleLayer;

expect(props.sourceID).toStrictEqual('DefaultSourceID');
expect(circleLayer).toHaveProp('sourceID', 'DefaultSourceID');
});

test('renders correctly with custom props', () => {
Expand Down
5 changes: 2 additions & 3 deletions __tests__/components/HeatmapLayer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import HeatmapLayer from '../../src/components/HeatmapLayer';

describe('HeatmapLayer', () => {
test('renders correctly with default props', () => {
const { UNSAFE_root: heatmapLayer } = render(
const { root: heatmapLayer } = render(
<HeatmapLayer id="requiredHeatmapLayerID" />,
);
const { props } = heatmapLayer;
expect(props.sourceID).toStrictEqual('DefaultSourceID');
expect(heatmapLayer).toHaveProp('sourceID', 'DefaultSourceID');
});

test('renders correctly with custom props', () => {
Expand Down
39 changes: 24 additions & 15 deletions __tests__/components/UserLocation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,20 @@

render(<UserLocation onUpdate={onUpdateCallback} />);

locationManager._onUpdate({
coords: {
accuracy: 9.977999687194824,
altitude: 44.64373779296875,
heading: 251.5358428955078,
latitude: 51.5462244,
longitude: 4.1036916,
speed: 0.08543474227190018,
course: 251.5358428955078,
},
timestamp: 1573730357879,
});
act(() => {
locationManager._onUpdate({
coords: {
accuracy: 9.977999687194824,
altitude: 44.64373779296875,
heading: 251.5358428955078,
latitude: 51.5462244,
longitude: 4.1036916,
speed: 0.08543474227190018,
course: 251.5358428955078,
},
timestamp: 1573730357879,
});
})

expect(onUpdateCallback).toHaveBeenCalled();
});
Expand Down Expand Up @@ -168,7 +170,7 @@
expect(ul.locationManagerRunning).toStrictEqual(false);
});

// TODO: replace object { running: boolean } argument with simple boolean

Check warning on line 173 in __tests__/components/UserLocation.test.js

View workflow job for this annotation

GitHub Actions / lint_test_generate

Unexpected 'todo' comment: 'TODO: replace object { running: boolean...'
describe('#setLocationManager', () => {
test('called with "running" true', async () => {
const lastKnownLocation = [4.1036916, 51.5462244];
Expand All @@ -176,7 +178,9 @@

expect(ul.locationManagerRunning).toStrictEqual(false);

await ul.setLocationManager({ running: true });
await act(async () => {
await ul.setLocationManager({ running: true });
})

expect(ul.locationManagerRunning).toStrictEqual(true);
expect(locationManager.start).toHaveBeenCalledTimes(1);
Expand All @@ -192,11 +196,16 @@
test('called with "running" false', async () => {
// start
expect(ul.locationManagerRunning).toStrictEqual(false);
await ul.setLocationManager({ running: true });
await act(async () => {
await ul.setLocationManager({ running: true });
})

expect(ul.locationManagerRunning).toStrictEqual(true);

// stop
await ul.setLocationManager({ running: false });
await act(async () => {
await ul.setLocationManager({ running: false });
})

expect(ul.locationManagerRunning).toStrictEqual(false);
// only once from start
Expand Down
49 changes: 28 additions & 21 deletions __tests__/utils/animated/AnimatedCoordinatesArray.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FakeTimers from '@sinonjs/fake-timers';
import { Animated, Easing } from 'react-native';
import TestRenderer from 'react-test-renderer';
import { act } from '@testing-library/react-native';
import React from 'react';

import { AnimatedShape, AnimatedCoordinatesArray } from '../../../src/classes';
Expand Down Expand Up @@ -47,13 +48,15 @@ describe('AnimatedShapeSource', () => {
]);

let shapeSourceRef;
// eslint-disable-next-line no-unused-vars
const testRenderer = TestRenderer.create(
<AnimatedShapeSource
shape={new AnimatedShape({ type: 'LineString', coordinates })}
ref={(ref) => (shapeSourceRef = ref)}
/>,
);
act(() => {
// eslint-disable-next-line no-unused-vars
const testRenderer = TestRenderer.create(
<AnimatedShapeSource
shape={new AnimatedShape({ type: 'LineString', coordinates })}
ref={(ref) => (shapeSourceRef = ref)}
/>,
);
});
const setNativeProps = jest.fn();
_nativeRef(shapeSourceRef).setNativeProps = setNativeProps;

Expand Down Expand Up @@ -96,13 +99,15 @@ describe('AnimatedShapeSource', () => {
]);

let shapeSourceRef;
// eslint-disable-next-line no-unused-vars
const testRenderer = TestRenderer.create(
<AnimatedShapeSource
shape={new AnimatedShape({ type: 'LineString', coordinates })}
ref={(ref) => (shapeSourceRef = ref)}
/>,
);
act(() => {
// eslint-disable-next-line no-unused-vars
const testRenderer = TestRenderer.create(
<AnimatedShapeSource
shape={new AnimatedShape({ type: 'LineString', coordinates })}
ref={(ref) => (shapeSourceRef = ref)}
/>,
);
})
const setNativeProps = jest.fn();
_nativeRef(shapeSourceRef).setNativeProps = setNativeProps;

Expand Down Expand Up @@ -148,13 +153,15 @@ describe('AnimatedShapeSource', () => {
]);

let shapeSourceRef;
// eslint-disable-next-line no-unused-vars
const testRenderer = TestRenderer.create(
<AnimatedShapeSource
shape={new AnimatedShape({ type: 'LineString', coordinates })}
ref={(ref) => (shapeSourceRef = ref)}
/>,
);
act(() => {
// eslint-disable-next-line no-unused-vars
const testRenderer = TestRenderer.create(
<AnimatedShapeSource
shape={new AnimatedShape({ type: 'LineString', coordinates })}
ref={(ref) => (shapeSourceRef = ref)}
/>,
);
})
const setNativeProps = jest.fn();
_nativeRef(shapeSourceRef).setNativeProps = setNativeProps;

Expand Down
10 changes: 10 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ android {
java.srcDirs += 'src/main/mapbox-v11-compat/v10'
}
java.srcDirs += 'src/main/rn-compat/rn75'

// Add lifecycle compatibility source sets
// Apps targeting SDK 35+ typically use Lifecycle 2.6+ which changed from getLifecycle() to lifecycle property
def targetSdk = safeExtGet("targetSdkVersion", 28)
if (targetSdk >= 35) {
java.srcDirs += 'src/main/lifecycle-compat/v26'
} else {
java.srcDirs += 'src/main/lifecycle-compat/v25'
}

if (safeExtGet("RNMapboxMapsUseV11", false)) {
logger.warn("RNMapboxMapsUseV11 is deprecated, just set RNMapboxMapsVersion to 11.*")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.view.View
import com.facebook.react.bridge.*
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.RNMBXImagesManagerInterface
Expand All @@ -15,7 +14,6 @@ import com.mapbox.maps.ImageStretches
import com.rnmapbox.rnmbx.components.AbstractEventEmitter
import com.rnmapbox.rnmbx.events.constants.EventKeys
import com.rnmapbox.rnmbx.events.constants.eventMapOf
import com.rnmapbox.rnmbx.rncompat.dynamic.*
import com.rnmapbox.rnmbx.utils.ImageEntry
import com.rnmapbox.rnmbx.utils.Logger
import com.rnmapbox.rnmbx.utils.ResourceUtils
Expand Down Expand Up @@ -249,10 +247,14 @@ class RNMBXImagesManager(private val mContext: ReactApplicationContext) :
Logger.e("RNMBXImages", "each element of strech should be an array but was: ${array.getDynamic(i)}")
} else {
val pair = array.getArray(i)
if (pair.size() != 2 || pair.getType(0) != ReadableType.Number || pair.getType(1) != ReadableType.Number) {
Logger.e("RNMBXImages", "each element of stretch should be pair of 2 integers but was ${pair}")
if (pair != null) {
if (pair.size() != 2 || pair.getType(0) != ReadableType.Number || pair.getType(1) != ReadableType.Number) {
Logger.e("RNMBXImages", "each element of stretch should be pair of 2 integers but was ${pair}")
}
result.add(ImageStretches(pair.getDouble(0).toFloat(), pair.getDouble(1).toFloat()))
} else {
Logger.e("RNMBXImages", "each element of stretch should be an array but was null")
}
result.add(ImageStretches(pair.getDouble(0).toFloat(), pair.getDouble(1).toFloat()))
}
}
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import android.view.View.OnLayoutChangeListener
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewTreeLifecycleOwner
import com.facebook.react.bridge.*
import com.mapbox.android.gestures.*
import com.mapbox.bindgen.Value
Expand Down Expand Up @@ -76,6 +73,7 @@ import com.rnmapbox.rnmbx.v11compat.ornamentsettings.*
import org.json.JSONException
import org.json.JSONObject


fun <T> MutableList<T>.removeIf21(predicate: (T) -> Boolean): Boolean {
var removed = false
val iterator = this.iterator()
Expand All @@ -99,86 +97,11 @@ enum class MapGestureType {
Move,Scale,Rotate,Fling,Shove
}

/***
* Mapbox's MapView observers lifecycle events see MapboxLifecyclePluginImpl - (ON_START, ON_STOP, ON_DESTROY)
* We need to emulate those.
*/
interface RNMBXLifeCycleOwner : LifecycleOwner {
fun handleLifecycleEvent(event: Lifecycle.Event)
}

fun interface Cancelable {
fun cancel()
}

class RNMBXLifeCycle {
private var lifecycleOwner : RNMBXLifeCycleOwner? = null

fun onAttachedToWindow(view: View) {
if (lifecycleOwner == null) {
lifecycleOwner = object : RNMBXLifeCycleOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
init {
lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.currentState = Lifecycle.State.CREATED
}

override fun handleLifecycleEvent(event: Lifecycle.Event) {
try {
lifecycleRegistry.handleLifecycleEvent(event)
} catch (e: RuntimeException) {
Log.e("RNMBXMapView", "handleLifecycleEvent, handleLifecycleEvent error: $e")
}
}

override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
}
ViewTreeLifecycleOwner.set(view, lifecycleOwner);
}
lifecycleOwner?.handleLifecycleEvent(Lifecycle.Event.ON_START)
}

fun onDetachedFromWindow() {
if (lifecycleOwner?.lifecycle?.currentState == Lifecycle.State.DESTROYED) {
return
}
lifecycleOwner?.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_STOP)
}

fun onDestroy() {
if (lifecycleOwner?.lifecycle?.currentState == Lifecycle.State.STARTED || lifecycleOwner?.lifecycle?.currentState == Lifecycle.State.RESUMED) {
lifecycleOwner?.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_STOP)
}
if (lifecycleOwner?.lifecycle?.currentState != Lifecycle.State.DESTROYED) {
lifecycleOwner?.handleLifecycleEvent(androidx.lifecycle.Lifecycle.Event.ON_DESTROY)
}
}

fun getState() : Lifecycle.State {
return lifecycleOwner?.lifecycle?.currentState ?: Lifecycle.State.INITIALIZED;
}

var attachedToWindowWaiters : MutableList<()-> Unit> = mutableListOf()

fun callIfAttachedToWindow(callback: () -> Unit) : com.rnmapbox.rnmbx.components.mapview.Cancelable {
if (getState() == Lifecycle.State.STARTED) {
callback()
return com.rnmapbox.rnmbx.components.mapview.Cancelable {}
} else {
attachedToWindowWaiters.add(callback)
return com.rnmapbox.rnmbx.components.mapview.Cancelable {
attachedToWindowWaiters.removeIf21 { it === callback }
}
}
}

fun afterAttachFromLooper() {
attachedToWindowWaiters.forEach { it() }
attachedToWindowWaiters.clear()
}
}
// RNMBXLifeCycle is now provided by lifecycle-compat layer

data class FeatureEntry(val feature: AbstractMapFeature?, val view: View?, var addedToMap: Boolean = false) {

Expand Down
Loading
Loading