react-native-acoustic-connect-beta 19.1.3 → 19.1.5

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
@@ -1,3 +1,13 @@
1
+ ## 19.1.5 (2026-10-01)
2
+
3
+ ### Reverts
4
+
5
+ * Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
6
+ ## 19.1.4 (2026-09-30)
7
+
8
+ ### Reverts
9
+
10
+ * Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
1
11
  ## 19.1.3 (2026-09-30)
2
12
 
3
13
  ### Reverts
package/README.md CHANGED
@@ -106,7 +106,7 @@ Field summary (see [API reference](#api-reference) for full semantics):
106
106
  | `AppKey` | _(required)_ | Your Connect application key. |
107
107
  | `PostMessageUrl` | _(required)_ | Collector endpoint URL. |
108
108
  | `KillSwitchUrl` | _(optional)_ | Kill-switch endpoint URL. |
109
- | `useRelease` | `false` | `true` selects the release AcousticConnect iOS pod over the debug variant. |
109
+ | `useRelease` | `false` | Release vs. beta native SDKs. iOS: `true` selects the release AcousticConnect pod over the debug variant. Android: `false` resolves Connect from the GitHub-hosted beta Maven repository (betas are not published to Maven Central); `true` resolves from Maven Central only and rejects beta versions. If your `settings.gradle` sets `repositoriesMode` to `PREFER_SETTINGS`, project repositories are ignored, so add the beta repository there yourself — the build prints the exact entry. |
110
110
  | `iOSVersion` | `""` | Pin a specific iOS pod version; empty = newest in the supported range. |
111
111
  | `AndroidVersion` | `""` | Pin a specific Android Connect SDK version; empty = newest in the supported range. |
112
112
  | `PushEnabled` | `false` | Master switch. On Android, also gates the `connect-push-fcm` artifact inclusion. |
@@ -165,6 +165,46 @@ android {
165
165
  }
166
166
  }
167
167
 
168
+ // Read consumer config at script-evaluation time so the right Connect Android
169
+ // artifact is selected without depending on config.gradle's text-rewriting
170
+ // pass (which only matches the analytics artifact name). Read first, because
171
+ // the repositories below depend on it:
172
+ // - `PushEnabled` picks between the analytics-only and push-fcm artifacts.
173
+ // - `AndroidVersion` (when set) pins the version against the active artifact.
174
+ // - `useRelease` picks where Connect comes from (see `allprojects` below).
175
+ import groovy.json.JsonSlurper
176
+ def acousticConfigFile = new File("$rootDir/../ConnectConfig.json")
177
+ def acousticConfig = acousticConfigFile.exists()
178
+ ? new JsonSlurper().parse(acousticConfigFile)
179
+ : [Connect: [:]]
180
+ def acousticPushEnabled = (acousticConfig.Connect?.PushEnabled ?: false).toString().toBoolean()
181
+ def acousticUseRelease = (acousticConfig.Connect?.useRelease ?: false).toString().toBoolean()
182
+ def acousticVersionPin = (acousticConfig.Connect?.AndroidVersion ?: '').trim()
183
+ def acousticVersion = acousticVersionPin.isEmpty() ? '+' : acousticVersionPin
184
+ def acousticArtifact = acousticPushEnabled ? 'connect-push-fcm' : 'connect'
185
+
186
+ println "AcousticConnectRN: PushEnabled=$acousticPushEnabled, useRelease=$acousticUseRelease, artifact=io.github.go-acoustic:$acousticArtifact:$acousticVersion"
187
+
188
+ // The supported Connect Android range — the same bounds as the `strictly`
189
+ // constraints below. Checked up front because the constraint cannot reject a
190
+ // pin on its own: a strict range is itself a request for the newest version in
191
+ // range, so an out-of-range pin was silently replaced by that version.
192
+ def acousticMinVersion = [11, 0, 11]
193
+ def acousticMaxVersionExclusive = [12, 0, 0]
194
+ def acousticSupportedRange = "[${acousticMinVersion.join('.')}, ${acousticMaxVersionExclusive.join('.')})"
195
+ if (!acousticVersionPin.isEmpty()) {
196
+ def m = acousticVersionPin =~ /^(\d+)\.(\d+)\.(\d+)/
197
+ def compare = { a, b -> [a, b].transpose().collect { x, y -> x <=> y }.find { it != 0 } ?: 0 }
198
+ def parts = m.find() ? [m.group(1), m.group(2), m.group(3)].collect { it as int } : null
199
+ if (parts == null || compare(parts, acousticMinVersion) < 0 || compare(parts, acousticMaxVersionExclusive) >= 0) {
200
+ throw new GradleException(
201
+ "AcousticConnectRN: ConnectConfig.json Connect.AndroidVersion '$acousticVersionPin' is outside the " +
202
+ "supported Connect Android range $acousticSupportedRange. " +
203
+ "Set a version in that range, or leave it empty to use the newest."
204
+ )
205
+ }
206
+ }
207
+
168
208
  repositories {
169
209
  mavenCentral()
170
210
  google()
@@ -174,36 +214,33 @@ allprojects {
174
214
  repositories {
175
215
  google()
176
216
 
177
- // Beta Connect artifacts publish HERE, not to Maven Central. Central is
178
- // still consulted below — it carries every version up to 11.0.31-beta —
179
- // but newer betas land only in this GitHub-hosted Maven tree, so a build
180
- // that does not know about it silently freezes `connect:+` at whatever
181
- // Central happens to hold. That failure is invisible: resolution
182
- // succeeds, just against a stale SDK, and the gap widens with each beta.
217
+ // `useRelease` decides where Connect comes from, for this module AND —
218
+ // via config.gradle, which declares the same switch in the consuming
219
+ // app — for the APK. The two must agree: this module compiles against
220
+ // whatever it resolves here, while the app packages what the app
221
+ // resolves, and a bridge compiled against a newer Connect than the APK
222
+ // carries fails at runtime with NoSuchMethodError, not at build time.
183
223
  //
184
- // Declaring it is what fixes that; the position is close to cosmetic.
185
- // `connect` is resolved through a dynamic version, and for those Gradle
186
- // queries every declared repository and takes the newest match across
187
- // all of them rather than stopping at the first repository that answers.
188
- // Verified: with this entry declared LAST, after mavenCentral(),
189
- // `connect:+` still resolves to the newest beta here rather than to
190
- // Central's older newest. Order only decides which repository serves a
191
- // PINNED version that both carry, where the artifact is identical
192
- // anyway — it is kept first so a pin on a beta is served by the tree
193
- // that is authoritative for betas.
224
+ // useRelease: false (default) — the GitHub-hosted Maven tree, where
225
+ // Connect betas publish (they do not reach Maven Central). Central
226
+ // is still consulted for versions this tree does not carry.
227
+ // useRelease: true — Maven Central only, betas rejected below.
194
228
  //
195
- // The content filter is not cosmetic. Without it every dependency in
196
- // the build (React Native, Kotlin, AndroidX, …) would hit
197
- // raw.githubusercontent and 404 before falling through to a repository
198
- // that has it. Inclusive `content`, not `exclusiveContent`, so Central
199
- // still answers for versions this tree does not carry.
229
+ // `connect` resolves through a dynamic version, so Gradle queries
230
+ // every declared repository and takes the newest match across all of
231
+ // them; position only matters for a pinned version both carry, where
232
+ // the artifact is identical. The content filter is not cosmetic:
233
+ // without it every dependency in the build would hit
234
+ // raw.githubusercontent and 404 before falling through.
200
235
  //
201
236
  // Declared only here, not also in the top-level `repositories` block:
202
237
  // `allprojects` covers this project too, so both would feed the same
203
238
  // resolution and the pair could drift.
204
- maven {
205
- url = "https://raw.githubusercontent.com/go-acoustic/Android_Maven/master"
206
- content { includeGroup "io.github.go-acoustic" }
239
+ if (!acousticUseRelease) {
240
+ maven {
241
+ url = "https://raw.githubusercontent.com/go-acoustic/Android_Maven/master"
242
+ content { includeGroup "io.github.go-acoustic" }
243
+ }
207
244
  }
208
245
 
209
246
  mavenCentral()
@@ -216,23 +253,22 @@ allprojects {
216
253
  }
217
254
  }
218
255
 
219
- // Read consumer config at script-evaluation time so the right Connect Android
220
- // artifact is selected without depending on config.gradle's text-rewriting
221
- // pass (which only matches the analytics artifact name). Both `PushEnabled`
222
- // and `AndroidVersion` are read directly here:
223
- // - `PushEnabled` picks between the analytics-only and push-fcm artifacts.
224
- // - `AndroidVersion` (when set) pins the version against the active artifact.
225
- import groovy.json.JsonSlurper
226
- def acousticConfigFile = new File("$rootDir/../ConnectConfig.json")
227
- def acousticConfig = acousticConfigFile.exists()
228
- ? new JsonSlurper().parse(acousticConfigFile)
229
- : [Connect: [:]]
230
- def acousticPushEnabled = (acousticConfig.Connect?.PushEnabled ?: false).toString().toBoolean()
231
- def acousticVersionPin = (acousticConfig.Connect?.AndroidVersion ?: '').trim()
232
- def acousticVersion = acousticVersionPin.isEmpty() ? '+' : acousticVersionPin
233
- def acousticArtifact = acousticPushEnabled ? 'connect-push-fcm' : 'connect'
234
-
235
- println "AcousticConnectRN: PushEnabled=$acousticPushEnabled, artifact=io.github.go-acoustic:$acousticArtifact:$acousticVersion"
256
+ // Same beta rejection config.gradle applies to the app, so with
257
+ // `useRelease: true` this module cannot compile against a beta either — a
258
+ // beta reached through the Sonatype staging repo or an `AndroidVersion` pin
259
+ // fails resolution here instead of diverging from the APK. eocore is exempt:
260
+ // its release line carries a `-beta` suffix (see config.gradle).
261
+ if (acousticUseRelease) {
262
+ configurations.all {
263
+ resolutionStrategy.componentSelection.all { selection ->
264
+ if (selection.candidate.group == 'io.github.go-acoustic' &&
265
+ selection.candidate.module != 'eocore' &&
266
+ selection.candidate.version.endsWith('-beta')) {
267
+ selection.reject("useRelease is true — rejecting beta version ${selection.candidate.version}")
268
+ }
269
+ }
270
+ }
271
+ }
236
272
 
237
273
  dependencies {
238
274
  // Provided by the host app at runtime — compileOnly avoids version conflicts
@@ -284,18 +320,33 @@ dependencies {
284
320
  // to releases that lack APIs this bridge calls; upper bound prevents a
285
321
  // future 12.x major (with potentially breaking changes) from being picked
286
322
  // up automatically by Gradle's `+` resolver. Both halves are `strictly` so
287
- // an attempted override via `Connect.AndroidVersion` fails loudly at
288
- // resolution time rather than surfacing as a runtime `ClassNotFoundException`
289
- // or method-missing error. Constraints are declared for both artifact
323
+ // nothing else in the build can move Connect outside them. A
324
+ // `Connect.AndroidVersion` outside the range fails at configuration time
325
+ // (the check after the config read above), not here. Constraints are declared for both artifact
290
326
  // names — Gradle only enforces the one that resolves into the graph, so
291
327
  // the unused entry is inert.
328
+ //
329
+ // An `AndroidVersion` pin is applied to BOTH constraints as `prefer`. With
330
+ // push enabled the only direct dependency is `connect-push-fcm`, so the pin
331
+ // used to reach `connect` only through that artifact's POM — and a strict
332
+ // range is itself a request that resolves to the newest version in range,
333
+ // so it outranked the transitive pin and floated `connect` past it
334
+ // (`connect-push-fcm:11.1.5-beta` shipped with `connect:11.1.6-beta`).
335
+ // `prefer` inside the strict range keeps the range as the hard bound and
336
+ // makes the pin the version chosen within it.
292
337
  constraints {
293
338
  implementation('io.github.go-acoustic:connect') {
294
- version { strictly '[11.0.11, 12.0.0)' }
339
+ version {
340
+ strictly acousticSupportedRange
341
+ if (!acousticVersionPin.isEmpty()) { prefer acousticVersionPin }
342
+ }
295
343
  because 'AcousticConnectRN requires Connect Android SDK in [11.0.11, 12.0.0). Bump the upper bound after validating compatibility with a new major.'
296
344
  }
297
345
  implementation('io.github.go-acoustic:connect-push-fcm') {
298
- version { strictly '[11.0.11, 12.0.0)' }
346
+ version {
347
+ strictly acousticSupportedRange
348
+ if (!acousticVersionPin.isEmpty()) { prefer acousticVersionPin }
349
+ }
299
350
  because 'connect-push-fcm tracks the same release line as the core connect artifact.'
300
351
  }
301
352
  }
@@ -425,13 +425,100 @@ def updateEOCoreAdvConfig(mp, json) {
425
425
  println("Updating:$eocoreAdvConfigPath")
426
426
  }
427
427
 
428
+ // With `useRelease: false`, the app has to resolve Connect from the beta tree
429
+ // too. The SDK module's android/build.gradle declares it for the module's own
430
+ // classpath, but the APK packages what *this* project — the app — resolves,
431
+ // and an app that cannot see the tree silently gets Central's newest release
432
+ // instead: a different Connect from the one the bridge was compiled against,
433
+ // which fails at runtime with NoSuchMethodError. This script is applied from
434
+ // the app's build.gradle, so the repository declared here is the app's.
435
+ //
436
+ // Apps that centralise repositories in settings.gradle cannot take a project
437
+ // repository: FAIL_ON_PROJECT_REPOS fails the build on one, PREFER_SETTINGS
438
+ // ignores it — for every project, the SDK module included, so the two still
439
+ // agree (on Central). For those, say what to add rather than fail or stay silent.
440
+ def addBetaRepository(proj) {
441
+ // The GitHub-hosted Maven tree Connect betas publish to. They do not reach
442
+ // Maven Central, which carries release versions only. Local to the method:
443
+ // a script-level `def` is not visible inside script methods.
444
+ def ACOUSTIC_BETA_REPO_URL = "https://raw.githubusercontent.com/go-acoustic/Android_Maven/master"
445
+ def mode = null
446
+ def inSettings = false
447
+ try {
448
+ def drm = proj.gradle.settings.dependencyResolutionManagement
449
+ mode = drm.repositoriesMode.get().toString()
450
+ inSettings = drm.repositories.any { repo ->
451
+ repo.hasProperty('url') && repo.url.toString().startsWith(ACOUSTIC_BETA_REPO_URL)
452
+ }
453
+ } catch (Throwable ignored) {
454
+ // Older Gradle, or no settings-level repository management: nothing to respect.
455
+ }
456
+ if (inSettings) {
457
+ println("AcousticConnect: useRelease is false — Connect beta repository already declared in settings.gradle")
458
+ return
459
+ }
460
+ if (mode == "FAIL_ON_PROJECT_REPOS" || mode == "PREFER_SETTINGS") {
461
+ println("WARNING: AcousticConnect: useRelease is false, but settings.gradle sets repositoriesMode " +
462
+ "$mode, so the Connect beta repository cannot be added to the app. Add it to " +
463
+ "dependencyResolutionManagement.repositories in settings.gradle:\n" +
464
+ " maven {\n" +
465
+ " url = \"$ACOUSTIC_BETA_REPO_URL\"\n" +
466
+ " content { includeGroup \"io.github.go-acoustic\" }\n" +
467
+ " }\n" +
468
+ "Without it Connect resolves from Maven Central only — for the app and this SDK alike, since " +
469
+ "settings repositories replace project ones — so betas are not used. Or set useRelease to " +
470
+ "true to make that explicit.")
471
+ return
472
+ }
473
+ proj.repositories {
474
+ maven {
475
+ url = ACOUSTIC_BETA_REPO_URL
476
+ content { includeGroup "io.github.go-acoustic" }
477
+ }
478
+ }
479
+ println("AcousticConnect: useRelease is false — resolving Connect from $ACOUSTIC_BETA_REPO_URL")
480
+ }
481
+
482
+ // With `useRelease: true`, reject every go-acoustic beta in the app's resolution.
483
+ // android/build.gradle applies the same rule to the SDK module.
484
+ def rejectBetaVersions(proj) {
485
+ proj.configurations.all {
486
+ resolutionStrategy {
487
+ componentSelection {
488
+ all { selection ->
489
+ // Exempt the eocore artifact from beta rejection.
490
+ // Full Maven coordinate: io.github.go-acoustic:eocore
491
+ // (verified on Maven Central — the POM declares
492
+ // <groupId>io.github.go-acoustic</groupId> +
493
+ // <artifactId>eocore</artifactId>, so candidate.module is
494
+ // literally 'eocore').
495
+ // Unlike the other go-acoustic artifacts, eocore's RELEASE
496
+ // line carries a `-beta` suffix — Maven Central lists
497
+ // 2.1.24-beta as <release>/<latest>, and the release
498
+ // connect:11.0.13 → tealeaf:10.4.28 chain pins eocore
499
+ // transitively to that version. Without this carve-out the
500
+ // blanket `-beta` reject would drop a legitimate release
501
+ // artifact and break every useRelease=true build.
502
+ if (selection.candidate.group == 'io.github.go-acoustic' &&
503
+ selection.candidate.module != 'eocore' &&
504
+ selection.candidate.version.endsWith('-beta')) {
505
+ selection.reject("useRelease is true — rejecting beta version ${selection.candidate.version}")
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+ }
512
+
428
513
  def updateGradle(mp, json){
429
514
  def buildGradlePath = "$mp/build.gradle"
430
515
  def buildGradleFile = new File(buildGradlePath)
431
516
  def updatedBuildGradle = buildGradleFile.text
432
517
 
433
518
  if (json != null && json.Connect) {
434
- def useRelease = json.Connect.useRelease ?: false
519
+ // Parsed as android/build.gradle parses it, so the module and the app agree on
520
+ // the value even when it is written as a string ("false" is truthy in Groovy).
521
+ def useRelease = (json.Connect.useRelease ?: false).toString().toBoolean()
435
522
 
436
523
  if(json.Connect.AndroidVersion != null) {
437
524
  def connectSDK = "io.github.go-acoustic:connect:"
@@ -443,33 +530,11 @@ def updateGradle(mp, json){
443
530
  updatedBuildGradle = updatedBuildGradle.replaceAll(~/io.github.go-acoustic:connect:(\+|\d+\.\d+\.\d+(-beta)?)/, connectSDK)
444
531
  }
445
532
 
533
+ // Exactly one of the two per build: release-only resolution, or the beta tree.
446
534
  if (useRelease) {
447
- project.configurations.all {
448
- resolutionStrategy {
449
- componentSelection {
450
- all { selection ->
451
- // Exempt the eocore artifact from beta rejection.
452
- // Full Maven coordinate: io.github.go-acoustic:eocore
453
- // (verified on Maven Central — the POM declares
454
- // <groupId>io.github.go-acoustic</groupId> +
455
- // <artifactId>eocore</artifactId>, so candidate.module is
456
- // literally 'eocore').
457
- // Unlike the other go-acoustic artifacts, eocore's RELEASE
458
- // line carries a `-beta` suffix — Maven Central lists
459
- // 2.1.24-beta as <release>/<latest>, and the release
460
- // connect:11.0.13 → tealeaf:10.4.28 chain pins eocore
461
- // transitively to that version. Without this carve-out the
462
- // blanket `-beta` reject would drop a legitimate release
463
- // artifact and break every useRelease=true build.
464
- if (selection.candidate.group == 'io.github.go-acoustic' &&
465
- selection.candidate.module != 'eocore' &&
466
- selection.candidate.version.endsWith('-beta')) {
467
- selection.reject("useRelease is true — rejecting beta version ${selection.candidate.version}")
468
- }
469
- }
470
- }
471
- }
472
- }
535
+ rejectBetaVersions(project)
536
+ } else {
537
+ addBetaRepository(project)
473
538
  }
474
539
  }
475
540
 
@@ -1,4 +1,4 @@
1
- #Wed Sep 30 09:13:37 GMT 2026
1
+ #Thu Oct 01 14:25:53 GMT 2026
2
2
  UseWhiteList=true
3
3
  PrintScreen=3
4
4
  UseRandomSample=false
@@ -819,32 +819,23 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
819
819
  /**
820
820
  * Log click events on react native control.
821
821
  *
822
+ * The view lookup and the SDK call both run on the main looper — see
823
+ * [withResolvedView].
824
+ *
822
825
  * @param target Target id of the control.
823
826
  * @param controlId Accessibility ID(virtual id).
824
- * @return True if the operation was successful, false otherwise.
827
+ * @return True if the event was handed to the main looper, false without a
828
+ * React context. It cannot report whether the view resolved.
825
829
  */
826
830
  override fun logClickEvent(target: Double, controlId: String): Boolean {
827
- val viewTag = target.toInt()
828
- return try {
829
- val ctx = applicationContext ?: return false
830
- val uiManager = UIManagerHelper.getUIManagerForReactTag(ctx, viewTag)
831
- ?: return false
832
- val view = uiManager.resolveView(viewTag) ?: return false
833
-
834
- Handler(Looper.getMainLooper()).post {
835
- val activity = getCurrentActivity() ?: return@post
836
- if (view is EditText) {
837
- addFocusAndRegister(view, null, activity)
838
- } else if (TextUtils.isEmpty(controlId)) {
839
- logEvent(view, "click")
840
- } else {
841
- logEvent(view, "click", controlId)
842
- }
831
+ return withResolvedView(target.toInt(), "logClickEvent") { view, activity ->
832
+ if (view is EditText) {
833
+ addFocusAndRegister(view, null, activity)
834
+ } else if (TextUtils.isEmpty(controlId)) {
835
+ logEvent(view, "click")
836
+ } else {
837
+ logEvent(view, "click", controlId)
843
838
  }
844
- true
845
- } catch (e: Exception) {
846
- Log.v(TAG, "logClickEvent error: ${e.message}", e)
847
- false
848
839
  }
849
840
  }
850
841
 
@@ -852,28 +843,16 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
852
843
  * Log click events on react native control.
853
844
  *
854
845
  * @param target Target id of the control.
855
- * @return True if the operation was successful, false otherwise.
846
+ * @return True if the event was handed to the main looper, false without a
847
+ * React context. It cannot report whether the view resolved.
856
848
  */
857
849
  fun logClickEvent(target: Double): Boolean {
858
- val viewTag = target.toInt()
859
- return try {
860
- val ctx = applicationContext ?: return false
861
- val uiManager = UIManagerHelper.getUIManagerForReactTag(ctx, viewTag)
862
- ?: return false
863
- val view = uiManager.resolveView(viewTag) ?: return false
864
-
865
- Handler(Looper.getMainLooper()).post {
866
- val activity = getCurrentActivity() ?: return@post
867
- if (view is EditText) {
868
- addFocusAndRegister(view, null, activity)
869
- } else {
870
- logEvent(view, "click")
871
- }
850
+ return withResolvedView(target.toInt(), "logClickEvent") { view, activity ->
851
+ if (view is EditText) {
852
+ addFocusAndRegister(view, null, activity)
853
+ } else {
854
+ logEvent(view, "click")
872
855
  }
873
- true
874
- } catch (e: Exception) {
875
- Log.v(TAG, "logClickEvent error: ${e.message}")
876
- false
877
856
  }
878
857
  }
879
858
 
@@ -883,30 +862,59 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
883
862
  * @param target A valid native View Id for lookup.
884
863
  * @param controlId Accessibility ID(virtual id).
885
864
  * @param text The input string.
886
- * @return True if the operation was successful, false otherwise.
865
+ * @return True if the event was handed to the main looper, false without a
866
+ * React context. It cannot report whether the view resolved.
887
867
  */
888
868
  override fun logTextChangeEvent(target: Double, controlId: String, text: Variant_NullType_String?): Boolean {
889
- val viewTag = target.toInt()
890
- return try {
891
- val ctx = applicationContext ?: return false
892
- val uiManager = UIManagerHelper.getUIManagerForReactTag(ctx, viewTag)
893
- ?: return false
894
- val view = uiManager.resolveView(viewTag) ?: return false
895
-
896
- Handler(Looper.getMainLooper()).post {
897
- val activity = getCurrentActivity() ?: return@post
898
- if (view is EditText && view.onFocusChangeListener == null) {
899
- logEvent(view, TLF_ON_FOCUS_CHANGE_IN, controlId)
900
- addFocusAndRegister(view, controlId, activity)
901
- }
869
+ return withResolvedView(target.toInt(), "logTextChangeEvent") { view, activity ->
870
+ if (view is EditText && view.onFocusChangeListener == null) {
871
+ logEvent(view, TLF_ON_FOCUS_CHANGE_IN, controlId)
872
+ addFocusAndRegister(view, controlId, activity)
902
873
  }
903
- true
904
- } catch (e: Exception) {
905
- Log.v(TAG, "logTextChangeEvent error: ${e.message}")
906
- false
907
874
  }
908
875
  }
909
876
 
877
+ /**
878
+ * Resolves the native view behind a React tag and hands it to [block], with
879
+ * the lookup and the block both running on the main looper.
880
+ *
881
+ * Nitro calls arrive on the JS thread, and Fabric's `resolveView` must run
882
+ * on the UI thread. The bridge used to resolve first and post second, so
883
+ * every tap logged `AssertionException: Expected to run on UI thread!` in
884
+ * debug builds. That assertion is soft — the view still came back — but
885
+ * the lookup was reading the mounting layer's view map while the UI thread
886
+ * mutates it.
887
+ *
888
+ * The whole block is guarded, not just the lookup. `getView` throws when
889
+ * the view was unmounted between the tap and this post running, and an
890
+ * exception escaping a posted block is uncaught on the main thread — a
891
+ * crash, where the old off-thread lookup only returned false.
892
+ *
893
+ * @param viewTag React tag of the touched view.
894
+ * @param api Name of the bridge method, for the log line.
895
+ * @param block Work to do with the resolved view and current activity;
896
+ * skipped when either is unavailable.
897
+ * @return False without a React context, true once the work is posted.
898
+ */
899
+ private fun withResolvedView(
900
+ viewTag: Int,
901
+ api: String,
902
+ block: (View, Activity) -> Unit
903
+ ): Boolean {
904
+ val ctx = applicationContext ?: return false
905
+ runOnMain {
906
+ try {
907
+ val view = UIManagerHelper.getUIManagerForReactTag(ctx, viewTag)
908
+ ?.resolveView(viewTag) ?: return@runOnMain
909
+ val activity = getCurrentActivity() ?: return@runOnMain
910
+ block(view, activity)
911
+ } catch (e: Exception) {
912
+ Log.v(TAG, "$api error: ${e.message}", e)
913
+ }
914
+ }
915
+ return true
916
+ }
917
+
910
918
  /**
911
919
  * Whether the bridge may touch capture at all.
912
920
  *
package/package.json CHANGED
@@ -228,7 +228,7 @@
228
228
  "source": "src/index",
229
229
  "summary": "react-native ios android tealeaf connect cxa wxca er enhanced-replay",
230
230
  "types": "./lib/typescript/src/index.d.ts",
231
- "version": "19.1.3",
231
+ "version": "19.1.5",
232
232
  "workspaces": [
233
233
  "example",
234
234
  "Examples/bare-workflow"