react-native-pointr 9.9.2 → 9.9.3

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [9.9.3] - 2026-07-22
8
+
9
+ ### Changed
10
+ - Mobile SDK 9.9.3 integration.
11
+
12
+
7
13
  ## [9.9.2] - 2026-07-09
8
14
 
9
15
  ### Changed
@@ -93,7 +93,7 @@ dependencies {
93
93
  //noinspection GradleDynamicVersion
94
94
  implementation "com.facebook.react:react-android:0.82.1"
95
95
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
96
- implementation("com.pointrlabs:pointr:9.9.2")
96
+ implementation("com.pointrlabs:pointr:9.9.3")
97
97
  implementation ("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
98
98
  implementation 'com.google.android.material:material:1.12.0'
99
99
  implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
@@ -245,6 +245,18 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
245
245
  Choreographer.getInstance().postFrameCallback(frameCallback)
246
246
  Log.v(name, "Adding itself as listener to map widget")
247
247
  ptrMapWidgetFragment?.addListener(this)
248
+ // The SDK adds the fragment via an ASYNCHRONOUS commit. Force it to complete
249
+ // now, while the container view is still attached. Otherwise, if the widget is
250
+ // closed before that add lands (e.g. closed mid-load), the queued transaction
251
+ // runs afterwards against the detached container and crashes with
252
+ // "No view found for id 0x…". Running it here removes that pending-add window.
253
+ try {
254
+ (reactContext.currentActivity as? FragmentActivity)
255
+ ?.supportFragmentManager
256
+ ?.executePendingTransactions()
257
+ } catch (e: Exception) {
258
+ Log.e(name, "executePendingTransactions after fragment create failed: ${e.message}")
259
+ }
248
260
  return ptrMapWidgetFragment!!
249
261
  }
250
262
 
@@ -263,6 +275,51 @@ class PTRMapWidgetManager(private val reactContext: ReactApplicationContext) :
263
275
  return frameLayout
264
276
  }
265
277
 
278
+ /**
279
+ * Called by React Native when the <PTRMapWidget /> view is unmounted.
280
+ *
281
+ * The map widget is a Fragment attached to the host Activity's FragmentManager,
282
+ * so removing the React view alone does NOT tear it down — its dialogs/popups
283
+ * (welcome, onboarding, init-error, info page, …) live at the window level and
284
+ * would otherwise remain visible after the widget is closed. Removing the
285
+ * Fragment runs its onDestroyView(), which dismisses every dialog it owns, and
286
+ * cancelling the frame callback stops the per-frame command loop from leaking.
287
+ */
288
+ override fun onDropViewInstance(view: FrameLayout) {
289
+ super.onDropViewInstance(view)
290
+
291
+ // Stop the per-frame loop that keeps re-measuring / re-triggering commands.
292
+ Choreographer.getInstance().removeFrameCallback(frameCallback)
293
+
294
+ val fragment = ptrMapWidgetFragment
295
+ ptrMapWidgetFragment = null
296
+ // Reset so a subsequent open builds a fresh widget and re-runs its command.
297
+ commandDidExecute = false
298
+ commandTriggered = false
299
+
300
+ if (fragment != null) {
301
+ fragment.removeListener(this)
302
+ val fragmentManager =
303
+ (reactContext.currentActivity as? FragmentActivity)?.supportFragmentManager
304
+ if (fragmentManager != null && !fragmentManager.isDestroyed && fragment.isAdded) {
305
+ // Remove the fragment SYNCHRONOUSLY (commitNow) while the container view
306
+ // still exists. An async commit()/commitAllowingStateLoss() would run a
307
+ // beat later — after React Native has already detached this FrameLayout —
308
+ // and the FragmentManager would then try to (re)create the fragment's view
309
+ // into a container that is gone, crashing with "No view found for id".
310
+ // Wrapped defensively so a lifecycle race can degrade to a no-op, never a
311
+ // crash, on the app's main thread.
312
+ try {
313
+ fragmentManager.beginTransaction()
314
+ .remove(fragment)
315
+ .commitNowAllowingStateLoss()
316
+ } catch (e: Exception) {
317
+ Log.e(name, "Failed to remove map widget fragment on view drop: ${e.message}")
318
+ }
319
+ }
320
+ }
321
+ }
322
+
266
323
  /**
267
324
  * Handle "create" command (called from JS) and call createFragment method
268
325
  */
@@ -155,6 +155,7 @@ class PointrModule(reactContext: ReactApplicationContext) :
155
155
 
156
156
  @ReactMethod
157
157
  fun stop() {
158
+ removePointrListeners()
158
159
  Pointr.getPointr()?.stop()
159
160
  }
160
161
 
@@ -339,6 +339,20 @@ import PointrKit
339
339
  }
340
340
  }
341
341
 
342
+ // React Native is unmounting this view when it is moved to a nil window.
343
+ // Tear the map widget down here so its popups/sheets cannot surface after the
344
+ // widget is closed: removing ourselves as a listener breaks the widget→container
345
+ // retain cycle, letting the view controller (and its whole view hierarchy,
346
+ // including any popup) deallocate instead of lingering.
347
+ override func willMove(toWindow newWindow: UIWindow?) {
348
+ super.willMove(toWindow: newWindow)
349
+ guard newWindow == nil, let mapWidget = mapWidget else { return }
350
+ mapWidget.removeListener(self)
351
+ mapWidget.view.removeFromSuperview()
352
+ wayfindingEventsAdapter = nil
353
+ self.mapWidget = nil
354
+ }
355
+
342
356
  private func applySDKConfigIfNeeded() {
343
357
  guard let jsonString = sdkConfig,
344
358
  let data = jsonString.data(using: .utf8),
@@ -224,10 +224,21 @@ class PTRNativeLibrary: RCTEventEmitter, PTREventManagerDelegate {
224
224
  } else {
225
225
  let dispatchGroup = DispatchGroup()
226
226
  dispatchGroup.enter()
227
+ // Pointr.shared.start(with:completion:) does not fire its completion
228
+ // exactly once — it's invoked on every subsequent state change too
229
+ // (e.g. the transition triggered by a later stop() call also matches
230
+ // `state.rawValue <= 0`). Guard so dispatchGroup.leave() only ever
231
+ // runs once; otherwise a second leave() on an already-balanced group
232
+ // is a fatal "BUG IN CLIENT OF LIBDISPATCH" crash.
233
+ var hasLeftDispatchGroup = false
234
+ let dispatchGroupLock = NSLock()
227
235
  Pointr.shared.start(with: PTRNativeLibrary.params) { state in
228
- if (state == .running || state.rawValue <= 0) {
229
- dispatchGroup.leave()
230
- }
236
+ guard state == .running || state.rawValue <= 0 else { return }
237
+ dispatchGroupLock.lock()
238
+ defer { dispatchGroupLock.unlock() }
239
+ guard !hasLeftDispatchGroup else { return }
240
+ hasLeftDispatchGroup = true
241
+ dispatchGroup.leave()
231
242
  }
232
243
  dispatchGroup.wait()
233
244
  if Pointr.shared.state == .running {
@@ -239,9 +250,20 @@ class PTRNativeLibrary: RCTEventEmitter, PTREventManagerDelegate {
239
250
  }
240
251
 
241
252
  @objc func stop() {
242
- Pointr.shared.positioningManager?.removeListener(self)
243
- Pointr.shared.geofenceManager?.removeListener(self)
244
- Pointr.shared.stop()
253
+ guard Pointr.shared.state == .running else {
254
+ // Nothing to tear down — avoids double-stop crashes if called twice
255
+ // in a row or before the SDK finished starting.
256
+ return
257
+ }
258
+ // This module reports `requiresMainQueueSetup() -> false`, so React Native
259
+ // invokes this method on a background queue. Pointr.shared.stop() tears down
260
+ // location/Bluetooth/map resources that must be released on the main thread,
261
+ // so hop over explicitly to avoid a crash.
262
+ DispatchQueue.main.async {
263
+ Pointr.shared.positioningManager?.removeListener(self)
264
+ Pointr.shared.geofenceManager?.removeListener(self)
265
+ Pointr.shared.stop()
266
+ }
245
267
  }
246
268
 
247
269
  @objc func requestPermissions() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-pointr",
3
- "version": "9.9.2",
3
+ "version": "9.9.3",
4
4
  "description": "Pointr React-Native Module",
5
5
  "main": "src/index",
6
6
  "files": [
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
16
16
 
17
17
  s.source_files = "ios/**/*.{h,m,mm,swift}"
18
18
 
19
- s.dependency 'PointrKit', '9.9.2'
19
+ s.dependency 'PointrKit', '9.9.3'
20
20
 
21
21
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
22
22
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.