react-native-acoustic-connect-beta 18.0.39 → 18.0.40
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 +22 -0
- package/CHANGELOG.md +1 -0
- package/README.md +68 -9
- package/android/src/main/assets/ConnectBasicConfig.properties +1 -1
- package/cli/doctor.mjs +174 -47
- package/cli/index.mjs +20 -8
- package/cli/ios/add_push_extensions.rb +21 -1
- package/cli/ios/setup-push.mjs +5 -0
- package/ios/AcousticConnectRNConfig.json +1 -0
- package/package.json +1 -1
- package/plugin/build/index.d.ts +12 -5
- package/plugin/build/index.d.ts.map +1 -1
- package/plugin/build/index.js +22 -7
- package/plugin/build/index.js.map +1 -1
- package/plugin/build/withConnectAndroidConfig.d.ts +14 -0
- package/plugin/build/withConnectAndroidConfig.d.ts.map +1 -1
- package/plugin/build/withConnectAndroidConfig.js +97 -1
- package/plugin/build/withConnectAndroidConfig.js.map +1 -1
- package/plugin/build/withConnectIosSigning.d.ts +34 -0
- package/plugin/build/withConnectIosSigning.d.ts.map +1 -0
- package/plugin/build/withConnectIosSigning.js +114 -0
- package/plugin/build/withConnectIosSigning.js.map +1 -0
- package/plugin/build/withConnectNSE.d.ts +7 -0
- package/plugin/build/withConnectNSE.d.ts.map +1 -1
- package/plugin/build/withConnectNSE.js.map +1 -1
- package/plugin/src/index.ts +30 -8
- package/plugin/src/withConnectAndroidConfig.ts +87 -1
- package/plugin/src/withConnectIosSigning.ts +107 -0
- package/plugin/src/withConnectNSE.ts +7 -0
|
@@ -51,6 +51,28 @@ iOSVersion = connectConfig["Connect"]["iOSVersion"]
|
|
|
51
51
|
sdkFloor = '>= 2.1.12'
|
|
52
52
|
dependencyRequirements = iOSVersion.to_s.empty? ? [sdkFloor] : [sdkFloor, iOSVersion]
|
|
53
53
|
|
|
54
|
+
# Normalize Connect.PushEnabled to a STRICT boolean before writing the native
|
|
55
|
+
# config bundle. A nil/absent value otherwise serializes as JSON `null`, and the
|
|
56
|
+
# iOS SDK then initializes push-off (ConnectSDK.shared.push == nil) — silently
|
|
57
|
+
# breaking push on the very first build (CA-144135 §7). Coercing here guarantees
|
|
58
|
+
# the bundle never ships `null`; the native lenient parser defends the runtime
|
|
59
|
+
# path, but this prevents shipping a non-boolean value in the first place.
|
|
60
|
+
#
|
|
61
|
+
# This NEVER raises: PushEnabled is the single push gate, so a config that has
|
|
62
|
+
# push infrastructure (e.g. an App Group) but PushEnabled false/absent is a
|
|
63
|
+
# perfectly valid push-off integration and must install cleanly. We only coerce,
|
|
64
|
+
# and warn when the value isn't a canonical boolean so a typo is visible.
|
|
65
|
+
connectSection = (connectConfig["Connect"] ||= {})
|
|
66
|
+
rawPush = connectSection["PushEnabled"]
|
|
67
|
+
pushEnabledBool =
|
|
68
|
+
rawPush == true || (rawPush.is_a?(String) && rawPush.strip.downcase == "true")
|
|
69
|
+
if rawPush.nil?
|
|
70
|
+
puts "WARNING: Connect.PushEnabled was absent/null in ConnectConfig.json — treating it as false in ios/AcousticConnectRNConfig.json. Set \"PushEnabled\": true (boolean) to enable iOS push."
|
|
71
|
+
elsif rawPush != true && rawPush != false
|
|
72
|
+
puts "WARNING: Connect.PushEnabled was #{rawPush.inspect} (not a JSON boolean) in ConnectConfig.json — coercing to #{pushEnabledBool}. Use a boolean true/false."
|
|
73
|
+
end
|
|
74
|
+
connectSection["PushEnabled"] = pushEnabledBool
|
|
75
|
+
|
|
54
76
|
# Write the merged consumer config to the resource-bundle source path AT POD INSTALL TIME.
|
|
55
77
|
# This MUST happen before CocoaPods builds the AcousticConnectRNConfig bundle target,
|
|
56
78
|
# which it does as a dependency of the parent target — too early for any build-time
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
## [18.0.40](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.39...18.0.40) (2026-06-24)
|
|
1
2
|
## [18.0.39](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.38...18.0.39) (2026-06-23)
|
|
2
3
|
## [18.0.38](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.37...18.0.38) (2026-06-22)
|
|
3
4
|
## [18.0.37](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.36...18.0.37) (2026-06-22)
|
package/README.md
CHANGED
|
@@ -119,7 +119,11 @@ npm install react-native-acoustic-connect-beta react-native-nitro-modules
|
|
|
119
119
|
|
|
120
120
|
1. Put `ConnectConfig.json` at the project root — the same file documented in
|
|
121
121
|
[Installation](#installation). For push, set `PushEnabled`, `iOSPushMode`,
|
|
122
|
-
and `
|
|
122
|
+
`iOSAppGroupIdentifier`, and `iOSDevelopmentTeam` (your 10-char Apple Team
|
|
123
|
+
ID) in the `Connect` block. Run `npx acoustic-connect doctor` to validate it
|
|
124
|
+
— when `PushEnabled` is `true`, the doctor **fails** (exits non-zero) on any
|
|
125
|
+
missing push input (collector URLs, App Group, signing team,
|
|
126
|
+
`google-services.json`, app ids); when push is off it needs only `AppKey`.
|
|
123
127
|
|
|
124
128
|
2. Add the plugin to `app.json`:
|
|
125
129
|
|
|
@@ -131,11 +135,11 @@ npm install react-native-acoustic-connect-beta react-native-nitro-modules
|
|
|
131
135
|
}
|
|
132
136
|
```
|
|
133
137
|
|
|
134
|
-
The plugin reads the App Group from `Connect.iOSAppGroupIdentifier`
|
|
135
|
-
`ConnectConfig.json` — the
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
The plugin reads the App Group from `Connect.iOSAppGroupIdentifier` and the
|
|
139
|
+
signing team from `Connect.iOSDevelopmentTeam` in `ConnectConfig.json` — the
|
|
140
|
+
same values the SDK reads at runtime, so the entitlement and the runtime config
|
|
141
|
+
agree by construction. To override either for the native project only (rarely
|
|
142
|
+
needed), pass plugin props; props take precedence over `ConnectConfig.json`:
|
|
139
143
|
|
|
140
144
|
```json
|
|
141
145
|
{
|
|
@@ -143,21 +147,41 @@ takes precedence over `ConnectConfig.json`:
|
|
|
143
147
|
"plugins": [
|
|
144
148
|
[
|
|
145
149
|
"react-native-acoustic-connect-beta",
|
|
146
|
-
{
|
|
150
|
+
{
|
|
151
|
+
"iosAppGroupIdentifier": "group.com.example.myapp",
|
|
152
|
+
"iosDevelopmentTeam": "ABCDE12345"
|
|
153
|
+
}
|
|
147
154
|
]
|
|
148
155
|
]
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
```
|
|
152
159
|
|
|
160
|
+
`iosDevelopmentTeam` is **required for push**: during `expo prebuild` the plugin
|
|
161
|
+
stamps `DEVELOPMENT_TEAM` onto the host app and both push extensions. Without a
|
|
162
|
+
team, a CLI build falls back to ad-hoc signing, which drops the `aps-environment`
|
|
163
|
+
entitlement — so iOS issues no APNs token and push silently fails.
|
|
164
|
+
|
|
153
165
|
### Build
|
|
154
166
|
|
|
155
167
|
```bash
|
|
156
168
|
npx expo prebuild
|
|
157
|
-
npx expo run:ios
|
|
169
|
+
npx expo run:ios -- --extra-params "-allowProvisioningUpdates" # or: eas build --profile development --platform ios
|
|
158
170
|
npx expo run:android # or: eas build --profile development --platform android
|
|
159
171
|
```
|
|
160
172
|
|
|
173
|
+
**iOS push needs provisioning.** Setting `iOSDevelopmentTeam` lets the plugin
|
|
174
|
+
stamp the team, but `xcodebuild` still has to fetch/create the Development
|
|
175
|
+
certificate and provisioning profiles (the host + both extension App IDs, with
|
|
176
|
+
the Push Notifications and App Groups capabilities). Pass
|
|
177
|
+
`-allowProvisioningUpdates` so it does that automatically — it requires your
|
|
178
|
+
Apple ID to be added once in **Xcode → Settings → Accounts**. `eas build`
|
|
179
|
+
manages the whole signing chain itself, so no flag is needed there; CI/headless
|
|
180
|
+
runs can use an App Store Connect API key
|
|
181
|
+
(`-authenticationKeyPath/-authenticationKeyID/-authenticationKeyIssuerID` with
|
|
182
|
+
`-allowProvisioningUpdates`). For the bare workflow, the equivalent is
|
|
183
|
+
`react-native run-ios --extra-params "-allowProvisioningUpdates"`.
|
|
184
|
+
|
|
161
185
|
During `expo prebuild` the plugin automatically:
|
|
162
186
|
|
|
163
187
|
- adds a `ConnectNSE` Notification Service Extension target to the Xcode
|
|
@@ -308,9 +332,15 @@ Stops data capture, flushes pending messages, releases push state. Idempotent.
|
|
|
308
332
|
| --- | --- | --- | --- |
|
|
309
333
|
| `PushEnabled` | boolean | `false` | Cross-platform master switch. On Android, drives `connect-push-fcm` artifact inclusion at build time. |
|
|
310
334
|
| `iOSPushMode` | `"automatic"` / `"manual"` | `"automatic"` | iOS-only. Ignored when `PushEnabled` is `false`. |
|
|
311
|
-
| `iOSAppGroupIdentifier` | string \| null | `null` | iOS App Group shared with NSE / NCE for rich push payloads. |
|
|
335
|
+
| `iOSAppGroupIdentifier` | string \| null | `null` | iOS App Group shared with NSE / NCE for rich push payloads. Required when push is on. |
|
|
336
|
+
| `iOSDevelopmentTeam` | string \| null | `null` | 10-char Apple Team ID used to sign the host + push extensions. Required for iOS push — without it the build drops `aps-environment` and the OS issues no APNs token. |
|
|
312
337
|
| `AndroidNotificationIconResName` | string \| null | `null` | Drawable resource name for the Android notification small icon. |
|
|
313
338
|
|
|
339
|
+
> When `PushEnabled` is `true`, `npx acoustic-connect doctor` treats the
|
|
340
|
+
> push-required inputs above (plus collector URLs, `google-services.json`, and
|
|
341
|
+
> the app ids) as **hard failures** and exits non-zero. When push is off it
|
|
342
|
+
> validates only `AppKey` — a non-push integration needs nothing more.
|
|
343
|
+
|
|
314
344
|
#### iOS push modes
|
|
315
345
|
|
|
316
346
|
- `"automatic"` — the iOS Connect SDK manages APNs token registration internally. The host app only requests user permission via `UNUserNotificationCenter`; token delivery and forwarding to the Connect backend are handled by the SDK.
|
|
@@ -377,6 +407,35 @@ init-time `ClassNotFoundException`), and bypassing means **you take on that
|
|
|
377
407
|
risk** — we cannot guard against breaking changes in arbitrary Nitro patch
|
|
378
408
|
releases. Match the version rather than bypass it.
|
|
379
409
|
|
|
410
|
+
### iOS — push session reaches the collector but no notifications arrive (no APNs token)
|
|
411
|
+
|
|
412
|
+
A CLI build (`expo run:ios` / `react-native run-ios` / `xcodebuild`) doesn't
|
|
413
|
+
auto-pick a signing team the way the Xcode GUI does. With no team, it signs
|
|
414
|
+
ad-hoc and **drops the `aps-environment` entitlement**, so iOS issues no APNs
|
|
415
|
+
token — the app still launches and analytics reach the collector, but no
|
|
416
|
+
`pushRegistration` (with a `mobileToken`) is ever sent. Fix:
|
|
417
|
+
|
|
418
|
+
1. Set `Connect.iOSDevelopmentTeam` (10-char Apple Team ID) in
|
|
419
|
+
`ConnectConfig.json` (or pass the `iosDevelopmentTeam` plugin prop). Re-run
|
|
420
|
+
`expo prebuild` / `npx acoustic-connect setup-ios-push` so the team is
|
|
421
|
+
stamped on the host + extensions.
|
|
422
|
+
2. Build with `-allowProvisioningUpdates` (see [Build](#build)) and your Apple
|
|
423
|
+
ID added in Xcode → Settings → Accounts, so a Development certificate +
|
|
424
|
+
profiles are provisioned.
|
|
425
|
+
|
|
426
|
+
`npx acoustic-connect doctor` fails with this exact remedy when push is on and
|
|
427
|
+
the team is missing (or when no Development certificate is in your keychain).
|
|
428
|
+
|
|
429
|
+
### iOS — Expo Android build fails: `No matching client found for package name …`
|
|
430
|
+
|
|
431
|
+
Your `app.json` `android.package` isn't registered in the active
|
|
432
|
+
`google-services.json` (FCM matches by package). The config plugin and
|
|
433
|
+
`acoustic-connect doctor` now catch this up front. Register that exact package
|
|
434
|
+
in the same Firebase project and re-download `google-services.json` — and note
|
|
435
|
+
that **changing `android.package` requires a clean prebuild**
|
|
436
|
+
(`npx expo prebuild --platform android --clean`); an incremental prebuild keeps
|
|
437
|
+
the stale `applicationId`.
|
|
438
|
+
|
|
380
439
|
### iOS — `pod install` fails with `[Connect] requires AcousticConnect >= 2.0.5`
|
|
381
440
|
|
|
382
441
|
You've pinned an older `iOSVersion` in `ConnectConfig.json`. Bump it to a
|
package/cli/doctor.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import path from 'node:path'
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
Reporter,
|
|
26
|
+
capture,
|
|
26
27
|
color,
|
|
27
28
|
copyIfMissing,
|
|
28
29
|
fileExists,
|
|
@@ -41,6 +42,29 @@ const APP_GROUP_PATTERN = /^group\.[A-Za-z0-9.-]+$/
|
|
|
41
42
|
// still on the placeholder so it isn't shipped by accident.
|
|
42
43
|
const PLACEHOLDER_ID_PREFIX = 'com.example.'
|
|
43
44
|
|
|
45
|
+
// Sentinels shipped in ConnectConfig.example.json — a value still on one of
|
|
46
|
+
// these means the consumer hasn't filled it in.
|
|
47
|
+
const PLACEHOLDER_APP_KEY = 'YOUR_CONNECT_APP_KEY_HERE'
|
|
48
|
+
const PLACEHOLDER_APP_GROUP = 'YOUR_APP_GROUP_ID_HERE'
|
|
49
|
+
const PLACEHOLDER_TEAM = 'YOUR_TEAM_ID'
|
|
50
|
+
// Host of the placeholder collector URLs in the bare sample's example config.
|
|
51
|
+
const PLACEHOLDER_COLLECTOR_HOST = 'collector.example.com'
|
|
52
|
+
// Apple Team IDs are 10 uppercase-alphanumeric characters.
|
|
53
|
+
const TEAM_PATTERN = /^[A-Z0-9]{10}$/
|
|
54
|
+
|
|
55
|
+
// A usable collector endpoint: a parseable https URL whose host isn't the
|
|
56
|
+
// example placeholder. Used for PostMessageUrl / KillSwitchUrl.
|
|
57
|
+
function isValidCollectorUrl(value) {
|
|
58
|
+
if (!value || typeof value !== 'string') return false
|
|
59
|
+
let u
|
|
60
|
+
try {
|
|
61
|
+
u = new URL(value)
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
return u.protocol === 'https:' && u.hostname !== PLACEHOLDER_COLLECTOR_HOST
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
// Java reserved words (+ literals) that cannot appear as a package segment.
|
|
45
69
|
// Expo uses android.package as the Java/Kotlin namespace, so any segment that
|
|
46
70
|
// is a keyword breaks the Gradle build ("not a valid Java package name").
|
|
@@ -91,23 +115,55 @@ function ensureConnectConfig(reporter, dir) {
|
|
|
91
115
|
else reporter.fail('ConnectConfig.json', 'ConnectConfig.example.json missing')
|
|
92
116
|
}
|
|
93
117
|
|
|
94
|
-
// Validate the
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
118
|
+
// Validate the ConnectConfig.json values. The push-required inputs (collector
|
|
119
|
+
// URLs, App Group, iOS signing team) are only HARD failures when push is on —
|
|
120
|
+
// or when `--require-push` forces it. A client that doesn't use push needs
|
|
121
|
+
// nothing beyond AppKey, so off-push these stay advisory and never block.
|
|
122
|
+
//
|
|
123
|
+
// A bad value otherwise fails the Config Plugin during `expo prebuild` / the
|
|
124
|
+
// iOS extensions at build time, not here, so catch it early. Returns
|
|
125
|
+
// { appGroup, pushEnabled, strict } for downstream checks.
|
|
126
|
+
function checkConnectConfigValues(reporter, dir, {requirePush} = {}) {
|
|
98
127
|
const cfg = readJson(path.join(dir, 'ConnectConfig.json'))
|
|
99
128
|
if (cfg === undefined) {
|
|
100
129
|
// Missing/failed was already reported by ensureConnectConfig; only flag a
|
|
101
130
|
// present-but-broken file here.
|
|
102
131
|
if (fileExists(path.join(dir, 'ConnectConfig.json')))
|
|
103
132
|
reporter.fail('ConnectConfig.json', 'present but not valid JSON')
|
|
104
|
-
return {}
|
|
133
|
+
return {strict: requirePush === true}
|
|
105
134
|
}
|
|
106
135
|
const connect = (cfg && cfg.Connect) || {}
|
|
107
136
|
const appGroup = connect.iOSAppGroupIdentifier
|
|
108
137
|
const pushEnabled = connect.PushEnabled === true
|
|
138
|
+
// Push-required inputs are strict iff push is on (or forced via flag).
|
|
139
|
+
const strict = pushEnabled || requirePush === true
|
|
140
|
+
const gate = (label, detail) =>
|
|
141
|
+
strict ? reporter.fail(label, detail) : reporter.warn(label, detail)
|
|
142
|
+
|
|
143
|
+
// AppKey — the one value the SDK always needs to reach the collector. Under
|
|
144
|
+
// push it's a hard gate; off push it stays advisory (a non-push client may
|
|
145
|
+
// simply not have wired it yet — but it still needs it to send anything).
|
|
146
|
+
const appKey = connect.AppKey
|
|
147
|
+
if (appKey && appKey !== PLACEHOLDER_APP_KEY) reporter.pass('AppKey', 'set')
|
|
148
|
+
else
|
|
149
|
+
gate(
|
|
150
|
+
'AppKey',
|
|
151
|
+
`not set (empty or still "${PLACEHOLDER_APP_KEY}") — required to send to the collector`,
|
|
152
|
+
)
|
|
109
153
|
|
|
110
|
-
|
|
154
|
+
// Collector URLs — the push registration (and every event) posts here. The
|
|
155
|
+
// example ships unfilled placeholders, so reject those under push.
|
|
156
|
+
for (const key of ['PostMessageUrl', 'KillSwitchUrl']) {
|
|
157
|
+
if (isValidCollectorUrl(connect[key])) reporter.pass(key, 'set')
|
|
158
|
+
else
|
|
159
|
+
gate(
|
|
160
|
+
key,
|
|
161
|
+
`"${connect[key] ?? ''}" is not a valid https collector URL (host must not be ${PLACEHOLDER_COLLECTOR_HOST})`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// App Group — the shared store the iOS NSE/NCE rich-push extensions read/write.
|
|
166
|
+
if (appGroup && appGroup !== PLACEHOLDER_APP_GROUP) {
|
|
111
167
|
if (APP_GROUP_PATTERN.test(appGroup))
|
|
112
168
|
reporter.pass('iOSAppGroupIdentifier', appGroup)
|
|
113
169
|
else
|
|
@@ -115,13 +171,29 @@ function checkConnectConfigValues(reporter, dir) {
|
|
|
115
171
|
'iOSAppGroupIdentifier',
|
|
116
172
|
`"${appGroup}" is invalid — must match group.<reverse-dns> (letters/digits/dots/hyphens)`,
|
|
117
173
|
)
|
|
118
|
-
} else
|
|
119
|
-
|
|
174
|
+
} else {
|
|
175
|
+
gate(
|
|
120
176
|
'iOSAppGroupIdentifier',
|
|
121
|
-
|
|
177
|
+
appGroup
|
|
178
|
+
? `still the placeholder "${appGroup}" — set your own App Group`
|
|
179
|
+
: 'not set — required for the iOS NSE/NCE rich-push extensions',
|
|
122
180
|
)
|
|
123
181
|
}
|
|
124
|
-
|
|
182
|
+
|
|
183
|
+
// iOS signing team — without it a CLI build drops aps-environment to ad-hoc
|
|
184
|
+
// signing and the OS issues no APNs token (silent on the Simulator).
|
|
185
|
+
const team = connect.iOSDevelopmentTeam
|
|
186
|
+
if (team && team !== PLACEHOLDER_TEAM && TEAM_PATTERN.test(team))
|
|
187
|
+
reporter.pass('iOSDevelopmentTeam', team)
|
|
188
|
+
else
|
|
189
|
+
gate(
|
|
190
|
+
'iOSDevelopmentTeam',
|
|
191
|
+
team && team !== PLACEHOLDER_TEAM
|
|
192
|
+
? `"${team}" is not a 10-char Apple Team ID`
|
|
193
|
+
: 'not set — required so the iOS build embeds aps-environment (no APNs token without it)',
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return {appGroup, pushEnabled, strict}
|
|
125
197
|
}
|
|
126
198
|
|
|
127
199
|
function validateAndroidPackage(reporter, label, pkg) {
|
|
@@ -138,8 +210,12 @@ function validateAndroidPackage(reporter, label, pkg) {
|
|
|
138
210
|
else reporter.pass(label, pkg)
|
|
139
211
|
}
|
|
140
212
|
|
|
141
|
-
// Expo: identifiers live in app.json's expo block.
|
|
142
|
-
|
|
213
|
+
// Expo: identifiers live in app.json's expo block. Under push (`strict`) the
|
|
214
|
+
// ids must be set and off the sample placeholder, because android.package must
|
|
215
|
+
// match a client in google-services.json (Gradle fails otherwise) and
|
|
216
|
+
// ios.bundleIdentifier drives APNs. Off push these stay advisory. Returns the
|
|
217
|
+
// resolved android.package and the configured googleServicesFile (if any).
|
|
218
|
+
function checkAppJson(reporter, dir, {strict} = {}) {
|
|
143
219
|
const appJson = readJson(path.join(dir, 'app.json'))
|
|
144
220
|
if (!appJson || !appJson.expo) {
|
|
145
221
|
reporter.fail('app.json', 'missing or has no "expo" block')
|
|
@@ -148,24 +224,68 @@ function checkAppJson(reporter, dir) {
|
|
|
148
224
|
const expo = appJson.expo
|
|
149
225
|
const androidPackage = expo.android && expo.android.package
|
|
150
226
|
const iosBundleId = expo.ios && expo.ios.bundleIdentifier
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
227
|
+
const gsFile = expo.android && expo.android.googleServicesFile
|
|
228
|
+
const gate = (label, detail) =>
|
|
229
|
+
strict ? reporter.fail(label, detail) : reporter.warn(label, detail)
|
|
230
|
+
|
|
231
|
+
// android.package: must be set + non-placeholder under push; always
|
|
232
|
+
// keyword-checked when present (a Java keyword segment breaks the build).
|
|
233
|
+
if (!androidPackage)
|
|
234
|
+
gate('android.package', 'not set in app.json — required for the Android build / FCM')
|
|
235
|
+
else if (androidPackage.startsWith(PLACEHOLDER_ID_PREFIX))
|
|
236
|
+
gate(
|
|
237
|
+
'android.package',
|
|
238
|
+
`still the sample placeholder "${androidPackage}" — set your own (must match a client in google-services.json)`,
|
|
239
|
+
)
|
|
240
|
+
else validateAndroidPackage(reporter, 'android.package', androidPackage)
|
|
241
|
+
|
|
242
|
+
// ios.bundleIdentifier: must be set + non-placeholder under push (drives APNs).
|
|
243
|
+
if (!iosBundleId) gate('ios.bundleIdentifier', 'not set in app.json')
|
|
244
|
+
else if (iosBundleId.startsWith(PLACEHOLDER_ID_PREFIX))
|
|
245
|
+
gate(
|
|
246
|
+
'ios.bundleIdentifier',
|
|
247
|
+
`still the sample placeholder "${iosBundleId}" — set your own in app.json before building`,
|
|
248
|
+
)
|
|
249
|
+
else reporter.pass('ios.bundleIdentifier', iosBundleId)
|
|
250
|
+
|
|
251
|
+
return {androidPackage, iosBundleId, gsFile}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Expo only: an incremental `expo prebuild` does NOT propagate an app.json
|
|
255
|
+
// android.package change into an already-generated android/app/build.gradle
|
|
256
|
+
// (prebuild is non-destructive). Catch the stale applicationId so the developer
|
|
257
|
+
// doesn't hit a confusing google-services mismatch on a config they "already
|
|
258
|
+
// fixed".
|
|
259
|
+
function checkExpoCleanPrebuild(reporter, dir, {androidPackage, strict} = {}) {
|
|
260
|
+
if (!androidPackage) return
|
|
261
|
+
const gradle = readText(path.join(dir, 'android', 'app', 'build.gradle'))
|
|
262
|
+
if (!gradle) return // not prebuilt yet — nothing stale to catch
|
|
263
|
+
const appId = gradle.match(/applicationId\s+["']([^"']+)["']/)?.[1]
|
|
264
|
+
if (appId && appId !== androidPackage) {
|
|
265
|
+
const detail = `generated applicationId "${appId}" ≠ app.json android.package "${androidPackage}" — run \`npx expo prebuild --platform android --clean\` to regenerate`
|
|
266
|
+
if (strict) reporter.fail('android/ (stale prebuild)', detail)
|
|
267
|
+
else reporter.warn('android/ (stale prebuild)', detail)
|
|
167
268
|
}
|
|
168
|
-
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// iOS only, macOS only, best-effort hint — NEVER a hard failure. A *local dev*
|
|
272
|
+
// CLI build with no Development certificate falls back to ad-hoc signing, which
|
|
273
|
+
// drops aps-environment → no APNs token. But a CI/release machine that signs
|
|
274
|
+
// with an Apple Distribution certificate (and has no Development cert) is a
|
|
275
|
+
// perfectly valid configuration, so blocking it would be wrong — this stays a
|
|
276
|
+
// warning either way. Skips silently when `security` is unavailable (non-mac /
|
|
277
|
+
// locked keychain).
|
|
278
|
+
function checkIosSigningIdentity(reporter) {
|
|
279
|
+
if (process.platform !== 'darwin') return
|
|
280
|
+
const {ok, stdout} = capture('security find-identity -p codesigning -v')
|
|
281
|
+
if (!ok) return
|
|
282
|
+
if (/Apple Development|iPhone Developer/.test(stdout))
|
|
283
|
+
reporter.pass('iOS signing identity', 'Apple Development certificate present')
|
|
284
|
+
else
|
|
285
|
+
reporter.warn(
|
|
286
|
+
'iOS signing identity',
|
|
287
|
+
'no Apple Development certificate in the keychain — fine for CI/release (Distribution signing), but a local dev push build needs one, else ad-hoc signing drops aps-environment (no APNs token). Add your Apple ID in Xcode → Accounts and build once with -allowProvisioningUpdates.',
|
|
288
|
+
)
|
|
169
289
|
}
|
|
170
290
|
|
|
171
291
|
// Bare: read identifiers from android/app/build.gradle. Two distinct fields:
|
|
@@ -190,10 +310,10 @@ function checkBareAndroidId(reporter, dir) {
|
|
|
190
310
|
// google-services.json must contain a client whose package_name matches the
|
|
191
311
|
// Android package (FCM matches by package); the gradle plugin fails otherwise
|
|
192
312
|
// ("No matching client found for package name").
|
|
193
|
-
function checkGoogleServices(reporter, {gsPath, androidPackage,
|
|
313
|
+
function checkGoogleServices(reporter, {gsPath, androidPackage, strict}) {
|
|
194
314
|
if (!fileExists(gsPath)) {
|
|
195
|
-
if (
|
|
196
|
-
reporter.
|
|
315
|
+
if (strict)
|
|
316
|
+
reporter.fail(
|
|
197
317
|
'google-services.json',
|
|
198
318
|
'missing — required for Android FCM builds (add the Firebase Android app, then download it here)',
|
|
199
319
|
)
|
|
@@ -211,12 +331,11 @@ function checkGoogleServices(reporter, {gsPath, androidPackage, pushEnabled}) {
|
|
|
211
331
|
const packages = (gs.client || [])
|
|
212
332
|
.map((c) => c.client_info?.android_client_info?.package_name)
|
|
213
333
|
.filter(Boolean)
|
|
214
|
-
if (isPlaceholder)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
else if (androidPackage && !packages.includes(androidPackage))
|
|
334
|
+
if (isPlaceholder) {
|
|
335
|
+
const detail = `placeholder values — replace with your real Firebase config (register package '${androidPackage}')`
|
|
336
|
+
if (strict) reporter.fail('google-services.json', detail)
|
|
337
|
+
else reporter.warn('google-services.json', detail)
|
|
338
|
+
} else if (androidPackage && !packages.includes(androidPackage))
|
|
220
339
|
reporter.fail(
|
|
221
340
|
'google-services.json',
|
|
222
341
|
`no client matches '${androidPackage}' (has: ${packages.join(', ') || 'none'}). Register that package in Firebase and re-download.`,
|
|
@@ -271,7 +390,8 @@ function checkBareIosEntitlements(reporter, dir) {
|
|
|
271
390
|
|
|
272
391
|
// Run all relevant checks for `dir`. Returns the Reporter (caller decides how
|
|
273
392
|
// to print the summary / exit).
|
|
274
|
-
export function runDoctor(dir, {reporter = new Reporter()} = {}) {
|
|
393
|
+
export function runDoctor(dir, {reporter = new Reporter(), flags = {}} = {}) {
|
|
394
|
+
const requirePush = flags.requirePush === true
|
|
275
395
|
const type = detectProjectType(dir)
|
|
276
396
|
console.log(
|
|
277
397
|
color.bold('\nAcoustic Connect doctor') +
|
|
@@ -283,12 +403,15 @@ export function runDoctor(dir, {reporter = new Reporter()} = {}) {
|
|
|
283
403
|
|
|
284
404
|
section('Configuration')
|
|
285
405
|
ensureConnectConfig(reporter, dir)
|
|
286
|
-
const {
|
|
406
|
+
const {strict = requirePush} = checkConnectConfigValues(reporter, dir, {
|
|
407
|
+
requirePush,
|
|
408
|
+
})
|
|
287
409
|
|
|
288
410
|
section('Identifiers')
|
|
289
411
|
let androidPackage
|
|
412
|
+
let gsFile
|
|
290
413
|
if (type === 'expo') {
|
|
291
|
-
;({androidPackage} = checkAppJson(reporter, dir))
|
|
414
|
+
;({androidPackage, gsFile} = checkAppJson(reporter, dir, {strict}))
|
|
292
415
|
} else if (type === 'bare') {
|
|
293
416
|
;({androidPackage} = checkBareAndroidId(reporter, dir))
|
|
294
417
|
} else {
|
|
@@ -301,14 +424,18 @@ export function runDoctor(dir, {reporter = new Reporter()} = {}) {
|
|
|
301
424
|
section('Android push (FCM)')
|
|
302
425
|
const gsPath =
|
|
303
426
|
type === 'expo'
|
|
304
|
-
? path.join(dir, 'google-services.json')
|
|
427
|
+
? path.join(dir, gsFile || 'google-services.json')
|
|
305
428
|
: path.join(dir, 'android', 'app', 'google-services.json')
|
|
306
|
-
checkGoogleServices(reporter, {gsPath, androidPackage,
|
|
429
|
+
checkGoogleServices(reporter, {gsPath, androidPackage, strict})
|
|
430
|
+
if (type === 'expo') checkExpoCleanPrebuild(reporter, dir, {androidPackage, strict})
|
|
307
431
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
432
|
+
section('iOS push')
|
|
433
|
+
if (type === 'bare') checkBareIosEntitlements(reporter, dir)
|
|
434
|
+
// Signing-identity hint is only relevant to push (it's about the
|
|
435
|
+
// aps-environment drop). Skip it for non-push projects to avoid noise; it's
|
|
436
|
+
// always a warning when it does run, so it never blocks a Distribution-only
|
|
437
|
+
// CI/release machine.
|
|
438
|
+
if (strict) checkIosSigningIdentity(reporter)
|
|
312
439
|
|
|
313
440
|
return reporter
|
|
314
441
|
}
|
package/cli/index.mjs
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
// Ships in the published package so client apps don't have to re-implement the
|
|
5
5
|
// integration plumbing our own sample apps used to carry. Subcommands:
|
|
6
6
|
//
|
|
7
|
-
// acoustic-connect doctor [dir]
|
|
8
|
-
// acoustic-connect setup-ios-push [dir]
|
|
7
|
+
// acoustic-connect doctor [dir] [--require-push] Validate config + scaffold ConnectConfig.json
|
|
8
|
+
// acoustic-connect setup-ios-push [dir] Create/repair the iOS NSE + NCE push extensions
|
|
9
9
|
//
|
|
10
10
|
// `dir` defaults to the current working directory (npm passes $INIT_CWD when
|
|
11
11
|
// run as a script). All commands are idempotent and safe to re-run.
|
|
12
|
+
//
|
|
13
|
+
// `doctor` treats the push-required inputs (collector URLs, App Group, iOS
|
|
14
|
+
// signing team, google-services, Expo ids) as HARD failures only when push is
|
|
15
|
+
// enabled (Connect.PushEnabled === true) or `--require-push` is passed. A
|
|
16
|
+
// non-push client needs nothing beyond AppKey and never trips these.
|
|
12
17
|
|
|
13
18
|
import path from 'node:path'
|
|
14
19
|
|
|
@@ -19,10 +24,13 @@ import {Reporter, color} from './lib.mjs'
|
|
|
19
24
|
const USAGE = `acoustic-connect — React Native Acoustic Connect SDK setup
|
|
20
25
|
|
|
21
26
|
Usage:
|
|
22
|
-
acoustic-connect doctor [dir]
|
|
23
|
-
acoustic-connect setup-ios-push [dir]
|
|
27
|
+
acoustic-connect doctor [dir] [--require-push] Validate config + scaffold ConnectConfig.json
|
|
28
|
+
acoustic-connect setup-ios-push [dir] Create/repair the iOS NSE + NCE push extensions
|
|
29
|
+
|
|
30
|
+
dir defaults to the current directory.
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
--require-push Treat the push-required inputs as hard failures even when
|
|
33
|
+
Connect.PushEnabled is not true (push is otherwise the gate).`
|
|
26
34
|
|
|
27
35
|
// Resolve the target directory: explicit arg > $INIT_CWD (npm) > cwd.
|
|
28
36
|
function targetDir(arg) {
|
|
@@ -32,18 +40,22 @@ function targetDir(arg) {
|
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
async function main() {
|
|
35
|
-
const
|
|
43
|
+
const args = process.argv.slice(2)
|
|
44
|
+
const command = args[0]
|
|
36
45
|
|
|
37
46
|
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
38
47
|
console.log(USAGE)
|
|
39
48
|
process.exit(command ? 0 : 1)
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
// First non-flag argument after the command is the target dir.
|
|
52
|
+
const dirArg = args.slice(1).find((a) => !a.startsWith('-'))
|
|
53
|
+
const dir = targetDir(dirArg)
|
|
54
|
+
const requirePush = args.includes('--require-push')
|
|
43
55
|
|
|
44
56
|
switch (command) {
|
|
45
57
|
case 'doctor': {
|
|
46
|
-
const reporter = runDoctor(dir)
|
|
58
|
+
const reporter = runDoctor(dir, {flags: {requirePush}})
|
|
47
59
|
process.exit(reporter.summary())
|
|
48
60
|
}
|
|
49
61
|
case 'setup-ios-push': {
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
# PRODUCT_BUNDLE_IDENTIFIER)
|
|
30
30
|
# ACOUSTIC_DEPLOYMENT_TARGET iOS deployment target (default 15.1)
|
|
31
31
|
# ACOUSTIC_SWIFT_VERSION Swift version (default 5.0)
|
|
32
|
+
# ACOUSTIC_DEVELOPMENT_TEAM Apple Team ID (10-char) (optional —
|
|
33
|
+
# when set, stamped onto the host + both extension
|
|
34
|
+
# targets so all three sign consistently. Without a
|
|
35
|
+
# team a CLI build falls back to ad-hoc signing,
|
|
36
|
+
# which drops aps-environment → no APNs token.)
|
|
32
37
|
#
|
|
33
38
|
# Requires the `xcodeproj` gem (ships with CocoaPods).
|
|
34
39
|
|
|
@@ -45,6 +50,9 @@ PROJECT_PATH = env!('ACOUSTIC_PROJECT_PATH')
|
|
|
45
50
|
APP_TARGET_NAME = env!('ACOUSTIC_APP_TARGET')
|
|
46
51
|
DEPLOYMENT_TARGET = ENV.fetch('ACOUSTIC_DEPLOYMENT_TARGET', '15.1')
|
|
47
52
|
SWIFT_VERSION = ENV.fetch('ACOUSTIC_SWIFT_VERSION', '5.0')
|
|
53
|
+
# Optional — only stamped when provided (and non-empty / not the placeholder).
|
|
54
|
+
DEVELOPMENT_TEAM = ENV['ACOUSTIC_DEVELOPMENT_TEAM'].to_s.strip
|
|
55
|
+
TEAM_SET = !DEVELOPMENT_TEAM.empty? && DEVELOPMENT_TEAM != 'YOUR_TEAM_ID'
|
|
48
56
|
|
|
49
57
|
EXTENSIONS = [
|
|
50
58
|
{
|
|
@@ -91,10 +99,12 @@ APP_BUNDLE_ID = resolve_bundle_id(app_target)
|
|
|
91
99
|
raise 'Could not determine the host bundle id; pass ACOUSTIC_APP_BUNDLE_ID' unless APP_BUNDLE_ID
|
|
92
100
|
|
|
93
101
|
# 1. Wire the host app's entitlements (App Group + aps-environment) onto both
|
|
94
|
-
# build configurations. Safe to re-run.
|
|
102
|
+
# build configurations. Safe to re-run. Stamp the signing team too (when
|
|
103
|
+
# provided) so the host doesn't fall back to ad-hoc signing.
|
|
95
104
|
app_target.build_configurations.each do |config|
|
|
96
105
|
config.build_settings['CODE_SIGN_ENTITLEMENTS'] =
|
|
97
106
|
"#{APP_TARGET_NAME}/#{APP_TARGET_NAME}.entitlements"
|
|
107
|
+
config.build_settings['DEVELOPMENT_TEAM'] = DEVELOPMENT_TEAM if TEAM_SET
|
|
98
108
|
end
|
|
99
109
|
|
|
100
110
|
# Make the host entitlements file visible in the project navigator.
|
|
@@ -186,6 +196,16 @@ EXTENSIONS.each do |ext|
|
|
|
186
196
|
puts "#{ext[:name]}: linked #{fw}.framework."
|
|
187
197
|
end
|
|
188
198
|
end
|
|
199
|
+
|
|
200
|
+
# Stamp the signing team (when provided) on the extension's build configs —
|
|
201
|
+
# runs over new AND pre-existing targets so all three (host + NSE + NCE) sign
|
|
202
|
+
# consistently. Idempotent.
|
|
203
|
+
next unless TEAM_SET
|
|
204
|
+
|
|
205
|
+
target.build_configurations.each do |config|
|
|
206
|
+
config.build_settings['DEVELOPMENT_TEAM'] = DEVELOPMENT_TEAM
|
|
207
|
+
end
|
|
208
|
+
puts "#{ext[:name]}: DEVELOPMENT_TEAM set to #{DEVELOPMENT_TEAM}."
|
|
189
209
|
end
|
|
190
210
|
|
|
191
211
|
project.save
|
package/cli/ios/setup-push.mjs
CHANGED
|
@@ -182,10 +182,15 @@ export async function setupIosPush(dir, {reporter}) {
|
|
|
182
182
|
// Pass the project path / target name as real environment variables (the
|
|
183
183
|
// Ruby script reads them via ENV[…]) rather than interpolating them into the
|
|
184
184
|
// command line — a path with shell metacharacters must not be re-parsed.
|
|
185
|
+
// The signing team (when configured) is forwarded so the scaffolder stamps
|
|
186
|
+
// DEVELOPMENT_TEAM on the host + both extensions; without it a CLI build
|
|
187
|
+
// drops aps-environment to ad-hoc and yields no APNs token.
|
|
188
|
+
const team = cfg?.Connect?.iOSDevelopmentTeam
|
|
185
189
|
const rubyEnv = {
|
|
186
190
|
ACOUSTIC_PROJECT_PATH: projectPath,
|
|
187
191
|
ACOUSTIC_APP_TARGET: appTarget,
|
|
188
192
|
}
|
|
193
|
+
if (team) rubyEnv.ACOUSTIC_DEVELOPMENT_TEAM = team
|
|
189
194
|
if (run(`ruby "${rubyScript}"`, {cwd: iosDir, env: rubyEnv}))
|
|
190
195
|
reporter.pass('Push extensions wired (NSE + NCE)')
|
|
191
196
|
else
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"PushEnabled": true,
|
|
8
8
|
"iOSPushMode": "automatic",
|
|
9
9
|
"iOSAppGroupIdentifier": "group.co.acoustic.mobile.connect.rn.expodemo",
|
|
10
|
+
"iOSDevelopmentTeam": "YOUR_TEAM_ID",
|
|
10
11
|
"AndroidNotificationIconResName": "ic_notification",
|
|
11
12
|
"iOSVersion": "",
|
|
12
13
|
"useRelease": false
|
package/package.json
CHANGED
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"source": "src/index",
|
|
215
215
|
"summary": "react-native ios android tealeaf connect cxa wxca er enhanced-replay",
|
|
216
216
|
"types": "./lib/typescript/src/index.d.ts",
|
|
217
|
-
"version": "18.0.
|
|
217
|
+
"version": "18.0.40",
|
|
218
218
|
"workspaces": [
|
|
219
219
|
"example",
|
|
220
220
|
"Examples/bare-workflow"
|