react-native-acoustic-connect-beta 19.0.18 → 19.0.20
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 +10 -0
- package/README.md +128 -0
- package/android/src/main/assets/ConnectBasicConfig.properties +1 -1
- package/android/src/main/java/com/acousticconnectrn/HybridAcousticConnectRN.kt +195 -19
- package/ios/HybridAcousticConnectRN.swift +168 -6
- package/lib/commonjs/TLTRN.js +90 -0
- package/lib/commonjs/TLTRN.js.map +1 -1
- package/lib/commonjs/components/Connect.js +144 -25
- package/lib/commonjs/components/Connect.js.map +1 -1
- package/lib/module/TLTRN.js +90 -0
- package/lib/module/TLTRN.js.map +1 -1
- package/lib/module/components/Connect.js +145 -26
- package/lib/module/components/Connect.js.map +1 -1
- package/lib/typescript/src/TLTRN.d.ts +63 -0
- package/lib/typescript/src/TLTRN.d.ts.map +1 -1
- package/lib/typescript/src/components/Connect.d.ts.map +1 -1
- package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts +30 -3
- package/lib/typescript/src/specs/react-native-acoustic-connect.nitro.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/TLTRN.ts +93 -0
- package/src/components/Connect.tsx +149 -25
- package/src/specs/react-native-acoustic-connect.nitro.ts +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 19.0.20 (2026-09-09)
|
|
2
|
+
|
|
3
|
+
### Reverts
|
|
4
|
+
|
|
5
|
+
* Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
|
|
6
|
+
## 19.0.19 (2026-09-08)
|
|
7
|
+
|
|
8
|
+
### Reverts
|
|
9
|
+
|
|
10
|
+
* Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
|
|
1
11
|
## 19.0.18 (2026-09-07)
|
|
2
12
|
|
|
3
13
|
### Reverts
|
package/README.md
CHANGED
|
@@ -396,6 +396,26 @@ for details.
|
|
|
396
396
|
|
|
397
397
|
## API reference
|
|
398
398
|
|
|
399
|
+
> **What the logging methods' `boolean` means.** Every `log*` method returns
|
|
400
|
+
> whether the native SDK **accepted the event for delivery** — never whether
|
|
401
|
+
> the collector received it, and never whether the collector kept it. Events
|
|
402
|
+
> are queued on device and posted in batches later, so no return value from
|
|
403
|
+
> these calls can attest to delivery, and a server-side rejection (a signal
|
|
404
|
+
> that fails schema validation, say) happens long after you have already been
|
|
405
|
+
> told `true`. Treat the value as "the SDK took this", and use the collector or
|
|
406
|
+
> the platform log as the source of truth for what shipped.
|
|
407
|
+
>
|
|
408
|
+
> A `false` means the SDK rejected the call outright and nothing was queued.
|
|
409
|
+
> The bridge also writes that to `logcat` (tag `AcousticConnectRN`) and
|
|
410
|
+
> `os_log` (subsystem `com.acoustic.AcousticConnectRN`, category `bridge`), so
|
|
411
|
+
> a rejection is visible without inspecting return values you would otherwise
|
|
412
|
+
> never read.
|
|
413
|
+
>
|
|
414
|
+
> `logScreenLayout` is weaker still: with the configured delay — the normal
|
|
415
|
+
> case — a `true` means only that the capture was **scheduled**. What the
|
|
416
|
+
> capture found when it eventually ran, and whether it produced a layout
|
|
417
|
+
> message at all, is reported in the platform log only.
|
|
418
|
+
|
|
399
419
|
### `AcousticConnectRN.enable(): boolean`
|
|
400
420
|
|
|
401
421
|
Re-enables the SDK after a prior `disable()`. Reads all configuration from
|
|
@@ -463,6 +483,13 @@ AcousticConnectRN.logSignal(pageView, 1)
|
|
|
463
483
|
> AcousticConnectRN.logSignal({ cart: { items: 3 } }, 1) // both platforms
|
|
464
484
|
> AcousticConnectRN.logSignal({ items: 3 }, 1) // iOS only
|
|
465
485
|
> ```
|
|
486
|
+
>
|
|
487
|
+
> The Android bridge logs a warning naming the keys the SDK is about to
|
|
488
|
+
> discard, so the loss shows up in `logcat` instead of only in a dashboard
|
|
489
|
+
> that never fills in. It does not coerce the value: making the number survive
|
|
490
|
+
> as a JSON string would replace a missing field with a differently-typed one
|
|
491
|
+
> and diverge from iOS in a new way. The fix belongs in the Android SDK's
|
|
492
|
+
> serializer.
|
|
466
493
|
|
|
467
494
|
### `AcousticConnectRN.logCustomEvent(eventName, values, level): boolean`
|
|
468
495
|
|
|
@@ -472,6 +499,20 @@ string values end to end, so there is no native route for nested JSON; widening
|
|
|
472
499
|
it would preserve the nesting on iOS and silently drop it on Android. Use
|
|
473
500
|
`logSignal` when the payload needs structure.
|
|
474
501
|
|
|
502
|
+
TypeScript will reject a nested value, but types are erased at runtime — a
|
|
503
|
+
payload built from an API response or widened through `any` still reaches the
|
|
504
|
+
bridge nested, gets reshaped by the native SDK, and returns `true`. The wrapper
|
|
505
|
+
logs a `console.warn` naming the offending keys when that happens (once per
|
|
506
|
+
call site, in dev and in production; the payload itself is passed through
|
|
507
|
+
unchanged). If you see it, move the payload to `logSignal`.
|
|
508
|
+
|
|
509
|
+
Numbers are rendered the same way on both platforms — a JS `2` arrives as `2`,
|
|
510
|
+
not `2.0`. Note the remaining difference in *type*: because the Android path is
|
|
511
|
+
string-typed, an Android custom-event number lands on the wire as a JSON string
|
|
512
|
+
(`"2"`) where iOS sends a JSON number (`2`). `logSignal` carries native types on
|
|
513
|
+
both platforms; reach for it when a dashboard or Composer rule compares the
|
|
514
|
+
value numerically.
|
|
515
|
+
|
|
475
516
|
### Push configuration (`ConnectConfig.json`)
|
|
476
517
|
|
|
477
518
|
| Field | Type | Default | Semantics |
|
|
@@ -560,6 +601,41 @@ layout config:
|
|
|
560
601
|
> clicks, and custom events still flow. It only drops the image data, which is
|
|
561
602
|
> what a subscription without session replay has no consumer for.
|
|
562
603
|
|
|
604
|
+
#### `NumberOfWebViews` turns off iOS layout capture — leave it at `0`
|
|
605
|
+
|
|
606
|
+
`AutoLayout` rules accept a `NumberOfWebViews` key. On iOS, any value greater
|
|
607
|
+
than zero marks the screens that rule covers as web-view screens, and the SDK's
|
|
608
|
+
automatic layout capture is skipped for a web-view screen. Its effect is not
|
|
609
|
+
limited to screens that actually host a `WebView`: the value is read as a
|
|
610
|
+
declaration about the rule, not a count that gets verified.
|
|
611
|
+
|
|
612
|
+
The trap is where it is usually set. `GlobalScreenSettings` is the rule that
|
|
613
|
+
applies to **every** screen with no more specific rule of its own, and React
|
|
614
|
+
Native screens are all in that position — they are hosted by the same generic
|
|
615
|
+
container class, so no per-screen native rule matches them. So a single
|
|
616
|
+
`"NumberOfWebViews": 1` under `GlobalScreenSettings` switches off
|
|
617
|
+
auto-instrumented layout capture for the entire app on iOS, including screens
|
|
618
|
+
with no web content at all:
|
|
619
|
+
|
|
620
|
+
```json
|
|
621
|
+
{
|
|
622
|
+
"Connect": {
|
|
623
|
+
"layoutConfigIos": {
|
|
624
|
+
"AutoLayout": {
|
|
625
|
+
"GlobalScreenSettings": { "NumberOfWebViews": 0 }
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Screen views, clicks, and custom events are unaffected — the symptom is layout
|
|
633
|
+
messages going missing while everything else keeps arriving, which reads like a
|
|
634
|
+
capture failure rather than a setting. Keep `NumberOfWebViews` at `0`, the value
|
|
635
|
+
every shipped template uses. To stand layout capture down deliberately, set
|
|
636
|
+
`CaptureLayoutOn: 0` on the rule instead — it is the key that means that, and it
|
|
637
|
+
leaves `NumberOfWebViews` free to describe the screen.
|
|
638
|
+
|
|
563
639
|
#### Capture timing (`CaptureLayoutDelay`)
|
|
564
640
|
|
|
565
641
|
`AutoLayout.GlobalScreenSettings.CaptureLayoutDelay` is how long, in
|
|
@@ -601,6 +677,58 @@ default, chosen so both platforms behave the same; the native SDKs' own bundled
|
|
|
601
677
|
defaults differ from each other and are tuned for the lifecycle trigger rather
|
|
602
678
|
than the React Navigation one.
|
|
603
679
|
|
|
680
|
+
#### Where screen-view and layout messages come from, and how to reduce duplicates
|
|
681
|
+
|
|
682
|
+
The two message kinds have different sources, and they differ per platform:
|
|
683
|
+
|
|
684
|
+
**Screen views (one source per platform).** On Android, the `<Connect>` wrapper
|
|
685
|
+
is the *only* source — it emits on React Navigation's `state` event. On iOS,
|
|
686
|
+
the native SDK's auto-instrumentation is the *only* source — it emits when a
|
|
687
|
+
screen's view controller reports its appearance. The wrapper never emits a
|
|
688
|
+
screen view on iOS; its role there is to hand the current *route name* to the
|
|
689
|
+
native SDK (which is what makes messages read `Checkout` rather than a native
|
|
690
|
+
container class) and to drive referrer chaining across transitions.
|
|
691
|
+
|
|
692
|
+
Duplicate screen views observed on iOS are therefore not two sources
|
|
693
|
+
overlapping — they are the single native source firing more than once when the
|
|
694
|
+
same screen's view controller reports several appearances for one transition.
|
|
695
|
+
They inflate event volumes and skew per-screen counts; they do not lose data.
|
|
696
|
+
There is no configuration key that collapses them today; a native-side dedupe
|
|
697
|
+
is under investigation. Do not try to solve it by removing the wrapper: on iOS
|
|
698
|
+
that costs route-based names and referrer chaining, and on Android it removes
|
|
699
|
+
screen views entirely. A same-name filter in the wrapper would not help either
|
|
700
|
+
— the wrapper is not the emitter on iOS, and such a filter would swallow
|
|
701
|
+
legitimate repeats like a stack pushing the same screen name twice.
|
|
702
|
+
|
|
703
|
+
**Layouts (two sources on iOS).** With automatic layout capture enabled in
|
|
704
|
+
config, an iOS transition can produce two layout messages: one from the
|
|
705
|
+
wrapper's `logScreenLayout` call and one from the native auto-instrumentation.
|
|
706
|
+
Control it from config, on the native side that has the duplicate:
|
|
707
|
+
|
|
708
|
+
```json
|
|
709
|
+
{
|
|
710
|
+
"Connect": {
|
|
711
|
+
"layoutConfigIos": {
|
|
712
|
+
"AutoLayout": {
|
|
713
|
+
"GlobalScreenSettings": { "CaptureLayoutOn": 0 }
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
`CaptureLayoutOn: 0` stands the **native** automatic layout capture down and
|
|
721
|
+
leaves the wrapper's route-named captures as the single source. Set it per
|
|
722
|
+
screen rather than globally if only some screens are noisy. Screen views,
|
|
723
|
+
clicks, and custom events are unaffected by this key.
|
|
724
|
+
|
|
725
|
+
If you would rather keep the native captures and have the wrapper stay out of
|
|
726
|
+
the way, omit `navigationRef` and do not rely on `<Connect>` for screen naming
|
|
727
|
+
— but expect native container class names in place of your route names.
|
|
728
|
+
|
|
729
|
+
Measure before changing either default: the volume depends on your navigator
|
|
730
|
+
structure, and both defaults are what every shipped sample uses.
|
|
731
|
+
|
|
604
732
|
### `<Connect>` props
|
|
605
733
|
|
|
606
734
|
| Prop | Type | Required | Description |
|
|
@@ -579,7 +579,12 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
579
579
|
value: Double,
|
|
580
580
|
moduleName: String
|
|
581
581
|
): Boolean {
|
|
582
|
-
|
|
582
|
+
// Rendered through formatJsNumber, not Double.toString: EOCore stores
|
|
583
|
+
// config items as strings, and a JS 0 spelled "0.0" is not the same
|
|
584
|
+
// token as the "0" the JSON config files carry, so a consumer setting
|
|
585
|
+
// a whole-number item at runtime wrote a value that no longer matched
|
|
586
|
+
// what the same key looks like on iOS (a native number) or on disk.
|
|
587
|
+
val result = EOCore.updateConfig(key, formatJsNumber(value), EOCore.getLifecycleObject(moduleName))
|
|
583
588
|
return result
|
|
584
589
|
}
|
|
585
590
|
|
|
@@ -667,7 +672,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
667
672
|
level: Double
|
|
668
673
|
): Boolean {
|
|
669
674
|
val result = Connect.logCustomEvent(eventName, convertToMap(values), level.toInt())
|
|
670
|
-
return result
|
|
675
|
+
return warnIfRejected(result, "logCustomEvent", eventName)
|
|
671
676
|
}
|
|
672
677
|
|
|
673
678
|
/**
|
|
@@ -688,6 +693,38 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
688
693
|
level: Double
|
|
689
694
|
): Boolean {
|
|
690
695
|
val result = Connect.logSignal(toSignalPayload(values.toHashMap()), level.toInt())
|
|
696
|
+
return warnIfRejected(result, "logSignal")
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Surfaces a `false` returned by a native logging call, and passes it
|
|
701
|
+
* through unchanged.
|
|
702
|
+
*
|
|
703
|
+
* The bridge's boolean means "the SDK accepted this for delivery", never
|
|
704
|
+
* "the collector received it" — nothing on the device knows the latter. But
|
|
705
|
+
* the *accepted* half was itself invisible: a `false` propagated to JS as a
|
|
706
|
+
* bare return value that the wrapper's callers almost never inspect, with
|
|
707
|
+
* nothing in logcat. Callers reporting "the event never arrived" had no way
|
|
708
|
+
* to tell a rejected call from a delivery problem. One line at warn level
|
|
709
|
+
* distinguishes them.
|
|
710
|
+
*
|
|
711
|
+
* Deliberately does not change the return value or throw: an analytics
|
|
712
|
+
* bridge must not turn a rejected event into an app-visible failure.
|
|
713
|
+
*
|
|
714
|
+
* @param result The value the native call returned.
|
|
715
|
+
* @param api Name of the bridge method, for the log line.
|
|
716
|
+
* @param detail Optional extra identifier, e.g. an event name.
|
|
717
|
+
* @return [result], unchanged.
|
|
718
|
+
*/
|
|
719
|
+
private fun warnIfRejected(result: Boolean, api: String, detail: String? = null): Boolean {
|
|
720
|
+
if (!result) {
|
|
721
|
+
val suffix = if (detail != null) " ($detail)" else ""
|
|
722
|
+
Log.w(
|
|
723
|
+
TAG,
|
|
724
|
+
"[bridge] $api$suffix: the Connect SDK did not accept the event; nothing was queued " +
|
|
725
|
+
"for delivery. Check that the SDK is enabled and the payload is valid."
|
|
726
|
+
)
|
|
727
|
+
}
|
|
691
728
|
return result
|
|
692
729
|
}
|
|
693
730
|
|
|
@@ -737,7 +774,11 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
737
774
|
signalType = resolvedSignalType,
|
|
738
775
|
additionalParameters = params
|
|
739
776
|
)
|
|
740
|
-
|
|
777
|
+
// An identity signal that the SDK accepts can still be rejected
|
|
778
|
+
// downstream by schema validation — a mismatched method attribute has
|
|
779
|
+
// cost a customer 22 signals while every call reported success. The
|
|
780
|
+
// bridge cannot see that; it can at least report the half it does see.
|
|
781
|
+
return Promise.resolved(warnIfRejected(result, "logIdentity", resolvedSignalType))
|
|
741
782
|
}
|
|
742
783
|
|
|
743
784
|
/**
|
|
@@ -969,16 +1010,61 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
969
1010
|
// crash on an ordinary screen change.
|
|
970
1011
|
val activity = getCurrentActivity()
|
|
971
1012
|
if (activity != null && LayoutUtil.canCaptureUserEvents(null, name)) {
|
|
972
|
-
result =
|
|
1013
|
+
result = logAutomaticScreenLayout(
|
|
973
1014
|
activity,
|
|
974
1015
|
name,
|
|
975
|
-
resolveCaptureLayoutDelayMs(name, delay)
|
|
976
|
-
true
|
|
1016
|
+
resolveCaptureLayoutDelayMs(name, delay)
|
|
977
1017
|
)
|
|
978
1018
|
}
|
|
979
1019
|
return result
|
|
980
1020
|
}
|
|
981
1021
|
|
|
1022
|
+
/**
|
|
1023
|
+
* Captures the layout of [activity] as an *automatic* capture — the kind
|
|
1024
|
+
* the wrapper triggers itself, on navigation or behind a dialog — rather
|
|
1025
|
+
* than a manual `logScreenLayout` API call from the host app.
|
|
1026
|
+
*
|
|
1027
|
+
* Every wrapper-initiated capture must go through here, because the
|
|
1028
|
+
* distinction is what makes the configured `ScreenShot` value reach the
|
|
1029
|
+
* native screenshot gate at all:
|
|
1030
|
+
*
|
|
1031
|
+
* - `Connect`'s four-argument overload hardcodes `manualLog = true`, and
|
|
1032
|
+
* the native gate reads the configured `ScreenShot` only on the
|
|
1033
|
+
* `manualLog = false` branch. Passing a literal `true` for the screenshot
|
|
1034
|
+
* flag — which every wrapper call site used to do — therefore took the
|
|
1035
|
+
* manual branch unconditionally, so `ScreenShot: false` could not be
|
|
1036
|
+
* honoured from config and a full-page image shipped in every layout
|
|
1037
|
+
* message regardless. Hence the five-argument overload, with
|
|
1038
|
+
* `manualLog = false`.
|
|
1039
|
+
* - [LayoutUtil.canTakeScreenShot] resolves the same merged layout rule the
|
|
1040
|
+
* native automatic path uses (global settings as a baseline, any
|
|
1041
|
+
* per-screen rule applied over them). It is preferred over reading
|
|
1042
|
+
* `ScreenShot` directly because it is `has()`-guarded: a per-screen rule
|
|
1043
|
+
* that omits the key defaults to capturing rather than throwing a
|
|
1044
|
+
* `JSONException`, which higher up is swallowed and costs the whole
|
|
1045
|
+
* layout message. Its first argument may be null while the page name is
|
|
1046
|
+
* non-empty, the same pattern as `canCaptureUserEvents(null, name)`.
|
|
1047
|
+
*
|
|
1048
|
+
* Note `CaptureScreenshotOn` is not consulted anywhere on Android: its only
|
|
1049
|
+
* native reader has no callers, so `ScreenShot` is the one wired-up switch.
|
|
1050
|
+
*
|
|
1051
|
+
* [activity] is nullable to match the five-argument overload; callers that
|
|
1052
|
+
* need a non-null one check or assert before calling.
|
|
1053
|
+
*/
|
|
1054
|
+
private fun logAutomaticScreenLayout(
|
|
1055
|
+
activity: Activity?,
|
|
1056
|
+
name: String,
|
|
1057
|
+
delayMs: Int
|
|
1058
|
+
): Boolean {
|
|
1059
|
+
return logScreenLayout(
|
|
1060
|
+
activity,
|
|
1061
|
+
name,
|
|
1062
|
+
delayMs,
|
|
1063
|
+
false,
|
|
1064
|
+
LayoutUtil.canTakeScreenShot(null, name)
|
|
1065
|
+
)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
982
1068
|
/**
|
|
983
1069
|
* Resolves the capture delay, in milliseconds, to apply for [name].
|
|
984
1070
|
*
|
|
@@ -1047,12 +1133,12 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1047
1133
|
} else {
|
|
1048
1134
|
Log.v(TAG, "Warning: Could not find dialog object for $dialogId after delay")
|
|
1049
1135
|
// Fallback to regular screen layout capture
|
|
1050
|
-
|
|
1136
|
+
logAutomaticScreenLayout(Objects.requireNonNull<Activity?>(getCurrentActivity()), dialogTitle, 0)
|
|
1051
1137
|
}
|
|
1052
1138
|
} catch (e: Exception) {
|
|
1053
1139
|
Log.v(TAG, "Error in delayed dialog capture: ${e.message}")
|
|
1054
1140
|
// Fallback to regular screen layout capture
|
|
1055
|
-
|
|
1141
|
+
logAutomaticScreenLayout(Objects.requireNonNull<Activity?>(getCurrentActivity()), dialogTitle, 0)
|
|
1056
1142
|
}
|
|
1057
1143
|
}, DIALOG_CAPTURE_DELAY_MS) // Use configurable delay
|
|
1058
1144
|
|
|
@@ -1071,7 +1157,7 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1071
1157
|
}
|
|
1072
1158
|
} else {
|
|
1073
1159
|
// Default fallback
|
|
1074
|
-
result =
|
|
1160
|
+
result = logAutomaticScreenLayout(Objects.requireNonNull<Activity?>(getCurrentActivity()), dialogTitle, 300)
|
|
1075
1161
|
}
|
|
1076
1162
|
}
|
|
1077
1163
|
} catch (e: Exception) {
|
|
@@ -1318,15 +1404,56 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1318
1404
|
* Unwraps a Nitro variant (see [unwrapVariant]) and renders it as a string,
|
|
1319
1405
|
* for the SDK entry points that take `HashMap<String, String>` payloads.
|
|
1320
1406
|
*
|
|
1321
|
-
* Booleans render as `"true"`/`"false"
|
|
1322
|
-
*
|
|
1323
|
-
*
|
|
1407
|
+
* Booleans render as `"true"`/`"false"`, strings pass through unchanged,
|
|
1408
|
+
* and numbers go through [formatJsNumber] so they read the way the same
|
|
1409
|
+
* JS value reads on iOS.
|
|
1324
1410
|
*
|
|
1325
1411
|
* @param value The variant to stringify.
|
|
1326
1412
|
* @return The wrapped value's string form.
|
|
1327
1413
|
*/
|
|
1328
1414
|
private fun variantToString(value: Variant_Boolean_String_Double): String =
|
|
1329
|
-
unwrapVariant(value)
|
|
1415
|
+
when (val unwrapped = unwrapVariant(value)) {
|
|
1416
|
+
is Double -> formatJsNumber(unwrapped)
|
|
1417
|
+
else -> unwrapped.toString()
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Renders a JS number the way JS itself — and therefore the iOS bridge —
|
|
1422
|
+
* renders it: `2` stays `"2"`, `2.5` stays `"2.5"`.
|
|
1423
|
+
*
|
|
1424
|
+
* Every JS number crosses Nitro as a [Double], so a JS `2` arrives as
|
|
1425
|
+
* `2.0`, and Kotlin's [Double.toString] spells that `"2.0"`. iOS hands the
|
|
1426
|
+
* same value to the SDK as a native number, which `NSJSONSerialization`
|
|
1427
|
+
* writes as `2`. The result was a cross-platform payload split on any
|
|
1428
|
+
* whole number — `"seats": "2.0"` on Android against `"seats": 2` on iOS —
|
|
1429
|
+
* which breaks dashboards and Composer rules that compare the two.
|
|
1430
|
+
* Dropping the trailing `.0` closes the *textual* half of that gap.
|
|
1431
|
+
*
|
|
1432
|
+
* It does not close the *type* half on the custom-event path: the SDK's
|
|
1433
|
+
* `logCustomEvent` chain is typed `HashMap<String, String>` end to end, so
|
|
1434
|
+
* an Android custom-event number is a JSON string (`"2"`) where iOS sends a
|
|
1435
|
+
* JSON number (`2`). Only `logSignal` carries native types on Android, and
|
|
1436
|
+
* that path deliberately does not come through here — see [toSignalPayload].
|
|
1437
|
+
*
|
|
1438
|
+
* Integral values are rendered through [Long] only below 2^53, which spans
|
|
1439
|
+
* every integer JS itself represents exactly; beyond that the [Double] form
|
|
1440
|
+
* is kept rather than inventing digits a `Long` round-trip would imply.
|
|
1441
|
+
* Non-finite values keep Kotlin's spelling (`"NaN"`, `"Infinity"`) — there
|
|
1442
|
+
* is no JSON representation to match, and stringifying is strictly safer
|
|
1443
|
+
* than throwing out of an analytics call.
|
|
1444
|
+
*
|
|
1445
|
+
* @param value The number to render.
|
|
1446
|
+
* @return Its string form, without a trailing `.0` for whole numbers.
|
|
1447
|
+
*/
|
|
1448
|
+
internal fun formatJsNumber(value: Double): String {
|
|
1449
|
+
if (!value.isFinite()) {
|
|
1450
|
+
return value.toString()
|
|
1451
|
+
}
|
|
1452
|
+
if (value == Math.floor(value) && Math.abs(value) < 9007199254740992.0) {
|
|
1453
|
+
return value.toLong().toString()
|
|
1454
|
+
}
|
|
1455
|
+
return value.toString()
|
|
1456
|
+
}
|
|
1330
1457
|
|
|
1331
1458
|
/**
|
|
1332
1459
|
* Converts a map of Variant_Boolean_String_Double to a HashMap<String?, String?>.
|
|
@@ -1363,12 +1490,26 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1363
1490
|
* `null` becomes [JSONObject.NULL] rather than a Kotlin `null`, which
|
|
1364
1491
|
* `org.json` requires for an explicit JSON `null`.
|
|
1365
1492
|
*
|
|
1366
|
-
* Known limitation: a **top-level** numeric value is still dropped,
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1493
|
+
* Known limitation: a **top-level** numeric value is still dropped, and
|
|
1494
|
+
* cannot be rescued from here. `Signal.getJSON()` serializes the payload
|
|
1495
|
+
* through EOCore's `JsonUtil.getHashValues`, whose `instanceof` ladder has
|
|
1496
|
+
* no `Number` branch, so the entry is skipped with no error. Numbers nested
|
|
1497
|
+
* inside an object or array are unaffected — the enclosing
|
|
1498
|
+
* [JSONObject]/[JSONArray] is built here, so `org.json` serializes them
|
|
1499
|
+
* normally. iOS carries top-level numbers fine; this is an SDK-side
|
|
1500
|
+
* asymmetry, pre-dating this conversion.
|
|
1501
|
+
*
|
|
1502
|
+
* Two non-fixes, so they are not attempted again: coercing the number to a
|
|
1503
|
+
* [String] would make it survive as a JSON string, diverging from iOS's
|
|
1504
|
+
* JSON number in a *new* way and silently changing the shape of data
|
|
1505
|
+
* already-shipping clients receive; and there is no `org.json` wrapper that
|
|
1506
|
+
* `getHashValues` accepts and that serializes as a bare number, so the
|
|
1507
|
+
* shape cannot be preserved either. The real fix is a `Number` branch in
|
|
1508
|
+
* `getHashValues` (Android SDK).
|
|
1509
|
+
*
|
|
1510
|
+
* What this method does do is [warnAboutDroppedTopLevelNumbers] — name the
|
|
1511
|
+
* keys the SDK is about to discard, so the loss is visible in logcat
|
|
1512
|
+
* instead of silent.
|
|
1372
1513
|
*
|
|
1373
1514
|
* A [JSONException] aborts the whole payload rather than escaping to the
|
|
1374
1515
|
* caller. Non-finite numbers are the case that raises it: `org.json`
|
|
@@ -1395,9 +1536,44 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1395
1536
|
Log.w(TAG, "[bridge] logSignal payload is not representable as JSON — dropping it: ${e.message}")
|
|
1396
1537
|
return HashMap()
|
|
1397
1538
|
}
|
|
1539
|
+
warnAboutDroppedTopLevelNumbers(map)
|
|
1398
1540
|
return map
|
|
1399
1541
|
}
|
|
1400
1542
|
|
|
1543
|
+
/**
|
|
1544
|
+
* Logs a warning naming the top-level numeric keys that the SDK's signal
|
|
1545
|
+
* serializer will discard (see [toSignalPayload] for why it does).
|
|
1546
|
+
*
|
|
1547
|
+
* The point is purely to stop the loss being silent. Before this, a JS
|
|
1548
|
+
* `logSignal({ orderTotal: 24.99 })` returned `true`, emitted a type-21
|
|
1549
|
+
* message, and simply had no `orderTotal` in it — indistinguishable, from
|
|
1550
|
+
* the caller's side, from a signal that arrived intact. The same call on
|
|
1551
|
+
* iOS carries the value, so a cross-platform dashboard shows the metric
|
|
1552
|
+
* populated on one platform and empty on the other with nothing anywhere
|
|
1553
|
+
* to explain it.
|
|
1554
|
+
*
|
|
1555
|
+
* Iterates the already-converted map so it reflects exactly what is handed
|
|
1556
|
+
* to the SDK. Booleans and strings are not mentioned: `getHashValues`
|
|
1557
|
+
* accumulates both.
|
|
1558
|
+
*
|
|
1559
|
+
* @param payload The converted payload, as returned to `logSignal`.
|
|
1560
|
+
*/
|
|
1561
|
+
private fun warnAboutDroppedTopLevelNumbers(payload: Map<String?, Any?>) {
|
|
1562
|
+
val dropped = payload.entries
|
|
1563
|
+
.filter { it.value is Number }
|
|
1564
|
+
.mapNotNull { it.key }
|
|
1565
|
+
if (dropped.isEmpty()) {
|
|
1566
|
+
return
|
|
1567
|
+
}
|
|
1568
|
+
Log.w(
|
|
1569
|
+
TAG,
|
|
1570
|
+
"[bridge] logSignal: top-level numeric value(s) ${dropped.sorted()} will be dropped by the " +
|
|
1571
|
+
"Android SDK's signal serializer, which accumulates only strings, booleans and nested " +
|
|
1572
|
+
"JSON. iOS sends them. Nest the value under an object to carry it on both platforms, " +
|
|
1573
|
+
"e.g. { cart: { total: 24.99 } } instead of { total: 24.99 }."
|
|
1574
|
+
)
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1401
1577
|
/**
|
|
1402
1578
|
* Recursive helper for [toSignalPayload]. Maps Kotlin containers onto their
|
|
1403
1579
|
* `org.json` equivalents and leaves scalars alone.
|