react-native-acoustic-connect-beta 19.0.13 → 19.0.15
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 +7 -7
- package/README.md +123 -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 +164 -18
- package/cli/index.mjs +0 -0
- package/ios/AcousticConnectRNConfig.json +153 -8
- package/ios/ConnectRNParsing.swift +152 -0
- package/ios/HybridAcousticConnectRN.swift +56 -15
- package/lib/commonjs/index.js +3 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.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 +139 -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/index.ts +5 -0
- package/src/specs/react-native-acoustic-connect.nitro.ts +141 -10
- package/Examples/SampleUI/ConnectConfig.json +0 -180
|
@@ -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,
|
|
@@ -1187,7 +1201,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1187
1201
|
for (key in values.keys) {
|
|
1188
1202
|
val value = values[key]
|
|
1189
1203
|
if (value != null) {
|
|
1190
|
-
eventValues[key] = value
|
|
1204
|
+
eventValues[key] = variantToString(value)
|
|
1191
1205
|
}
|
|
1192
1206
|
}
|
|
1193
1207
|
|
|
@@ -1200,9 +1214,42 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1200
1214
|
return result
|
|
1201
1215
|
}
|
|
1202
1216
|
|
|
1217
|
+
/**
|
|
1218
|
+
* Unwraps a Nitro variant to the underlying JS value it carries — a
|
|
1219
|
+
* [Boolean], [String], or [Double].
|
|
1220
|
+
*
|
|
1221
|
+
* The generated `Variant_*` type is a sealed **data** class, so calling
|
|
1222
|
+
* `toString()` on the variant itself yields the case wrapper
|
|
1223
|
+
* (`"Second(value=pro)"`) instead of the value (`"pro"`). Every conversion
|
|
1224
|
+
* that reads a variant must unwrap it first. Mirrors the iOS bridge's
|
|
1225
|
+
* `ConnectRNParsing.convertVariantToAny`, keeping emitted payloads
|
|
1226
|
+
* identical across platforms.
|
|
1227
|
+
*
|
|
1228
|
+
* @param value The variant to unwrap.
|
|
1229
|
+
* @return The wrapped Boolean, String, or Double.
|
|
1230
|
+
*/
|
|
1231
|
+
private fun unwrapVariant(value: Variant_Boolean_String_Double): Any =
|
|
1232
|
+
value.match(first = { it }, second = { it }, third = { it })
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Unwraps a Nitro variant (see [unwrapVariant]) and renders it as a string,
|
|
1236
|
+
* for the SDK entry points that take `HashMap<String, String>` payloads.
|
|
1237
|
+
*
|
|
1238
|
+
* Booleans render as `"true"`/`"false"` and numbers via Kotlin's [Double]
|
|
1239
|
+
* formatting (a JS `2` arrives as `2.0` and renders `"2.0"`); strings pass
|
|
1240
|
+
* through unchanged.
|
|
1241
|
+
*
|
|
1242
|
+
* @param value The variant to stringify.
|
|
1243
|
+
* @return The wrapped value's string form.
|
|
1244
|
+
*/
|
|
1245
|
+
private fun variantToString(value: Variant_Boolean_String_Double): String =
|
|
1246
|
+
unwrapVariant(value).toString()
|
|
1247
|
+
|
|
1203
1248
|
/**
|
|
1204
1249
|
* Converts a map of Variant_Boolean_String_Double to a HashMap<String?, String?>.
|
|
1205
1250
|
*
|
|
1251
|
+
* Values are unwrapped before stringifying (see [variantToString]).
|
|
1252
|
+
*
|
|
1206
1253
|
* @param values The map to be converted.
|
|
1207
1254
|
* @return A HashMap<String?, String?> representation of the input map which library can use.
|
|
1208
1255
|
*/
|
|
@@ -1212,30 +1259,106 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1212
1259
|
for (key in values.keys) {
|
|
1213
1260
|
val value = values[key]
|
|
1214
1261
|
if (value != null) {
|
|
1215
|
-
map[key] = value
|
|
1262
|
+
map[key] = variantToString(value)
|
|
1216
1263
|
}
|
|
1217
1264
|
}
|
|
1218
1265
|
return map
|
|
1219
1266
|
}
|
|
1220
1267
|
|
|
1221
1268
|
/**
|
|
1222
|
-
* Converts
|
|
1269
|
+
* Converts an [AnyMap]-derived map into the payload `Connect.logSignal`
|
|
1270
|
+
* expects, rebuilding nested structures as [JSONObject] / [JSONArray].
|
|
1223
1271
|
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1272
|
+
* The rebuild is mandatory, not cosmetic. The SDK serializes the signal
|
|
1273
|
+
* through EOCore's `JsonUtil.getHashValues`, which walks the top-level
|
|
1274
|
+
* entries and accumulates only `String`, `Boolean`, `JSONObject`,
|
|
1275
|
+
* `JSONArray` and `ByteArray` — anything else is **silently dropped**. So
|
|
1276
|
+
* a nested `Map`/`List` handed over as-is would vanish from the emitted
|
|
1277
|
+
* signal without any error. Converting here is what makes nested payloads
|
|
1278
|
+
* survive the wire.
|
|
1279
|
+
*
|
|
1280
|
+
* `null` becomes [JSONObject.NULL] rather than a Kotlin `null`, which
|
|
1281
|
+
* `org.json` requires for an explicit JSON `null`.
|
|
1282
|
+
*
|
|
1283
|
+
* Known limitation: a **top-level** numeric value is still dropped, because
|
|
1284
|
+
* `getHashValues` has no `Number` branch and the bridge cannot influence
|
|
1285
|
+
* that walk. Numbers nested inside an object or array are unaffected — the
|
|
1286
|
+
* enclosing [JSONObject]/[JSONArray] is built here, so `org.json`
|
|
1287
|
+
* serializes them normally. iOS carries top-level numbers fine; this is an
|
|
1288
|
+
* SDK-side asymmetry, pre-dating this conversion.
|
|
1289
|
+
*
|
|
1290
|
+
* A [JSONException] aborts the whole payload rather than escaping to the
|
|
1291
|
+
* caller. Non-finite numbers are the case that raises it: `org.json`
|
|
1292
|
+
* rejects them, and [toJsonValue] raises them explicitly so a top-level one
|
|
1293
|
+
* behaves like a nested one. Either way a JS `Infinity` or `NaN` would
|
|
1294
|
+
* otherwise throw straight out of `logSignal` and take the host app's call
|
|
1295
|
+
* site down — unacceptable for an analytics SDK, and a behaviour change
|
|
1296
|
+
* from the scalar-only bridge, which could not throw. Dropping the whole
|
|
1297
|
+
* payload also matches iOS, where a single non-finite value fails
|
|
1298
|
+
* `isValidJSONObject:` and the `signal` key is omitted entirely.
|
|
1299
|
+
*
|
|
1300
|
+
* @param values The map from [AnyMap.toHashMap].
|
|
1301
|
+
* @return A HashMap the SDK's signal serializer can consume, or an empty
|
|
1302
|
+
* map if the payload could not be represented as JSON.
|
|
1226
1303
|
*/
|
|
1227
1304
|
// 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
|
|
1305
|
+
internal fun toSignalPayload(values: Map<String, Any?>): java.util.HashMap<String?, Any?> {
|
|
1306
|
+
val map = HashMap<String?, Any?>(values.size)
|
|
1307
|
+
try {
|
|
1308
|
+
for ((key, value) in values) {
|
|
1309
|
+
map[key] = toJsonValue(value)
|
|
1234
1310
|
}
|
|
1311
|
+
} catch (e: JSONException) {
|
|
1312
|
+
Log.w(TAG, "[bridge] logSignal payload is not representable as JSON — dropping it: ${e.message}")
|
|
1313
|
+
return HashMap()
|
|
1235
1314
|
}
|
|
1236
1315
|
return map
|
|
1237
1316
|
}
|
|
1238
1317
|
|
|
1318
|
+
/**
|
|
1319
|
+
* Recursive helper for [toSignalPayload]. Maps Kotlin containers onto their
|
|
1320
|
+
* `org.json` equivalents and leaves scalars alone.
|
|
1321
|
+
*
|
|
1322
|
+
* Nitro's JNI layer converts the whole tree to plain Java types before it
|
|
1323
|
+
* reaches Kotlin — a JS object becomes a `HashMap`, a JS array an
|
|
1324
|
+
* `ArrayList`, and scalars become `Double` / `Boolean` / `Long` / `String`
|
|
1325
|
+
* — so there are no `AnyValue` wrappers left to unwrap here. [List] is
|
|
1326
|
+
* therefore the branch a JS array actually takes; [Array] is handled too,
|
|
1327
|
+
* to guard against a future Nitro representation change.
|
|
1328
|
+
*
|
|
1329
|
+
* @param value A value from an [AnyMap]-derived container.
|
|
1330
|
+
* @return The `org.json`-compatible equivalent.
|
|
1331
|
+
*/
|
|
1332
|
+
private fun toJsonValue(value: Any?): Any =
|
|
1333
|
+
when (value) {
|
|
1334
|
+
null -> JSONObject.NULL
|
|
1335
|
+
is Map<*, *> -> JSONObject().apply {
|
|
1336
|
+
for ((nestedKey, nestedValue) in value) {
|
|
1337
|
+
put(nestedKey.toString(), toJsonValue(nestedValue))
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
is Array<*> -> JSONArray().apply {
|
|
1341
|
+
for (element in value) put(toJsonValue(element))
|
|
1342
|
+
}
|
|
1343
|
+
is List<*> -> JSONArray().apply {
|
|
1344
|
+
for (element in value) put(toJsonValue(element))
|
|
1345
|
+
}
|
|
1346
|
+
// Checked here rather than left to `org.json`. A non-finite number
|
|
1347
|
+
// nested in an object or array would be rejected by that container's
|
|
1348
|
+
// `put`, but a TOP-LEVEL one is only ever handed to `HashMap.put`,
|
|
1349
|
+
// which accepts anything — so without this the payload would reach
|
|
1350
|
+
// the SDK and lose just that one key, while the nested case loses
|
|
1351
|
+
// the whole payload. Raising it uniformly keeps the two consistent
|
|
1352
|
+
// and matches iOS, where one non-finite value fails
|
|
1353
|
+
// `isValidJSONObject:` for the entire signal.
|
|
1354
|
+
is Double -> if (value.isFinite()) {
|
|
1355
|
+
value
|
|
1356
|
+
} else {
|
|
1357
|
+
throw JSONException("non-finite number: $value")
|
|
1358
|
+
}
|
|
1359
|
+
else -> value
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1239
1362
|
/**
|
|
1240
1363
|
* Gets the current activity from the ReactApplicationContext.
|
|
1241
1364
|
*
|
|
@@ -1390,5 +1513,28 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1390
1513
|
// by the connect-push-fcm artifact.
|
|
1391
1514
|
private const val CONNECT_PUSH_FCM_PROBE_CLASS =
|
|
1392
1515
|
"com.acoustic.connect.android.connectmod.push.services.fcm.FCMPushService"
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Default `additionalParameters` for an identity signal, matched to the
|
|
1519
|
+
* signal type the bridge will actually send.
|
|
1520
|
+
*
|
|
1521
|
+
* Connect's signal schema requires a *method* attribute on identity
|
|
1522
|
+
* signals, and the required key differs per type: `loggedIn` requires
|
|
1523
|
+
* `loginMethod`, `accountRegistered` requires `registrationMethod`.
|
|
1524
|
+
* Supplying the wrong one fails schema validation and the signal is
|
|
1525
|
+
* discarded, while [Connect.logIdentificationEvent] still reports
|
|
1526
|
+
* success — so the caller sees nothing wrong. Before this mapping
|
|
1527
|
+
* existed the bridge paired its `loggedIn` default with
|
|
1528
|
+
* `registrationMethod`, which meant every defaulted identity call was
|
|
1529
|
+
* silently dropped.
|
|
1530
|
+
*
|
|
1531
|
+
* Only consulted when the caller omits `additionalParameters`
|
|
1532
|
+
* entirely; an explicitly-provided map is passed through untouched.
|
|
1533
|
+
*/
|
|
1534
|
+
internal fun defaultIdentityParameters(signalType: String): Map<String, String> =
|
|
1535
|
+
when (signalType) {
|
|
1536
|
+
"loggedIn" -> mapOf("loginMethod" to "email")
|
|
1537
|
+
else -> mapOf("registrationMethod" to "email")
|
|
1538
|
+
}
|
|
1393
1539
|
}
|
|
1394
1540
|
}
|
package/cli/index.mjs
CHANGED
|
File without changes
|
|
@@ -1,15 +1,160 @@
|
|
|
1
1
|
{
|
|
2
2
|
"Connect": {
|
|
3
|
+
"AppKey": "b6c3709b7a4c479bb4b5a9fb8fec324c",
|
|
4
|
+
"KillSwitchUrl": "https://lib-us-2.brilliantcollector.com/collector/switch/b6c3709b7a4c479bb4b5a9fb8fec324c",
|
|
5
|
+
"PostMessageUrl": "https://lib-us-2.brilliantcollector.com/collector/collectorPost",
|
|
3
6
|
"AndroidVersion": "",
|
|
4
|
-
"AppKey": "YOUR_CONNECT_APP_KEY_HERE",
|
|
5
|
-
"KillSwitchUrl": "YOUR_KILL_SWITCH_URL_HERE",
|
|
6
|
-
"PostMessageUrl": "YOUR_POST_MESSAGE_URL_HERE",
|
|
7
|
-
"PushEnabled": true,
|
|
8
|
-
"iOSPushMode": "automatic",
|
|
9
|
-
"iOSAppGroupIdentifier": "group.co.acoustic.mobile.connect.rn.expodemo",
|
|
10
|
-
"iOSDevelopmentTeam": "YOUR_TEAM_ID",
|
|
11
|
-
"AndroidNotificationIconResName": "ic_notification",
|
|
12
7
|
"iOSVersion": "",
|
|
8
|
+
"PushEnabled": false,
|
|
9
|
+
"iOSPushMode": "automatic",
|
|
10
|
+
"iOSAppGroupIdentifier": null,
|
|
11
|
+
"AndroidNotificationIconResName": null,
|
|
12
|
+
"layoutConfigAndroid": {
|
|
13
|
+
"AppendMapIds": {
|
|
14
|
+
"[w,9290],[v,0]": {
|
|
15
|
+
"mid": "ASimpleUIView"
|
|
16
|
+
},
|
|
17
|
+
"idxPathValue": {
|
|
18
|
+
"mid": "giveAdditionalId2"
|
|
19
|
+
},
|
|
20
|
+
"tag2999999": {
|
|
21
|
+
"mid": "giveAdditionalId1"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"AutoLayout": {
|
|
25
|
+
"ExampleMaskingPage": {
|
|
26
|
+
"CaptureLayoutDelay": 0,
|
|
27
|
+
"CaptureLayoutOn": 0,
|
|
28
|
+
"CaptureScreenVisits": false,
|
|
29
|
+
"CaptureScreenshotOn": 0,
|
|
30
|
+
"CaptureUserEvents": false,
|
|
31
|
+
"DisplayName": "",
|
|
32
|
+
"Masking": {
|
|
33
|
+
"HasCustomMask": true,
|
|
34
|
+
"HasMasking": true,
|
|
35
|
+
"MaskAccessibilityIdList": [],
|
|
36
|
+
"MaskAccessibilityLabelList": [],
|
|
37
|
+
"MaskIdList": [
|
|
38
|
+
"^9[0-9][0-9][0-9]$",
|
|
39
|
+
"^\\[wvv,0\\],\\[dddv,0\\],\\[v,0\\],\\[v,0\\],\\[v,0\\],\\[b,0\\](.)*$"
|
|
40
|
+
],
|
|
41
|
+
"MaskValueList": [
|
|
42
|
+
"^4[0-9]{12}(?:[0-9]{3})?$",
|
|
43
|
+
"^3[47][0-9]{13}$",
|
|
44
|
+
"^65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})$",
|
|
45
|
+
"^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$"
|
|
46
|
+
],
|
|
47
|
+
"Sensitive": {
|
|
48
|
+
"capitalCaseAlphabet": "X",
|
|
49
|
+
"number": "9",
|
|
50
|
+
"smallCaseAlphabet": "x",
|
|
51
|
+
"symbol": "#"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"NumberOfWebViews": 0,
|
|
55
|
+
"ScreenChange": false,
|
|
56
|
+
"ScreenShot": false
|
|
57
|
+
},
|
|
58
|
+
"GlobalScreenSettings": {
|
|
59
|
+
"CaptureLayoutDelay": 1,
|
|
60
|
+
"CaptureLayoutOn": 2,
|
|
61
|
+
"CaptureScreenVisits": false,
|
|
62
|
+
"CaptureScreenshotOn": 2,
|
|
63
|
+
"CaptureUserEvents": true,
|
|
64
|
+
"DisplayName": "",
|
|
65
|
+
"Masking": {
|
|
66
|
+
"HasCustomMask": true,
|
|
67
|
+
"HasMasking": true,
|
|
68
|
+
"MaskAccessibilityIdList": [],
|
|
69
|
+
"MaskAccessibilityLabelList": [],
|
|
70
|
+
"MaskIdList": [],
|
|
71
|
+
"MaskValueList": [],
|
|
72
|
+
"Sensitive": {
|
|
73
|
+
"capitalCaseAlphabet": "X",
|
|
74
|
+
"number": "9",
|
|
75
|
+
"smallCaseAlphabet": "x",
|
|
76
|
+
"symbol": "#"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"NumberOfWebViews": 0,
|
|
80
|
+
"ScreenChange": true,
|
|
81
|
+
"ScreenShot": true
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"layoutConfigIos": {
|
|
86
|
+
"AppendMapIds": {
|
|
87
|
+
"[w,9290],[v,0]": {
|
|
88
|
+
"mid": "ASimpleUIView"
|
|
89
|
+
},
|
|
90
|
+
"idxPathValue": {
|
|
91
|
+
"mid": "giveAdditionalId2"
|
|
92
|
+
},
|
|
93
|
+
"tag2999999": {
|
|
94
|
+
"mid": "giveAdditionalId1"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"AutoLayout": {
|
|
98
|
+
"ExampleMaskingPage": {
|
|
99
|
+
"CaptureLayoutDelay": 0,
|
|
100
|
+
"CaptureLayoutOn": 0,
|
|
101
|
+
"CaptureScreenVisits": false,
|
|
102
|
+
"CaptureScreenshotOn": 0,
|
|
103
|
+
"CaptureUserEvents": false,
|
|
104
|
+
"DisplayName": "",
|
|
105
|
+
"Masking": {
|
|
106
|
+
"HasCustomMask": true,
|
|
107
|
+
"HasMasking": true,
|
|
108
|
+
"MaskAccessibilityIdList": [],
|
|
109
|
+
"MaskAccessibilityLabelList": [],
|
|
110
|
+
"MaskIdList": [
|
|
111
|
+
"^9[0-9][0-9][0-9]$",
|
|
112
|
+
"^\\[wvv,0\\],\\[dddv,0\\],\\[v,0\\],\\[v,0\\],\\[v,0\\],\\[b,0\\](.)*$"
|
|
113
|
+
],
|
|
114
|
+
"MaskValueList": [
|
|
115
|
+
"^4[0-9]{12}(?:[0-9]{3})?$",
|
|
116
|
+
"^3[47][0-9]{13}$",
|
|
117
|
+
"^65[4-9][0-9]{13}|64[4-9][0-9]{13}|6011[0-9]{12}|(622(?:12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9[01][0-9]|92[0-5])[0-9]{10})$",
|
|
118
|
+
"^(5[1-5][0-9]{14}|2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12}))$"
|
|
119
|
+
],
|
|
120
|
+
"Sensitive": {
|
|
121
|
+
"capitalCaseAlphabet": "X",
|
|
122
|
+
"number": "9",
|
|
123
|
+
"smallCaseAlphabet": "x",
|
|
124
|
+
"symbol": "#"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"NumberOfWebViews": 0,
|
|
128
|
+
"ScreenChange": false,
|
|
129
|
+
"ScreenShot": false
|
|
130
|
+
},
|
|
131
|
+
"GlobalScreenSettings": {
|
|
132
|
+
"CaptureLayoutDelay": 1,
|
|
133
|
+
"CaptureLayoutOn": 2,
|
|
134
|
+
"CaptureScreenVisits": false,
|
|
135
|
+
"CaptureScreenshotOn": 2,
|
|
136
|
+
"CaptureUserEvents": true,
|
|
137
|
+
"DisplayName": "",
|
|
138
|
+
"Masking": {
|
|
139
|
+
"HasCustomMask": true,
|
|
140
|
+
"HasMasking": true,
|
|
141
|
+
"MaskAccessibilityIdList": [],
|
|
142
|
+
"MaskAccessibilityLabelList": [],
|
|
143
|
+
"MaskIdList": [],
|
|
144
|
+
"MaskValueList": [],
|
|
145
|
+
"Sensitive": {
|
|
146
|
+
"capitalCaseAlphabet": "X",
|
|
147
|
+
"number": "9",
|
|
148
|
+
"smallCaseAlphabet": "x",
|
|
149
|
+
"symbol": "#"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"NumberOfWebViews": 0,
|
|
153
|
+
"ScreenChange": true,
|
|
154
|
+
"ScreenShot": true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
13
158
|
"useRelease": false
|
|
14
159
|
}
|
|
15
160
|
}
|
|
@@ -104,6 +104,90 @@ 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
|
+
|
|
107
191
|
/// Maps the JS-facing module name to the config store's module name
|
|
108
192
|
/// ("Connect" → "TLFCoreModule").
|
|
109
193
|
static func storeModuleName(for moduleName: String) -> String {
|
|
@@ -130,6 +214,51 @@ internal enum ConnectRNParsing {
|
|
|
130
214
|
return result
|
|
131
215
|
}
|
|
132
216
|
|
|
217
|
+
/// Normalises a dictionary produced by `AnyMap.toDictionary()` into one
|
|
218
|
+
/// that `NSJSONSerialization.isValidJSONObject:` accepts, which is the
|
|
219
|
+
/// only gate `CTSignalMessage` applies before embedding the payload.
|
|
220
|
+
///
|
|
221
|
+
/// Two things need fixing up. `AnyMap` models JS `null` as a Swift `nil`,
|
|
222
|
+
/// and a dictionary holding `Optional.none` is not JSON-representable —
|
|
223
|
+
/// `NSNull` is. And the nesting is optional-typed all the way down
|
|
224
|
+
/// (`[String: Any?]` / `[Any?]`), so the walk has to be recursive rather
|
|
225
|
+
/// than a single top-level pass. Values that are already scalars pass
|
|
226
|
+
/// through untouched, so a flat payload converts to exactly what the
|
|
227
|
+
/// previous scalar-only bridge produced.
|
|
228
|
+
///
|
|
229
|
+
/// Deliberately takes plain Swift collections rather than `AnyMap`: this
|
|
230
|
+
/// file is compiled into the UnitTests bundle, which builds without C++
|
|
231
|
+
/// interop and therefore cannot see nitro's `AnyMap`/`AnyValue` types.
|
|
232
|
+
/// The `AnyMap` unwrapping stays in `HybridAcousticConnectRN`.
|
|
233
|
+
///
|
|
234
|
+
/// - Parameter input: dictionary from `AnyMap.toDictionary()`.
|
|
235
|
+
/// - Returns: the same structure with optionals resolved.
|
|
236
|
+
static func jsonSafeDictionary(_ input: [String: Any?]) -> [String: Any] {
|
|
237
|
+
var result = [String: Any](minimumCapacity: input.count)
|
|
238
|
+
for (key, value) in input {
|
|
239
|
+
result[key] = jsonSafeValue(value)
|
|
240
|
+
}
|
|
241
|
+
return result
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Recursive helper for ``jsonSafeDictionary(_:)``. Maps `nil` to `NSNull`
|
|
245
|
+
/// and rebuilds nested dictionaries/arrays; every other value — `String`,
|
|
246
|
+
/// `Double`, `Int64`, `Bool` — is already JSON-representable and is
|
|
247
|
+
/// returned as-is.
|
|
248
|
+
///
|
|
249
|
+
/// - Parameter value: a value from an `AnyMap`-derived container.
|
|
250
|
+
/// - Returns: a JSON-representable equivalent.
|
|
251
|
+
static func jsonSafeValue(_ value: Any?) -> Any {
|
|
252
|
+
guard let value else { return NSNull() }
|
|
253
|
+
if let nested = value as? [String: Any?] {
|
|
254
|
+
return jsonSafeDictionary(nested)
|
|
255
|
+
}
|
|
256
|
+
if let array = value as? [Any?] {
|
|
257
|
+
return array.map { jsonSafeValue($0) }
|
|
258
|
+
}
|
|
259
|
+
return value
|
|
260
|
+
}
|
|
261
|
+
|
|
133
262
|
static func convertVariantToAny(_ variant: Variant_Bool_String_Double) -> Any {
|
|
134
263
|
switch variant {
|
|
135
264
|
case .first(let boolValue):
|
|
@@ -141,6 +270,29 @@ internal enum ConnectRNParsing {
|
|
|
141
270
|
}
|
|
142
271
|
}
|
|
143
272
|
|
|
273
|
+
/// Default `additionalParameters` for an identity signal, matched to the
|
|
274
|
+
/// signal type the bridge will actually send.
|
|
275
|
+
///
|
|
276
|
+
/// Connect's signal schema requires a *method* attribute on identity
|
|
277
|
+
/// signals, and the required key differs per type: `loggedIn` requires
|
|
278
|
+
/// `loginMethod`, `accountRegistered` requires `registrationMethod`.
|
|
279
|
+
/// Supplying the wrong one fails schema validation and the signal is
|
|
280
|
+
/// discarded, while `identity.log` still reports success — so the caller
|
|
281
|
+
/// sees nothing wrong. Before this mapping existed the bridge paired its
|
|
282
|
+
/// `loggedIn` default with `registrationMethod`, which meant every
|
|
283
|
+
/// defaulted identity call was silently dropped.
|
|
284
|
+
///
|
|
285
|
+
/// Only consulted when the caller omits `additionalParameters` entirely;
|
|
286
|
+
/// an explicitly-provided map is passed through untouched.
|
|
287
|
+
static func defaultIdentityParameters(for signalType: String) -> [String: String] {
|
|
288
|
+
switch signalType {
|
|
289
|
+
case "loggedIn":
|
|
290
|
+
return ["loginMethod": "email"]
|
|
291
|
+
default:
|
|
292
|
+
return ["registrationMethod": "email"]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
144
296
|
static func logLevel(from level: Double) -> kConnectMonitoringLevelType {
|
|
145
297
|
let intValue: Int = Int(level)
|
|
146
298
|
if intValue == 0 {
|