react-native-acoustic-connect 19.0.3 → 19.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AcousticConnectRN.podspec +40 -3
- package/CHANGELOG.md +18 -0
- package/ConnectConfig.example.json +15 -0
- package/README.md +121 -7
- package/android/build.gradle +71 -0
- package/android/src/main/java/com/acousticconnectrn/HybridAcousticConnectRN.kt +4 -2
- package/cli/__tests__/doctor.test.mjs +88 -0
- package/cli/doctor.mjs +23 -5
- package/cli/ios/connect_pods.rb +2 -2
- package/ios/ConnectRNParsing.swift +154 -0
- package/ios/HybridAcousticConnectRN.swift +15 -115
- package/package.json +18 -3
- package/scripts/reviewConnectConfig.js +6 -1
- package/scripts/ConnectConfig.json +0 -180
|
@@ -48,13 +48,17 @@ iOSVersion = connectConfig["Connect"]["iOSVersion"]
|
|
|
48
48
|
# 2.1.12 floor: `ConnectSDK.shared.push.requestAuthorization()` and
|
|
49
49
|
# `getCurrentAuthorization()` land in 2.1.12. The push permission
|
|
50
50
|
# bridge methods call them directly, so an older pod would fail to compile.
|
|
51
|
-
|
|
51
|
+
# 2.1.13 floor: carries a vital fix for automatic push mode (the
|
|
52
|
+
# `ConnectDelegateProxy`-swizzled `UNUserNotificationCenter` delegate path
|
|
53
|
+
# the bridge relies on in `pushMode: 'automatic'`). 2.1.12 and older mis-wire
|
|
54
|
+
# that path, so the floor is raised to guarantee the fix is present.
|
|
55
|
+
sdkFloor = '>= 2.1.13'
|
|
52
56
|
dependencyRequirements = iOSVersion.to_s.empty? ? [sdkFloor] : [sdkFloor, iOSVersion]
|
|
53
57
|
|
|
54
58
|
# Normalize Connect.PushEnabled to a STRICT boolean before writing the native
|
|
55
59
|
# config bundle. A nil/absent value otherwise serializes as JSON `null`, and the
|
|
56
60
|
# iOS SDK then initializes push-off (ConnectSDK.shared.push == nil) — silently
|
|
57
|
-
# breaking push on the very first build
|
|
61
|
+
# breaking push on the very first build. Coercing here guarantees
|
|
58
62
|
# the bundle never ships `null`; the native lenient parser defends the runtime
|
|
59
63
|
# path, but this prevents shipping a non-boolean value in the first place.
|
|
60
64
|
#
|
|
@@ -115,12 +119,45 @@ Pod::Spec.new do |s|
|
|
|
115
119
|
# Implementation (C++ objects)
|
|
116
120
|
"cpp/**/*.{hpp,cpp}",
|
|
117
121
|
]
|
|
122
|
+
# Unit tests live in ios/Tests and belong to the UnitTests test_spec below,
|
|
123
|
+
# not to the library target (source_files globs ios/**/*.swift).
|
|
124
|
+
s.exclude_files = "ios/Tests/**"
|
|
118
125
|
|
|
119
126
|
s.pod_target_xcconfig = {
|
|
120
127
|
# C++ compiler flags, mainly for folly.
|
|
121
|
-
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES"
|
|
128
|
+
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES",
|
|
129
|
+
# @testable import (UnitTests test_spec) needs testability in Debug.
|
|
130
|
+
"ENABLE_TESTABILITY[config=Debug]" => "YES"
|
|
122
131
|
}
|
|
123
132
|
|
|
133
|
+
# Native unit tests. Run through a consumer app's Pods project:
|
|
134
|
+
# Podfile: pod 'AcousticConnectRN', :path => '../..', :testspecs => ['UnitTests']
|
|
135
|
+
# xcodebuild test -workspace <app>.xcworkspace -scheme AcousticConnectRN \
|
|
136
|
+
# -destination 'platform=iOS Simulator,name=<booted sim>'
|
|
137
|
+
# The tests use Swift Testing. They do NOT `@testable import` the pod
|
|
138
|
+
# module — see the source_files comment below for why that's impossible.
|
|
139
|
+
s.test_spec 'UnitTests' do |ts|
|
|
140
|
+
# The sources under test are compiled directly into the test bundle
|
|
141
|
+
# instead of `@testable import`ing the pod module: nitrogen's generated
|
|
142
|
+
# Swift compatibility header emits C++ thunks referencing nitro C++ types
|
|
143
|
+
# (margelo::nitro::ArrayBufferHolder) without including their headers, so
|
|
144
|
+
# the AcousticConnectRN clang module cannot be built by any Swift client —
|
|
145
|
+
# including a test target. Compiling the files here is safe: the linker
|
|
146
|
+
# pulls static-archive members lazily, so the parent lib's copies of
|
|
147
|
+
# these objects are never loaded and no duplicate symbols arise.
|
|
148
|
+
ts.source_files = [
|
|
149
|
+
'ios/Tests/**/*.swift',
|
|
150
|
+
'ios/ConnectRNParsing.swift',
|
|
151
|
+
'nitrogen/generated/ios/swift/Variant_Bool_String_Double.swift',
|
|
152
|
+
]
|
|
153
|
+
# ios/ConnectRNParsing.swift does `import Connect`. CocoaPods test_specs
|
|
154
|
+
# inherit the parent spec's dependencies implicitly, so this line is
|
|
155
|
+
# observably redundant today — declared explicitly anyway so the test
|
|
156
|
+
# bundle's dependency on the Connect SDK doesn't rely on that inheritance
|
|
157
|
+
# behavior remaining unchanged (e.g. a future migration off CocoaPods).
|
|
158
|
+
ts.dependency dependencyName, *dependencyRequirements
|
|
159
|
+
end
|
|
160
|
+
|
|
124
161
|
s.resource_bundle = {
|
|
125
162
|
'AcousticConnectRNConfig' => ['ios/AcousticConnectRNConfig.json'],
|
|
126
163
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 19.0.9 (2026-07-03)
|
|
2
|
+
|
|
3
|
+
### Reverts
|
|
4
|
+
|
|
5
|
+
* Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/go-acoustic/react-native-acoustic-connect/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
|
|
6
|
+
## 19.0.8 (2026-07-03)
|
|
7
|
+
|
|
8
|
+
### Reverts
|
|
9
|
+
|
|
10
|
+
* Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/go-acoustic/react-native-acoustic-connect/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
|
|
11
|
+
## 19.0.7 (2026-07-03)
|
|
12
|
+
|
|
13
|
+
### Reverts
|
|
14
|
+
|
|
15
|
+
* Revert "Beta ReactNativeConnect build: 18.0.36" ([609ad7d](https://github.com/go-acoustic/react-native-acoustic-connect/commit/609ad7d3244ec06f27dced5864d40585c5b3c9cf))
|
|
16
|
+
## [19.0.6](https://github.com/go-acoustic/react-native-acoustic-connect/compare/19.0.5...19.0.6) (2026-07-02)
|
|
17
|
+
## [19.0.5](https://github.com/go-acoustic/react-native-acoustic-connect/compare/19.0.4...19.0.5) (2026-07-02)
|
|
18
|
+
## [19.0.4](https://github.com/go-acoustic/react-native-acoustic-connect/compare/19.0.3...19.0.4) (2026-07-01)
|
|
1
19
|
## [19.0.3](https://github.com/go-acoustic/react-native-acoustic-connect/compare/19.0.2...19.0.3) (2026-06-30)
|
|
2
20
|
## [19.0.2](https://github.com/go-acoustic/react-native-acoustic-connect/compare/19.0.1...19.0.2) (2026-06-30)
|
|
3
21
|
## [19.0.1](https://github.com/go-acoustic/react-native-acoustic-connect/compare/18.0.44...19.0.1) (2026-06-29)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Connect": {
|
|
3
|
+
"AndroidNotificationIconResName": "ic_notification",
|
|
4
|
+
"AndroidVersion": "11.0.11",
|
|
5
|
+
"AppKey": "YOUR_CONNECT_APP_KEY_HERE",
|
|
6
|
+
"KillSwitchUrl": "YOUR_KILL_SWITCH_URL_HERE",
|
|
7
|
+
"PostMessageUrl": "YOUR_POST_MESSAGE_URL_HERE",
|
|
8
|
+
"PushEnabled": false,
|
|
9
|
+
"iOSAppGroupIdentifier": "YOUR_APP_GROUP_ID_HERE",
|
|
10
|
+
"iOSDevelopmentTeam": "YOUR_TEAM_ID",
|
|
11
|
+
"iOSPushMode": "automatic",
|
|
12
|
+
"iOSVersion": "",
|
|
13
|
+
"useRelease": false
|
|
14
|
+
}
|
|
15
|
+
}
|
package/README.md
CHANGED
|
@@ -84,6 +84,81 @@ Field summary (see [API reference](#api-reference) for full semantics):
|
|
|
84
84
|
| `iOSAppGroupIdentifier` | `null` | iOS App Group shared with the Notification Service / Notification Content extension. |
|
|
85
85
|
| `AndroidNotificationIconResName` | `null` | Drawable resource name (no extension) for the Android notification small icon. |
|
|
86
86
|
|
|
87
|
+
### What `npm install` / `pod install` do for you
|
|
88
|
+
|
|
89
|
+
Both commands run bootstrap steps against **your** project, not just the
|
|
90
|
+
package's own build:
|
|
91
|
+
|
|
92
|
+
- `npm install` triggers a postinstall step that (idempotently, best-effort):
|
|
93
|
+
- scaffolds a starter `ConnectConfig.json` at your project root if one
|
|
94
|
+
isn't already there,
|
|
95
|
+
- adds the Android permissions Connect needs
|
|
96
|
+
(`INTERNET`, `ACCESS_NETWORK_STATE`, `ACCESS_WIFI_STATE`,
|
|
97
|
+
`ACCESS_FINE_LOCATION`) to `android/app/src/main/AndroidManifest.xml`,
|
|
98
|
+
- wires `android/app/build.gradle` to apply the SDK's `config.gradle`.
|
|
99
|
+
- `pod install` merges your `ConnectConfig.json` into the iOS pod's
|
|
100
|
+
generated config bundle at install time (see the podspec).
|
|
101
|
+
|
|
102
|
+
None of this touches files outside your project's `android/`/`ios/`
|
|
103
|
+
directories, and re-running either command is safe. After install, run
|
|
104
|
+
`npx acoustic-connect doctor` to validate the result — see
|
|
105
|
+
[Setup CLI](#setup-cli-acoustic-connect) below.
|
|
106
|
+
|
|
107
|
+
## Setup CLI (`acoustic-connect`)
|
|
108
|
+
|
|
109
|
+
The package ships a small CLI, installed as `acoustic-connect` (invoke it with
|
|
110
|
+
`npx acoustic-connect <command>`), that validates and scaffolds the native
|
|
111
|
+
side of an integration. Both commands auto-detect bare RN vs. Expo and are
|
|
112
|
+
idempotent — safe to re-run, and they never overwrite a file that's already
|
|
113
|
+
there.
|
|
114
|
+
|
|
115
|
+
### `npx acoustic-connect doctor [dir] [--require-push]`
|
|
116
|
+
|
|
117
|
+
Checks the things that otherwise fail late and confusingly at build time:
|
|
118
|
+
|
|
119
|
+
- Node version.
|
|
120
|
+
- `ConnectConfig.json` values — `AppKey`, `PostMessageUrl` / `KillSwitchUrl`
|
|
121
|
+
(must be real `https` URLs, not the placeholder host), `iOSAppGroupIdentifier`
|
|
122
|
+
format, `iOSDevelopmentTeam` format.
|
|
123
|
+
- Android identifiers — the package/namespace is a Java-safe name, and it has
|
|
124
|
+
a matching client in `google-services.json` (Expo also checks for a stale
|
|
125
|
+
`android/` from an incremental `expo prebuild` after an `android.package`
|
|
126
|
+
change).
|
|
127
|
+
- iOS entitlements — bare projects need `aps-environment` + an App Group
|
|
128
|
+
entitlement (from `setup-ios-push`, below, or hand-authored).
|
|
129
|
+
- (macOS only) whether a local Apple Development signing identity is
|
|
130
|
+
installed, when push is on.
|
|
131
|
+
|
|
132
|
+
The push-related checks above are **hard failures** only when
|
|
133
|
+
`Connect.PushEnabled` is `true` (or `--require-push` is passed) — a non-push
|
|
134
|
+
integration only needs `AppKey`. `doctor` exits non-zero on any failure, so
|
|
135
|
+
it's usable as a CI gate (`npx acoustic-connect doctor --require-push`).
|
|
136
|
+
|
|
137
|
+
`dir` defaults to the current directory. If `ConnectConfig.json` is missing,
|
|
138
|
+
`doctor` scaffolds it first (from a project-local `ConnectConfig.example.json`
|
|
139
|
+
if you have one, else from the copy bundled with the package) before running
|
|
140
|
+
the checks above — so it's also a fine first command to run in a fresh
|
|
141
|
+
project.
|
|
142
|
+
|
|
143
|
+
### `npx acoustic-connect setup-ios-push [dir]`
|
|
144
|
+
|
|
145
|
+
Bare-workflow only — the Expo Config Plugin does the equivalent automatically
|
|
146
|
+
on `expo prebuild`. Scaffolds the two iOS push extensions:
|
|
147
|
+
|
|
148
|
+
- `ConnectNSE` (Notification Service Extension — rich-media attachments,
|
|
149
|
+
delivery tracking)
|
|
150
|
+
- `ConnectNCE` (Notification Content Extension — expanded rich-media UI)
|
|
151
|
+
|
|
152
|
+
It writes each extension's Swift source, `Info.plist`, and entitlements from
|
|
153
|
+
the SDK's templates (substituting your App Group), adds the App Group +
|
|
154
|
+
`aps-environment` entitlement to the host app if it doesn't have one yet, and
|
|
155
|
+
wires both targets into your `.xcodeproj` via a bundled Ruby script — which
|
|
156
|
+
needs `ruby` and the `xcodeproj` gem (`gem install xcodeproj`; ships with
|
|
157
|
+
CocoaPods, so most Mac dev setups already have it).
|
|
158
|
+
|
|
159
|
+
Requires macOS and `Connect.iOSAppGroupIdentifier` already set in
|
|
160
|
+
`ConnectConfig.json` — run `doctor` first if it's missing.
|
|
161
|
+
|
|
87
162
|
## Using with Expo SDK 55+
|
|
88
163
|
|
|
89
164
|
Expo SDK 55 and newer is supported via the bundled Expo Config Plugin.
|
|
@@ -409,11 +484,36 @@ releases. Match the version rather than bypass it.
|
|
|
409
484
|
|
|
410
485
|
### iOS — push session reaches the collector but no notifications arrive (no APNs token)
|
|
411
486
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
487
|
+
**Fingerprint:** analytics and `pushRegistration` still reach the collector
|
|
488
|
+
normally, but `mobileToken` stays `null` forever — and, notably, **no
|
|
489
|
+
`didFailToRegisterForRemoteNotificationsWithError` (or any push-related
|
|
490
|
+
failure) shows up in logs either.** It just silently never happens. That
|
|
491
|
+
absence of any error is what makes this look like a missing/broken app-side
|
|
492
|
+
callback or an SDK bug at first glance; it's almost always a signing
|
|
493
|
+
artifact instead. Run `npx acoustic-connect doctor` first — it detects this
|
|
494
|
+
exact condition (missing/invalid `iOSDevelopmentTeam`, no Development
|
|
495
|
+
certificate) up front, before you go digging through portal config.
|
|
496
|
+
|
|
497
|
+
Cause: a CLI build (`expo run:ios` / `react-native run-ios` / `xcodebuild`)
|
|
498
|
+
doesn't auto-pick a signing team the way the Xcode GUI does. With no team, it
|
|
499
|
+
signs ad-hoc and **drops the `aps-environment` entitlement**, so iOS issues no
|
|
500
|
+
APNs token — the app still launches and analytics reach the collector, but no
|
|
501
|
+
`pushRegistration` with a `mobileToken` is ever sent.
|
|
502
|
+
|
|
503
|
+
**Setting `Connect.iOSDevelopmentTeam` in `ConnectConfig.json` by itself does
|
|
504
|
+
nothing.** It's just a source value — it has no effect on the build until
|
|
505
|
+
`expo prebuild` / `npx acoustic-connect setup-ios-push` actually stamps
|
|
506
|
+
`DEVELOPMENT_TEAM` onto the host **and** the `ConnectNSE` **and** `ConnectNCE`
|
|
507
|
+
targets in the generated `.pbxproj`. Re-run that step any time
|
|
508
|
+
`iOSDevelopmentTeam` changes, and after every fresh clone — a teammate's
|
|
509
|
+
machine may already have the stamp baked in from a prior run; yours doesn't
|
|
510
|
+
until you run it too. This is the classic "works for me, not for them" trap:
|
|
511
|
+
every portal-side check (App ID push capability, provisioning profile,
|
|
512
|
+
entitlements file contents, App Group registration, `.p8` key upload) can
|
|
513
|
+
look perfect and still not reflect what's actually embedded in the binary
|
|
514
|
+
that got installed on the device.
|
|
515
|
+
|
|
516
|
+
Fix:
|
|
417
517
|
|
|
418
518
|
1. Set `Connect.iOSDevelopmentTeam` (10-char Apple Team ID) in
|
|
419
519
|
`ConnectConfig.json` (or pass the `iosDevelopmentTeam` plugin prop). Re-run
|
|
@@ -423,8 +523,22 @@ token — the app still launches and analytics reach the collector, but no
|
|
|
423
523
|
ID added in Xcode → Settings → Accounts, so a Development certificate +
|
|
424
524
|
profiles are provisioned.
|
|
425
525
|
|
|
426
|
-
|
|
427
|
-
|
|
526
|
+
**Verify the fix actually landed** — rerunning the steps above isn't proof by
|
|
527
|
+
itself; confirm the build artifacts directly:
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
# 1. DEVELOPMENT_TEAM must be stamped on the host AND both push extensions
|
|
531
|
+
grep DEVELOPMENT_TEAM ios/*.xcodeproj/project.pbxproj
|
|
532
|
+
|
|
533
|
+
# 2. The *installed* binary must carry the entitlement — check the actual
|
|
534
|
+
# .app, not just the source .entitlements file in your repo
|
|
535
|
+
codesign -d --entitlements :- --xml /path/to/YourApp.app
|
|
536
|
+
# must show aps-environment in the output
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
If `DEVELOPMENT_TEAM` is missing for any of the three targets, or `codesign`
|
|
540
|
+
doesn't show `aps-environment`, the stamp didn't take (or a stale build was
|
|
541
|
+
reused) — re-run `setup-ios-push` / `prebuild` and do a clean rebuild.
|
|
428
542
|
|
|
429
543
|
### iOS — Expo Android build fails: `No matching client found for package name …`
|
|
430
544
|
|
package/android/build.gradle
CHANGED
|
@@ -32,6 +32,7 @@ def isNewArchitectureEnabled() {
|
|
|
32
32
|
|
|
33
33
|
apply plugin: "com.android.library"
|
|
34
34
|
apply plugin: 'org.jetbrains.kotlin.android'
|
|
35
|
+
apply plugin: 'jacoco'
|
|
35
36
|
apply from: '../nitrogen/generated/android/AcousticConnectRN+autolinking.gradle'
|
|
36
37
|
|
|
37
38
|
if (isNewArchitectureEnabled()) {
|
|
@@ -117,6 +118,17 @@ android {
|
|
|
117
118
|
disable "GradleCompatible"
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
testOptions {
|
|
122
|
+
unitTests {
|
|
123
|
+
// Unstubbed android.jar methods return defaults instead of throwing —
|
|
124
|
+
// needed for the few framework calls Robolectric doesn't cover.
|
|
125
|
+
returnDefaultValues = true
|
|
126
|
+
// Robolectric needs the library manifest + resources on the test
|
|
127
|
+
// classpath.
|
|
128
|
+
includeAndroidResources = true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
120
132
|
compileOptions {
|
|
121
133
|
sourceCompatibility JavaVersion.VERSION_1_8
|
|
122
134
|
targetCompatibility JavaVersion.VERSION_1_8
|
|
@@ -233,3 +245,62 @@ if (isNewArchitectureEnabled()) {
|
|
|
233
245
|
codegenJavaPackageName = "com.acousticconnectrn"
|
|
234
246
|
}
|
|
235
247
|
}
|
|
248
|
+
|
|
249
|
+
// JVM unit tests (android/src/test) — run from a consumer app's gradle, e.g.
|
|
250
|
+
// cd example/android && ./gradlew :react-native-acoustic-connect:testDebugUnitTest
|
|
251
|
+
// No emulator: testDebugUnitTest does not trigger externalNativeBuild/CMake.
|
|
252
|
+
// RN + nitro are compileOnly above (provided by the host app), so they are
|
|
253
|
+
// re-declared for the test runtime classpath; the Connect SDK is already
|
|
254
|
+
// `implementation` and flows to tests automatically. Note: nitro's Promise is
|
|
255
|
+
// JNI-backed, so the push*/logIdentity Promise-returning methods cannot be
|
|
256
|
+
// exercised in JVM tests — they are covered by the sample-build CI gates.
|
|
257
|
+
dependencies {
|
|
258
|
+
testImplementation "junit:junit:4.13.2"
|
|
259
|
+
testImplementation "io.mockk:mockk:1.13.16"
|
|
260
|
+
// @Config(sdk = [35]) in the tests until Robolectric supports SDK 36.
|
|
261
|
+
testImplementation "org.robolectric:robolectric:4.14.1"
|
|
262
|
+
// Deliberately `+`, matching the compileOnly declaration above: this
|
|
263
|
+
// coordinate never resolves from a repository — the RN Gradle plugin
|
|
264
|
+
// substitutes it with the HOST app's `com.facebook.react:react-android`
|
|
265
|
+
// at resolution time, so tests always run against the same RN version the
|
|
266
|
+
// consuming build uses. Pinning (e.g. `0.82.1`) would name an artifact
|
|
267
|
+
// version that doesn't exist on Maven for RN >= 0.72 and adds only false
|
|
268
|
+
// precision; without the substitution both forms fail loudly.
|
|
269
|
+
//noinspection GradleDynamicVersion
|
|
270
|
+
testImplementation "com.facebook.react:react-native:+"
|
|
271
|
+
testImplementation project(":react-native-nitro-modules")
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Coverage for the JVM unit tests. Robolectric needs
|
|
275
|
+
// includeNoLocationClasses to attribute coverage to instrumented classes.
|
|
276
|
+
tasks.withType(Test).configureEach {
|
|
277
|
+
jacoco {
|
|
278
|
+
includeNoLocationClasses = true
|
|
279
|
+
excludes = ['jdk.internal.*']
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// XML (parsed by the Jenkins Slack summary) + HTML (archived artifact)
|
|
284
|
+
// coverage report over the debug unit tests. Registered afterEvaluate
|
|
285
|
+
// because AGP creates the variant test task then. Generated nitro bindings
|
|
286
|
+
// are excluded — they are codegen output, not logic this module owns.
|
|
287
|
+
project.afterEvaluate {
|
|
288
|
+
tasks.register('jacocoDebugUnitTestReport', JacocoReport) {
|
|
289
|
+
dependsOn 'testDebugUnitTest'
|
|
290
|
+
group = 'verification'
|
|
291
|
+
description = 'JaCoCo coverage report for the debug JVM unit tests.'
|
|
292
|
+
|
|
293
|
+
def generatedExcludes = ['com/margelo/nitro/**', '**/BuildConfig*']
|
|
294
|
+
classDirectories.setFrom(files(
|
|
295
|
+
fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: generatedExcludes),
|
|
296
|
+
fileTree(dir: "${buildDir}/intermediates/javac/debug", includes: ['**/classes/**'], excludes: generatedExcludes)
|
|
297
|
+
))
|
|
298
|
+
sourceDirectories.setFrom(files("${projectDir}/src/main/java"))
|
|
299
|
+
executionData.setFrom(fileTree(dir: buildDir, includes: ['jacoco/testDebugUnitTest.exec']))
|
|
300
|
+
|
|
301
|
+
reports {
|
|
302
|
+
xml.required = true
|
|
303
|
+
html.required = true
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -1206,7 +1206,8 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1206
1206
|
* @param values The map to be converted.
|
|
1207
1207
|
* @return A HashMap<String?, String?> representation of the input map which library can use.
|
|
1208
1208
|
*/
|
|
1209
|
-
|
|
1209
|
+
// internal (not private) so the unit tests in src/test can exercise it.
|
|
1210
|
+
internal fun convertToMap(values: Map<String, Variant_Boolean_String_Double>): HashMap<String?, String?> {
|
|
1210
1211
|
val map = HashMap<String?, String?>()
|
|
1211
1212
|
for (key in values.keys) {
|
|
1212
1213
|
val value = values[key]
|
|
@@ -1223,7 +1224,8 @@ class HybridAcousticConnectRN : HybridAcousticConnectRNSpec(),
|
|
|
1223
1224
|
* @param values The map to be converted.
|
|
1224
1225
|
* @return A HashMap<String?, String?> representation of the input map which library can use.
|
|
1225
1226
|
*/
|
|
1226
|
-
|
|
1227
|
+
// internal (not private) so the unit tests in src/test can exercise it.
|
|
1228
|
+
internal fun convertToMapAny(values: Map<String, Variant_Boolean_String_Double>): java.util.HashMap<String?, Any?>? {
|
|
1227
1229
|
val map = HashMap<String?, Any?>()
|
|
1228
1230
|
for (key in values.keys) {
|
|
1229
1231
|
val value = values[key]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Copyright (C) 2026 Acoustic, L.P. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for ensureConnectConfig's local→bundled→missing fallback in
|
|
4
|
+
// cli/doctor.mjs. Uses Node's built-in test runner (node:test) — no build
|
|
5
|
+
// step needed, cli/ is plain ESM.
|
|
6
|
+
//
|
|
7
|
+
// npm run test:cli
|
|
8
|
+
|
|
9
|
+
import { test } from 'node:test'
|
|
10
|
+
import assert from 'node:assert/strict'
|
|
11
|
+
import * as fs from 'node:fs'
|
|
12
|
+
import * as os from 'node:os'
|
|
13
|
+
import * as path from 'node:path'
|
|
14
|
+
|
|
15
|
+
import { ensureConnectConfig } from '../doctor.mjs'
|
|
16
|
+
import { Reporter } from '../lib.mjs'
|
|
17
|
+
|
|
18
|
+
function tmpDir() {
|
|
19
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'connect-doctor-test-'))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ensureConnectConfig's 'exists' branch reports as `pass('ConnectConfig.json present')`
|
|
23
|
+
// (label includes the detail) while the other branches use a plain
|
|
24
|
+
// 'ConnectConfig.json' label with a separate detail string — match by prefix
|
|
25
|
+
// so the assertion doesn't depend on that formatting quirk.
|
|
26
|
+
function statusFor(reporter, labelPrefix) {
|
|
27
|
+
return reporter.results.find((r) => r.label.startsWith(labelPrefix))?.status
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('ensureConnectConfig: prefers a project-local ConnectConfig.example.json over the bundled one', () => {
|
|
31
|
+
const dir = tmpDir()
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(dir, 'ConnectConfig.example.json'),
|
|
34
|
+
JSON.stringify({ Connect: { AppKey: 'from-local-example' } }),
|
|
35
|
+
)
|
|
36
|
+
const reporter = new Reporter()
|
|
37
|
+
|
|
38
|
+
ensureConnectConfig(reporter, dir, {
|
|
39
|
+
bundledExample: path.join(dir, 'never-read.json'),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const written = JSON.parse(fs.readFileSync(path.join(dir, 'ConnectConfig.json'), 'utf8'))
|
|
43
|
+
assert.equal(written.Connect.AppKey, 'from-local-example')
|
|
44
|
+
assert.equal(statusFor(reporter, 'ConnectConfig.json'), 'warn')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('ensureConnectConfig: falls back to the bundled example when no project-local one exists', () => {
|
|
48
|
+
const dir = tmpDir()
|
|
49
|
+
const bundledDir = tmpDir()
|
|
50
|
+
const bundledExample = path.join(bundledDir, 'ConnectConfig.example.json')
|
|
51
|
+
fs.writeFileSync(bundledExample, JSON.stringify({ Connect: { AppKey: 'from-bundled-example' } }))
|
|
52
|
+
const reporter = new Reporter()
|
|
53
|
+
|
|
54
|
+
ensureConnectConfig(reporter, dir, { bundledExample })
|
|
55
|
+
|
|
56
|
+
const written = JSON.parse(fs.readFileSync(path.join(dir, 'ConnectConfig.json'), 'utf8'))
|
|
57
|
+
assert.equal(written.Connect.AppKey, 'from-bundled-example')
|
|
58
|
+
assert.equal(statusFor(reporter, 'ConnectConfig.json'), 'warn')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('ensureConnectConfig: fails cleanly (no crash, no file written) when both are missing', () => {
|
|
62
|
+
const dir = tmpDir()
|
|
63
|
+
const reporter = new Reporter()
|
|
64
|
+
|
|
65
|
+
ensureConnectConfig(reporter, dir, {
|
|
66
|
+
bundledExample: path.join(dir, 'does-not-exist.json'),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
assert.equal(fs.existsSync(path.join(dir, 'ConnectConfig.json')), false)
|
|
70
|
+
assert.equal(statusFor(reporter, 'ConnectConfig.json'), 'fail')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('ensureConnectConfig: never overwrites an existing ConnectConfig.json', () => {
|
|
74
|
+
const dir = tmpDir()
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(dir, 'ConnectConfig.json'),
|
|
77
|
+
JSON.stringify({ Connect: { AppKey: 'already-configured' } }),
|
|
78
|
+
)
|
|
79
|
+
const reporter = new Reporter()
|
|
80
|
+
|
|
81
|
+
ensureConnectConfig(reporter, dir, {
|
|
82
|
+
bundledExample: path.join(dir, 'never-read.json'),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const written = JSON.parse(fs.readFileSync(path.join(dir, 'ConnectConfig.json'), 'utf8'))
|
|
86
|
+
assert.equal(written.Connect.AppKey, 'already-configured')
|
|
87
|
+
assert.equal(statusFor(reporter, 'ConnectConfig.json'), 'pass')
|
|
88
|
+
})
|
package/cli/doctor.mjs
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// Connect integration that otherwise fail late and confusingly at build time:
|
|
5
5
|
// the App Group format, a Java-safe Android package, and the
|
|
6
6
|
// google-services.json package match. It also scaffolds the gitignored
|
|
7
|
-
// ConnectConfig.json from
|
|
8
|
-
//
|
|
7
|
+
// ConnectConfig.json — from a project-local ConnectConfig.example.json if
|
|
8
|
+
// present, else from the copy bundled at the package root — so a fresh
|
|
9
|
+
// project has something to edit.
|
|
9
10
|
//
|
|
10
11
|
// Auto-detects the project shape:
|
|
11
12
|
// - Expo : a root app.json with an `expo` block (identifiers live there;
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
|
|
21
22
|
import fs from 'node:fs'
|
|
22
23
|
import path from 'node:path'
|
|
24
|
+
import {fileURLToPath} from 'node:url'
|
|
23
25
|
|
|
24
26
|
import {
|
|
25
27
|
Reporter,
|
|
@@ -32,6 +34,10 @@ import {
|
|
|
32
34
|
section,
|
|
33
35
|
} from './lib.mjs'
|
|
34
36
|
|
|
37
|
+
// Package root (one level up from cli/) — where the bundled
|
|
38
|
+
// ConnectConfig.example.json ships for real (published) consumers.
|
|
39
|
+
const sdkRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
40
|
+
|
|
35
41
|
// App Group format per Apple + the Config Plugin's own validator
|
|
36
42
|
// (plugin/src/withConnectNSE.ts: APP_GROUP_PATTERN).
|
|
37
43
|
const APP_GROUP_PATTERN = /^group\.[A-Za-z0-9.-]+$/
|
|
@@ -101,10 +107,22 @@ function checkNode(reporter) {
|
|
|
101
107
|
)
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
// Scaffold the per-developer ConnectConfig.json from
|
|
105
|
-
|
|
110
|
+
// Scaffold the per-developer ConnectConfig.json from an example. Prefers a
|
|
111
|
+
// project-local ConnectConfig.example.json (e.g. this repo's own
|
|
112
|
+
// Examples/bare-workflow and Examples/expo, which commit a customized one) and
|
|
113
|
+
// falls back to the copy bundled with the package — the project-local file
|
|
114
|
+
// generally isn't there for a real (published) consumer.
|
|
115
|
+
//
|
|
116
|
+
// `bundledExample` is overridable purely so cli/__tests__/doctor.test.mjs can
|
|
117
|
+
// exercise the "both missing" branch without touching the real package file.
|
|
118
|
+
export function ensureConnectConfig(
|
|
119
|
+
reporter,
|
|
120
|
+
dir,
|
|
121
|
+
{bundledExample = path.join(sdkRoot, 'ConnectConfig.example.json')} = {},
|
|
122
|
+
) {
|
|
106
123
|
const dest = path.join(dir, 'ConnectConfig.json')
|
|
107
|
-
const
|
|
124
|
+
const localExample = path.join(dir, 'ConnectConfig.example.json')
|
|
125
|
+
const src = fileExists(localExample) ? localExample : bundledExample
|
|
108
126
|
const result = copyIfMissing(src, dest)
|
|
109
127
|
if (result === 'created')
|
|
110
128
|
reporter.warn(
|
package/cli/ios/connect_pods.rb
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# app target and the push extensions (ConnectNSE / ConnectNCE) all link the SAME
|
|
8
8
|
# SDK build:
|
|
9
9
|
# - useRelease => 'AcousticConnect', otherwise 'AcousticConnectDebug'
|
|
10
|
-
# - floor '>= 2.1.
|
|
10
|
+
# - floor '>= 2.1.13' (push permission API + automatic-mode fix floor)
|
|
11
11
|
# - optional exact pin via Connect.iOSVersion
|
|
12
12
|
#
|
|
13
13
|
# Usage from a Podfile (resolve the file the same way the RN template resolves
|
|
@@ -42,7 +42,7 @@ def acoustic_connect_pod(podfile_dir, config_path: nil)
|
|
|
42
42
|
use_release = connect_config['useRelease']
|
|
43
43
|
ios_version = connect_config['iOSVersion'].to_s
|
|
44
44
|
name = use_release ? 'AcousticConnect' : 'AcousticConnectDebug'
|
|
45
|
-
floor = '>= 2.1.
|
|
45
|
+
floor = '>= 2.1.13'
|
|
46
46
|
requirements = ios_version.empty? ? [floor] : [floor, ios_version]
|
|
47
47
|
[name, requirements]
|
|
48
48
|
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Copyright (C) 2026 Acoustic, L.P. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// NOTICE: This file contains material that is confidential and proprietary to
|
|
4
|
+
// Acoustic, L.P. and/or other developers. No license is granted under any intellectual or
|
|
5
|
+
// industrial property rights of Acoustic, L.P. except as may be provided in an agreement with
|
|
6
|
+
// Acoustic, L.P. Any unauthorized copying or distribution of content from this file is
|
|
7
|
+
// prohibited.
|
|
8
|
+
|
|
9
|
+
import Foundation
|
|
10
|
+
import Connect
|
|
11
|
+
import OSLog
|
|
12
|
+
|
|
13
|
+
// Same subsystem/category as the bridge's config logger so the extraction
|
|
14
|
+
// does not change where these lines surface in Console.app.
|
|
15
|
+
private let configLog = Logger(subsystem: "com.acoustic.AcousticConnectRN", category: "config")
|
|
16
|
+
|
|
17
|
+
/// Pure parsing / mapping / conversion logic extracted from
|
|
18
|
+
/// `HybridAcousticConnectRN` (behaviour-preserving extraction) so it can be
|
|
19
|
+
/// unit-tested without constructing the hybrid — the hybrid's `init`
|
|
20
|
+
/// dispatches `load()`, which enables the real SDK with the bundled AppKey.
|
|
21
|
+
internal enum ConnectRNParsing {
|
|
22
|
+
|
|
23
|
+
internal enum IOSPushMode: Equatable {
|
|
24
|
+
case automatic, manual
|
|
25
|
+
var descriptor: String { self == .automatic ? "automatic" : "manual" }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Lenient `PushEnabled` parser — accepts a `Bool` directly, or a string
|
|
29
|
+
/// like `"true"`/`"false"`/`"yes"`/`"no"`. Anything else falls back to
|
|
30
|
+
/// `false` with a warning so a typo in the JSON doesn't silently enable
|
|
31
|
+
/// push when the developer thought they'd turned it off.
|
|
32
|
+
static func parsePushEnabled(_ raw: Any?) -> Bool {
|
|
33
|
+
if let b = raw as? Bool { return b }
|
|
34
|
+
if let n = raw as? NSNumber { return n.boolValue }
|
|
35
|
+
if let s = raw as? String {
|
|
36
|
+
switch s.lowercased() {
|
|
37
|
+
case "true", "yes", "1": return true
|
|
38
|
+
case "false", "no", "0", "": return false
|
|
39
|
+
default:
|
|
40
|
+
configLog.warning("PushEnabled string \"\(s, privacy: .public)\" not recognised. Falling back to false.")
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if raw != nil {
|
|
45
|
+
configLog.warning("PushEnabled is set but is neither a Bool nor a recognised string. Falling back to false.")
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static func parseIOSPushMode(_ raw: String?) -> IOSPushMode {
|
|
51
|
+
switch raw?.lowercased() {
|
|
52
|
+
case "automatic", "auto", nil, "":
|
|
53
|
+
return .automatic
|
|
54
|
+
case "manual":
|
|
55
|
+
return .manual
|
|
56
|
+
default:
|
|
57
|
+
configLog.error("iOSPushMode \"\(raw ?? "", privacy: .public)\" not recognised. Allowed values: \"automatic\", \"manual\". Falling back to \"automatic\".")
|
|
58
|
+
return .automatic
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Resolves `PushEnabled` + `iOSPushMode` + `iOSAppGroupIdentifier` from the
|
|
63
|
+
/// bundled config into a `ConnectPushConfig`. Validation errors are logged
|
|
64
|
+
/// at `.error`; soft mismatches at `.warning`. The SDK always falls back to
|
|
65
|
+
/// a safe default (`.off`) so an invalid config never crashes the app.
|
|
66
|
+
static func resolvePushConfig(from connectData: [String: Any]) -> ConnectPushConfig {
|
|
67
|
+
let pushEnabled = parsePushEnabled(connectData["PushEnabled"])
|
|
68
|
+
let iOSPushModeRaw = connectData["iOSPushMode"] as? String
|
|
69
|
+
let groupId = connectData["iOSAppGroupIdentifier"] as? String
|
|
70
|
+
|
|
71
|
+
configLog.info("PushEnabled: \(pushEnabled)")
|
|
72
|
+
|
|
73
|
+
// Inconsistency: iOSPushMode is set but PushEnabled is false. The
|
|
74
|
+
// mode value would never take effect; warn the developer so they
|
|
75
|
+
// know to fix one or the other.
|
|
76
|
+
if !pushEnabled, let raw = iOSPushModeRaw, !raw.isEmpty {
|
|
77
|
+
configLog.error("iOSPushMode \"\(raw, privacy: .public)\" is set but PushEnabled is false. Ignoring iOSPushMode; SDK will run with push disabled.")
|
|
78
|
+
return .off
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
guard pushEnabled else {
|
|
82
|
+
configLog.info("iOSPushMode: (not used, PushEnabled is false)")
|
|
83
|
+
configLog.info("iOSAppGroupIdentifier: (not used, PushEnabled is false)")
|
|
84
|
+
return .off
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let mode = parseIOSPushMode(iOSPushModeRaw)
|
|
88
|
+
configLog.info("iOSPushMode: \(mode.descriptor, privacy: .public)")
|
|
89
|
+
if let groupId = groupId, !groupId.isEmpty {
|
|
90
|
+
configLog.info("iOSAppGroupIdentifier: \(groupId, privacy: .public)")
|
|
91
|
+
} else {
|
|
92
|
+
configLog.warning("iOSAppGroupIdentifier is not set. Required if your app uses a Notification Service or Notification Content extension to render rich push payloads.")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
switch mode {
|
|
96
|
+
case .automatic:
|
|
97
|
+
return ConnectPushConfig(mode: .automatic, appGroupIdentifier: groupId)
|
|
98
|
+
case .manual:
|
|
99
|
+
return ConnectPushConfig(mode: .manual, appGroupIdentifier: groupId)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Note: nsError(from: PushErrorInfo) stays in HybridAcousticConnectRN —
|
|
104
|
+
// PushErrorInfo is a C++-backed nitro type, and this file is also
|
|
105
|
+
// compiled into the UnitTests bundle, which builds without C++ interop.
|
|
106
|
+
|
|
107
|
+
/// Maps the JS-facing module name to the config store's module name
|
|
108
|
+
/// ("Connect" → "TLFCoreModule").
|
|
109
|
+
static func storeModuleName(for moduleName: String) -> String {
|
|
110
|
+
if moduleName.caseInsensitiveCompare("Connect") == .orderedSame {
|
|
111
|
+
return "TLFCoreModule"
|
|
112
|
+
}
|
|
113
|
+
return moduleName
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static func convertToAnyDictionary(input: [String: Variant_Bool_String_Double]) -> [String: Any] {
|
|
117
|
+
var result: [String: Any] = [:]
|
|
118
|
+
|
|
119
|
+
for (key, value) in input {
|
|
120
|
+
switch value {
|
|
121
|
+
case .first(let boolValue):
|
|
122
|
+
result[key] = boolValue
|
|
123
|
+
case .second(let stringValue):
|
|
124
|
+
result[key] = stringValue
|
|
125
|
+
case .third(let doubleValue):
|
|
126
|
+
result[key] = doubleValue
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static func convertVariantToAny(_ variant: Variant_Bool_String_Double) -> Any {
|
|
134
|
+
switch variant {
|
|
135
|
+
case .first(let boolValue):
|
|
136
|
+
return boolValue
|
|
137
|
+
case .second(let stringValue):
|
|
138
|
+
return stringValue
|
|
139
|
+
case .third(let doubleValue):
|
|
140
|
+
return doubleValue
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static func logLevel(from level: Double) -> kConnectMonitoringLevelType {
|
|
145
|
+
let intValue: Int = Int(level)
|
|
146
|
+
if intValue == 0 {
|
|
147
|
+
return kConnectMonitoringLevelType.connectMonitoringLevelIgnore
|
|
148
|
+
} else if intValue == 1 {
|
|
149
|
+
return kConnectMonitoringLevelType.connectMonitoringLevelCellularAndWiFi
|
|
150
|
+
} else {
|
|
151
|
+
return kConnectMonitoringLevelType.connectMonitoringLevelWiFi
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -143,7 +143,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
|
|
|
143
143
|
configLog.info("KillSwitchUrl: \(killSwitch, privacy: .public)")
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
let pushConfig =
|
|
146
|
+
let pushConfig = ConnectRNParsing.resolvePushConfig(from: connectData)
|
|
147
147
|
|
|
148
148
|
// Non-release integrations (useRelease:false — the AcousticConnectDebug
|
|
149
149
|
// SDK variant) turn on the Connect/Tealeaf/EOCore verbose native logging
|
|
@@ -174,84 +174,9 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
|
|
|
174
174
|
bridgeLog.info("SDK initialised: appKey length=\(appKey.count), push=\(pushConfig.modeDescription, privacy: .public), isReactNative=true, wrapNavigationContainer=true")
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
/// a safe default (`.off`) so an invalid config never crashes the app.
|
|
181
|
-
@MainActor
|
|
182
|
-
private func resolvePushConfig(from connectData: [String: Any]) -> ConnectPushConfig {
|
|
183
|
-
let pushEnabled = self.parsePushEnabled(connectData["PushEnabled"])
|
|
184
|
-
let iOSPushModeRaw = connectData["iOSPushMode"] as? String
|
|
185
|
-
let groupId = connectData["iOSAppGroupIdentifier"] as? String
|
|
186
|
-
|
|
187
|
-
configLog.info("PushEnabled: \(pushEnabled)")
|
|
188
|
-
|
|
189
|
-
// Inconsistency: iOSPushMode is set but PushEnabled is false. The
|
|
190
|
-
// mode value would never take effect; warn the developer so they
|
|
191
|
-
// know to fix one or the other.
|
|
192
|
-
if !pushEnabled, let raw = iOSPushModeRaw, !raw.isEmpty {
|
|
193
|
-
configLog.error("iOSPushMode \"\(raw, privacy: .public)\" is set but PushEnabled is false. Ignoring iOSPushMode; SDK will run with push disabled.")
|
|
194
|
-
return .off
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
guard pushEnabled else {
|
|
198
|
-
configLog.info("iOSPushMode: (not used, PushEnabled is false)")
|
|
199
|
-
configLog.info("iOSAppGroupIdentifier: (not used, PushEnabled is false)")
|
|
200
|
-
return .off
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
let mode = self.parseIOSPushMode(iOSPushModeRaw)
|
|
204
|
-
configLog.info("iOSPushMode: \(mode.descriptor, privacy: .public)")
|
|
205
|
-
if let groupId = groupId, !groupId.isEmpty {
|
|
206
|
-
configLog.info("iOSAppGroupIdentifier: \(groupId, privacy: .public)")
|
|
207
|
-
} else {
|
|
208
|
-
configLog.warning("iOSAppGroupIdentifier is not set. Required if your app uses a Notification Service or Notification Content extension to render rich push payloads.")
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
switch mode {
|
|
212
|
-
case .automatic:
|
|
213
|
-
return ConnectPushConfig(mode: .automatic, appGroupIdentifier: groupId)
|
|
214
|
-
case .manual:
|
|
215
|
-
return ConnectPushConfig(mode: .manual, appGroupIdentifier: groupId)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/// Lenient `PushEnabled` parser — accepts a `Bool` directly, or a string
|
|
220
|
-
/// like `"true"`/`"false"`/`"yes"`/`"no"`. Anything else falls back to
|
|
221
|
-
/// `false` with a warning so a typo in the JSON doesn't silently enable
|
|
222
|
-
/// push when the developer thought they'd turned it off.
|
|
223
|
-
private func parsePushEnabled(_ raw: Any?) -> Bool {
|
|
224
|
-
if let b = raw as? Bool { return b }
|
|
225
|
-
if let n = raw as? NSNumber { return n.boolValue }
|
|
226
|
-
if let s = raw as? String {
|
|
227
|
-
switch s.lowercased() {
|
|
228
|
-
case "true", "yes", "1": return true
|
|
229
|
-
case "false", "no", "0", "": return false
|
|
230
|
-
default:
|
|
231
|
-
configLog.warning("PushEnabled string \"\(s, privacy: .public)\" not recognised. Falling back to false.")
|
|
232
|
-
return false
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if raw != nil {
|
|
236
|
-
configLog.warning("PushEnabled is set but is neither a Bool nor a recognised string. Falling back to false.")
|
|
237
|
-
}
|
|
238
|
-
return false
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private enum IOSPushMode { case automatic, manual
|
|
242
|
-
var descriptor: String { self == .automatic ? "automatic" : "manual" } }
|
|
243
|
-
|
|
244
|
-
private func parseIOSPushMode(_ raw: String?) -> IOSPushMode {
|
|
245
|
-
switch raw?.lowercased() {
|
|
246
|
-
case "automatic", "auto", nil, "":
|
|
247
|
-
return .automatic
|
|
248
|
-
case "manual":
|
|
249
|
-
return .manual
|
|
250
|
-
default:
|
|
251
|
-
configLog.error("iOSPushMode \"\(raw ?? "", privacy: .public)\" not recognised. Allowed values: \"automatic\", \"manual\". Falling back to \"automatic\".")
|
|
252
|
-
return .automatic
|
|
253
|
-
}
|
|
254
|
-
}
|
|
177
|
+
// Push-config resolution and the PushEnabled / iOSPushMode parsers moved
|
|
178
|
+
// to ConnectRNParsing (behaviour-preserving extraction) so
|
|
179
|
+
// they are unit-testable without constructing this hybrid.
|
|
255
180
|
|
|
256
181
|
/// Reads and returns the `Connect` dictionary from the bundled
|
|
257
182
|
/// `AcousticConnectRNConfig.json`. The bundle is populated at pod install
|
|
@@ -488,7 +413,10 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
|
|
|
488
413
|
// MARK: - Push: helpers
|
|
489
414
|
|
|
490
415
|
/// Builds an `NSError` from the structured bridge error object, shared by
|
|
491
|
-
/// `pushDidFailToRegister` and `pushDidReceiveAuthorization`.
|
|
416
|
+
/// `pushDidFailToRegister` and `pushDidReceiveAuthorization`. Kept here
|
|
417
|
+
/// (not in ConnectRNParsing) because `PushErrorInfo` is a C++-backed
|
|
418
|
+
/// nitro type and ConnectRNParsing is also compiled into the UnitTests
|
|
419
|
+
/// bundle, which builds without C++ interop.
|
|
492
420
|
private static func nsError(from info: PushErrorInfo) -> NSError {
|
|
493
421
|
NSError(
|
|
494
422
|
domain: info.domain ?? "ConnectRNBridge",
|
|
@@ -816,39 +744,18 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
|
|
|
816
744
|
return true
|
|
817
745
|
}
|
|
818
746
|
|
|
747
|
+
// The bodies below moved to ConnectRNParsing (behaviour-preserving
|
|
748
|
+
// extraction); these thin delegates keep the class API stable.
|
|
819
749
|
func testModuleName(moduleName: String) throws -> String {
|
|
820
|
-
|
|
821
|
-
return "TLFCoreModule"
|
|
822
|
-
}
|
|
823
|
-
return moduleName
|
|
750
|
+
return ConnectRNParsing.storeModuleName(for: moduleName)
|
|
824
751
|
}
|
|
825
|
-
|
|
826
|
-
func convertToAnyDictionary(input: [String: Variant_Bool_String_Double]) -> [String: Any] {
|
|
827
|
-
var result: [String: Any] = [:]
|
|
828
|
-
|
|
829
|
-
for (key, value) in input {
|
|
830
|
-
switch value {
|
|
831
|
-
case .first(let boolValue):
|
|
832
|
-
result[key] = boolValue
|
|
833
|
-
case .second(let stringValue):
|
|
834
|
-
result[key] = stringValue
|
|
835
|
-
case .third(let doubleValue):
|
|
836
|
-
result[key] = doubleValue
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
752
|
|
|
840
|
-
|
|
753
|
+
func convertToAnyDictionary(input: [String: Variant_Bool_String_Double]) -> [String: Any] {
|
|
754
|
+
return ConnectRNParsing.convertToAnyDictionary(input: input)
|
|
841
755
|
}
|
|
842
756
|
|
|
843
757
|
func convertVariantToAny(_ variant: Variant_Bool_String_Double) -> Any {
|
|
844
|
-
|
|
845
|
-
case .first(let boolValue):
|
|
846
|
-
return boolValue
|
|
847
|
-
case .second(let stringValue):
|
|
848
|
-
return stringValue
|
|
849
|
-
case .third(let doubleValue):
|
|
850
|
-
return doubleValue
|
|
851
|
-
}
|
|
758
|
+
return ConnectRNParsing.convertVariantToAny(variant)
|
|
852
759
|
}
|
|
853
760
|
|
|
854
761
|
// func convertKeyValueMapToDictionary(_ keyValueMap: KeyValueMap) -> [AnyHashable: Any] {
|
|
@@ -863,14 +770,7 @@ class HybridAcousticConnectRN: HybridAcousticConnectRNSpec {
|
|
|
863
770
|
// }
|
|
864
771
|
|
|
865
772
|
func getLogLevelHelper(level: Double) throws -> kConnectMonitoringLevelType {
|
|
866
|
-
|
|
867
|
-
if intValue == 0 {
|
|
868
|
-
return kConnectMonitoringLevelType.connectMonitoringLevelIgnore
|
|
869
|
-
} else if intValue == 1 {
|
|
870
|
-
return kConnectMonitoringLevelType.connectMonitoringLevelCellularAndWiFi
|
|
871
|
-
} else {
|
|
872
|
-
return kConnectMonitoringLevelType.connectMonitoringLevelWiFi
|
|
873
|
-
}
|
|
773
|
+
return ConnectRNParsing.logLevel(from: level)
|
|
874
774
|
}
|
|
875
775
|
|
|
876
776
|
func getLogLevel(level: Double) throws -> kConnectMonitoringLevelType {
|
package/package.json
CHANGED
|
@@ -11,13 +11,19 @@
|
|
|
11
11
|
},
|
|
12
12
|
"description": "React native plugin for Acoustic Connect",
|
|
13
13
|
"devDependencies": {
|
|
14
|
+
"@babel/core": "^7.25.2",
|
|
15
|
+
"@babel/runtime": "^7.25.0",
|
|
14
16
|
"@expo/config-plugins": "~55.0.10",
|
|
15
17
|
"@expo/config-types": "^55.0.5",
|
|
16
18
|
"@jamesacarr/eslint-formatter-github-actions": "^0.2.0",
|
|
19
|
+
"@react-native/babel-preset": "0.82.1",
|
|
17
20
|
"@release-it/bumper": "^7.0.1",
|
|
18
21
|
"@release-it/conventional-changelog": "^10.0.0",
|
|
22
|
+
"@testing-library/react-native": "^13.3.3",
|
|
19
23
|
"@types/jest": "^29.5.12",
|
|
20
24
|
"@types/react": "^19.0.10",
|
|
25
|
+
"jest": "^29.7.0",
|
|
26
|
+
"jest-junit": "^16.0.0",
|
|
21
27
|
"jetifier": "^1.6.3",
|
|
22
28
|
"nitrogen": "0.35.9",
|
|
23
29
|
"prettier-plugin-kotlin": "^2.1.0",
|
|
@@ -25,6 +31,7 @@
|
|
|
25
31
|
"react-native": "0.82.1",
|
|
26
32
|
"react-native-builder-bob": "^0.37.0",
|
|
27
33
|
"react-native-nitro-modules": "0.35.9",
|
|
34
|
+
"react-test-renderer": "19.1.1",
|
|
28
35
|
"release-it": "^19.0.2",
|
|
29
36
|
"typescript": "5.0.4"
|
|
30
37
|
},
|
|
@@ -59,6 +66,8 @@
|
|
|
59
66
|
],
|
|
60
67
|
"files": [
|
|
61
68
|
"src",
|
|
69
|
+
"!src/**/__tests__/**",
|
|
70
|
+
"!src/**/*.test.*",
|
|
62
71
|
"react-native.config.js",
|
|
63
72
|
"lib",
|
|
64
73
|
"nitrogen",
|
|
@@ -67,6 +76,7 @@
|
|
|
67
76
|
"android/gradle.properties",
|
|
68
77
|
"android/CMakeLists.txt",
|
|
69
78
|
"android/src",
|
|
79
|
+
"!android/src/test/**",
|
|
70
80
|
"android/config.gradle",
|
|
71
81
|
"android/src/main/assets/**",
|
|
72
82
|
"ios/**/*.h",
|
|
@@ -74,12 +84,14 @@
|
|
|
74
84
|
"ios/**/*.mm",
|
|
75
85
|
"ios/**/*.cpp",
|
|
76
86
|
"ios/**/*.swift",
|
|
87
|
+
"!ios/Tests/**",
|
|
77
88
|
"ios/AcousticConnectRNConfig.json",
|
|
78
89
|
"app.plugin.js",
|
|
79
90
|
"plugin/build",
|
|
80
91
|
"plugin/swift",
|
|
81
92
|
"plugin/src",
|
|
82
93
|
"cli",
|
|
94
|
+
"ConnectConfig.example.json",
|
|
83
95
|
"*.podspec",
|
|
84
96
|
"README.md",
|
|
85
97
|
"CHANGELOG.md",
|
|
@@ -138,7 +150,7 @@
|
|
|
138
150
|
[
|
|
139
151
|
"typescript",
|
|
140
152
|
{
|
|
141
|
-
"project": "tsconfig.json"
|
|
153
|
+
"project": "tsconfig.build.json"
|
|
142
154
|
}
|
|
143
155
|
]
|
|
144
156
|
]
|
|
@@ -205,8 +217,11 @@
|
|
|
205
217
|
"prepack": "npm run build:plugin",
|
|
206
218
|
"rebuild": "npm run codegen && npm run rebuildExample --prefix ./example",
|
|
207
219
|
"release": "release-it",
|
|
208
|
-
"test": "
|
|
220
|
+
"test": "jest",
|
|
221
|
+
"test:all": "npm test && npm run test:plugin && npm run test:cli",
|
|
222
|
+
"test:cli": "node --test cli/__tests__/",
|
|
209
223
|
"test:plugin": "npm run build:plugin && node --test plugin/__tests__/",
|
|
224
|
+
"test:watch": "jest --watch",
|
|
210
225
|
"typecheck": "tsc --noEmit",
|
|
211
226
|
"verifyConnectSetup": "node cli/index.mjs doctor $INIT_CWD",
|
|
212
227
|
"yalcPublish": "npm run rebuild && yalc publish"
|
|
@@ -214,7 +229,7 @@
|
|
|
214
229
|
"source": "src/index",
|
|
215
230
|
"summary": "react-native ios android tealeaf connect cxa wxca er enhanced-replay",
|
|
216
231
|
"types": "./lib/typescript/src/index.d.ts",
|
|
217
|
-
"version": "19.0.
|
|
232
|
+
"version": "19.0.9",
|
|
218
233
|
"workspaces": [
|
|
219
234
|
"example",
|
|
220
235
|
"Examples/bare-workflow"
|
|
@@ -12,8 +12,13 @@
|
|
|
12
12
|
* Used review ConnectConfig.json is present ot will need to add one.
|
|
13
13
|
*/
|
|
14
14
|
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
15
16
|
const filePathBaseConnectConfig = "../../ConnectConfig.json"
|
|
16
|
-
|
|
17
|
+
// Single source of truth for the template — same file cli/doctor.mjs falls
|
|
18
|
+
// back to for bare/Expo consumers. Do not reintroduce a separate copy here.
|
|
19
|
+
// __dirname-relative (not CWD-relative) so this resolves correctly regardless
|
|
20
|
+
// of the working directory the caller runs this script from.
|
|
21
|
+
const filePathTemplateConnectConfig = path.resolve(__dirname, '..', 'ConnectConfig.example.json')
|
|
17
22
|
|
|
18
23
|
console.log("Run reviewConnectConfig.js");
|
|
19
24
|
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"Connect": {
|
|
3
|
-
"AndroidVersion": "11.0.11",
|
|
4
|
-
"AppKey": "b6c3709b7a4c479bb4b5a9fb8fec324c",
|
|
5
|
-
"KillSwitchUrl": "https://lib-us-2.brilliantcollector.com/collector/switch/b6c3709b7a4c479bb4b5a9fb8fec324c",
|
|
6
|
-
"PostMessageUrl": "https://lib-us-2.brilliantcollector.com/collector/collectorPost",
|
|
7
|
-
"iOSVersion": "",
|
|
8
|
-
"layoutConfigAndroid": {
|
|
9
|
-
"AppendMapIds": {
|
|
10
|
-
"[w,9290],[v,0]": {
|
|
11
|
-
"mid": "ASimpleUIView"
|
|
12
|
-
},
|
|
13
|
-
"idxPathValue": {
|
|
14
|
-
"mid": "giveAdditionalId2"
|
|
15
|
-
},
|
|
16
|
-
"tag2999999": {
|
|
17
|
-
"mid": "giveAdditionalId1"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"AutoLayout": {
|
|
21
|
-
"ExampleMaskingPage": {
|
|
22
|
-
"CaptureLayoutDelay": 0,
|
|
23
|
-
"CaptureLayoutOn": 0,
|
|
24
|
-
"CaptureScreenVisits": false,
|
|
25
|
-
"CaptureScreenshotOn": 0,
|
|
26
|
-
"CaptureUserEvents": false,
|
|
27
|
-
"DisplayName": "",
|
|
28
|
-
"Masking": {
|
|
29
|
-
"HasCustomMask": true,
|
|
30
|
-
"HasMasking": true,
|
|
31
|
-
"MaskAccessibilityIdList": [
|
|
32
|
-
|
|
33
|
-
],
|
|
34
|
-
"MaskAccessibilityLabelList": [
|
|
35
|
-
|
|
36
|
-
],
|
|
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
|
-
|
|
70
|
-
],
|
|
71
|
-
"MaskAccessibilityLabelList": [
|
|
72
|
-
|
|
73
|
-
],
|
|
74
|
-
"MaskIdList": [
|
|
75
|
-
|
|
76
|
-
],
|
|
77
|
-
"MaskValueList": [
|
|
78
|
-
|
|
79
|
-
],
|
|
80
|
-
"Sensitive": {
|
|
81
|
-
"capitalCaseAlphabet": "X",
|
|
82
|
-
"number": "9",
|
|
83
|
-
"smallCaseAlphabet": "x",
|
|
84
|
-
"symbol": "#"
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
"NumberOfWebViews": 0,
|
|
88
|
-
"ScreenChange": true,
|
|
89
|
-
"ScreenShot": true
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
"layoutConfigIos": {
|
|
94
|
-
"AppendMapIds": {
|
|
95
|
-
"[w,9290],[v,0]": {
|
|
96
|
-
"mid": "ASimpleUIView"
|
|
97
|
-
},
|
|
98
|
-
"idxPathValue": {
|
|
99
|
-
"mid": "giveAdditionalId2"
|
|
100
|
-
},
|
|
101
|
-
"tag2999999": {
|
|
102
|
-
"mid": "giveAdditionalId1"
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
"AutoLayout": {
|
|
106
|
-
"ExampleMaskingPage": {
|
|
107
|
-
"CaptureLayoutDelay": 0,
|
|
108
|
-
"CaptureLayoutOn": 0,
|
|
109
|
-
"CaptureScreenVisits": false,
|
|
110
|
-
"CaptureScreenshotOn": 0,
|
|
111
|
-
"CaptureUserEvents": false,
|
|
112
|
-
"DisplayName": "",
|
|
113
|
-
"Masking": {
|
|
114
|
-
"HasCustomMask": true,
|
|
115
|
-
"HasMasking": true,
|
|
116
|
-
"MaskAccessibilityIdList": [
|
|
117
|
-
|
|
118
|
-
],
|
|
119
|
-
"MaskAccessibilityLabelList": [
|
|
120
|
-
|
|
121
|
-
],
|
|
122
|
-
"MaskIdList": [
|
|
123
|
-
"^9[0-9][0-9][0-9]$",
|
|
124
|
-
"^\\[wvv,0\\],\\[dddv,0\\],\\[v,0\\],\\[v,0\\],\\[v,0\\],\\[b,0\\](.)*$"
|
|
125
|
-
],
|
|
126
|
-
"MaskValueList": [
|
|
127
|
-
"^4[0-9]{12}(?:[0-9]{3})?$",
|
|
128
|
-
"^3[47][0-9]{13}$",
|
|
129
|
-
"^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})$",
|
|
130
|
-
"^(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}))$"
|
|
131
|
-
],
|
|
132
|
-
"Sensitive": {
|
|
133
|
-
"capitalCaseAlphabet": "X",
|
|
134
|
-
"number": "9",
|
|
135
|
-
"smallCaseAlphabet": "x",
|
|
136
|
-
"symbol": "#"
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
"NumberOfWebViews": 0,
|
|
140
|
-
"ScreenChange": false,
|
|
141
|
-
"ScreenShot": false
|
|
142
|
-
},
|
|
143
|
-
"GlobalScreenSettings": {
|
|
144
|
-
"CaptureLayoutDelay": 1,
|
|
145
|
-
"CaptureLayoutOn": 2,
|
|
146
|
-
"CaptureScreenVisits": false,
|
|
147
|
-
"CaptureScreenshotOn": 2,
|
|
148
|
-
"CaptureUserEvents": true,
|
|
149
|
-
"DisplayName": "",
|
|
150
|
-
"Masking": {
|
|
151
|
-
"HasCustomMask": true,
|
|
152
|
-
"HasMasking": true,
|
|
153
|
-
"MaskAccessibilityIdList": [
|
|
154
|
-
|
|
155
|
-
],
|
|
156
|
-
"MaskAccessibilityLabelList": [
|
|
157
|
-
|
|
158
|
-
],
|
|
159
|
-
"MaskIdList": [
|
|
160
|
-
|
|
161
|
-
],
|
|
162
|
-
"MaskValueList": [
|
|
163
|
-
|
|
164
|
-
],
|
|
165
|
-
"Sensitive": {
|
|
166
|
-
"capitalCaseAlphabet": "X",
|
|
167
|
-
"number": "9",
|
|
168
|
-
"smallCaseAlphabet": "x",
|
|
169
|
-
"symbol": "#"
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
"NumberOfWebViews": 0,
|
|
173
|
-
"ScreenChange": true,
|
|
174
|
-
"ScreenShot": true
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
"useRelease": false
|
|
179
|
-
}
|
|
180
|
-
}
|