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.
- package/AcousticConnectRN.podspec +7 -4
- package/README.md +36 -7
- package/android/CMakeLists.txt +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/assets/ConnectBasicConfig.properties +1 -1
- package/android/src/main/java/com/acousticconnectrn/AcousticConnectRNPackage.java +1 -1
- package/android/src/main/java/com/acousticconnectrn/HybridAcousticConnectRN.kt +221 -13
- package/ios/Bridge.h +1 -1
- package/ios/HybridAcousticConnectRN.swift +179 -2
- package/lib/commonjs/TLTRN.js +23 -3
- package/lib/commonjs/TLTRN.js.map +1 -1
- package/lib/commonjs/components/Connect.js +1 -1
- package/lib/commonjs/index.js +13 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/withAcousticAutoDialog.js +1 -1
- package/lib/module/TLTRN.js +23 -3
- package/lib/module/TLTRN.js.map +1 -1
- package/lib/module/components/Connect.js +1 -1
- package/lib/module/index.js +14 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/withAcousticAutoDialog.js +1 -1
- package/lib/typescript/src/TLTRN.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts +109 -0
- package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts.map +1 -1
- package/lib/typescript/src/utils/withAcousticAutoDialog.d.ts +1 -1
- package/nitrogen/generated/android/AcousticConnectRN+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.cpp +137 -1
- package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.hpp +7 -0
- package/nitrogen/generated/android/c++/JPushErrorInfo.hpp +66 -0
- package/nitrogen/generated/android/c++/JPushPermissionResult.hpp +66 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Boolean.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Boolean.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/HybridAcousticConnectRNSpec.kt +30 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/PushErrorInfo.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/PushPermissionResult.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/Variant_NullType_Boolean.kt +62 -0
- package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Bridge.cpp +32 -0
- package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Bridge.hpp +249 -0
- package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Umbrella.hpp +8 -0
- package/nitrogen/generated/ios/c++/HybridAcousticConnectRNSpecSwift.hpp +67 -1
- package/nitrogen/generated/ios/swift/Func_void_PushPermissionResult.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__bool_.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec.swift +7 -0
- package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec_cxx.swift +205 -0
- package/nitrogen/generated/ios/swift/PushErrorInfo.swift +65 -0
- package/nitrogen/generated/ios/swift/PushPermissionResult.swift +66 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_Bool.swift +30 -0
- package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.cpp +7 -0
- package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.hpp +15 -1
- package/nitrogen/generated/shared/c++/PushErrorInfo.hpp +92 -0
- package/nitrogen/generated/shared/c++/PushPermissionResult.hpp +90 -0
- package/package.json +5 -5
- package/src/TLTRN.ts +31 -11
- package/src/components/Connect.tsx +1 -1
- package/src/index.ts +19 -2
- package/src/specs/react-native-acoustic-connect.nitro.ts +129 -1
- 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
|
-
#
|
|
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
|
-
# (
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
253
|
+
npm ls react-native-nitro-modules
|
|
230
254
|
```
|
|
231
255
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
package/android/CMakeLists.txt
CHANGED
package/android/build.gradle
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// prohibited.
|
|
8
8
|
//
|
|
9
9
|
//
|
|
10
|
-
// Created
|
|
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
|
|
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
|
-
*
|
|
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).
|
|
236
|
-
* Connect push API on Android
|
|
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.
|
|
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
|
|
249
|
-
//
|
|
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.
|
|
1071
|
-
//
|
|
1072
|
-
//
|
|
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.
|
|
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
|
|
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-
|
|
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.
|