react-native-acoustic-connect-beta 18.0.26 → 18.0.28

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 (61) hide show
  1. package/AcousticConnectRN.podspec +7 -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 +221 -13
  8. package/ios/Bridge.h +1 -1
  9. package/ios/HybridAcousticConnectRN.swift +179 -2
  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 +109 -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/AcousticConnectRN+autolinking.cmake +1 -0
  29. package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.cpp +137 -1
  30. package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.hpp +7 -0
  31. package/nitrogen/generated/android/c++/JPushErrorInfo.hpp +66 -0
  32. package/nitrogen/generated/android/c++/JPushPermissionResult.hpp +66 -0
  33. package/nitrogen/generated/android/c++/JVariant_NullType_Boolean.cpp +26 -0
  34. package/nitrogen/generated/android/c++/JVariant_NullType_Boolean.hpp +69 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/HybridAcousticConnectRNSpec.kt +30 -0
  36. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/PushErrorInfo.kt +61 -0
  37. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/PushPermissionResult.kt +56 -0
  38. package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/Variant_NullType_Boolean.kt +62 -0
  39. package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Bridge.cpp +32 -0
  40. package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Bridge.hpp +249 -0
  41. package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Umbrella.hpp +8 -0
  42. package/nitrogen/generated/ios/c++/HybridAcousticConnectRNSpecSwift.hpp +67 -1
  43. package/nitrogen/generated/ios/swift/Func_void_PushPermissionResult.swift +46 -0
  44. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  45. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  46. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__bool_.swift +58 -0
  47. package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec.swift +7 -0
  48. package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec_cxx.swift +205 -0
  49. package/nitrogen/generated/ios/swift/PushErrorInfo.swift +65 -0
  50. package/nitrogen/generated/ios/swift/PushPermissionResult.swift +66 -0
  51. package/nitrogen/generated/ios/swift/Variant_NullType_Bool.swift +30 -0
  52. package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.cpp +7 -0
  53. package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.hpp +15 -1
  54. package/nitrogen/generated/shared/c++/PushErrorInfo.hpp +92 -0
  55. package/nitrogen/generated/shared/c++/PushPermissionResult.hpp +90 -0
  56. package/package.json +5 -5
  57. package/src/TLTRN.ts +31 -11
  58. package/src/components/Connect.tsx +1 -1
  59. package/src/index.ts +19 -2
  60. package/src/specs/react-native-acoustic-connect.nitro.ts +129 -1
  61. 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.
@@ -28,7 +28,10 @@ iOSVersion = connectConfig["Connect"]["iOSVersion"]
28
28
  # place to introduce a conditional floor keyed off
29
29
  # `ConnectConfig.json -> Connect.PushEnabled` (mirrors the Android conditional
30
30
  # on `connect-push-fcm` in `android/build.gradle`).
31
- sdkFloor = '>= 2.0.5'
31
+ # 2.1.12 floor: `ConnectSDK.shared.push.requestAuthorization()` and
32
+ # `getCurrentAuthorization()` land in 2.1.12. The push permission
33
+ # bridge methods call them directly, so an older pod would fail to compile.
34
+ sdkFloor = '>= 2.1.12'
32
35
  dependencyRequirements = iOSVersion.to_s.empty? ? [sdkFloor] : [sdkFloor, iOSVersion]
33
36
 
34
37
  # Write the merged consumer config to the resource-bundle source path AT POD INSTALL TIME.
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
- #Fri May 29 08:54:08 PDT 2026
1
+ #Fri Jun 05 01:23:00 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
@@ -54,8 +56,14 @@ import com.ibm.eo.EOCore
54
56
  import com.ibm.eo.model.EOMonitoringLevel
55
57
  import com.margelo.nitro.NitroModules.Companion.applicationContext
56
58
  import com.margelo.nitro.acousticconnectrn.HybridAcousticConnectRNSpec
59
+ import com.margelo.nitro.acousticconnectrn.PushErrorInfo
60
+ import com.margelo.nitro.acousticconnectrn.PushPermissionResult
57
61
  import com.margelo.nitro.acousticconnectrn.Variant_Boolean_String_Double
62
+ import com.margelo.nitro.acousticconnectrn.Variant_NullType_Boolean
58
63
  import com.margelo.nitro.acousticconnectrn.Variant_NullType_String
64
+ import com.margelo.nitro.core.ArrayBuffer
65
+ import com.margelo.nitro.core.NullType
66
+ import com.margelo.nitro.core.Promise
59
67
  import com.tl.uic.Tealeaf
60
68
  import com.tl.uic.model.ScreenviewType
61
69
  import com.tl.uic.util.DialogUtil
@@ -168,7 +176,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
168
176
  return app
169
177
  }
170
178
 
171
- // region Gate-keeper API (CA-137696)
179
+ // region Gate-keeper API
172
180
 
173
181
  /**
174
182
  * Re-enables the Connect SDK after a prior `disable()`. All configuration
@@ -185,8 +193,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
185
193
  * Push wiring on Android is gated at build time by `Connect.PushEnabled`
186
194
  * in `ConnectConfig.json`, which `android/build.gradle` consults to
187
195
  * include the `connect-push-fcm` artifact. Token forwarding to Connect
188
- * lives in CA-137698 and runs through the host app's
189
- * `FirebaseMessagingService`.
196
+ * runs through the host app's `FirebaseMessagingService`.
190
197
  *
191
198
  * @return true on accepted dispatch, false if no application context.
192
199
  */
@@ -226,27 +233,227 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
226
233
  return true
227
234
  }
228
235
 
236
+ // region Push (Android)
237
+ //
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.
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
+ */
261
+ override fun pushDidRegisterWithToken(deviceToken: ArrayBuffer): Promise<Boolean> {
262
+ Log.d(TAG, "[bridge] pushDidRegisterWithToken: no-op on Android (native SDK owns token registration)")
263
+ return Promise.resolved(true)
264
+ }
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
+ */
270
+ override fun pushDidFailToRegister(error: PushErrorInfo): Promise<Boolean> {
271
+ Log.d(TAG, "[bridge] pushDidFailToRegister: no-op on Android (domain=${error.domain}, code=${error.code})")
272
+ return Promise.resolved(true)
273
+ }
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
+ */
285
+ override fun pushDidReceiveNotification(userInfo: Map<String, Variant_Boolean_String_Double>): Promise<Boolean> {
286
+ Log.d(TAG, "[bridge] pushDidReceiveNotification: no-op on Android (native FCM service owns delivery + logging)")
287
+ return Promise.resolved(true)
288
+ }
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
+ */
295
+ override fun pushDidReceiveResponse(
296
+ actionIdentifier: String,
297
+ userInfo: Map<String, Variant_Boolean_String_Double>,
298
+ ): Promise<Boolean> {
299
+ Log.d(TAG, "[bridge] pushDidReceiveResponse: no-op on Android (native SDK handles tap actions)")
300
+ return Promise.resolved(true)
301
+ }
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
+ */
309
+ override fun pushDidReceiveAuthorization(granted: Variant_NullType_Boolean?, error: PushErrorInfo?): Promise<Boolean> {
310
+ Log.d(TAG, "[bridge] pushDidReceiveAuthorization: no-op on Android (state self-heals via lifecycle)")
311
+ return Promise.resolved(true)
312
+ }
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
+ */
337
+ override fun pushRequestPermission(): Promise<PushPermissionResult> {
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
374
+ }
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
+ */
398
+ override fun pushGetPermissionState(): Promise<Variant_NullType_Boolean> {
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
432
+ }
433
+
434
+ // endregion
435
+
229
436
  /**
230
437
  * Checks whether the `connect-push-fcm` artifact is on the classpath and
231
438
  * logs the result. The artifact is gated by `Connect.PushEnabled` in
232
439
  * `ConnectConfig.json` via `android/build.gradle`'s conditional
233
440
  * `implementation` clause. A missing artifact when `PushEnabled` was set
234
441
  * to true points at a build-pipeline issue (didn't run `config.gradle`
235
- * or the conditional didn't fire). Once CA-137698 lands the actual
236
- * 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.
237
444
  */
238
445
  private fun logResolvedPushAvailability() {
239
446
  val pushAvailable = isConnectPushFcmAvailable()
240
447
  Log.i(TAG, "[config] connect-push-fcm on classpath: $pushAvailable")
241
448
  if (!pushAvailable) {
242
- 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.")
243
450
  }
244
451
  }
245
452
 
246
453
  private fun isConnectPushFcmAvailable(): Boolean {
247
454
  // Probe a known class shipped by the connect-push-fcm artifact. The
248
- // exact class lives in the Connect Android SDK's push module and is
249
- // 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
250
457
  // package-name changes because it falls through silently when the
251
458
  // class is missing (which is the default in a no-push build).
252
459
  return try {
@@ -1067,10 +1274,11 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
1067
1274
  const val DIALOG_CAPTURE_DELAY_MS = 500L // Configurable delay for dialog screenshot capture
1068
1275
 
1069
1276
  // Class probed at runtime to detect whether the connect-push-fcm
1070
- // artifact was included in the build. Conservative placeholder — the
1071
- // actual class name is finalised under CA-137698; if that ticket lands
1072
- // a different fully-qualified name, update this constant.
1277
+ // transport artifact was included in the build (i.e. automatic mode,
1278
+ // where Connect's own FirebaseMessagingService is the PushReceived
1279
+ // sink). Points at the FCM messaging service shipped
1280
+ // by the connect-push-fcm artifact.
1073
1281
  private const val CONNECT_PUSH_FCM_PROBE_CLASS =
1074
- "com.acoustic.connect.android.push.fcm.ConnectPushFcm"
1282
+ "com.acoustic.connect.android.connectmod.push.services.fcm.FCMPushService"
1075
1283
  }
1076
1284
  }
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
@@ -62,10 +62,39 @@ private enum ConnectConfigStore {
62
62
  }
63
63
  }
64
64
 
65
+ // MARK: - Push adapter wrappers
66
+
67
+ /// Bridges a JS-supplied `userInfo` dictionary to the SDK's `ConnectNotification`
68
+ /// protocol (a single `userInfo` requirement).
69
+ private struct HybridAcousticConnectNotification: ConnectNotification {
70
+ let userInfo: [AnyHashable: Any]
71
+
72
+ init(userInfo: [String: Any]) {
73
+ self.userInfo = userInfo.reduce(into: [AnyHashable: Any]()) { $0[$1.key] = $1.value }
74
+ }
75
+ }
76
+
77
+ /// Bridges a JS-supplied response to the SDK's `ConnectNotificationResponse`
78
+ /// protocol (`actionIdentifier` + `userInfo`).
79
+ private struct HybridAcousticConnectNotificationResponse: ConnectNotificationResponse {
80
+ let actionIdentifier: String
81
+ let userInfo: [AnyHashable: Any]
82
+
83
+ init(actionIdentifier: String, userInfo: [String: Any]) {
84
+ self.actionIdentifier = actionIdentifier
85
+ self.userInfo = userInfo.reduce(into: [AnyHashable: Any]()) { $0[$1.key] = $1.value }
86
+ }
87
+ }
88
+
65
89
  class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
66
90
 
91
+ /// Push mode resolved from `ConnectConfig.json` at `load()` time. The push
92
+ /// bridge methods branch on this synchronously rather than calling into the
93
+ /// `@MainActor` SDK to detect mode (mirrors the Android bridge): manual mode
94
+ /// forwards events to the SDK; automatic/off surface `EAC-RN-007` (`false`).
95
+ ///
67
96
  // Constructor — auto-inits the SDK from `ConnectConfig.json`. Matches the
68
- // 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
69
98
  // `disable()` immediately and `enable()` once they have permission.
70
99
  override init() {
71
100
  super.init()
@@ -304,6 +333,154 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
304
333
  return true
305
334
  }
306
335
 
336
+ // MARK: - Push: APNs lifecycle
337
+
338
+ /// Forwards the raw APNs device token to the SDK. Nitro hands us native
339
+ /// `Data` via `ArrayBuffer` — no hex conversion, no validation. Idempotent
340
+ /// in automatic mode (the SDK already captured the token via its swizzle).
341
+ ///
342
+ /// Resolves `true` once the SDK accepted the token, `false` if the call was
343
+ /// rejected (e.g. push not enabled). Never rejects.
344
+ func pushDidRegisterWithToken(deviceToken: ArrayBuffer) throws -> Promise<Bool> {
345
+ let token = deviceToken.toData(copyIfNeeded: true)
346
+ return Promise.async { @MainActor in
347
+ do {
348
+ try ConnectSDK.shared.push.didRegisterWithToken(token)
349
+ return true
350
+ } catch {
351
+ bridgeLog.error("pushDidRegisterWithToken failed: \(error.localizedDescription, privacy: .public)")
352
+ return false
353
+ }
354
+ }
355
+ }
356
+
357
+ /// Forwards an APNs registration failure to the SDK as an `NSError`.
358
+ /// Resolves `true` once forwarded, `false` on failure. Never rejects.
359
+ func pushDidFailToRegister(error: PushErrorInfo) throws -> Promise<Bool> {
360
+ let nsError = Self.nsError(from: error)
361
+ return Promise.async { @MainActor in
362
+ do {
363
+ try ConnectSDK.shared.push.didFailToRegisterWithError(nsError)
364
+ return true
365
+ } catch {
366
+ bridgeLog.error("pushDidFailToRegister failed: \(error.localizedDescription, privacy: .public)")
367
+ return false
368
+ }
369
+ }
370
+ }
371
+
372
+ // MARK: - Push: notification delivery — manual mode only
373
+
374
+ /// Forwards a received notification so the SDK logs `pushReceived` (manual
375
+ /// mode). Resolves `true` when processed. In automatic/off mode the SDK
376
+ /// throws `pushModeNotManual` (its own delegate already handles delivery);
377
+ /// the bridge catches it and resolves `false` — the `EAC-RN-007` surface.
378
+ /// Never rejects.
379
+ func pushDidReceiveNotification(userInfo: [String: Variant_Bool_String_Double]) throws -> Promise<Bool> {
380
+ let notification = HybridAcousticConnectNotification(userInfo: convertToAnyDictionary(input: userInfo))
381
+ return Promise.async { @MainActor in
382
+ do {
383
+ try ConnectSDK.shared.push.didReceiveNotification(notification)
384
+ return true
385
+ } catch {
386
+ bridgeLog.error("pushDidReceiveNotification failed: \(error.localizedDescription, privacy: .public)")
387
+ return false
388
+ }
389
+ }
390
+ }
391
+
392
+ /// Forwards a notification response (tap / action) so the SDK runs the
393
+ /// built-in action and logs `pushAction` (manual mode). Resolves `true` when
394
+ /// processed; `false` (EAC-RN-007) in automatic/off mode via the same
395
+ /// caught `pushModeNotManual`. Never rejects.
396
+ func pushDidReceiveResponse(actionIdentifier: String, userInfo: [String: Variant_Bool_String_Double]) throws -> Promise<Bool> {
397
+ let response = HybridAcousticConnectNotificationResponse(
398
+ actionIdentifier: actionIdentifier,
399
+ userInfo: convertToAnyDictionary(input: userInfo)
400
+ )
401
+ return Promise.async { @MainActor in
402
+ do {
403
+ try ConnectSDK.shared.push.didReceive(response)
404
+ return true
405
+ } catch {
406
+ bridgeLog.error("pushDidReceiveResponse failed: \(error.localizedDescription, privacy: .public)")
407
+ return false
408
+ }
409
+ }
410
+ }
411
+
412
+ // MARK: - Push: permission management
413
+
414
+ /// Forwards externally-obtained permission state to the SDK. Tri-state
415
+ /// `granted`: `true`/`false` forward; `nil` (notDetermined) is recorded by
416
+ /// the OS, not forwarded — the SDK has no notion of an "unknown" state.
417
+ func pushDidReceiveAuthorization(granted: Variant_NullType_Bool?, error: PushErrorInfo?) throws -> Promise<Bool> {
418
+ // `nil` (notDetermined) is intentionally not forwarded — the SDK has no
419
+ // notion of an "unknown" authorization. This resolves `true` ("handled —
420
+ // accepted, not forwarded"), not a failure.
421
+ guard let triState = granted?.asType(Bool.self) else {
422
+ return Promise.resolved(withResult: true)
423
+ }
424
+ // Not gated on push mode: the SDK's didReceiveAuthorization is safe in
425
+ // both modes, so externally-obtained permission state is always forwarded.
426
+ let nsError = error.map(Self.nsError(from:))
427
+ return Promise.async { @MainActor in
428
+ do {
429
+ try ConnectSDK.shared.push.didReceiveAuthorization(granted: triState, error: nsError)
430
+ return true
431
+ } catch {
432
+ bridgeLog.error("pushDidReceiveAuthorization failed: \(error.localizedDescription, privacy: .public)")
433
+ return false
434
+ }
435
+ }
436
+ }
437
+
438
+ /// Requests notification permission via the SDK, presenting the system
439
+ /// prompt when the status is undetermined, and resolves with the structured
440
+ /// result. Never rejects — a system error (or push-not-enabled) surfaces in
441
+ /// `error` with `granted: false`.
442
+ func pushRequestPermission() throws -> Promise<PushPermissionResult> {
443
+ return Promise.async { @MainActor in
444
+ do {
445
+ let result = try await ConnectSDK.shared.push.requestAuthorization()
446
+ let error: Variant_NullType_String? = result.error.map { .second($0.localizedDescription) }
447
+ return PushPermissionResult(granted: result.granted, error: error)
448
+ } catch {
449
+ bridgeLog.error("pushRequestPermission failed: \(error.localizedDescription, privacy: .public)")
450
+ return PushPermissionResult(granted: false, error: .second(error.localizedDescription))
451
+ }
452
+ }
453
+ }
454
+
455
+ /// Reads the current notification permission state without prompting, mapped
456
+ /// to a tri-state: `true` granted, `false` denied, `null` not determined.
457
+ /// Never rejects — push-not-enabled resolves as `null`.
458
+ func pushGetPermissionState() throws -> Promise<Variant_NullType_Bool> {
459
+ return Promise.async { @MainActor in
460
+ do {
461
+ if let granted = try await ConnectSDK.shared.push.getCurrentAuthorization() {
462
+ return .second(granted)
463
+ }
464
+ return .first(.null)
465
+ } catch {
466
+ bridgeLog.error("pushGetPermissionState failed: \(error.localizedDescription, privacy: .public)")
467
+ return .first(.null)
468
+ }
469
+ }
470
+ }
471
+
472
+ // MARK: - Push: helpers
473
+
474
+ /// Builds an `NSError` from the structured bridge error object, shared by
475
+ /// `pushDidFailToRegister` and `pushDidReceiveAuthorization`.
476
+ private static func nsError(from info: PushErrorInfo) -> NSError {
477
+ NSError(
478
+ domain: info.domain ?? "ConnectRNBridge",
479
+ code: Int(info.code ?? -1),
480
+ userInfo: [NSLocalizedDescriptionKey: info.message]
481
+ )
482
+ }
483
+
307
484
  /// Sets the module's configuration item from AdvancedConfig.json or BasicConfig.plist that matches the specified key as a BOOL value.
308
485
  /// - Parameters:
309
486
  /// - key: Key to update value in configuration settings.