react-native-acoustic-connect-beta 18.0.27 → 18.0.29

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.
Files changed (44) hide show
  1. package/AcousticConnectRN.podspec +4 -4
  2. package/README.md +36 -7
  3. package/android/CMakeLists.txt +1 -1
  4. package/android/build.gradle +1 -1
  5. package/android/src/main/assets/ConnectBasicConfig.properties +1 -1
  6. package/android/src/main/java/com/acousticconnectrn/AcousticConnectRNPackage.java +1 -1
  7. package/android/src/main/java/com/acousticconnectrn/HybridAcousticConnectRN.kt +237 -37
  8. package/ios/Bridge.h +1 -1
  9. package/ios/HybridAcousticConnectRN.swift +36 -6
  10. package/lib/commonjs/TLTRN.js +23 -3
  11. package/lib/commonjs/TLTRN.js.map +1 -1
  12. package/lib/commonjs/components/Connect.js +1 -1
  13. package/lib/commonjs/index.js +13 -2
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/utils/withAcousticAutoDialog.js +1 -1
  16. package/lib/module/TLTRN.js +23 -3
  17. package/lib/module/TLTRN.js.map +1 -1
  18. package/lib/module/components/Connect.js +1 -1
  19. package/lib/module/index.js +14 -2
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/utils/withAcousticAutoDialog.js +1 -1
  22. package/lib/typescript/src/TLTRN.d.ts.map +1 -1
  23. package/lib/typescript/src/index.d.ts +2 -2
  24. package/lib/typescript/src/index.d.ts.map +1 -1
  25. package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts +40 -0
  26. package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/withAcousticAutoDialog.d.ts +1 -1
  28. package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.cpp +22 -0
  29. package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.hpp +1 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/HybridAcousticConnectRNSpec.kt +4 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/PushErrorInfo.kt +17 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/PushPermissionResult.kt +15 -0
  33. package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Bridge.hpp +54 -14
  34. package/nitrogen/generated/ios/c++/HybridAcousticConnectRNSpecSwift.hpp +8 -0
  35. package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec.swift +1 -0
  36. package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec_cxx.swift +41 -0
  37. package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.cpp +1 -0
  38. package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.hpp +1 -0
  39. package/package.json +5 -5
  40. package/src/TLTRN.ts +31 -11
  41. package/src/components/Connect.tsx +1 -1
  42. package/src/index.ts +19 -2
  43. package/src/specs/react-native-acoustic-connect.nitro.ts +47 -1
  44. package/src/utils/withAcousticAutoDialog.tsx +1 -1
@@ -10,10 +10,10 @@ dependencyName = useRelease ? 'AcousticConnect' : 'AcousticConnectDebug'
10
10
  iOSVersion = connectConfig["Connect"]["iOSVersion"]
11
11
 
12
12
  # Floor required by the RN bridge:
13
- # - `ConnectSDK.shared.enable(with:)` and `ConnectConfig` landed in 2.0.0
14
- # (CA-135041) — anything older lacks the Swift symbols this file imports.
13
+ # - `ConnectSDK.shared.enable(with:)` and `ConnectConfig` landed in 2.0.0;
14
+ # anything older lacks the Swift symbols this file imports.
15
15
  # - Bundle-less init via `_connectApplyBundleDefaults` landed in 2.0.5
16
- # (CA-136977, Release_Connect_Module_2_0_5, 2026-04-13). Without it,
16
+ # (Release_Connect_Module_2_0_5, 2026-04-13). Without it,
17
17
  # consumers who don't bundle `EOCoreSettings.bundle` + `TLFResources.bundle`
18
18
  # fail to enable the SDK at all — the bridge ships no bundles and relies
19
19
  # on programmatic config, so this fix is load-bearing here.
@@ -29,7 +29,7 @@ iOSVersion = connectConfig["Connect"]["iOSVersion"]
29
29
  # `ConnectConfig.json -> Connect.PushEnabled` (mirrors the Android conditional
30
30
  # on `connect-push-fcm` in `android/build.gradle`).
31
31
  # 2.1.12 floor: `ConnectSDK.shared.push.requestAuthorization()` and
32
- # `getCurrentAuthorization()` (CA-144303) land in 2.1.12. The push permission
32
+ # `getCurrentAuthorization()` land in 2.1.12. The push permission
33
33
  # bridge methods call them directly, so an older pod would fail to compile.
34
34
  sdkFloor = '>= 2.1.12'
35
35
  dependencyRequirements = iOSVersion.to_s.empty? ? [sdkFloor] : [sdkFloor, iOSVersion]
package/README.md CHANGED
@@ -11,11 +11,34 @@ For the full product overview see the
11
11
 
12
12
  - React Native 0.82.x – 0.85.x with the new architecture
13
13
  - React 19.1.1 or newer (or whatever your RN version pins)
14
+ - `react-native-nitro-modules` at the **exact** version this package pins in `peerDependencies` (currently **`0.35.9`**) — your app must resolve exactly this version; see [Nitro version pin](#nitro-version-pin)
14
15
  - Node 20 or newer
15
16
  - iOS deployment target ≥ 15.1, AcousticConnect / AcousticConnectDebug pod ≥ 2.0.5
16
17
  - Android `minSdk` ≥ 26, `compileSdk` ≥ 35, `io.github.go-acoustic:connect` in `[11.0.11, 12.0.0)`
17
18
  - **Expo is not supported by this package.** Expo apps should use the sibling
18
- Config Plugin (tracked separately under PES-4002 / CA-137701).
19
+ Config Plugin (shipped separately).
20
+
21
+ ### Nitro version pin
22
+
23
+ This package pins `react-native-nitro-modules` to an **exact version**
24
+ (currently **`0.35.9`**) in its `peerDependencies` — deliberately *not* a range.
25
+ **Your app must resolve exactly that version.**
26
+
27
+ **Why (important): Nitro patch releases can contain breaking changes.** The SDK
28
+ ships native bindings generated against one specific Nitro version, and Nitro's
29
+ generated-code ↔ runtime contract is **not patch-safe**. For example, a
30
+ `0.35.4` → `0.35.9` *patch* bump changed Nitro's native registration and caused
31
+ this SDK's HybridObject to fail to load with a `ClassNotFoundException`, taking
32
+ the entire module offline at runtime. Because even a patch can break it, a
33
+ version *range* (`^`/`~`/`>=…<…`) cannot guarantee compatibility — so the pin is
34
+ exact.
35
+
36
+ If your app resolves a different Nitro version, the SDK may fail to start. You
37
+ *can* force past the peer check with `--legacy-peer-deps` / `--force`, **but
38
+ then you own making it work** — we cannot guard against breaking changes in
39
+ arbitrary future Nitro patch releases. When this SDK adopts a newer Nitro, it
40
+ ships in a new SDK release with the pin bumped; upgrade the SDK and Nitro
41
+ together.
19
42
 
20
43
  ## Installation
21
44
 
@@ -222,16 +245,22 @@ and the `<Connect>` component.
222
245
 
223
246
  ### `npm install` peer-dependency conflicts
224
247
 
225
- If `npm install` errors out on a peer-dependency resolution involving Expo or
226
- older `react-native-nitro-modules` versions, retry with:
248
+ This package pins `react-native-nitro-modules` to an **exact** version (see
249
+ [Nitro version pin](#nitro-version-pin)). If `npm install` errors on the Nitro
250
+ peer, your app is resolving a *different* Nitro version. Check it with:
227
251
 
228
252
  ```bash
229
- npm install react-native-nitro-modules --legacy-peer-deps
253
+ npm ls react-native-nitro-modules
230
254
  ```
231
255
 
232
- This usually only happens when an existing project pulls in stale Expo
233
- transitives. Permanent fix: align RN, React, and Nitro versions with the
234
- [Requirements](#requirements) section above.
256
+ **Supported fix:** align your app to the exact version this package requires
257
+ (and match React Native / React per the [Requirements](#requirements)).
258
+
259
+ `--legacy-peer-deps` / `--force` will silence the error, but the pin is
260
+ intentional: a mismatched Nitro version can break the SDK at runtime (an
261
+ init-time `ClassNotFoundException`), and bypassing means **you take on that
262
+ risk** — we cannot guard against breaking changes in arbitrary Nitro patch
263
+ releases. Match the version rather than bypass it.
235
264
 
236
265
  ### iOS — `pod install` fails with `[Connect] requires AcousticConnect >= 2.0.5`
237
266
 
@@ -7,7 +7,7 @@
7
7
  # prohibited.
8
8
  #
9
9
  #
10
- # Created by Omar Hernandez on 5/9/25.
10
+ # Created on 5/9/25.
11
11
  #
12
12
 
13
13
  project(AcousticConnectRN)
@@ -7,7 +7,7 @@
7
7
  // prohibited.
8
8
  //
9
9
  //
10
- // Created by Omar Hernandez on 5/9/25.
10
+ // Created on 5/9/25.
11
11
  //
12
12
 
13
13
  buildscript {
@@ -1,4 +1,4 @@
1
- #Mon Jun 01 09:10:13 PDT 2026
1
+ #Fri Jun 05 02:59:23 PDT 2026
2
2
  UseWhiteList=true
3
3
  PrintScreen=3
4
4
  UseRandomSample=false
@@ -7,7 +7,7 @@
7
7
  // prohibited.
8
8
  //
9
9
  //
10
- // Created by Omar Hernandez on 5/9/25.
10
+ // Created on 5/9/25.
11
11
  //
12
12
 
13
13
  package com.acousticconnectrn;
@@ -7,7 +7,7 @@
7
7
  // prohibited.
8
8
  //
9
9
  //
10
- // Created by Omar Hernandez on 5/9/25.
10
+ // Created on 5/9/25.
11
11
  //
12
12
 
13
13
  package com.acousticconnectrn
@@ -29,6 +29,7 @@ import android.view.WindowManager
29
29
  import android.view.inputmethod.InputMethodManager
30
30
  import android.widget.EditText
31
31
  import android.widget.TextView
32
+ import androidx.activity.ComponentActivity
32
33
  import androidx.fragment.app.DialogFragment
33
34
  import com.acoustic.connect.android.connectmod.Connect
34
35
  import com.acoustic.connect.android.connectmod.Connect.TLF_ON_FOCUS_CHANGE_IN
@@ -47,6 +48,7 @@ import com.acoustic.connect.android.connectmod.Connect.logScreenview
47
48
  import com.acoustic.connect.android.connectmod.Connect.onResume
48
49
  import com.acoustic.connect.android.connectmod.Connect.registerFormField
49
50
  import com.acoustic.connect.android.connectmod.Connect.resumeConnect
51
+ import com.acoustic.connect.android.connectmod.push.PushPermissionState
50
52
  import com.facebook.react.bridge.LifecycleEventListener
51
53
  import com.facebook.react.bridge.ReactApplicationContext
52
54
  import com.facebook.react.uimanager.UIManagerHelper
@@ -60,6 +62,7 @@ import com.margelo.nitro.acousticconnectrn.Variant_Boolean_String_Double
60
62
  import com.margelo.nitro.acousticconnectrn.Variant_NullType_Boolean
61
63
  import com.margelo.nitro.acousticconnectrn.Variant_NullType_String
62
64
  import com.margelo.nitro.core.ArrayBuffer
65
+ import com.margelo.nitro.core.NullType
63
66
  import com.margelo.nitro.core.Promise
64
67
  import com.tl.uic.Tealeaf
65
68
  import com.tl.uic.model.ScreenviewType
@@ -173,7 +176,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
173
176
  return app
174
177
  }
175
178
 
176
- // region Gate-keeper API (CA-137696)
179
+ // region Gate-keeper API
177
180
 
178
181
  /**
179
182
  * Re-enables the Connect SDK after a prior `disable()`. All configuration
@@ -190,8 +193,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
190
193
  * Push wiring on Android is gated at build time by `Connect.PushEnabled`
191
194
  * in `ConnectConfig.json`, which `android/build.gradle` consults to
192
195
  * include the `connect-push-fcm` artifact. Token forwarding to Connect
193
- * lives in CA-137698 and runs through the host app's
194
- * `FirebaseMessagingService`.
196
+ * runs through the host app's `FirebaseMessagingService`.
195
197
  *
196
198
  * @return true on accepted dispatch, false if no application context.
197
199
  */
@@ -231,76 +233,227 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
231
233
  return true
232
234
  }
233
235
 
234
- // MARK: - Push (Android) — STUBS
236
+ // region Push (Android)
235
237
  //
236
- // The shared Nitro spec (added by the iOS push story, CA-137697) declares
237
- // these as abstract members on BOTH platforms, so the Android library must
238
- // implement them to compile. These are intentional NO-OP placeholders to
239
- // keep the shared spec landable; the real Android behaviour lands with:
240
- // - CA-144821: pushGetToken / pushGetTokenAsync (separate session)
241
- // - CA-144822: pushDidReceiveNotification manual-mode dispatch
242
- // - CA-144823: permission methods (request / get / authorization)
243
- // until then they return inert defaults and never throw.
238
+ // The shared Nitro spec declares these push methods on both platforms. On
239
+ // Android the native SDK owns the ENTIRE push lifecycle Connect's own
240
+ // FirebaseMessagingService (shipped in connect-push-fcm) handles inbound
241
+ // delivery, PushReceived / PushAction logging, and token registration
242
+ // automatically. Unlike iOS, Android has no manual JS-forwarding API, so
243
+ // every JS→native forwarder is a no-op that reports "handled", kept only
244
+ // for cross-platform API symmetry:
245
+ // - pushDidReceiveNotification, pushDidReceiveResponse,
246
+ // pushDidRegisterWithToken, pushDidFailToRegister,
247
+ // pushDidReceiveAuthorization
248
+ //
249
+ // The only Android-relevant behaviour is permission wiring:
250
+ // - pushRequestPermission / pushGetPermissionState — POST_NOTIFICATIONS
251
+ //
252
+ // pushGetToken / pushGetTokenAsync are intentionally NOT on the surface —
253
+ // parked (the native SDK registers the token with Connect automatically;
254
+ // no JS accessor is needed). None of these methods reject.
244
255
 
256
+ /**
257
+ * iOS-primary forwarder. On Android the FCM token is captured and registered
258
+ * with Connect automatically by the native messaging service, so this is a
259
+ * no-op that reports "handled". Never rejects.
260
+ */
245
261
  override fun pushDidRegisterWithToken(deviceToken: ArrayBuffer): Promise<Boolean> {
246
- Log.w(TAG, "[bridge] pushDidRegisterWithToken: Android stub (CA-144822/823 pending)")
247
- return Promise.resolved(false)
262
+ Log.d(TAG, "[bridge] pushDidRegisterWithToken: no-op on Android (native SDK owns token registration)")
263
+ return Promise.resolved(true)
248
264
  }
249
265
 
266
+ /**
267
+ * iOS-primary forwarder. Android surfaces registration failures through the
268
+ * native SDK's own error path, so this is a no-op. Never rejects.
269
+ */
250
270
  override fun pushDidFailToRegister(error: PushErrorInfo): Promise<Boolean> {
251
- Log.w(TAG, "[bridge] pushDidFailToRegister: Android stub (CA-144822/823 pending)")
252
- return Promise.resolved(false)
271
+ Log.d(TAG, "[bridge] pushDidFailToRegister: no-op on Android (domain=${error.domain}, code=${error.code})")
272
+ return Promise.resolved(true)
253
273
  }
254
274
 
275
+ /**
276
+ * iOS-primary forwarder. Android has **no** manual push-delivery API: the
277
+ * native SDK's own `FirebaseMessagingService` (shipped in `connect-push-fcm`)
278
+ * receives inbound messages and logs the PushReceived signal automatically.
279
+ * There is no sanctioned JS-forwarding entry point on Android — and outside
280
+ * Connect's push transport there is no registered device to attribute a
281
+ * PushReceived to — so this is a no-op that reports "handled". Kept for
282
+ * cross-platform API symmetry with iOS (where it forwards to the SDK in
283
+ * manual mode). Never rejects.
284
+ */
255
285
  override fun pushDidReceiveNotification(userInfo: Map<String, Variant_Boolean_String_Double>): Promise<Boolean> {
256
- Log.w(TAG, "[bridge] pushDidReceiveNotification: Android stub (CA-144822 pending)")
257
- return Promise.resolved(false)
286
+ Log.d(TAG, "[bridge] pushDidReceiveNotification: no-op on Android (native FCM service owns delivery + logging)")
287
+ return Promise.resolved(true)
258
288
  }
259
289
 
290
+ /**
291
+ * iOS-primary forwarder. Tap (PushAction) handling on Android routes through
292
+ * the native SDK's `NotificationActionActivity`, so this is a no-op. Never
293
+ * rejects.
294
+ */
260
295
  override fun pushDidReceiveResponse(
261
296
  actionIdentifier: String,
262
- userInfo: Map<String, Variant_Boolean_String_Double>
297
+ userInfo: Map<String, Variant_Boolean_String_Double>,
263
298
  ): Promise<Boolean> {
264
- Log.w(TAG, "[bridge] pushDidReceiveResponse: Android stub (CA-144822 pending)")
265
- return Promise.resolved(false)
299
+ Log.d(TAG, "[bridge] pushDidReceiveResponse: no-op on Android (native SDK handles tap actions)")
300
+ return Promise.resolved(true)
266
301
  }
267
302
 
303
+ /**
304
+ * No-op on Android. Permission state is auto-detected on every activity
305
+ * start by the SDK's ActivityLifecycleHandler, so an externally-supplied
306
+ * authorization result needs no forwarding. Kept for API symmetry with iOS
307
+ * (where it forwards to the native SDK). Never rejects.
308
+ */
268
309
  override fun pushDidReceiveAuthorization(granted: Variant_NullType_Boolean?, error: PushErrorInfo?): Promise<Boolean> {
269
- Log.w(TAG, "[bridge] pushDidReceiveAuthorization: Android stub (CA-144823 pending)")
270
- return Promise.resolved(false)
310
+ Log.d(TAG, "[bridge] pushDidReceiveAuthorization: no-op on Android (state self-heals via lifecycle)")
311
+ return Promise.resolved(true)
271
312
  }
272
313
 
314
+ /**
315
+ * Requests the POST_NOTIFICATIONS permission. Delegates to
316
+ * [com.acoustic.connect.android.connectmod.push.PushApi.requestNotificationPermission],
317
+ * which fully manages the system dialog and the Activity Result registration,
318
+ * then resolves from its one-shot callback. On pre-TIRAMISU devices the SDK
319
+ * resolves `granted = true` immediately.
320
+ *
321
+ * **Never rejects.** If no foreground [ComponentActivity] is available, or
322
+ * the SDK call throws, it resolves `{ granted: false, error: <reason> }`.
323
+ *
324
+ * Intentionally NOT gated by [isConnectPushFcmAvailable]. The permission
325
+ * API ([PushApi]/`ConnectPush` and the `NotificationPermission*` classes)
326
+ * ships in the core `connect` artifact, which is always on the classpath —
327
+ * the `connect-push-fcm` variant pulls `connect` in transitively (see
328
+ * `android/build.gradle`). So `Connect.push` is a non-null core object, not
329
+ * a stub, and accessing it cannot raise `NoClassDefFoundError` from a
330
+ * missing FCM artifact. POST_NOTIFICATIONS is an OS-level concern that is
331
+ * meaningful regardless of whether FCM message delivery is bundled; gating
332
+ * it on the FCM probe would wrongly disable a working capability in
333
+ * analytics-only (`PushEnabled=false`) builds. The only FCM-adjacent path
334
+ * the SDK runs from here — `sendToken()` in the result callback — is itself
335
+ * guarded by the SDK's `isInitialized()` and no-ops when transport is absent.
336
+ */
273
337
  override fun pushRequestPermission(): Promise<PushPermissionResult> {
274
- Log.w(TAG, "[bridge] pushRequestPermission: Android stub (CA-144823 pending)")
275
- return Promise.resolved(PushPermissionResult(granted = false, error = null))
338
+ val promise = Promise<PushPermissionResult>()
339
+ runOnMain {
340
+ // Re-read the foreground activity inside the main-looper block: it can
341
+ // change between the bridge call and this dispatch (e.g. a rotation /
342
+ // configuration change). Guard against a finishing or destroyed
343
+ // activity, not just null — handing such an activity to the SDK lets it
344
+ // register an ActivityResultLauncher whose result callback never fires,
345
+ // which would hang this Promise forever. Resolving deterministically
346
+ // here is preferable to a silent never-resolving Promise.
347
+ val activity = getCurrentActivity() as? ComponentActivity
348
+ if (activity == null || activity.isFinishing || activity.isDestroyed) {
349
+ Log.w(TAG, "[bridge] pushRequestPermission: no usable foreground ComponentActivity")
350
+ promise.resolve(
351
+ PushPermissionResult(false, Variant_NullType_String.create("no-foreground-activity")),
352
+ )
353
+ return@runOnMain
354
+ }
355
+ try {
356
+ // The `granted` callback is always delivered on the main thread:
357
+ // either synchronously here (pre-TIRAMISU / already-granted paths,
358
+ // still on this looper) or via AndroidX ActivityResultRegistry,
359
+ // which dispatches results on the main thread. Promise.resolve is
360
+ // additionally thread-agnostic — it delegates to native, which
361
+ // marshals onto the JS thread — so resolution is safe regardless of
362
+ // the calling thread.
363
+ Connect.push.requestNotificationPermission(activity) { granted ->
364
+ promise.resolve(PushPermissionResult(granted, null))
365
+ }
366
+ } catch (e: Exception) {
367
+ Log.w(TAG, "[bridge] pushRequestPermission: request failed — ${e.message}")
368
+ promise.resolve(
369
+ PushPermissionResult(false, Variant_NullType_String.create(e.message ?: "permission-request-failed")),
370
+ )
371
+ }
372
+ }
373
+ return promise
276
374
  }
277
375
 
376
+ /**
377
+ * Returns the current POST_NOTIFICATIONS permission as the cross-platform
378
+ * tri-state: `true` granted, `false` denied, `null` not yet
379
+ * determined. Maps [PushPermissionState] from the native SDK. Does not
380
+ * prompt. Never rejects — resolves `null` if the context is unavailable or
381
+ * the SDK call throws.
382
+ *
383
+ * Runs on the main looper (via [runOnMain]), matching every other
384
+ * `Connect.push.*` invocation in this bridge rather than executing on the
385
+ * Nitro bridge thread the HybridObject method is dispatched on. Besides
386
+ * keeping the threading contract consistent, this serialises the call with
387
+ * [pushRequestPermission]'s SDK callback: both ultimately touch the same
388
+ * notification-permission `SharedPreferences` store (the SDK's
389
+ * `getPushPermissionState` does a read-then-write to reconcile a
390
+ * Settings-side revocation), so funnelling both through main avoids an
391
+ * interleaved read/write across threads.
392
+ *
393
+ * Like [pushRequestPermission], this is intentionally NOT gated by
394
+ * [isConnectPushFcmAvailable] — the permission API lives in the core
395
+ * `connect` artifact (always on the classpath) and never touches the FCM
396
+ * transport classes, so there is no missing-artifact / stub-object risk.
397
+ */
278
398
  override fun pushGetPermissionState(): Promise<Variant_NullType_Boolean> {
279
- Log.w(TAG, "[bridge] pushGetPermissionState: Android stub (CA-144823 pending)")
280
- return Promise.resolved(Variant_NullType_Boolean.create(false))
399
+ val promise = Promise<Variant_NullType_Boolean>()
400
+ runOnMain {
401
+ val context = applicationContext?.applicationContext
402
+ if (context == null) {
403
+ promise.resolve(Variant_NullType_Boolean.create(NullType.NULL))
404
+ return@runOnMain
405
+ }
406
+ try {
407
+ // Redundant against today's 3-value enum, but kept deliberately
408
+ // for forward-compatibility with future SDK enum values (see the
409
+ // `else` branch below), so silence the redundancy warning.
410
+ @Suppress("REDUNDANT_ELSE_IN_WHEN")
411
+ val triState = when (Connect.push.getPushPermissionState(context)) {
412
+ PushPermissionState.GRANTED -> Variant_NullType_Boolean.create(true)
413
+ PushPermissionState.DENIED -> Variant_NullType_Boolean.create(false)
414
+ PushPermissionState.NOT_DETERMINED -> Variant_NullType_Boolean.create(NullType.NULL)
415
+ // PushPermissionState is a Connect-SDK enum that may gain
416
+ // values in a future release. An explicit `else` keeps this
417
+ // forward-compatible: it avoids a compile break if the bridge
418
+ // is rebuilt against an SDK with a new state, and replaces the
419
+ // synthetic NoWhenBranchMatchedException (binary-incompat case:
420
+ // bridge built against the old enum, run against a newer one)
421
+ // with a deliberate fallback. Any unknown/new state maps to the
422
+ // tri-state `null` ("not determined") — the safest default.
423
+ else -> Variant_NullType_Boolean.create(NullType.NULL)
424
+ }
425
+ promise.resolve(triState)
426
+ } catch (e: Exception) {
427
+ Log.w(TAG, "[bridge] pushGetPermissionState: query failed — ${e.message}")
428
+ promise.resolve(Variant_NullType_Boolean.create(NullType.NULL))
429
+ }
430
+ }
431
+ return promise
281
432
  }
282
433
 
434
+ // endregion
435
+
283
436
  /**
284
437
  * Checks whether the `connect-push-fcm` artifact is on the classpath and
285
438
  * logs the result. The artifact is gated by `Connect.PushEnabled` in
286
439
  * `ConnectConfig.json` via `android/build.gradle`'s conditional
287
440
  * `implementation` clause. A missing artifact when `PushEnabled` was set
288
441
  * to true points at a build-pipeline issue (didn't run `config.gradle`
289
- * or the conditional didn't fire). Once CA-137698 lands the actual
290
- * Connect push API on Android, this method also gates the wire-up.
442
+ * or the conditional didn't fire). This method also gates the push
443
+ * wire-up against the resolved Connect push API on Android.
291
444
  */
292
445
  private fun logResolvedPushAvailability() {
293
446
  val pushAvailable = isConnectPushFcmAvailable()
294
447
  Log.i(TAG, "[config] connect-push-fcm on classpath: $pushAvailable")
295
448
  if (!pushAvailable) {
296
- Log.i(TAG, "[config] Push is not active on Android in this build. To enable, set Connect.PushEnabled=true in ConnectConfig.json and re-sync Gradle to include connect-push-fcm. Token forwarding wires in CA-137698.")
449
+ Log.i(TAG, "[config] Push is not active on Android in this build. To enable, set Connect.PushEnabled=true in ConnectConfig.json and re-sync Gradle to include connect-push-fcm.")
297
450
  }
298
451
  }
299
452
 
300
453
  private fun isConnectPushFcmAvailable(): Boolean {
301
454
  // Probe a known class shipped by the connect-push-fcm artifact. The
302
- // exact class lives in the Connect Android SDK's push module and is
303
- // finalised under CA-137698; this Class.forName probe is robust to
455
+ // exact class lives in the Connect Android SDK's push module. This
456
+ // Class.forName probe is robust to
304
457
  // package-name changes because it falls through silently when the
305
458
  // class is missing (which is the default in a no-push build).
306
459
  return try {
@@ -466,6 +619,52 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
466
619
  return result
467
620
  }
468
621
 
622
+ /**
623
+ * Logs a user identity so device activity can be associated with a known
624
+ * Connect contact. Wraps [Connect.logIdentificationEvent].
625
+ *
626
+ * Returns a [Promise] to match the cross-platform spec — the iOS identity
627
+ * API is main-actor isolated and async. The native call is synchronous and
628
+ * returns false (emitting no signal) when either identifier is blank, so no
629
+ * extra guard is needed here.
630
+ *
631
+ * A `"url"` entry in [additionalParameters] is routed to the SDK's explicit
632
+ * `url` parameter so the JS surface matches iOS, where `url` rides inside the
633
+ * parameter map. When absent, the SDK's own default URL applies.
634
+ *
635
+ * @param identifierName Identifier name, e.g. "Email".
636
+ * @param identifierValue Identifier value, e.g. "user@example.com".
637
+ * @param signalType Optional signal type; defaults to "loggedIn" when omitted.
638
+ * @param additionalParameters Optional extra key/value pairs merged into the
639
+ * signal. Defaults to `{ "registrationMethod": "email" }` only when null
640
+ * (omitted); an explicit map — including an empty one — is used as-is.
641
+ * @return A promise resolving to true if the signal was queued, false otherwise.
642
+ */
643
+ override fun logIdentity(
644
+ identifierName: String,
645
+ identifierValue: String,
646
+ signalType: String?,
647
+ additionalParameters: Map<String, String>?
648
+ ): Promise<Boolean> {
649
+ val params = additionalParameters ?: mapOf("registrationMethod" to "email")
650
+ val resolvedSignalType = signalType ?: "loggedIn"
651
+ val result = params["url"]?.let { url ->
652
+ Connect.logIdentificationEvent(
653
+ identifierName,
654
+ identifierValue,
655
+ url,
656
+ resolvedSignalType,
657
+ params - "url"
658
+ )
659
+ } ?: Connect.logIdentificationEvent(
660
+ identifierName = identifierName,
661
+ identifierValue = identifierValue,
662
+ signalType = resolvedSignalType,
663
+ additionalParameters = params
664
+ )
665
+ return Promise.resolved(result)
666
+ }
667
+
469
668
  /**
470
669
  * Logs an exception event with the specified message and stack information.
471
670
  *
@@ -1121,10 +1320,11 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
1121
1320
  const val DIALOG_CAPTURE_DELAY_MS = 500L // Configurable delay for dialog screenshot capture
1122
1321
 
1123
1322
  // Class probed at runtime to detect whether the connect-push-fcm
1124
- // artifact was included in the build. Conservative placeholder — the
1125
- // actual class name is finalised under CA-137698; if that ticket lands
1126
- // a different fully-qualified name, update this constant.
1323
+ // transport artifact was included in the build (i.e. automatic mode,
1324
+ // where Connect's own FirebaseMessagingService is the PushReceived
1325
+ // sink). Points at the FCM messaging service shipped
1326
+ // by the connect-push-fcm artifact.
1127
1327
  private const val CONNECT_PUSH_FCM_PROBE_CLASS =
1128
- "com.acoustic.connect.android.push.fcm.ConnectPushFcm"
1328
+ "com.acoustic.connect.android.connectmod.push.services.fcm.FCMPushService"
1129
1329
  }
1130
1330
  }
package/ios/Bridge.h CHANGED
@@ -7,7 +7,7 @@
7
7
  // prohibited.
8
8
  //
9
9
  //
10
- // Created by Omar Hernandez on 5/9/25.
10
+ // Created on 5/9/25.
11
11
  //
12
12
 
13
13
  #pragma once
@@ -7,7 +7,7 @@
7
7
  // prohibited.
8
8
  //
9
9
  //
10
- // Created by Omar Hernandez on 5/9/25.
10
+ // Created on 5/9/25.
11
11
  //
12
12
 
13
13
  import Foundation
@@ -94,7 +94,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
94
94
  /// forwards events to the SDK; automatic/off surface `EAC-RN-007` (`false`).
95
95
  ///
96
96
  // Constructor — auto-inits the SDK from `ConnectConfig.json`. Matches the
97
- // pre-CA-137696 behaviour; consumers that need consent-gated init can call
97
+ // pre-existing auto-init behaviour; consumers that need consent-gated init can call
98
98
  // `disable()` immediately and `enable()` once they have permission.
99
99
  override init() {
100
100
  super.init()
@@ -333,7 +333,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
333
333
  return true
334
334
  }
335
335
 
336
- // MARK: - Push: APNs lifecycle (CA-144306)
336
+ // MARK: - Push: APNs lifecycle
337
337
 
338
338
  /// Forwards the raw APNs device token to the SDK. Nitro hands us native
339
339
  /// `Data` via `ArrayBuffer` — no hex conversion, no validation. Idempotent
@@ -369,7 +369,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
369
369
  }
370
370
  }
371
371
 
372
- // MARK: - Push: notification delivery — manual mode only (CA-144307)
372
+ // MARK: - Push: notification delivery — manual mode only
373
373
 
374
374
  /// Forwards a received notification so the SDK logs `pushReceived` (manual
375
375
  /// mode). Resolves `true` when processed. In automatic/off mode the SDK
@@ -409,7 +409,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
409
409
  }
410
410
  }
411
411
 
412
- // MARK: - Push: permission management (CA-144308)
412
+ // MARK: - Push: permission management
413
413
 
414
414
  /// Forwards externally-obtained permission state to the SDK. Tri-state
415
415
  /// `granted`: `true`/`false` forward; `nil` (notDetermined) is recorded by
@@ -575,7 +575,37 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
575
575
  let result = ConnectCustomEvent().logSignal(convertToAnyDictionary(input: values), level: logLevel)
576
576
  return result
577
577
  }
578
-
578
+
579
+ /// Logs a user identity so device activity can be associated with a known
580
+ /// Connect contact. Wraps `ConnectSDK.shared.identity.log(...)`.
581
+ ///
582
+ /// `ConnectSDK.shared` is `@MainActor`-isolated, so — unlike the synchronous
583
+ /// loggers above (which use the legacy non-actor `ConnectCustomEvent`) —
584
+ /// this hops to the main actor and resolves with the SDK's real return
585
+ /// value. The native API returns `false` (and emits no signal) when either
586
+ /// identifier is blank, satisfying the bridge's blank-input contract without
587
+ /// extra guards here. `url`, if supplied, rides inside `additionalParameters`
588
+ /// (the iOS convention).
589
+ /// - Parameters:
590
+ /// - identifierName: Identifier name, e.g. "Email".
591
+ /// - identifierValue: Identifier value, e.g. "user@example.com".
592
+ /// - signalType: Optional signal type; defaults to "loggedIn" when omitted.
593
+ /// - additionalParameters: Optional extra key/value pairs merged into the
594
+ /// signal. Defaults to `["registrationMethod": "email"]` only when `nil`
595
+ /// (omitted); an explicit map — including an empty one — is used as-is.
596
+ /// - Returns: A promise resolving to `true` if the signal was dispatched,
597
+ /// `false` otherwise. Never rejects.
598
+ func logIdentity(identifierName: String, identifierValue: String, signalType: String?, additionalParameters: [String: String]?) throws -> Promise<Bool> {
599
+ return Promise.async { @MainActor in
600
+ ConnectSDK.shared.identity.log(
601
+ identifierName: identifierName,
602
+ identifierValue: identifierValue,
603
+ signalType: signalType ?? "loggedIn",
604
+ additionalParameters: additionalParameters ?? ["registrationMethod": "email"]
605
+ )
606
+ }
607
+ }
608
+
579
609
  /// Log exception.
580
610
  /// - Parameters:
581
611
  /// - message: the message of the error/exception to be logged this will appear in the posted json.
@@ -22,9 +22,25 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
22
22
 
23
23
  // @ts-ignore
24
24
 
25
+ // Preserve any previously-installed handler (e.g. React Native's red-box
26
+ // handler) so we augment rather than replace it.
25
27
  // @ts-ignore
26
- global.ErrorUtils.setGlobalHandler((e, _isFatal) => {
27
- _index.default.logExceptionEvent(JSON.stringify(e), JSON.stringify(e), true);
28
+ const previousGlobalErrorHandler = global.ErrorUtils?.getGlobalHandler?.();
29
+
30
+ // @ts-ignore
31
+ global.ErrorUtils?.setGlobalHandler?.((e, isFatal) => {
32
+ // A global error handler must never throw — otherwise it masks the ORIGINAL
33
+ // error. `AcousticConnectRN` can be undefined here (e.g. the native module
34
+ // failed to load, or during the module-load circular import), so guard the
35
+ // call and always forward to the previous handler so the real error still
36
+ // surfaces and fatals still crash.
37
+ try {
38
+ _index.default?.logExceptionEvent?.(JSON.stringify(e), JSON.stringify(e), true);
39
+ } catch (loggingError) {
40
+ console.warn('TLTRN: failed to log uncaught exception:', loggingError?.message);
41
+ } finally {
42
+ previousGlobalErrorHandler?.(e, isFatal);
43
+ }
28
44
  });
29
45
  class TLTRN {
30
46
  static currentScreen = "***initialCurrentScreen not set in ConnectLogger constructor***";
@@ -249,7 +265,11 @@ class TLTRN {
249
265
  console.log("Message data:", data);
250
266
  }
251
267
  if (message.module === "ExceptionsManager") {
252
- _index.default.logExceptionEvent(message.args[0], JSON.stringify(message.args[1]), true);
268
+ try {
269
+ _index.default?.logExceptionEvent?.(message.args[0], JSON.stringify(message.args[1]), true);
270
+ } catch (error) {
271
+ console.log('logExceptionEvent (bridge) error: ', error?.message);
272
+ }
253
273
  }
254
274
  };
255
275
  static checkTime = () => {