react-native-acoustic-connect-beta 19.0.14 → 19.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/ConnectConfig.example.json +1 -1
- package/Examples/SampleUI/ConnectConfig.json +2 -2
- package/README.md +164 -1
- package/android/build.gradle +64 -0
- package/android/config.gradle +123 -12
- package/android/src/main/assets/ConnectAdvancedConfig.json +1 -1
- package/android/src/main/assets/ConnectBasicConfig.properties +1 -1
- package/android/src/main/java/com/acousticconnectrn/HybridAcousticConnectRN.kt +210 -20
- package/ios/ConnectRNParsing.swift +229 -0
- package/ios/HybridAcousticConnectRN.swift +125 -27
- package/lib/commonjs/TLTRN.js +26 -3
- package/lib/commonjs/TLTRN.js.map +1 -1
- package/lib/commonjs/index.js +3 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/TLTRN.js +26 -3
- package/lib/module/TLTRN.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/TLTRN.d.ts +22 -1
- package/lib/typescript/src/TLTRN.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts +150 -10
- package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.cpp +5 -9
- package/nitrogen/generated/android/c++/JHybridAcousticConnectRNSpec.hpp +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/acousticconnectrn/HybridAcousticConnectRNSpec.kt +2 -1
- package/nitrogen/generated/ios/AcousticConnectRN-Swift-Cxx-Umbrella.hpp +1 -0
- package/nitrogen/generated/ios/c++/HybridAcousticConnectRNSpecSwift.hpp +2 -1
- package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec.swift +1 -1
- package/nitrogen/generated/ios/swift/HybridAcousticConnectRNSpec_cxx.swift +2 -25
- package/nitrogen/generated/shared/c++/HybridAcousticConnectRNSpec.hpp +2 -1
- package/package.json +2 -1
- package/src/TLTRN.ts +31 -3
- package/src/index.ts +5 -0
- package/src/specs/react-native-acoustic-connect.nitro.ts +152 -10
|
@@ -62,6 +62,7 @@ import com.margelo.nitro.acousticconnectrn.PushPermissionResult
|
|
|
62
62
|
import com.margelo.nitro.acousticconnectrn.Variant_Boolean_String_Double
|
|
63
63
|
import com.margelo.nitro.acousticconnectrn.Variant_NullType_Boolean
|
|
64
64
|
import com.margelo.nitro.acousticconnectrn.Variant_NullType_String
|
|
65
|
+
import com.margelo.nitro.core.AnyMap
|
|
65
66
|
import com.margelo.nitro.core.ArrayBuffer
|
|
66
67
|
import com.margelo.nitro.core.NullType
|
|
67
68
|
import com.margelo.nitro.core.Promise
|
|
@@ -70,6 +71,9 @@ import com.tl.uic.model.ScreenviewType
|
|
|
70
71
|
import com.tl.uic.util.DialogUtil
|
|
71
72
|
import com.tl.uic.util.LayoutUtil
|
|
72
73
|
import com.tl.uic.util.keyboardview.KeyboardView
|
|
74
|
+
import org.json.JSONArray
|
|
75
|
+
import org.json.JSONException
|
|
76
|
+
import org.json.JSONObject
|
|
73
77
|
import java.util.Objects
|
|
74
78
|
|
|
75
79
|
|
|
@@ -592,7 +596,8 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
592
596
|
value: Variant_Boolean_String_Double,
|
|
593
597
|
moduleName: String
|
|
594
598
|
): Boolean {
|
|
595
|
-
val result =
|
|
599
|
+
val result =
|
|
600
|
+
EOCore.updateConfig(key, variantToString(value), EOCore.getLifecycleObject(moduleName))
|
|
596
601
|
return result
|
|
597
602
|
}
|
|
598
603
|
|
|
@@ -668,15 +673,21 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
668
673
|
/**
|
|
669
674
|
* Logs a signal with the specified values.
|
|
670
675
|
*
|
|
671
|
-
*
|
|
676
|
+
* Takes an [AnyMap] rather than a map of Nitro variants so JS callers can
|
|
677
|
+
* send arbitrary JSON — nested objects and arrays included. See
|
|
678
|
+
* [toSignalPayload] for why the nesting has to be rebuilt as
|
|
679
|
+
* [JSONObject]/[JSONArray] rather than handed over as plain Kotlin
|
|
680
|
+
* collections.
|
|
681
|
+
*
|
|
682
|
+
* @param values The signal payload; objects, arrays and scalars are all carried through.
|
|
672
683
|
* @param level Set a custom log level to the event. This will override the configured log level for that event.
|
|
673
684
|
* @return True if the operation was successful, false otherwise.
|
|
674
685
|
*/
|
|
675
686
|
override fun logSignal(
|
|
676
|
-
values:
|
|
687
|
+
values: AnyMap,
|
|
677
688
|
level: Double
|
|
678
689
|
): Boolean {
|
|
679
|
-
val result = Connect.logSignal(
|
|
690
|
+
val result = Connect.logSignal(toSignalPayload(values.toHashMap()), level.toInt())
|
|
680
691
|
return result
|
|
681
692
|
}
|
|
682
693
|
|
|
@@ -697,8 +708,11 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
697
708
|
* @param identifierValue Identifier value, e.g. "user@example.com".
|
|
698
709
|
* @param signalType Optional signal type; defaults to "loggedIn" when omitted.
|
|
699
710
|
* @param additionalParameters Optional extra key/value pairs merged into the
|
|
700
|
-
* signal. Defaults
|
|
701
|
-
*
|
|
711
|
+
* signal. Defaults, only when null (omitted), to the method attribute the
|
|
712
|
+
* resolved signal type requires — `{ "loginMethod": "email" }` for
|
|
713
|
+
* `loggedIn`, `{ "registrationMethod": "email" }` otherwise (see
|
|
714
|
+
* [defaultIdentityParameters]). An explicit map — including an empty one
|
|
715
|
+
* — is used as-is.
|
|
702
716
|
* @return A promise resolving to true if the signal was queued, false otherwise.
|
|
703
717
|
*/
|
|
704
718
|
override fun logIdentity(
|
|
@@ -707,8 +721,8 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
707
721
|
signalType: String?,
|
|
708
722
|
additionalParameters: Map<String, String>?
|
|
709
723
|
): Promise<Boolean> {
|
|
710
|
-
val params = additionalParameters ?: mapOf("registrationMethod" to "email")
|
|
711
724
|
val resolvedSignalType = signalType ?: "loggedIn"
|
|
725
|
+
val params = additionalParameters ?: defaultIdentityParameters(resolvedSignalType)
|
|
712
726
|
val result = params["url"]?.let { url ->
|
|
713
727
|
Connect.logIdentificationEvent(
|
|
714
728
|
identifierName,
|
|
@@ -896,7 +910,10 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
896
910
|
* Log Current Screen Layout using native side background thread.
|
|
897
911
|
*
|
|
898
912
|
* @param name Page name or title e.g. "Login View Controller"; Must not be empty.
|
|
899
|
-
* @param delay The delay in milliseconds before logging the event.
|
|
913
|
+
* @param delay The delay in milliseconds before logging the event. A negative
|
|
914
|
+
* value means "use the `CaptureLayoutDelay` configured for this
|
|
915
|
+
* screen", which is what the JS wrapper sends when the caller
|
|
916
|
+
* names no delay.
|
|
900
917
|
* @return True if the operation was successful, false otherwise.
|
|
901
918
|
*/
|
|
902
919
|
override fun logScreenLayout(name: String, delay: Double): Boolean {
|
|
@@ -907,13 +924,45 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
907
924
|
result = logScreenLayout(
|
|
908
925
|
Objects.requireNonNull<Activity?>(getCurrentActivity()),
|
|
909
926
|
name,
|
|
910
|
-
|
|
927
|
+
resolveCaptureLayoutDelayMs(name, delay),
|
|
911
928
|
true
|
|
912
929
|
)
|
|
913
930
|
}
|
|
914
931
|
return result
|
|
915
932
|
}
|
|
916
933
|
|
|
934
|
+
/**
|
|
935
|
+
* Resolves the capture delay, in milliseconds, to apply for [name].
|
|
936
|
+
*
|
|
937
|
+
* A non-negative [delay] is the caller's explicit choice and passes
|
|
938
|
+
* straight through. A negative one defers to `CaptureLayoutDelay` in the
|
|
939
|
+
* layout config, which is what makes that setting reach the React Native
|
|
940
|
+
* wrapper at all — the wrapper used to hardcode 0 here, so the capture
|
|
941
|
+
* fired at the start of the screen transition and the configured value
|
|
942
|
+
* was inert.
|
|
943
|
+
*
|
|
944
|
+
* [LayoutUtil.getLayoutInfo] returns the merged rule for the screen: the
|
|
945
|
+
* global settings as a baseline with any per-screen rule applied over
|
|
946
|
+
* them, so this picks up either without re-implementing the precedence.
|
|
947
|
+
* It reads the config off disk, so a malformed or absent block falls back
|
|
948
|
+
* to [DEFAULT_CAPTURE_LAYOUT_DELAY_MS] rather than propagating a throw
|
|
949
|
+
* into a logging call.
|
|
950
|
+
*/
|
|
951
|
+
internal fun resolveCaptureLayoutDelayMs(name: String, delay: Double): Int {
|
|
952
|
+
if (delay >= 0) {
|
|
953
|
+
return delay.toInt()
|
|
954
|
+
}
|
|
955
|
+
return try {
|
|
956
|
+
LayoutUtil.getLayoutInfo(name)
|
|
957
|
+
?.optInt(CAPTURE_LAYOUT_DELAY_KEY, DEFAULT_CAPTURE_LAYOUT_DELAY_MS)
|
|
958
|
+
?.coerceAtLeast(0)
|
|
959
|
+
?: DEFAULT_CAPTURE_LAYOUT_DELAY_MS
|
|
960
|
+
} catch (e: Exception) {
|
|
961
|
+
Log.w(TAG, "Could not read $CAPTURE_LAYOUT_DELAY_KEY for \"$name\"; using ${DEFAULT_CAPTURE_LAYOUT_DELAY_MS}ms.", e)
|
|
962
|
+
DEFAULT_CAPTURE_LAYOUT_DELAY_MS
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
917
966
|
/**
|
|
918
967
|
* Logs a dialog show event with the specified dialog information.
|
|
919
968
|
*
|
|
@@ -1187,7 +1236,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1187
1236
|
for (key in values.keys) {
|
|
1188
1237
|
val value = values[key]
|
|
1189
1238
|
if (value != null) {
|
|
1190
|
-
eventValues[key] = value
|
|
1239
|
+
eventValues[key] = variantToString(value)
|
|
1191
1240
|
}
|
|
1192
1241
|
}
|
|
1193
1242
|
|
|
@@ -1200,9 +1249,42 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1200
1249
|
return result
|
|
1201
1250
|
}
|
|
1202
1251
|
|
|
1252
|
+
/**
|
|
1253
|
+
* Unwraps a Nitro variant to the underlying JS value it carries — a
|
|
1254
|
+
* [Boolean], [String], or [Double].
|
|
1255
|
+
*
|
|
1256
|
+
* The generated `Variant_*` type is a sealed **data** class, so calling
|
|
1257
|
+
* `toString()` on the variant itself yields the case wrapper
|
|
1258
|
+
* (`"Second(value=pro)"`) instead of the value (`"pro"`). Every conversion
|
|
1259
|
+
* that reads a variant must unwrap it first. Mirrors the iOS bridge's
|
|
1260
|
+
* `ConnectRNParsing.convertVariantToAny`, keeping emitted payloads
|
|
1261
|
+
* identical across platforms.
|
|
1262
|
+
*
|
|
1263
|
+
* @param value The variant to unwrap.
|
|
1264
|
+
* @return The wrapped Boolean, String, or Double.
|
|
1265
|
+
*/
|
|
1266
|
+
private fun unwrapVariant(value: Variant_Boolean_String_Double): Any =
|
|
1267
|
+
value.match(first = { it }, second = { it }, third = { it })
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Unwraps a Nitro variant (see [unwrapVariant]) and renders it as a string,
|
|
1271
|
+
* for the SDK entry points that take `HashMap<String, String>` payloads.
|
|
1272
|
+
*
|
|
1273
|
+
* Booleans render as `"true"`/`"false"` and numbers via Kotlin's [Double]
|
|
1274
|
+
* formatting (a JS `2` arrives as `2.0` and renders `"2.0"`); strings pass
|
|
1275
|
+
* through unchanged.
|
|
1276
|
+
*
|
|
1277
|
+
* @param value The variant to stringify.
|
|
1278
|
+
* @return The wrapped value's string form.
|
|
1279
|
+
*/
|
|
1280
|
+
private fun variantToString(value: Variant_Boolean_String_Double): String =
|
|
1281
|
+
unwrapVariant(value).toString()
|
|
1282
|
+
|
|
1203
1283
|
/**
|
|
1204
1284
|
* Converts a map of Variant_Boolean_String_Double to a HashMap<String?, String?>.
|
|
1205
1285
|
*
|
|
1286
|
+
* Values are unwrapped before stringifying (see [variantToString]).
|
|
1287
|
+
*
|
|
1206
1288
|
* @param values The map to be converted.
|
|
1207
1289
|
* @return A HashMap<String?, String?> representation of the input map which library can use.
|
|
1208
1290
|
*/
|
|
@@ -1212,30 +1294,106 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1212
1294
|
for (key in values.keys) {
|
|
1213
1295
|
val value = values[key]
|
|
1214
1296
|
if (value != null) {
|
|
1215
|
-
map[key] = value
|
|
1297
|
+
map[key] = variantToString(value)
|
|
1216
1298
|
}
|
|
1217
1299
|
}
|
|
1218
1300
|
return map
|
|
1219
1301
|
}
|
|
1220
1302
|
|
|
1221
1303
|
/**
|
|
1222
|
-
* Converts
|
|
1304
|
+
* Converts an [AnyMap]-derived map into the payload `Connect.logSignal`
|
|
1305
|
+
* expects, rebuilding nested structures as [JSONObject] / [JSONArray].
|
|
1223
1306
|
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1307
|
+
* The rebuild is mandatory, not cosmetic. The SDK serializes the signal
|
|
1308
|
+
* through EOCore's `JsonUtil.getHashValues`, which walks the top-level
|
|
1309
|
+
* entries and accumulates only `String`, `Boolean`, `JSONObject`,
|
|
1310
|
+
* `JSONArray` and `ByteArray` — anything else is **silently dropped**. So
|
|
1311
|
+
* a nested `Map`/`List` handed over as-is would vanish from the emitted
|
|
1312
|
+
* signal without any error. Converting here is what makes nested payloads
|
|
1313
|
+
* survive the wire.
|
|
1314
|
+
*
|
|
1315
|
+
* `null` becomes [JSONObject.NULL] rather than a Kotlin `null`, which
|
|
1316
|
+
* `org.json` requires for an explicit JSON `null`.
|
|
1317
|
+
*
|
|
1318
|
+
* Known limitation: a **top-level** numeric value is still dropped, because
|
|
1319
|
+
* `getHashValues` has no `Number` branch and the bridge cannot influence
|
|
1320
|
+
* that walk. Numbers nested inside an object or array are unaffected — the
|
|
1321
|
+
* enclosing [JSONObject]/[JSONArray] is built here, so `org.json`
|
|
1322
|
+
* serializes them normally. iOS carries top-level numbers fine; this is an
|
|
1323
|
+
* SDK-side asymmetry, pre-dating this conversion.
|
|
1324
|
+
*
|
|
1325
|
+
* A [JSONException] aborts the whole payload rather than escaping to the
|
|
1326
|
+
* caller. Non-finite numbers are the case that raises it: `org.json`
|
|
1327
|
+
* rejects them, and [toJsonValue] raises them explicitly so a top-level one
|
|
1328
|
+
* behaves like a nested one. Either way a JS `Infinity` or `NaN` would
|
|
1329
|
+
* otherwise throw straight out of `logSignal` and take the host app's call
|
|
1330
|
+
* site down — unacceptable for an analytics SDK, and a behaviour change
|
|
1331
|
+
* from the scalar-only bridge, which could not throw. Dropping the whole
|
|
1332
|
+
* payload also matches iOS, where a single non-finite value fails
|
|
1333
|
+
* `isValidJSONObject:` and the `signal` key is omitted entirely.
|
|
1334
|
+
*
|
|
1335
|
+
* @param values The map from [AnyMap.toHashMap].
|
|
1336
|
+
* @return A HashMap the SDK's signal serializer can consume, or an empty
|
|
1337
|
+
* map if the payload could not be represented as JSON.
|
|
1226
1338
|
*/
|
|
1227
1339
|
// internal (not private) so the unit tests in src/test can exercise it.
|
|
1228
|
-
internal fun
|
|
1229
|
-
val map = HashMap<String?, Any?>()
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
map[key] = value
|
|
1340
|
+
internal fun toSignalPayload(values: Map<String, Any?>): java.util.HashMap<String?, Any?> {
|
|
1341
|
+
val map = HashMap<String?, Any?>(values.size)
|
|
1342
|
+
try {
|
|
1343
|
+
for ((key, value) in values) {
|
|
1344
|
+
map[key] = toJsonValue(value)
|
|
1234
1345
|
}
|
|
1346
|
+
} catch (e: JSONException) {
|
|
1347
|
+
Log.w(TAG, "[bridge] logSignal payload is not representable as JSON — dropping it: ${e.message}")
|
|
1348
|
+
return HashMap()
|
|
1235
1349
|
}
|
|
1236
1350
|
return map
|
|
1237
1351
|
}
|
|
1238
1352
|
|
|
1353
|
+
/**
|
|
1354
|
+
* Recursive helper for [toSignalPayload]. Maps Kotlin containers onto their
|
|
1355
|
+
* `org.json` equivalents and leaves scalars alone.
|
|
1356
|
+
*
|
|
1357
|
+
* Nitro's JNI layer converts the whole tree to plain Java types before it
|
|
1358
|
+
* reaches Kotlin — a JS object becomes a `HashMap`, a JS array an
|
|
1359
|
+
* `ArrayList`, and scalars become `Double` / `Boolean` / `Long` / `String`
|
|
1360
|
+
* — so there are no `AnyValue` wrappers left to unwrap here. [List] is
|
|
1361
|
+
* therefore the branch a JS array actually takes; [Array] is handled too,
|
|
1362
|
+
* to guard against a future Nitro representation change.
|
|
1363
|
+
*
|
|
1364
|
+
* @param value A value from an [AnyMap]-derived container.
|
|
1365
|
+
* @return The `org.json`-compatible equivalent.
|
|
1366
|
+
*/
|
|
1367
|
+
private fun toJsonValue(value: Any?): Any =
|
|
1368
|
+
when (value) {
|
|
1369
|
+
null -> JSONObject.NULL
|
|
1370
|
+
is Map<*, *> -> JSONObject().apply {
|
|
1371
|
+
for ((nestedKey, nestedValue) in value) {
|
|
1372
|
+
put(nestedKey.toString(), toJsonValue(nestedValue))
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
is Array<*> -> JSONArray().apply {
|
|
1376
|
+
for (element in value) put(toJsonValue(element))
|
|
1377
|
+
}
|
|
1378
|
+
is List<*> -> JSONArray().apply {
|
|
1379
|
+
for (element in value) put(toJsonValue(element))
|
|
1380
|
+
}
|
|
1381
|
+
// Checked here rather than left to `org.json`. A non-finite number
|
|
1382
|
+
// nested in an object or array would be rejected by that container's
|
|
1383
|
+
// `put`, but a TOP-LEVEL one is only ever handed to `HashMap.put`,
|
|
1384
|
+
// which accepts anything — so without this the payload would reach
|
|
1385
|
+
// the SDK and lose just that one key, while the nested case loses
|
|
1386
|
+
// the whole payload. Raising it uniformly keeps the two consistent
|
|
1387
|
+
// and matches iOS, where one non-finite value fails
|
|
1388
|
+
// `isValidJSONObject:` for the entire signal.
|
|
1389
|
+
is Double -> if (value.isFinite()) {
|
|
1390
|
+
value
|
|
1391
|
+
} else {
|
|
1392
|
+
throw JSONException("non-finite number: $value")
|
|
1393
|
+
}
|
|
1394
|
+
else -> value
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1239
1397
|
/**
|
|
1240
1398
|
* Gets the current activity from the ReactApplicationContext.
|
|
1241
1399
|
*
|
|
@@ -1383,6 +1541,15 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1383
1541
|
const val TAG = "AcousticConnectRN"
|
|
1384
1542
|
const val DIALOG_CAPTURE_DELAY_MS = 500L // Configurable delay for dialog screenshot capture
|
|
1385
1543
|
|
|
1544
|
+
/** Key holding the per-screen capture delay, in milliseconds, inside a layout-config rule. */
|
|
1545
|
+
const val CAPTURE_LAYOUT_DELAY_KEY = "CaptureLayoutDelay"
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Delay applied when the layout config names none. Matches the value the
|
|
1549
|
+
* Connect SDK ships in its own `TealeafLayoutConfig.json`.
|
|
1550
|
+
*/
|
|
1551
|
+
const val DEFAULT_CAPTURE_LAYOUT_DELAY_MS = 500
|
|
1552
|
+
|
|
1386
1553
|
// Class probed at runtime to detect whether the connect-push-fcm
|
|
1387
1554
|
// transport artifact was included in the build (i.e. automatic mode,
|
|
1388
1555
|
// where Connect's own FirebaseMessagingService is the PushReceived
|
|
@@ -1390,5 +1557,28 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1390
1557
|
// by the connect-push-fcm artifact.
|
|
1391
1558
|
private const val CONNECT_PUSH_FCM_PROBE_CLASS =
|
|
1392
1559
|
"com.acoustic.connect.android.connectmod.push.services.fcm.FCMPushService"
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Default `additionalParameters` for an identity signal, matched to the
|
|
1563
|
+
* signal type the bridge will actually send.
|
|
1564
|
+
*
|
|
1565
|
+
* Connect's signal schema requires a *method* attribute on identity
|
|
1566
|
+
* signals, and the required key differs per type: `loggedIn` requires
|
|
1567
|
+
* `loginMethod`, `accountRegistered` requires `registrationMethod`.
|
|
1568
|
+
* Supplying the wrong one fails schema validation and the signal is
|
|
1569
|
+
* discarded, while [Connect.logIdentificationEvent] still reports
|
|
1570
|
+
* success — so the caller sees nothing wrong. Before this mapping
|
|
1571
|
+
* existed the bridge paired its `loggedIn` default with
|
|
1572
|
+
* `registrationMethod`, which meant every defaulted identity call was
|
|
1573
|
+
* silently dropped.
|
|
1574
|
+
*
|
|
1575
|
+
* Only consulted when the caller omits `additionalParameters`
|
|
1576
|
+
* entirely; an explicitly-provided map is passed through untouched.
|
|
1577
|
+
*/
|
|
1578
|
+
internal fun defaultIdentityParameters(signalType: String): Map<String, String> =
|
|
1579
|
+
when (signalType) {
|
|
1580
|
+
"loggedIn" -> mapOf("loginMethod" to "email")
|
|
1581
|
+
else -> mapOf("registrationMethod" to "email")
|
|
1582
|
+
}
|
|
1393
1583
|
}
|
|
1394
1584
|
}
|
|
@@ -104,6 +104,167 @@ internal enum ConnectRNParsing {
|
|
|
104
104
|
// PushErrorInfo is a C++-backed nitro type, and this file is also
|
|
105
105
|
// compiled into the UnitTests bundle, which builds without C++ interop.
|
|
106
106
|
|
|
107
|
+
/// `Connect` config keys that can carry an iOS layout-config block, in
|
|
108
|
+
/// **base-to-override order**: the shared block is the base, and the
|
|
109
|
+
/// iOS-specific block is layered on top of it.
|
|
110
|
+
///
|
|
111
|
+
/// `layoutConfigIos` is what the shipped `ConnectConfig.json` template
|
|
112
|
+
/// writes, and the podspec copies the consumer's file into the resource
|
|
113
|
+
/// bundle verbatim (no key rename), so that is the key that actually
|
|
114
|
+
/// arrives at runtime. `layoutConfig` is the shared, cross-platform block:
|
|
115
|
+
/// a consumer can set a common baseline there and refine it per platform.
|
|
116
|
+
///
|
|
117
|
+
/// `layoutConfigAndroid` is deliberately absent: applying it here would
|
|
118
|
+
/// push Android screen rules onto the iOS SDK.
|
|
119
|
+
static let layoutConfigKeys = ["layoutConfig", "layoutConfigIos"]
|
|
120
|
+
|
|
121
|
+
/// Entries of the layout-config block that map onto config-store overrides.
|
|
122
|
+
private static let layoutOverrideKeys = ["AutoLayout", "AppendMapIds"]
|
|
123
|
+
|
|
124
|
+
/// Recursively merges `override` onto `base`.
|
|
125
|
+
///
|
|
126
|
+
/// Nested objects merge key by key, so a shared baseline survives a
|
|
127
|
+
/// platform block that only refines part of it. Every other value —
|
|
128
|
+
/// including arrays such as `MaskIdList` — is replaced outright rather than
|
|
129
|
+
/// concatenated, so a platform block can shorten a shared list.
|
|
130
|
+
static func deepMerging(_ base: [String: Any], _ override: [String: Any]) -> [String: Any] {
|
|
131
|
+
var merged = base
|
|
132
|
+
for (key, overrideValue) in override {
|
|
133
|
+
if let baseChild = merged[key] as? [String: Any],
|
|
134
|
+
let overrideChild = overrideValue as? [String: Any] {
|
|
135
|
+
merged[key] = deepMerging(baseChild, overrideChild)
|
|
136
|
+
} else {
|
|
137
|
+
merged[key] = overrideValue
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return merged
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Resolves the iOS layout-config block into the config-store overrides to
|
|
144
|
+
/// apply, keyed by the name the native SDK reads. `AutoLayout` becomes
|
|
145
|
+
/// `kConfigurableItemAutoLayout`, which `TLFAutoInstrumentationManger`
|
|
146
|
+
/// consumes for per-screen settings such as `ScreenShot` and
|
|
147
|
+
/// `CaptureScreenshotOn`.
|
|
148
|
+
///
|
|
149
|
+
/// Both keys in ``layoutConfigKeys`` contribute: the shared `layoutConfig`
|
|
150
|
+
/// is applied first and `layoutConfigIos` is deep-merged over it, so an
|
|
151
|
+
/// iOS-specific value wins key by key while the shared baseline is kept for
|
|
152
|
+
/// everything the iOS block does not mention. A malformed block is logged
|
|
153
|
+
/// and skipped rather than aborting the rest of the resolution.
|
|
154
|
+
///
|
|
155
|
+
/// Returns an empty dictionary when nothing is applicable, in which case
|
|
156
|
+
/// the caller applies nothing and the SDK keeps the defaults from its own
|
|
157
|
+
/// bundled `ConnectLayoutConfig.json`.
|
|
158
|
+
static func resolveLayoutOverrides(from connectData: [String: Any]) -> [String: Any] {
|
|
159
|
+
var block: [String: Any] = [:]
|
|
160
|
+
var contributing: [String] = []
|
|
161
|
+
|
|
162
|
+
for name in layoutConfigKeys {
|
|
163
|
+
guard let value = connectData[name] else { continue }
|
|
164
|
+
guard let candidate = value as? [String: Any] else {
|
|
165
|
+
configLog.error("Connect.\(name, privacy: .public) in ConnectConfig.json is not a JSON object. Ignoring it; iOS screen-capture settings fall back to the SDK defaults.")
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
block = deepMerging(block, candidate)
|
|
169
|
+
contributing.append(name)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
guard !contributing.isEmpty else { return [:] }
|
|
173
|
+
|
|
174
|
+
var overrides: [String: Any] = [:]
|
|
175
|
+
for name in layoutOverrideKeys {
|
|
176
|
+
if let override = block[name] {
|
|
177
|
+
overrides[name] = override
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let sources = contributing.joined(separator: " <- ")
|
|
182
|
+
if overrides.isEmpty {
|
|
183
|
+
configLog.warning("Connect.\(sources, privacy: .public) carries no recognised override (expected AutoLayout and/or AppendMapIds). iOS screen-capture settings fall back to the SDK defaults.")
|
|
184
|
+
} else {
|
|
185
|
+
let applied = overrides.keys.sorted().joined(separator: ", ")
|
|
186
|
+
configLog.info("Applying layout overrides [\(applied, privacy: .public)] resolved from Connect.\(sources, privacy: .public)")
|
|
187
|
+
}
|
|
188
|
+
return overrides
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// MARK: - Capture-layout delay
|
|
192
|
+
|
|
193
|
+
/// Config-store key holding the resolved `AutoLayout` block. Spelled out
|
|
194
|
+
/// rather than taken from Tealeaf's `kConfigurableItemAutoLayout` macro
|
|
195
|
+
/// because the bridge deliberately links against `Connect` only — see the
|
|
196
|
+
/// note on `ConnectConfigStore`. Same string either way.
|
|
197
|
+
static let autoLayoutConfigKey = "AutoLayout"
|
|
198
|
+
|
|
199
|
+
/// Key holding the per-screen capture delay inside an `AutoLayout` rule.
|
|
200
|
+
/// The value is in **milliseconds** — the auto-instrumentation path
|
|
201
|
+
/// dispatches with `NSEC_PER_MSEC`, and `TLFAutoLayoutConfig` names its
|
|
202
|
+
/// property `delayInMS`.
|
|
203
|
+
private static let captureLayoutDelayKey = "CaptureLayoutDelay"
|
|
204
|
+
|
|
205
|
+
/// Rule names holding the settings that apply to every screen. The second
|
|
206
|
+
/// is the legacy spelling; the native SDK reads whichever is present, so
|
|
207
|
+
/// this does too.
|
|
208
|
+
private static let globalScreenSettingsKeys = ["GlobalScreenSettings", "IBMGlobalScreenSettings"]
|
|
209
|
+
|
|
210
|
+
/// Delay applied when nothing in the resolved `AutoLayout` block names one.
|
|
211
|
+
/// Matches `TLFAutoInstrumentationManger`'s own fallback.
|
|
212
|
+
static let defaultCaptureLayoutDelayMs: Double = 500
|
|
213
|
+
|
|
214
|
+
/// Resolves the `CaptureLayoutDelay` (milliseconds) that applies to
|
|
215
|
+
/// `name`, from the `AutoLayout` block the native SDK holds in its config
|
|
216
|
+
/// store.
|
|
217
|
+
///
|
|
218
|
+
/// Resolution mirrors `TLFAutoInstrumentationManger`: the global rule
|
|
219
|
+
/// supplies the baseline, and a rule named after the screen overrides it.
|
|
220
|
+
/// The JS wrapper looks the screen up by its React Navigation route name,
|
|
221
|
+
/// which is the key `ConnectConfig.json`'s `layoutConfig`/`layoutConfigIos`
|
|
222
|
+
/// blocks are written against.
|
|
223
|
+
///
|
|
224
|
+
/// Returns ``defaultCaptureLayoutDelayMs`` when the block is missing,
|
|
225
|
+
/// malformed, or names no delay — never a value that would make the caller
|
|
226
|
+
/// silently skip the deferral it asked for.
|
|
227
|
+
static func captureLayoutDelayMs(for name: String, in autoLayout: [String: Any]?) -> Double {
|
|
228
|
+
guard let autoLayout else { return defaultCaptureLayoutDelayMs }
|
|
229
|
+
|
|
230
|
+
if let screenDelay = delayValue(in: autoLayout[name]) {
|
|
231
|
+
return screenDelay
|
|
232
|
+
}
|
|
233
|
+
for key in globalScreenSettingsKeys {
|
|
234
|
+
if let globalDelay = delayValue(in: autoLayout[key]) {
|
|
235
|
+
return globalDelay
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return defaultCaptureLayoutDelayMs
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Pulls `CaptureLayoutDelay` out of one `AutoLayout` rule. `nil` means
|
|
242
|
+
/// "this rule says nothing about the delay", which is what lets the caller
|
|
243
|
+
/// fall through to the next source rather than treating a missing rule as
|
|
244
|
+
/// an explicit zero.
|
|
245
|
+
private static func delayValue(in rule: Any?) -> Double? {
|
|
246
|
+
guard let rule = rule as? [String: Any] else { return nil }
|
|
247
|
+
guard let raw = rule[captureLayoutDelayKey] else { return nil }
|
|
248
|
+
if let number = raw as? NSNumber { return max(0, number.doubleValue) }
|
|
249
|
+
if let text = raw as? String, let parsed = Double(text) { return max(0, parsed) }
|
|
250
|
+
configLog.warning("\(captureLayoutDelayKey, privacy: .public) is set but is not a number. Ignoring it.")
|
|
251
|
+
return nil
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Whether `ConnectConfig.json` supplies an `AutoLayout` block at all.
|
|
255
|
+
///
|
|
256
|
+
/// Equivalent to asking whether ``resolveLayoutOverrides(from:)`` would
|
|
257
|
+
/// return an `AutoLayout` override — `deepMerging` only ever adds keys, so
|
|
258
|
+
/// the merged block carries `AutoLayout` exactly when some contributing
|
|
259
|
+
/// block does — but silent, and cheap enough to run at construction. The
|
|
260
|
+
/// resolver logs which sources it applied; calling it twice would print
|
|
261
|
+
/// that summary twice.
|
|
262
|
+
static func suppliesAutoLayout(_ connectData: [String: Any]) -> Bool {
|
|
263
|
+
return layoutConfigKeys.contains { name in
|
|
264
|
+
(connectData[name] as? [String: Any])?[autoLayoutConfigKey] != nil
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
107
268
|
/// Maps the JS-facing module name to the config store's module name
|
|
108
269
|
/// ("Connect" → "TLFCoreModule").
|
|
109
270
|
static func storeModuleName(for moduleName: String) -> String {
|
|
@@ -130,6 +291,51 @@ internal enum ConnectRNParsing {
|
|
|
130
291
|
return result
|
|
131
292
|
}
|
|
132
293
|
|
|
294
|
+
/// Normalises a dictionary produced by `AnyMap.toDictionary()` into one
|
|
295
|
+
/// that `NSJSONSerialization.isValidJSONObject:` accepts, which is the
|
|
296
|
+
/// only gate `CTSignalMessage` applies before embedding the payload.
|
|
297
|
+
///
|
|
298
|
+
/// Two things need fixing up. `AnyMap` models JS `null` as a Swift `nil`,
|
|
299
|
+
/// and a dictionary holding `Optional.none` is not JSON-representable —
|
|
300
|
+
/// `NSNull` is. And the nesting is optional-typed all the way down
|
|
301
|
+
/// (`[String: Any?]` / `[Any?]`), so the walk has to be recursive rather
|
|
302
|
+
/// than a single top-level pass. Values that are already scalars pass
|
|
303
|
+
/// through untouched, so a flat payload converts to exactly what the
|
|
304
|
+
/// previous scalar-only bridge produced.
|
|
305
|
+
///
|
|
306
|
+
/// Deliberately takes plain Swift collections rather than `AnyMap`: this
|
|
307
|
+
/// file is compiled into the UnitTests bundle, which builds without C++
|
|
308
|
+
/// interop and therefore cannot see nitro's `AnyMap`/`AnyValue` types.
|
|
309
|
+
/// The `AnyMap` unwrapping stays in `HybridAcousticConnectRN`.
|
|
310
|
+
///
|
|
311
|
+
/// - Parameter input: dictionary from `AnyMap.toDictionary()`.
|
|
312
|
+
/// - Returns: the same structure with optionals resolved.
|
|
313
|
+
static func jsonSafeDictionary(_ input: [String: Any?]) -> [String: Any] {
|
|
314
|
+
var result = [String: Any](minimumCapacity: input.count)
|
|
315
|
+
for (key, value) in input {
|
|
316
|
+
result[key] = jsonSafeValue(value)
|
|
317
|
+
}
|
|
318
|
+
return result
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Recursive helper for ``jsonSafeDictionary(_:)``. Maps `nil` to `NSNull`
|
|
322
|
+
/// and rebuilds nested dictionaries/arrays; every other value — `String`,
|
|
323
|
+
/// `Double`, `Int64`, `Bool` — is already JSON-representable and is
|
|
324
|
+
/// returned as-is.
|
|
325
|
+
///
|
|
326
|
+
/// - Parameter value: a value from an `AnyMap`-derived container.
|
|
327
|
+
/// - Returns: a JSON-representable equivalent.
|
|
328
|
+
static func jsonSafeValue(_ value: Any?) -> Any {
|
|
329
|
+
guard let value else { return NSNull() }
|
|
330
|
+
if let nested = value as? [String: Any?] {
|
|
331
|
+
return jsonSafeDictionary(nested)
|
|
332
|
+
}
|
|
333
|
+
if let array = value as? [Any?] {
|
|
334
|
+
return array.map { jsonSafeValue($0) }
|
|
335
|
+
}
|
|
336
|
+
return value
|
|
337
|
+
}
|
|
338
|
+
|
|
133
339
|
static func convertVariantToAny(_ variant: Variant_Bool_String_Double) -> Any {
|
|
134
340
|
switch variant {
|
|
135
341
|
case .first(let boolValue):
|
|
@@ -141,6 +347,29 @@ internal enum ConnectRNParsing {
|
|
|
141
347
|
}
|
|
142
348
|
}
|
|
143
349
|
|
|
350
|
+
/// Default `additionalParameters` for an identity signal, matched to the
|
|
351
|
+
/// signal type the bridge will actually send.
|
|
352
|
+
///
|
|
353
|
+
/// Connect's signal schema requires a *method* attribute on identity
|
|
354
|
+
/// signals, and the required key differs per type: `loggedIn` requires
|
|
355
|
+
/// `loginMethod`, `accountRegistered` requires `registrationMethod`.
|
|
356
|
+
/// Supplying the wrong one fails schema validation and the signal is
|
|
357
|
+
/// discarded, while `identity.log` still reports success — so the caller
|
|
358
|
+
/// sees nothing wrong. Before this mapping existed the bridge paired its
|
|
359
|
+
/// `loggedIn` default with `registrationMethod`, which meant every
|
|
360
|
+
/// defaulted identity call was silently dropped.
|
|
361
|
+
///
|
|
362
|
+
/// Only consulted when the caller omits `additionalParameters` entirely;
|
|
363
|
+
/// an explicitly-provided map is passed through untouched.
|
|
364
|
+
static func defaultIdentityParameters(for signalType: String) -> [String: String] {
|
|
365
|
+
switch signalType {
|
|
366
|
+
case "loggedIn":
|
|
367
|
+
return ["loginMethod": "email"]
|
|
368
|
+
default:
|
|
369
|
+
return ["registrationMethod": "email"]
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
144
373
|
static func logLevel(from level: Double) -> kConnectMonitoringLevelType {
|
|
145
374
|
let intValue: Int = Int(level)
|
|
146
375
|
if intValue == 0 {
|