react-native-acoustic-connect-beta 19.1.4 → 19.1.6

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.
@@ -148,6 +148,7 @@ Pod::Spec.new do |s|
148
148
  ts.source_files = [
149
149
  'ios/Tests/**/*.swift',
150
150
  'ios/ConnectRNParsing.swift',
151
+ 'ios/ConnectRNScreenResolution.swift',
151
152
  'nitrogen/generated/ios/swift/Variant_Bool_String_Double.swift',
152
153
  ]
153
154
  # ios/ConnectRNParsing.swift does `import Connect`. CocoaPods test_specs
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 19.1.6 (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.5 (2026-10-01)
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.4 (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 13:22:22 GMT 2026
1
+ #Thu Oct 01 15:10:29 GMT 2026
2
2
  UseWhiteList=true
3
3
  PrintScreen=3
4
4
  UseRandomSample=false
@@ -0,0 +1,116 @@
1
+ //
2
+ // Copyright (C) 2026 Acoustic, L.P. All rights reserved.
3
+ //
4
+ // NOTICE: This file contains material that is confidential and proprietary to
5
+ // Acoustic, L.P. and/or other developers. No license is granted under any
6
+ // intellectual or industrial property rights of Acoustic, L.P. except as may
7
+ // be provided in an agreement with Acoustic, L.P. Any unauthorized copying or
8
+ // distribution of content from this file is prohibited.
9
+ //
10
+
11
+ import UIKit
12
+
13
+ /// Walks from a window's root view controller down to the screen a layout
14
+ /// capture should describe.
15
+ ///
16
+ /// Split out of `HybridAcousticConnectRN` so the walk can be unit-tested
17
+ /// without a window, and compiled into the UnitTests bundle (see the
18
+ /// podspec's test_spec for why the pod module itself cannot be imported).
19
+ ///
20
+ /// Must be called on the main thread.
21
+ enum ConnectRNScreenResolution {
22
+
23
+ /// The view controller that is actually showing under `root`.
24
+ ///
25
+ /// Walks the `presentedViewController` chain, unwraps navigation and tab
26
+ /// controllers to the child on screen, and descends into
27
+ /// react-native-screens' controllers (see `activeScreen(in:)`).
28
+ ///
29
+ /// The react-native-screens step is what makes a capture name the same
30
+ /// controller the SDK records for the screen. Those controllers are plain
31
+ /// child view controllers of React Native's root, not navigation or tab
32
+ /// containers, so without it the walk stopped at the root. The SDK, which
33
+ /// records the `RNSScreen` from its `viewDidAppear`, then saw a capture of
34
+ /// some other controller and posted a second LOAD for a screen already on
35
+ /// display, naming itself as its referrer.
36
+ ///
37
+ /// - Parameter root: The window's root view controller.
38
+ /// - Returns: The view controller to capture, or `nil` when `root` is nil.
39
+ static func topViewController(from root: UIViewController?) -> UIViewController? {
40
+ var top = root
41
+ // Bounded rather than an open `while`. In practice the chain is a short
42
+ // linked list, but this runs on every navigation, so a pathological
43
+ // cycle must not hang the main thread.
44
+ var hops = 0
45
+ while hops < 32, let current = top {
46
+ hops += 1
47
+ // Presentation first: `presentedViewController` also reports a
48
+ // modal put up by a descendant, so checking it before unwrapping a
49
+ // container lands on the modal directly instead of on the
50
+ // container's child underneath it.
51
+ if let presented = current.presentedViewController {
52
+ top = presented
53
+ continue
54
+ }
55
+ if let nav = current as? UINavigationController,
56
+ let visible = nav.topViewController {
57
+ top = visible
58
+ continue
59
+ }
60
+ if let tabs = current as? UITabBarController,
61
+ let selected = tabs.selectedViewController {
62
+ top = selected
63
+ continue
64
+ }
65
+ if let screen = activeScreen(in: current) {
66
+ top = screen
67
+ continue
68
+ }
69
+ break
70
+ }
71
+ return top
72
+ }
73
+
74
+ /// The react-native-screens child of `viewController` that is showing, if
75
+ /// it has one.
76
+ ///
77
+ /// - A screen container (`RNSViewController`) is asked through its own
78
+ /// `findActiveChildVC`, which react-native-screens also uses to route
79
+ /// status-bar and orientation queries to the screen on top; during a
80
+ /// transition several screens are attached and only it knows which one.
81
+ /// - Anything else descends into its last visible react-native-screens
82
+ /// child: React Native's root holds the container that way, and a
83
+ /// screen holds a nested native stack (`RNSNavigationController`) that way.
84
+ ///
85
+ /// Only react-native-screens controllers are descended into, recognised by
86
+ /// the class-name prefix the SDK itself matches on (`isRNSScreen:`), so an
87
+ /// app that embeds React Native next to its own child controllers keeps
88
+ /// the controller it had before.
89
+ ///
90
+ /// - Parameter viewController: The controller the walk has reached.
91
+ /// - Returns: The child to continue from, or `nil` to stop at `viewController`.
92
+ static func activeScreen(in viewController: UIViewController) -> UIViewController? {
93
+ if isScreensController(viewController), let active = findActiveChild(of: viewController) {
94
+ return active
95
+ }
96
+ return viewController.children.last { child in
97
+ isScreensController(child) && child.viewIfLoaded?.isHidden != true
98
+ }
99
+ }
100
+
101
+ /// Whether `viewController` belongs to react-native-screens.
102
+ static func isScreensController(_ viewController: UIViewController) -> Bool {
103
+ NSStringFromClass(type(of: viewController)).hasPrefix("RNS")
104
+ }
105
+
106
+ private static let findActiveChildSelector = NSSelectorFromString("findActiveChildVC")
107
+
108
+ private static func findActiveChild(of container: UIViewController) -> UIViewController? {
109
+ guard container.responds(to: findActiveChildSelector),
110
+ let active = container.perform(findActiveChildSelector)?.takeUnretainedValue() as? UIViewController,
111
+ active.parent === container else {
112
+ return nil
113
+ }
114
+ return active
115
+ }
116
+ }
@@ -790,6 +790,11 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
790
790
  /// The framework only unwraps navigation controllers; capturing a tab
791
791
  /// controller is not wrong (its view contains the selected child), but
792
792
  /// the selected child is the screen the capture is named after.
793
+ /// - It descends into react-native-screens' controllers to the screen on
794
+ /// top (`ConnectRNScreenResolution`). The framework records that
795
+ /// `RNSScreen` as the current screen, and only a capture of the same
796
+ /// controller adds a layout to its screen view instead of posting a
797
+ /// second, self-referencing LOAD.
793
798
  ///
794
799
  /// Must be called on the main thread — see `captureScreenLayout`.
795
800
  private static func resolveTopViewController() -> UIViewController? {
@@ -810,34 +815,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
810
815
  return nil
811
816
  }
812
817
 
813
- var top = window.rootViewController
814
- // Bounded rather than an open `while`. In practice the chain is a short
815
- // linked list, but this runs on every navigation, so a pathological
816
- // cycle must not hang the main thread.
817
- var hops = 0
818
- while hops < 32 {
819
- hops += 1
820
- // Presentation first: `presentedViewController` also reports a
821
- // modal put up by a descendant, so checking it before unwrapping a
822
- // container lands on the modal directly instead of on the
823
- // container's child underneath it.
824
- if let presented = top?.presentedViewController {
825
- top = presented
826
- continue
827
- }
828
- if let nav = top as? UINavigationController,
829
- let visible = nav.topViewController {
830
- top = visible
831
- continue
832
- }
833
- if let tabs = top as? UITabBarController,
834
- let selected = tabs.selectedViewController {
835
- top = selected
836
- continue
837
- }
838
- break
839
- }
840
- return top
818
+ return ConnectRNScreenResolution.topViewController(from: window.rootViewController)
841
819
  }
842
820
 
843
821
  /// Resolves the top view controller and hands it to the framework, as one
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.4",
231
+ "version": "19.1.6",
232
232
  "workspaces": [
233
233
  "example",
234
234
  "Examples/bare-workflow"