react-native-acoustic-connect-beta 18.0.39 → 18.0.41

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.
@@ -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,5 @@
1
+ ## [18.0.41](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.40...18.0.41) (2026-06-25)
2
+ ## [18.0.40](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.39...18.0.40) (2026-06-24)
1
3
  ## [18.0.39](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.38...18.0.39) (2026-06-23)
2
4
  ## [18.0.38](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.37...18.0.38) (2026-06-22)
3
5
  ## [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 `iOSAppGroupIdentifier` in the `Connect` block.
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` in
135
- `ConnectConfig.json` — the same value the SDK reads at runtime, so the
136
- entitlement and the runtime config agree by construction. To use a different
137
- value for the entitlement only (rarely needed), pass a plugin prop; the prop
138
- takes precedence over `ConnectConfig.json`:
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
- { "iosAppGroupIdentifier": "group.com.example.myapp" }
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 # or: eas build --profile development --platform 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
@@ -1,4 +1,4 @@
1
- #Tue Jun 23 01:25:25 PDT 2026
1
+ #Thu Jun 25 05:42:26 PDT 2026
2
2
  UseWhiteList=true
3
3
  PrintScreen=3
4
4
  UseRandomSample=false
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 App Group identifier (when set). A bad value fails the Config
95
- // Plugin during `expo prebuild` / the iOS extensions at build time, not here,
96
- // so catch it early. Returns { appGroup, pushEnabled } for downstream checks.
97
- function checkConnectConfigValues(reporter, dir) {
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
- if (appGroup) {
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 if (pushEnabled) {
119
- reporter.warn(
174
+ } else {
175
+ gate(
120
176
  'iOSAppGroupIdentifier',
121
- 'PushEnabled is true but no App Group set — required for the NSE/NCE rich-push extensions',
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
- return {appGroup, pushEnabled}
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
- function checkAppJson(reporter, dir) {
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
- validateAndroidPackage(reporter, 'android.package', androidPackage)
152
- if (iosBundleId) reporter.pass('ios.bundleIdentifier', iosBundleId)
153
- else reporter.warn('ios.bundleIdentifier', 'not set in app.json')
154
-
155
- // The sample ships placeholder ids; warn if they haven't been replaced. We
156
- // only flag setting them is the standard Expo step, owned by the developer
157
- // (or injected by the test-automation pipeline), not rewritten by the SDK.
158
- for (const [label, value] of [
159
- ['ios.bundleIdentifier', iosBundleId],
160
- ['android.package', androidPackage],
161
- ]) {
162
- if (value && value.startsWith(PLACEHOLDER_ID_PREFIX))
163
- reporter.warn(
164
- label,
165
- `still the sample placeholder "${value}" — set your own in app.json before building (standard Expo step)`,
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
- return {androidPackage, iosBundleId}
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, pushEnabled}) {
313
+ function checkGoogleServices(reporter, {gsPath, androidPackage, strict}) {
194
314
  if (!fileExists(gsPath)) {
195
- if (pushEnabled)
196
- reporter.warn(
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
- reporter.warn(
216
- 'google-services.json',
217
- `placeholder values — replace with your real Firebase config (register package '${androidPackage}')`,
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 {pushEnabled} = checkConnectConfigValues(reporter, dir)
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, pushEnabled})
429
+ checkGoogleServices(reporter, {gsPath, androidPackage, strict})
430
+ if (type === 'expo') checkExpoCleanPrebuild(reporter, dir, {androidPackage, strict})
307
431
 
308
- if (type === 'bare') {
309
- section('iOS push')
310
- checkBareIosEntitlements(reporter, dir)
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] Validate config + scaffold ConnectConfig.json
8
- // acoustic-connect setup-ios-push [dir] Create/repair the iOS NSE + NCE push extensions
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] Validate config + scaffold ConnectConfig.json
23
- acoustic-connect setup-ios-push [dir] Create/repair the iOS NSE + NCE push extensions
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
- dir defaults to the current directory.`
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 [command, maybeDir] = process.argv.slice(2)
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
- const dir = targetDir(maybeDir)
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
@@ -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.39",
217
+ "version": "18.0.41",
218
218
  "workspaces": [
219
219
  "example",
220
220
  "Examples/bare-workflow"