react-native-acoustic-connect-beta 18.0.37 → 18.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/android/src/main/assets/ConnectBasicConfig.properties +1 -1
- package/cli/doctor.mjs +294 -0
- package/cli/index.mjs +64 -0
- package/cli/ios/add_push_extensions.rb +192 -0
- package/cli/ios/connect_pods.rb +48 -0
- package/cli/ios/setup-push.mjs +196 -0
- package/cli/ios/templates/nce/ConnectNCE.entitlements +10 -0
- package/cli/ios/templates/nce/Info.plist +45 -0
- package/cli/ios/templates/nse/ConnectNSE.entitlements +10 -0
- package/cli/ios/templates/nse/Info.plist +33 -0
- package/cli/lib.mjs +240 -0
- package/package.json +7 -3
- package/scripts/postinstall.mjs +35 -0
- package/scripts/xmlparser.js +1 -1
- package/scripts/postInstallScripts.sh +0 -10
- package/scripts/verifyConnectSetup.js +0 -87
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
## [18.0.38](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.37...18.0.38) (2026-06-22)
|
|
1
2
|
## [18.0.37](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.36...18.0.37) (2026-06-22)
|
|
2
3
|
## [18.0.35](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.34...18.0.35) (2026-06-15)
|
|
3
4
|
## [18.0.34](https://github.com/aipoweredmarketer/react-native-acoustic-connect-beta/compare/18.0.33...18.0.34) (2026-06-15)
|
package/cli/doctor.mjs
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// `acoustic-connect doctor [dir]` — pre-build doctor + config scaffolder.
|
|
2
|
+
//
|
|
3
|
+
// One command that checks the prerequisites and validates the bits of Acoustic
|
|
4
|
+
// Connect integration that otherwise fail late and confusingly at build time:
|
|
5
|
+
// the App Group format, a Java-safe Android package, and the
|
|
6
|
+
// google-services.json package match. It also scaffolds the gitignored
|
|
7
|
+
// ConnectConfig.json from the committed example so a fresh clone has something
|
|
8
|
+
// to edit.
|
|
9
|
+
//
|
|
10
|
+
// Auto-detects the project shape:
|
|
11
|
+
// - Expo : a root app.json with an `expo` block (identifiers live there;
|
|
12
|
+
// google-services.json sits at the project root).
|
|
13
|
+
// - bare : an android/app/build.gradle (identifiers come from
|
|
14
|
+
// applicationId; google-services.json sits under android/app/).
|
|
15
|
+
//
|
|
16
|
+
// It is read-mostly: the only thing it writes is a missing ConnectConfig.json
|
|
17
|
+
// (copied from the example). It never touches native projects, installs
|
|
18
|
+
// dependencies, or creates Apple/Firebase resources — those are owned by the
|
|
19
|
+
// platform tooling and the developer's accounts.
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
Reporter,
|
|
26
|
+
color,
|
|
27
|
+
copyIfMissing,
|
|
28
|
+
fileExists,
|
|
29
|
+
readJson,
|
|
30
|
+
readText,
|
|
31
|
+
section,
|
|
32
|
+
} from './lib.mjs'
|
|
33
|
+
|
|
34
|
+
// App Group format per Apple + the Config Plugin's own validator
|
|
35
|
+
// (plugin/src/withConnectNSE.ts: APP_GROUP_PATTERN).
|
|
36
|
+
const APP_GROUP_PATTERN = /^group\.[A-Za-z0-9.-]+$/
|
|
37
|
+
|
|
38
|
+
// Java reserved words (+ literals) that cannot appear as a package segment.
|
|
39
|
+
// Expo uses android.package as the Java/Kotlin namespace, so any segment that
|
|
40
|
+
// is a keyword breaks the Gradle build ("not a valid Java package name").
|
|
41
|
+
const JAVA_KEYWORDS = new Set([
|
|
42
|
+
'abstract', 'assert', 'boolean', 'break', 'byte', 'case', 'catch', 'char',
|
|
43
|
+
'class', 'const', 'continue', 'default', 'do', 'double', 'else', 'enum',
|
|
44
|
+
'extends', 'final', 'finally', 'float', 'for', 'goto', 'if', 'implements',
|
|
45
|
+
'import', 'instanceof', 'int', 'interface', 'long', 'native', 'new',
|
|
46
|
+
'package', 'private', 'protected', 'public', 'return', 'short', 'static',
|
|
47
|
+
'strictfp', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws',
|
|
48
|
+
'transient', 'try', 'void', 'volatile', 'while', 'true', 'false', 'null',
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
// Decide whether `dir` is an Expo or a bare React Native project.
|
|
52
|
+
export function detectProjectType(dir) {
|
|
53
|
+
const appJson = readJson(path.join(dir, 'app.json'))
|
|
54
|
+
if (appJson && appJson.expo) return 'expo'
|
|
55
|
+
// A bare RN app always declares `react-native` in its manifest. Use that as
|
|
56
|
+
// the discriminator rather than a bare `android/` directory — any Flutter or
|
|
57
|
+
// native-Android project also has one, and would be misclassified as bare.
|
|
58
|
+
const pkg = readJson(path.join(dir, 'package.json'))
|
|
59
|
+
const deps = {...pkg?.dependencies, ...pkg?.devDependencies}
|
|
60
|
+
if (deps['react-native']) return 'bare'
|
|
61
|
+
return 'unknown'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function checkNode(reporter) {
|
|
65
|
+
const major = Number(process.versions.node.split('.')[0])
|
|
66
|
+
if (major >= 20) reporter.pass(`Node ${process.versions.node}`)
|
|
67
|
+
else
|
|
68
|
+
reporter.fail(
|
|
69
|
+
`Node ${process.versions.node}`,
|
|
70
|
+
'Node >= 20 is required — upgrade Node and re-run',
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Scaffold the per-developer ConnectConfig.json from the committed example.
|
|
75
|
+
function ensureConnectConfig(reporter, dir) {
|
|
76
|
+
const dest = path.join(dir, 'ConnectConfig.json')
|
|
77
|
+
const src = path.join(dir, 'ConnectConfig.example.json')
|
|
78
|
+
const result = copyIfMissing(src, dest)
|
|
79
|
+
if (result === 'created')
|
|
80
|
+
reporter.warn(
|
|
81
|
+
'ConnectConfig.json',
|
|
82
|
+
'created from example — EDIT it: set AppKey + collector URLs (PostMessageUrl/KillSwitchUrl), and iOSAppGroupIdentifier for push',
|
|
83
|
+
)
|
|
84
|
+
else if (result === 'exists') reporter.pass('ConnectConfig.json present')
|
|
85
|
+
else reporter.fail('ConnectConfig.json', 'ConnectConfig.example.json missing')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate the App Group identifier (when set). A bad value fails the Config
|
|
89
|
+
// Plugin during `expo prebuild` / the iOS extensions at build time, not here,
|
|
90
|
+
// so catch it early. Returns { appGroup, pushEnabled } for downstream checks.
|
|
91
|
+
function checkConnectConfigValues(reporter, dir) {
|
|
92
|
+
const cfg = readJson(path.join(dir, 'ConnectConfig.json'))
|
|
93
|
+
if (cfg === undefined) {
|
|
94
|
+
// Missing/failed was already reported by ensureConnectConfig; only flag a
|
|
95
|
+
// present-but-broken file here.
|
|
96
|
+
if (fileExists(path.join(dir, 'ConnectConfig.json')))
|
|
97
|
+
reporter.fail('ConnectConfig.json', 'present but not valid JSON')
|
|
98
|
+
return {}
|
|
99
|
+
}
|
|
100
|
+
const connect = (cfg && cfg.Connect) || {}
|
|
101
|
+
const appGroup = connect.iOSAppGroupIdentifier
|
|
102
|
+
const pushEnabled = connect.PushEnabled === true
|
|
103
|
+
|
|
104
|
+
if (appGroup) {
|
|
105
|
+
if (APP_GROUP_PATTERN.test(appGroup))
|
|
106
|
+
reporter.pass('iOSAppGroupIdentifier', appGroup)
|
|
107
|
+
else
|
|
108
|
+
reporter.fail(
|
|
109
|
+
'iOSAppGroupIdentifier',
|
|
110
|
+
`"${appGroup}" is invalid — must match group.<reverse-dns> (letters/digits/dots/hyphens)`,
|
|
111
|
+
)
|
|
112
|
+
} else if (pushEnabled) {
|
|
113
|
+
reporter.warn(
|
|
114
|
+
'iOSAppGroupIdentifier',
|
|
115
|
+
'PushEnabled is true but no App Group set — required for the NSE/NCE rich-push extensions',
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
return {appGroup, pushEnabled}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateAndroidPackage(reporter, label, pkg) {
|
|
122
|
+
if (!pkg) {
|
|
123
|
+
reporter.warn(label, 'not set')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
const badSegment = pkg.split('.').find((seg) => JAVA_KEYWORDS.has(seg))
|
|
127
|
+
if (badSegment)
|
|
128
|
+
reporter.fail(
|
|
129
|
+
label,
|
|
130
|
+
`"${pkg}" — segment "${badSegment}" is a Java keyword; the Android build will fail. Use a Java-safe package (the iOS bundle id may keep it).`,
|
|
131
|
+
)
|
|
132
|
+
else reporter.pass(label, pkg)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Expo: identifiers live in app.json's expo block.
|
|
136
|
+
function checkAppJson(reporter, dir) {
|
|
137
|
+
const appJson = readJson(path.join(dir, 'app.json'))
|
|
138
|
+
if (!appJson || !appJson.expo) {
|
|
139
|
+
reporter.fail('app.json', 'missing or has no "expo" block')
|
|
140
|
+
return {}
|
|
141
|
+
}
|
|
142
|
+
const expo = appJson.expo
|
|
143
|
+
const androidPackage = expo.android && expo.android.package
|
|
144
|
+
const iosBundleId = expo.ios && expo.ios.bundleIdentifier
|
|
145
|
+
validateAndroidPackage(reporter, 'android.package', androidPackage)
|
|
146
|
+
if (iosBundleId) reporter.pass('ios.bundleIdentifier', iosBundleId)
|
|
147
|
+
else reporter.warn('ios.bundleIdentifier', 'not set in app.json')
|
|
148
|
+
return {androidPackage, iosBundleId}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Bare: read identifiers from android/app/build.gradle. Two distinct fields:
|
|
152
|
+
// - `namespace` → the R-class / Java package; MUST be a valid Java
|
|
153
|
+
// package (keyword check applies here).
|
|
154
|
+
// - `applicationId` → the published app id; may contain segments that are
|
|
155
|
+
// not valid Java identifiers (e.g. `new`), so it is NOT
|
|
156
|
+
// keyword-checked. It is what FCM matches in
|
|
157
|
+
// google-services.json, so it's returned for that check.
|
|
158
|
+
// (Expo collapses both into android.package, which is why checkAppJson
|
|
159
|
+
// keyword-checks that single field.)
|
|
160
|
+
function checkBareAndroidId(reporter, dir) {
|
|
161
|
+
const gradle = readText(path.join(dir, 'android', 'app', 'build.gradle'))
|
|
162
|
+
const namespace = gradle?.match(/namespace\s+["']([^"']+)["']/)?.[1] || null
|
|
163
|
+
const appId = gradle?.match(/applicationId\s+["']([^"']+)["']/)?.[1] || null
|
|
164
|
+
validateAndroidPackage(reporter, 'namespace', namespace)
|
|
165
|
+
if (appId) reporter.pass('applicationId', appId)
|
|
166
|
+
else reporter.warn('applicationId', 'not set')
|
|
167
|
+
return {androidPackage: appId}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// google-services.json must contain a client whose package_name matches the
|
|
171
|
+
// Android package (FCM matches by package); the gradle plugin fails otherwise
|
|
172
|
+
// ("No matching client found for package name").
|
|
173
|
+
function checkGoogleServices(reporter, {gsPath, androidPackage, pushEnabled}) {
|
|
174
|
+
if (!fileExists(gsPath)) {
|
|
175
|
+
if (pushEnabled)
|
|
176
|
+
reporter.warn(
|
|
177
|
+
'google-services.json',
|
|
178
|
+
'missing — required for Android FCM builds (add the Firebase Android app, then download it here)',
|
|
179
|
+
)
|
|
180
|
+
else reporter.pass('google-services.json', 'not present (push disabled)')
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
const gs = readJson(gsPath)
|
|
184
|
+
if (gs === undefined) {
|
|
185
|
+
reporter.fail('google-services.json', 'present but not valid JSON')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
const isPlaceholder =
|
|
189
|
+
gs.project_info?.project_id === 'your-firebase-project-id' ||
|
|
190
|
+
JSON.stringify(gs).includes('REPLACE_WITH_YOUR_FIREBASE')
|
|
191
|
+
const packages = (gs.client || [])
|
|
192
|
+
.map((c) => c.client_info?.android_client_info?.package_name)
|
|
193
|
+
.filter(Boolean)
|
|
194
|
+
if (isPlaceholder)
|
|
195
|
+
reporter.warn(
|
|
196
|
+
'google-services.json',
|
|
197
|
+
`placeholder values — replace with your real Firebase config (register package '${androidPackage}')`,
|
|
198
|
+
)
|
|
199
|
+
else if (androidPackage && !packages.includes(androidPackage))
|
|
200
|
+
reporter.fail(
|
|
201
|
+
'google-services.json',
|
|
202
|
+
`no client matches '${androidPackage}' (has: ${packages.join(', ') || 'none'}). Register that package in Firebase and re-download.`,
|
|
203
|
+
)
|
|
204
|
+
else reporter.pass('google-services.json', 'package match')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Collect *.entitlements files at most one level deep under iosDir (host +
|
|
208
|
+
// extension target folders), skipping Pods/build.
|
|
209
|
+
function findEntitlements(iosDir) {
|
|
210
|
+
const out = []
|
|
211
|
+
let entries
|
|
212
|
+
try {
|
|
213
|
+
entries = fs.readdirSync(iosDir, {withFileTypes: true})
|
|
214
|
+
} catch {
|
|
215
|
+
return out
|
|
216
|
+
}
|
|
217
|
+
for (const e of entries) {
|
|
218
|
+
const p = path.join(iosDir, e.name)
|
|
219
|
+
if (e.isFile() && e.name.endsWith('.entitlements')) out.push(p)
|
|
220
|
+
else if (e.isDirectory() && e.name !== 'Pods' && e.name !== 'build') {
|
|
221
|
+
try {
|
|
222
|
+
for (const f of fs.readdirSync(p))
|
|
223
|
+
if (f.endsWith('.entitlements')) out.push(path.join(p, f))
|
|
224
|
+
} catch {
|
|
225
|
+
/* ignore unreadable subdir */
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return out
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Bare iOS only: the host entitlements must declare aps-environment + an App
|
|
233
|
+
// Group (the Config Plugin generates these for Expo).
|
|
234
|
+
function checkBareIosEntitlements(reporter, dir) {
|
|
235
|
+
const iosDir = path.join(dir, 'ios')
|
|
236
|
+
if (!fileExists(iosDir)) {
|
|
237
|
+
reporter.warn('ios/', 'no ios/ directory found')
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
const hasBoth = findEntitlements(iosDir).some((p) => {
|
|
241
|
+
const t = readText(p) || ''
|
|
242
|
+
return t.includes('aps-environment') && t.includes('application-groups')
|
|
243
|
+
})
|
|
244
|
+
if (hasBoth) reporter.pass('App entitlements (aps-environment + App Group)')
|
|
245
|
+
else
|
|
246
|
+
reporter.warn(
|
|
247
|
+
'App entitlements',
|
|
248
|
+
'no *.entitlements with both aps-environment and application-groups — run `acoustic-connect setup-ios-push`',
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Run all relevant checks for `dir`. Returns the Reporter (caller decides how
|
|
253
|
+
// to print the summary / exit).
|
|
254
|
+
export function runDoctor(dir, {reporter = new Reporter()} = {}) {
|
|
255
|
+
const type = detectProjectType(dir)
|
|
256
|
+
console.log(
|
|
257
|
+
color.bold('\nAcoustic Connect doctor') +
|
|
258
|
+
color.dim(` (${type} project — ${dir})`),
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
section('Prerequisites')
|
|
262
|
+
checkNode(reporter)
|
|
263
|
+
|
|
264
|
+
section('Configuration')
|
|
265
|
+
ensureConnectConfig(reporter, dir)
|
|
266
|
+
const {pushEnabled} = checkConnectConfigValues(reporter, dir)
|
|
267
|
+
|
|
268
|
+
section('Identifiers')
|
|
269
|
+
let androidPackage
|
|
270
|
+
if (type === 'expo') {
|
|
271
|
+
;({androidPackage} = checkAppJson(reporter, dir))
|
|
272
|
+
} else if (type === 'bare') {
|
|
273
|
+
;({androidPackage} = checkBareAndroidId(reporter, dir))
|
|
274
|
+
} else {
|
|
275
|
+
reporter.warn(
|
|
276
|
+
'project type',
|
|
277
|
+
'could not detect Expo or bare RN layout — skipping identifier checks',
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
section('Android push (FCM)')
|
|
282
|
+
const gsPath =
|
|
283
|
+
type === 'expo'
|
|
284
|
+
? path.join(dir, 'google-services.json')
|
|
285
|
+
: path.join(dir, 'android', 'app', 'google-services.json')
|
|
286
|
+
checkGoogleServices(reporter, {gsPath, androidPackage, pushEnabled})
|
|
287
|
+
|
|
288
|
+
if (type === 'bare') {
|
|
289
|
+
section('iOS push')
|
|
290
|
+
checkBareIosEntitlements(reporter, dir)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return reporter
|
|
294
|
+
}
|
package/cli/index.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `acoustic-connect` — setup CLI for the React Native Acoustic Connect SDK.
|
|
3
|
+
//
|
|
4
|
+
// Ships in the published package so client apps don't have to re-implement the
|
|
5
|
+
// integration plumbing our own sample apps used to carry. Subcommands:
|
|
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
|
|
9
|
+
//
|
|
10
|
+
// `dir` defaults to the current working directory (npm passes $INIT_CWD when
|
|
11
|
+
// run as a script). All commands are idempotent and safe to re-run.
|
|
12
|
+
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
|
|
15
|
+
import {runDoctor} from './doctor.mjs'
|
|
16
|
+
import {setupIosPush} from './ios/setup-push.mjs'
|
|
17
|
+
import {Reporter, color} from './lib.mjs'
|
|
18
|
+
|
|
19
|
+
const USAGE = `acoustic-connect — React Native Acoustic Connect SDK setup
|
|
20
|
+
|
|
21
|
+
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
|
|
24
|
+
|
|
25
|
+
dir defaults to the current directory.`
|
|
26
|
+
|
|
27
|
+
// Resolve the target directory: explicit arg > $INIT_CWD (npm) > cwd.
|
|
28
|
+
function targetDir(arg) {
|
|
29
|
+
if (arg && !arg.startsWith('-')) return path.resolve(arg)
|
|
30
|
+
if (process.env.INIT_CWD) return path.resolve(process.env.INIT_CWD)
|
|
31
|
+
return process.cwd()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
const [command, maybeDir] = process.argv.slice(2)
|
|
36
|
+
|
|
37
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
38
|
+
console.log(USAGE)
|
|
39
|
+
process.exit(command ? 0 : 1)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dir = targetDir(maybeDir)
|
|
43
|
+
|
|
44
|
+
switch (command) {
|
|
45
|
+
case 'doctor': {
|
|
46
|
+
const reporter = runDoctor(dir)
|
|
47
|
+
process.exit(reporter.summary())
|
|
48
|
+
}
|
|
49
|
+
case 'setup-ios-push': {
|
|
50
|
+
const reporter = new Reporter()
|
|
51
|
+
await setupIosPush(dir, {reporter})
|
|
52
|
+
process.exit(reporter.summary())
|
|
53
|
+
}
|
|
54
|
+
default:
|
|
55
|
+
console.error(color.red(`Unknown command: ${command}\n`))
|
|
56
|
+
console.log(USAGE)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main().catch((err) => {
|
|
62
|
+
console.error(color.red('\nacoustic-connect crashed:'), err)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
})
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# add_push_extensions.rb (SDK-shipped, parameterized)
|
|
5
|
+
#
|
|
6
|
+
# Idempotently adds the two Acoustic Connect push extensions to a host app's
|
|
7
|
+
# Xcode project:
|
|
8
|
+
#
|
|
9
|
+
# * ConnectNSE — Notification Service Extension (rich-media download +
|
|
10
|
+
# PushReceived signal logging via the App Group pending store)
|
|
11
|
+
# * ConnectNCE — Notification Content Extension (rich expansion UI)
|
|
12
|
+
#
|
|
13
|
+
# Both subclass the Connect SDK base classes and share the host app's App
|
|
14
|
+
# Group. This is the MANUAL setup path for bare React Native projects; the Expo
|
|
15
|
+
# Config Plugin performs the equivalent target surgery automatically for Expo
|
|
16
|
+
# projects.
|
|
17
|
+
#
|
|
18
|
+
# This is the generalized version that ships inside the SDK package. It is
|
|
19
|
+
# normally invoked by `acoustic-connect setup-ios-push` (cli/ios/setup-push.mjs),
|
|
20
|
+
# which derives the parameters from the project and creates the per-extension
|
|
21
|
+
# source/plist/entitlements files from templates BEFORE running this — this
|
|
22
|
+
# script only performs the pbxproj surgery and assumes those files exist.
|
|
23
|
+
#
|
|
24
|
+
# Parameters come from the environment (the Node wrapper sets them):
|
|
25
|
+
# ACOUSTIC_PROJECT_PATH absolute path to the .xcodeproj (required)
|
|
26
|
+
# ACOUSTIC_APP_TARGET host app target name (required)
|
|
27
|
+
# ACOUSTIC_APP_BUNDLE_ID host app bundle identifier (optional —
|
|
28
|
+
# falls back to the app target's
|
|
29
|
+
# PRODUCT_BUNDLE_IDENTIFIER)
|
|
30
|
+
# ACOUSTIC_DEPLOYMENT_TARGET iOS deployment target (default 15.1)
|
|
31
|
+
# ACOUSTIC_SWIFT_VERSION Swift version (default 5.0)
|
|
32
|
+
#
|
|
33
|
+
# Requires the `xcodeproj` gem (ships with CocoaPods).
|
|
34
|
+
|
|
35
|
+
require 'xcodeproj'
|
|
36
|
+
|
|
37
|
+
def env!(key)
|
|
38
|
+
value = ENV[key]
|
|
39
|
+
raise "#{key} is required" if value.nil? || value.empty?
|
|
40
|
+
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
PROJECT_PATH = env!('ACOUSTIC_PROJECT_PATH')
|
|
45
|
+
APP_TARGET_NAME = env!('ACOUSTIC_APP_TARGET')
|
|
46
|
+
DEPLOYMENT_TARGET = ENV.fetch('ACOUSTIC_DEPLOYMENT_TARGET', '15.1')
|
|
47
|
+
SWIFT_VERSION = ENV.fetch('ACOUSTIC_SWIFT_VERSION', '5.0')
|
|
48
|
+
|
|
49
|
+
EXTENSIONS = [
|
|
50
|
+
{
|
|
51
|
+
name: 'ConnectNSE',
|
|
52
|
+
source: 'NotificationService.swift',
|
|
53
|
+
suffix: 'ConnectNSE',
|
|
54
|
+
# UserNotifications declares the service-extension point
|
|
55
|
+
# (com.apple.usernotifications.service / UNNotificationServiceExtension).
|
|
56
|
+
frameworks: %w[UserNotifications],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'ConnectNCE',
|
|
60
|
+
source: 'NotificationViewController.swift',
|
|
61
|
+
suffix: 'ConnectNCE',
|
|
62
|
+
# UserNotificationsUI declares the content-extension point
|
|
63
|
+
# (com.apple.usernotifications.content-extension) and provides the
|
|
64
|
+
# UNNotificationContentExtension context class. WITHOUT it the extension
|
|
65
|
+
# traps on first connection — "Unable to find NSExtensionContextClass
|
|
66
|
+
# (_UNNotificationContentExtensionVendorContext) … did you link the
|
|
67
|
+
# framework that declares the extension point?" — and no custom rich UI
|
|
68
|
+
# renders. UIKit + UserNotifications back the view controller. The Xcode
|
|
69
|
+
# template links these automatically; this scaffolder must do the same.
|
|
70
|
+
frameworks: %w[UserNotificationsUI UserNotifications UIKit],
|
|
71
|
+
},
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
project = Xcodeproj::Project.open(PROJECT_PATH)
|
|
75
|
+
app_target = project.targets.find { |t| t.name == APP_TARGET_NAME }
|
|
76
|
+
raise "App target #{APP_TARGET_NAME} not found in #{PROJECT_PATH}" unless app_target
|
|
77
|
+
|
|
78
|
+
# Bundle id: explicit override, else read it off the host app target so the
|
|
79
|
+
# extension ids (<bundle>.ConnectNSE / .ConnectNCE) match the real app.
|
|
80
|
+
def resolve_bundle_id(app_target)
|
|
81
|
+
explicit = ENV['ACOUSTIC_APP_BUNDLE_ID']
|
|
82
|
+
return explicit unless explicit.nil? || explicit.empty?
|
|
83
|
+
|
|
84
|
+
app_target.build_configurations
|
|
85
|
+
.map { |c| c.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] }
|
|
86
|
+
.compact
|
|
87
|
+
.find { |v| !v.include?('$(') }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
APP_BUNDLE_ID = resolve_bundle_id(app_target)
|
|
91
|
+
raise 'Could not determine the host bundle id; pass ACOUSTIC_APP_BUNDLE_ID' unless APP_BUNDLE_ID
|
|
92
|
+
|
|
93
|
+
# 1. Wire the host app's entitlements (App Group + aps-environment) onto both
|
|
94
|
+
# build configurations. Safe to re-run.
|
|
95
|
+
app_target.build_configurations.each do |config|
|
|
96
|
+
config.build_settings['CODE_SIGN_ENTITLEMENTS'] =
|
|
97
|
+
"#{APP_TARGET_NAME}/#{APP_TARGET_NAME}.entitlements"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Make the host entitlements file visible in the project navigator.
|
|
101
|
+
app_group = project.main_group.find_subpath(APP_TARGET_NAME, true)
|
|
102
|
+
ent_rel = "#{APP_TARGET_NAME}/#{APP_TARGET_NAME}.entitlements"
|
|
103
|
+
unless app_group.files.any? { |f| f.path == ent_rel }
|
|
104
|
+
app_group.new_reference(ent_rel)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# 2. Ensure there is an "Embed Foundation Extensions" copy-files phase on the
|
|
108
|
+
# app target (dstSubfolderSpec = PlugIns).
|
|
109
|
+
embed_phase = app_target.copy_files_build_phases.find do |p|
|
|
110
|
+
p.symbol_dst_subfolder_spec == :plug_ins
|
|
111
|
+
end
|
|
112
|
+
unless embed_phase
|
|
113
|
+
embed_phase = app_target.new_copy_files_build_phase('Embed Foundation Extensions')
|
|
114
|
+
embed_phase.symbol_dst_subfolder_spec = :plug_ins
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
EXTENSIONS.each do |ext|
|
|
118
|
+
if project.targets.any? { |t| t.name == ext[:name] }
|
|
119
|
+
puts "#{ext[:name]}: target already exists — skipping creation."
|
|
120
|
+
next
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
puts "#{ext[:name]}: creating app-extension target."
|
|
124
|
+
target = project.new_target(
|
|
125
|
+
:app_extension,
|
|
126
|
+
ext[:name],
|
|
127
|
+
:ios,
|
|
128
|
+
DEPLOYMENT_TARGET,
|
|
129
|
+
nil,
|
|
130
|
+
:swift
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Group + source file (added to the Compile Sources phase).
|
|
134
|
+
group = project.main_group.find_subpath(ext[:name], true)
|
|
135
|
+
source_ref = group.new_reference("#{ext[:name]}/#{ext[:source]}")
|
|
136
|
+
target.add_file_references([source_ref])
|
|
137
|
+
# Info.plist + entitlements are referenced via build settings only — add as
|
|
138
|
+
# plain references for navigator visibility (not compiled / copied).
|
|
139
|
+
group.new_reference("#{ext[:name]}/Info.plist")
|
|
140
|
+
group.new_reference("#{ext[:name]}/#{ext[:suffix]}.entitlements")
|
|
141
|
+
|
|
142
|
+
target.build_configurations.each do |config|
|
|
143
|
+
bs = config.build_settings
|
|
144
|
+
bs['PRODUCT_BUNDLE_IDENTIFIER'] = "#{APP_BUNDLE_ID}.#{ext[:suffix]}"
|
|
145
|
+
bs['PRODUCT_NAME'] = '$(TARGET_NAME)'
|
|
146
|
+
bs['INFOPLIST_FILE'] = "#{ext[:name]}/Info.plist"
|
|
147
|
+
bs['GENERATE_INFOPLIST_FILE'] = 'NO'
|
|
148
|
+
bs['CODE_SIGN_ENTITLEMENTS'] = "#{ext[:name]}/#{ext[:suffix]}.entitlements"
|
|
149
|
+
bs['CODE_SIGN_STYLE'] = 'Automatic'
|
|
150
|
+
bs['IPHONEOS_DEPLOYMENT_TARGET'] = DEPLOYMENT_TARGET
|
|
151
|
+
bs['SWIFT_VERSION'] = SWIFT_VERSION
|
|
152
|
+
bs['SKIP_INSTALL'] = 'YES'
|
|
153
|
+
bs['APPLICATION_EXTENSION_API_ONLY'] = 'YES'
|
|
154
|
+
bs['CLANG_ENABLE_MODULES'] = 'YES'
|
|
155
|
+
bs['MARKETING_VERSION'] = '1.0'
|
|
156
|
+
bs['CURRENT_PROJECT_VERSION'] = '1'
|
|
157
|
+
bs['TARGETED_DEVICE_FAMILY'] = '1,2'
|
|
158
|
+
bs['LD_RUNPATH_SEARCH_PATHS'] = [
|
|
159
|
+
'$(inherited)',
|
|
160
|
+
'@executable_path/Frameworks',
|
|
161
|
+
'@executable_path/../../Frameworks',
|
|
162
|
+
]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# App depends on the extension and embeds it in PlugIns.
|
|
166
|
+
app_target.add_dependency(target)
|
|
167
|
+
build_file = embed_phase.add_file_reference(target.product_reference)
|
|
168
|
+
build_file.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] }
|
|
169
|
+
puts "#{ext[:name]}: target created, embedded, and added as app dependency."
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# 3. Ensure each extension links the system frameworks that declare its
|
|
173
|
+
# extension point. Runs every time (also repairs pre-existing targets that
|
|
174
|
+
# were created before this step was added), and is idempotent — a framework
|
|
175
|
+
# already in the Link Binary phase is left untouched.
|
|
176
|
+
EXTENSIONS.each do |ext|
|
|
177
|
+
target = project.targets.find { |t| t.name == ext[:name] }
|
|
178
|
+
next unless target
|
|
179
|
+
|
|
180
|
+
linked = target.frameworks_build_phase.files.map(&:display_name)
|
|
181
|
+
Array(ext[:frameworks]).each do |fw|
|
|
182
|
+
if linked.include?("#{fw}.framework")
|
|
183
|
+
puts "#{ext[:name]}: #{fw}.framework already linked."
|
|
184
|
+
else
|
|
185
|
+
target.add_system_framework(fw)
|
|
186
|
+
puts "#{ext[:name]}: linked #{fw}.framework."
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
project.save
|
|
192
|
+
puts "Saved #{PROJECT_PATH}"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# connect_pods.rb — shared CocoaPods helper for the Acoustic Connect SDK.
|
|
4
|
+
#
|
|
5
|
+
# Ships in the published package so consumer Podfiles don't have to re-implement
|
|
6
|
+
# the Connect SDK pod resolution. Mirrors AcousticConnectRN.podspec so the host
|
|
7
|
+
# app target and the push extensions (ConnectNSE / ConnectNCE) all link the SAME
|
|
8
|
+
# SDK build:
|
|
9
|
+
# - useRelease => 'AcousticConnect', otherwise 'AcousticConnectDebug'
|
|
10
|
+
# - floor '>= 2.1.12' (the push permission API floor)
|
|
11
|
+
# - optional exact pin via Connect.iOSVersion
|
|
12
|
+
#
|
|
13
|
+
# Usage from a Podfile (resolve the file the same way the RN template resolves
|
|
14
|
+
# react_native_pods.rb):
|
|
15
|
+
#
|
|
16
|
+
# require Pod::Executable.execute_command('node', ['-p',
|
|
17
|
+
# 'require.resolve("react-native-acoustic-connect-beta/cli/ios/connect_pods.rb", {paths: [process.argv[1]]})',
|
|
18
|
+
# __dir__]).strip
|
|
19
|
+
#
|
|
20
|
+
# target 'MyApp' do
|
|
21
|
+
# # ...
|
|
22
|
+
# pod *acoustic_connect_pod(__dir__)
|
|
23
|
+
# end
|
|
24
|
+
# target 'ConnectNSE' do
|
|
25
|
+
# pod *acoustic_connect_pod(__dir__)
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# ConnectConfig.json is resolved at <podfile_dir>/../ConnectConfig.json (the
|
|
29
|
+
# consumer app root), matching where the podspec and config.gradle read it.
|
|
30
|
+
|
|
31
|
+
require 'json'
|
|
32
|
+
|
|
33
|
+
# Returns [pod_name, requirements_array] for `pod *acoustic_connect_pod(__dir__)`.
|
|
34
|
+
#
|
|
35
|
+
# podfile_dir : the Podfile's own directory (pass `__dir__`). ConnectConfig.json
|
|
36
|
+
# is read from its parent, i.e. the app root.
|
|
37
|
+
# config_path : optional explicit path to ConnectConfig.json (overrides the
|
|
38
|
+
# default lookup).
|
|
39
|
+
def acoustic_connect_pod(podfile_dir, config_path: nil)
|
|
40
|
+
path = config_path || File.join(podfile_dir, '..', 'ConnectConfig.json')
|
|
41
|
+
connect_config = JSON.parse(File.read(path))['Connect'] || {}
|
|
42
|
+
use_release = connect_config['useRelease']
|
|
43
|
+
ios_version = connect_config['iOSVersion'].to_s
|
|
44
|
+
name = use_release ? 'AcousticConnect' : 'AcousticConnectDebug'
|
|
45
|
+
floor = '>= 2.1.12'
|
|
46
|
+
requirements = ios_version.empty? ? [floor] : [floor, ios_version]
|
|
47
|
+
[name, requirements]
|
|
48
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// `acoustic-connect setup-ios-push [dir]` — create/repair the iOS push extensions.
|
|
2
|
+
//
|
|
3
|
+
// The manual-setup counterpart to what the Expo Config Plugin does on
|
|
4
|
+
// `prebuild`: it provisions the two app-extension targets a bare React Native
|
|
5
|
+
// app needs for Acoustic Connect rich push —
|
|
6
|
+
//
|
|
7
|
+
// * ConnectNSE — Notification Service Extension
|
|
8
|
+
// * ConnectNCE — Notification Content Extension
|
|
9
|
+
//
|
|
10
|
+
// Division of labour:
|
|
11
|
+
// - THIS script (Node) owns the files: it writes the per-extension Swift,
|
|
12
|
+
// Info.plist and entitlements from the SDK templates (substituting the App
|
|
13
|
+
// Group from ConnectConfig.json), and ensures the host app has an
|
|
14
|
+
// entitlements file with aps-environment + the App Group.
|
|
15
|
+
// - add_push_extensions.rb (Ruby + xcodeproj) owns the pbxproj surgery:
|
|
16
|
+
// it creates/links the targets, the embed phase, and the system frameworks.
|
|
17
|
+
//
|
|
18
|
+
// Everything is idempotent — existing files are left untouched, existing
|
|
19
|
+
// targets are skipped. Re-running repairs missing pieces without duplicating.
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
import {fileURLToPath} from 'node:url'
|
|
24
|
+
|
|
25
|
+
import {color, commandExists, fileExists, readJson, readText, run, section} from '../lib.mjs'
|
|
26
|
+
|
|
27
|
+
const cliDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))) // .../cli
|
|
28
|
+
const sdkRoot = path.dirname(cliDir) // package root
|
|
29
|
+
const templatesDir = path.join(cliDir, 'ios', 'templates')
|
|
30
|
+
// Swift principal-class templates are shared with the Expo Config Plugin.
|
|
31
|
+
const swiftDir = path.join(sdkRoot, 'plugin', 'swift')
|
|
32
|
+
|
|
33
|
+
const APP_GROUP_TOKEN = 'CONNECT_APP_GROUP_IDENTIFIER_PLACEHOLDER'
|
|
34
|
+
|
|
35
|
+
const EXTENSIONS = [
|
|
36
|
+
{name: 'ConnectNSE', swift: 'NotificationService.swift'},
|
|
37
|
+
{name: 'ConnectNCE', swift: 'NotificationViewController.swift'},
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
// Find the single .xcodeproj under <dir>/ios. The app target name follows the
|
|
41
|
+
// RN convention (the project basename), which is what `setup-ios-push` assumes.
|
|
42
|
+
function findXcodeproj(iosDir) {
|
|
43
|
+
let entries = []
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(iosDir)
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
const proj = entries.find((e) => e.endsWith('.xcodeproj'))
|
|
50
|
+
return proj ? path.join(iosDir, proj) : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Write `content` to `dest` only if missing; report which happened.
|
|
54
|
+
function writeIfMissing(reporter, label, dest, content) {
|
|
55
|
+
if (fileExists(dest)) {
|
|
56
|
+
reporter.pass(label, 'present')
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
fs.mkdirSync(path.dirname(dest), {recursive: true})
|
|
60
|
+
fs.writeFileSync(dest, content)
|
|
61
|
+
reporter.pass(label, 'created')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Render a template file, substituting the App Group placeholder. Only the
|
|
65
|
+
// real occurrences are replaced — the Swift string literal and the plist
|
|
66
|
+
// <string> entry — so any explanatory comment that mentions the token by name
|
|
67
|
+
// (e.g. "The <token> is replaced by …") stays readable.
|
|
68
|
+
function renderTemplate(srcPath, appGroup) {
|
|
69
|
+
const raw = readText(srcPath)
|
|
70
|
+
if (raw == null) return null
|
|
71
|
+
return raw
|
|
72
|
+
.split(`"${APP_GROUP_TOKEN}"`)
|
|
73
|
+
.join(`"${appGroup}"`)
|
|
74
|
+
.split(`<string>${APP_GROUP_TOKEN}</string>`)
|
|
75
|
+
.join(`<string>${appGroup}</string>`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Minimal host entitlements when the app has none yet: aps-environment +
|
|
79
|
+
// the App Group, matching what the extensions share.
|
|
80
|
+
function hostEntitlements(appGroup) {
|
|
81
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
83
|
+
<plist version="1.0">
|
|
84
|
+
<dict>
|
|
85
|
+
\t<key>aps-environment</key>
|
|
86
|
+
\t<string>development</string>
|
|
87
|
+
\t<key>com.apple.security.application-groups</key>
|
|
88
|
+
\t<array>
|
|
89
|
+
\t\t<string>${appGroup}</string>
|
|
90
|
+
\t</array>
|
|
91
|
+
</dict>
|
|
92
|
+
</plist>
|
|
93
|
+
`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function setupIosPush(dir, {reporter}) {
|
|
97
|
+
console.log(
|
|
98
|
+
color.bold('\nAcoustic Connect — iOS push extensions') +
|
|
99
|
+
color.dim(` (${dir})`),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (process.platform !== 'darwin') {
|
|
103
|
+
reporter.warn('iOS', 'skipped — iOS project surgery requires macOS')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const iosDir = path.join(dir, 'ios')
|
|
108
|
+
if (!fileExists(iosDir)) {
|
|
109
|
+
reporter.fail('ios/', `no ios/ directory under ${dir}`)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const projectPath = findXcodeproj(iosDir)
|
|
114
|
+
if (!projectPath) {
|
|
115
|
+
reporter.fail('.xcodeproj', `none found under ${iosDir}`)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const appTarget = path.basename(projectPath, '.xcodeproj')
|
|
119
|
+
reporter.pass('Xcode project', `${path.basename(projectPath)} (target ${appTarget})`)
|
|
120
|
+
|
|
121
|
+
// App Group is the linchpin — host + both extensions must share it.
|
|
122
|
+
const cfg = readJson(path.join(dir, 'ConnectConfig.json'))
|
|
123
|
+
const appGroup = cfg?.Connect?.iOSAppGroupIdentifier
|
|
124
|
+
if (!appGroup) {
|
|
125
|
+
reporter.fail(
|
|
126
|
+
'iOSAppGroupIdentifier',
|
|
127
|
+
'not set in ConnectConfig.json — required for the NSE/NCE shared store. Run `acoustic-connect doctor` first.',
|
|
128
|
+
)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
reporter.pass('App Group', appGroup)
|
|
132
|
+
|
|
133
|
+
// ── Files (idempotent) ───────────────────────────────────────────────────
|
|
134
|
+
section('Source files')
|
|
135
|
+
|
|
136
|
+
// Host entitlements — only created if absent (never clobber a real one).
|
|
137
|
+
const hostEntPath = path.join(iosDir, appTarget, `${appTarget}.entitlements`)
|
|
138
|
+
writeIfMissing(
|
|
139
|
+
reporter,
|
|
140
|
+
`${appTarget}/${appTarget}.entitlements`,
|
|
141
|
+
hostEntPath,
|
|
142
|
+
hostEntitlements(appGroup),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
for (const ext of EXTENSIONS) {
|
|
146
|
+
const extDir = path.join(iosDir, ext.name)
|
|
147
|
+
|
|
148
|
+
const swift = renderTemplate(path.join(swiftDir, ext.swift), appGroup)
|
|
149
|
+
if (swift == null) {
|
|
150
|
+
reporter.fail(`${ext.name}/${ext.swift}`, `template missing in ${swiftDir}`)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
writeIfMissing(reporter, `${ext.name}/${ext.swift}`, path.join(extDir, ext.swift), swift)
|
|
154
|
+
|
|
155
|
+
const tplKey = ext.name === 'ConnectNSE' ? 'nse' : 'nce'
|
|
156
|
+
const plist = readText(path.join(templatesDir, tplKey, 'Info.plist'))
|
|
157
|
+
writeIfMissing(reporter, `${ext.name}/Info.plist`, path.join(extDir, 'Info.plist'), plist)
|
|
158
|
+
|
|
159
|
+
const ent = renderTemplate(
|
|
160
|
+
path.join(templatesDir, tplKey, `${ext.name}.entitlements`),
|
|
161
|
+
appGroup,
|
|
162
|
+
)
|
|
163
|
+
writeIfMissing(
|
|
164
|
+
reporter,
|
|
165
|
+
`${ext.name}/${ext.name}.entitlements`,
|
|
166
|
+
path.join(extDir, `${ext.name}.entitlements`),
|
|
167
|
+
ent,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── pbxproj surgery (Ruby) ─────────────────────────────────────────────────
|
|
172
|
+
section('Xcode targets')
|
|
173
|
+
if (!commandExists('ruby')) {
|
|
174
|
+
reporter.warn(
|
|
175
|
+
'Push extensions',
|
|
176
|
+
'ruby not found — files written, but run add_push_extensions.rb on a machine with ruby + the xcodeproj gem to wire the targets',
|
|
177
|
+
)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const rubyScript = path.join(cliDir, 'ios', 'add_push_extensions.rb')
|
|
182
|
+
// Pass the project path / target name as real environment variables (the
|
|
183
|
+
// Ruby script reads them via ENV[…]) rather than interpolating them into the
|
|
184
|
+
// command line — a path with shell metacharacters must not be re-parsed.
|
|
185
|
+
const rubyEnv = {
|
|
186
|
+
ACOUSTIC_PROJECT_PATH: projectPath,
|
|
187
|
+
ACOUSTIC_APP_TARGET: appTarget,
|
|
188
|
+
}
|
|
189
|
+
if (run(`ruby "${rubyScript}"`, {cwd: iosDir, env: rubyEnv}))
|
|
190
|
+
reporter.pass('Push extensions wired (NSE + NCE)')
|
|
191
|
+
else
|
|
192
|
+
reporter.fail(
|
|
193
|
+
'Push extensions',
|
|
194
|
+
'add_push_extensions.rb failed — needs the xcodeproj gem (ships with CocoaPods: `gem install xcodeproj`)',
|
|
195
|
+
)
|
|
196
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.application-groups</key>
|
|
6
|
+
<array>
|
|
7
|
+
<string>CONNECT_APP_GROUP_IDENTIFIER_PLACEHOLDER</string>
|
|
8
|
+
</array>
|
|
9
|
+
</dict>
|
|
10
|
+
</plist>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
+
<key>CFBundleDisplayName</key>
|
|
8
|
+
<string>ConnectNCE</string>
|
|
9
|
+
<key>CFBundleExecutable</key>
|
|
10
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
11
|
+
<key>CFBundleIdentifier</key>
|
|
12
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
13
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
14
|
+
<string>6.0</string>
|
|
15
|
+
<key>CFBundleName</key>
|
|
16
|
+
<string>$(PRODUCT_NAME)</string>
|
|
17
|
+
<key>CFBundlePackageType</key>
|
|
18
|
+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
19
|
+
<key>CFBundleShortVersionString</key>
|
|
20
|
+
<string>$(MARKETING_VERSION)</string>
|
|
21
|
+
<key>CFBundleVersion</key>
|
|
22
|
+
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
23
|
+
<key>NSExtension</key>
|
|
24
|
+
<dict>
|
|
25
|
+
<key>NSExtensionAttributes</key>
|
|
26
|
+
<dict>
|
|
27
|
+
<key>UNNotificationExtensionCategory</key>
|
|
28
|
+
<array>
|
|
29
|
+
<string>ACOUSTIC_RICH_NOTIFICATION</string>
|
|
30
|
+
<string>ACTIONABLE_NOTIFICATION</string>
|
|
31
|
+
</array>
|
|
32
|
+
<key>UNNotificationExtensionDefaultContentHidden</key>
|
|
33
|
+
<false/>
|
|
34
|
+
<key>UNNotificationExtensionInitialContentSizeRatio</key>
|
|
35
|
+
<real>1.0</real>
|
|
36
|
+
</dict>
|
|
37
|
+
<key>NSExtensionPointIdentifier</key>
|
|
38
|
+
<string>com.apple.usernotifications.content-extension</string>
|
|
39
|
+
<key>NSExtensionPrincipalClass</key>
|
|
40
|
+
<string>$(PRODUCT_MODULE_NAME).NotificationViewController</string>
|
|
41
|
+
</dict>
|
|
42
|
+
<key>RCTNewArchEnabled</key>
|
|
43
|
+
<true/>
|
|
44
|
+
</dict>
|
|
45
|
+
</plist>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.application-groups</key>
|
|
6
|
+
<array>
|
|
7
|
+
<string>CONNECT_APP_GROUP_IDENTIFIER_PLACEHOLDER</string>
|
|
8
|
+
</array>
|
|
9
|
+
</dict>
|
|
10
|
+
</plist>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>CFBundleDevelopmentRegion</key>
|
|
6
|
+
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
7
|
+
<key>CFBundleDisplayName</key>
|
|
8
|
+
<string>ConnectNSE</string>
|
|
9
|
+
<key>CFBundleExecutable</key>
|
|
10
|
+
<string>$(EXECUTABLE_NAME)</string>
|
|
11
|
+
<key>CFBundleIdentifier</key>
|
|
12
|
+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
13
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
14
|
+
<string>6.0</string>
|
|
15
|
+
<key>CFBundleName</key>
|
|
16
|
+
<string>$(PRODUCT_NAME)</string>
|
|
17
|
+
<key>CFBundlePackageType</key>
|
|
18
|
+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
19
|
+
<key>CFBundleShortVersionString</key>
|
|
20
|
+
<string>$(MARKETING_VERSION)</string>
|
|
21
|
+
<key>CFBundleVersion</key>
|
|
22
|
+
<string>$(CURRENT_PROJECT_VERSION)</string>
|
|
23
|
+
<key>NSExtension</key>
|
|
24
|
+
<dict>
|
|
25
|
+
<key>NSExtensionPointIdentifier</key>
|
|
26
|
+
<string>com.apple.usernotifications.service</string>
|
|
27
|
+
<key>NSExtensionPrincipalClass</key>
|
|
28
|
+
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
|
29
|
+
</dict>
|
|
30
|
+
<key>RCTNewArchEnabled</key>
|
|
31
|
+
<true/>
|
|
32
|
+
</dict>
|
|
33
|
+
</plist>
|
package/cli/lib.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Shared helpers for the Acoustic Connect SDK setup CLI (`acoustic-connect`).
|
|
2
|
+
//
|
|
3
|
+
// This is the single source of truth that used to be duplicated across the
|
|
4
|
+
// Examples/expo and Examples/bare-workflow `scripts/lib.mjs` files. It ships in
|
|
5
|
+
// the published SDK package so client apps (and our own samples) can run the
|
|
6
|
+
// same doctor / scaffolding without copying it.
|
|
7
|
+
//
|
|
8
|
+
// Cross-platform (macOS / Linux / Windows): only node: built-ins, commands run
|
|
9
|
+
// through the platform shell. iOS-only commands (pod/bundle/ruby) are guarded
|
|
10
|
+
// by the callers, never run on Windows.
|
|
11
|
+
|
|
12
|
+
import {spawnSync} from 'node:child_process'
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
const isWindows = process.platform === 'win32'
|
|
17
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR
|
|
18
|
+
|
|
19
|
+
const paint = (code, s) => (useColor ? `[${code}m${s}[0m` : s)
|
|
20
|
+
export const color = {
|
|
21
|
+
bold: (s) => paint('1', s),
|
|
22
|
+
dim: (s) => paint('2', s),
|
|
23
|
+
red: (s) => paint('31', s),
|
|
24
|
+
green: (s) => paint('32', s),
|
|
25
|
+
yellow: (s) => paint('33', s),
|
|
26
|
+
cyan: (s) => paint('36', s),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function section(title) {
|
|
30
|
+
console.log('\n' + color.bold(color.cyan(`▸ ${title}`)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function info(msg) {
|
|
34
|
+
console.log(` ${msg}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Run a shell command, streaming its output. Returns true on exit code 0.
|
|
38
|
+
// `command` is a full command line (shell-interpreted) so callers can write it
|
|
39
|
+
// the way they'd type it. Pass dynamic, path-derived values through `env`
|
|
40
|
+
// (merged over the parent environment) rather than interpolating them into
|
|
41
|
+
// `command` — that keeps a value containing shell metacharacters (`$`,
|
|
42
|
+
// backticks, quotes, spaces) from being re-parsed by the shell.
|
|
43
|
+
export function run(command, {cwd, env} = {}) {
|
|
44
|
+
return (
|
|
45
|
+
spawnSync(command, {
|
|
46
|
+
cwd,
|
|
47
|
+
env: {...process.env, ...env},
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
shell: true,
|
|
50
|
+
}).status === 0
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Run a command and capture stdout/stderr instead of streaming. Used for quick
|
|
55
|
+
// version/state probes — never throws (spawnSync returns on a missing binary
|
|
56
|
+
// rather than throwing), and is bounded by `timeout` (default 60s) so an
|
|
57
|
+
// unresponsive probe — e.g. `adb shell` against an offline device — can't hang
|
|
58
|
+
// the caller. On timeout the child is killed and `ok` is false.
|
|
59
|
+
export function capture(command, {cwd, timeout = 60000} = {}) {
|
|
60
|
+
const result = spawnSync(command, {
|
|
61
|
+
cwd,
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
shell: true,
|
|
64
|
+
timeout,
|
|
65
|
+
killSignal: 'SIGKILL',
|
|
66
|
+
})
|
|
67
|
+
return {
|
|
68
|
+
ok: result.status === 0,
|
|
69
|
+
stdout: result.stdout || '',
|
|
70
|
+
stderr: result.stderr || '',
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Is a binary resolvable on PATH? Uses `where` on Windows, `command -v` elsewhere.
|
|
75
|
+
export function commandExists(cmd) {
|
|
76
|
+
const probe = isWindows ? `where ${cmd}` : `command -v ${cmd}`
|
|
77
|
+
return spawnSync(probe, {stdio: 'ignore', shell: true}).status === 0
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function fileExists(p) {
|
|
81
|
+
try {
|
|
82
|
+
return fs.existsSync(p)
|
|
83
|
+
} catch {
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function readText(p) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readFileSync(p, 'utf8')
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Read + parse JSON. Returns the parsed value, or `undefined` for any
|
|
97
|
+
// "no usable object" outcome — missing, unreadable, or malformed — so callers
|
|
98
|
+
// only need a single `=== undefined` check and can never hit a TypeError on a
|
|
99
|
+
// null result. Callers that must distinguish missing from malformed should call
|
|
100
|
+
// fileExists() first.
|
|
101
|
+
export function readJson(p) {
|
|
102
|
+
const raw = readText(p)
|
|
103
|
+
if (raw == null) return undefined
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(raw)
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Copy `src` to `dest` only when `dest` is missing. Returns 'created' |
|
|
112
|
+
// 'exists' | 'no-template'. Callers surface the result so the developer knows
|
|
113
|
+
// to fill the freshly-copied file in.
|
|
114
|
+
export function copyIfMissing(src, dest) {
|
|
115
|
+
if (fileExists(dest)) return 'exists'
|
|
116
|
+
if (!fileExists(src)) return 'no-template'
|
|
117
|
+
fs.mkdirSync(path.dirname(dest), {recursive: true})
|
|
118
|
+
fs.copyFileSync(src, dest)
|
|
119
|
+
return 'created'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Walk up from `startDir` for the nearest package.json declaring a `workspaces`
|
|
123
|
+
// array that actually includes `startDir` — that's where `npm install` must run
|
|
124
|
+
// so the SDK is hoisted/symlinked. Returns null for a standalone (published)
|
|
125
|
+
// checkout with no workspace.
|
|
126
|
+
//
|
|
127
|
+
// Only a workspace that actually declares THIS app as a member counts — we
|
|
128
|
+
// verify `target` matches one of the candidate's `workspaces` globs. That way
|
|
129
|
+
// an unrelated outer workspace (e.g. a CI tool's own monorepo root that happens
|
|
130
|
+
// to sit above the checkout) is skipped rather than mistaken for our root.
|
|
131
|
+
export function findWorkspaceRoot(startDir) {
|
|
132
|
+
const target = path.resolve(startDir)
|
|
133
|
+
let dir = target
|
|
134
|
+
for (;;) {
|
|
135
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
136
|
+
if (fileExists(pkgPath)) {
|
|
137
|
+
try {
|
|
138
|
+
const pkg = JSON.parse(readText(pkgPath))
|
|
139
|
+
if (pkg && pkg.workspaces && workspaceIncludes(dir, pkg.workspaces, target))
|
|
140
|
+
return dir
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore malformed package.json and keep walking up
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const parent = path.dirname(dir)
|
|
146
|
+
if (parent === dir) return null
|
|
147
|
+
dir = parent
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Does `rootDir`'s `workspaces` declaration cover `target`? Handles the array
|
|
152
|
+
// form and the { packages: [...] } object form, with npm's glob semantics
|
|
153
|
+
// (`*` within a path segment, `**` across segments).
|
|
154
|
+
function workspaceIncludes(rootDir, workspaces, target) {
|
|
155
|
+
const globs = Array.isArray(workspaces)
|
|
156
|
+
? workspaces
|
|
157
|
+
: Array.isArray(workspaces.packages)
|
|
158
|
+
? workspaces.packages
|
|
159
|
+
: []
|
|
160
|
+
if (rootDir === target) return true // the workspace root itself
|
|
161
|
+
const rel = path.relative(rootDir, target).split(path.sep).join('/')
|
|
162
|
+
return globs.some((glob) => {
|
|
163
|
+
const pattern = String(glob).replace(/\/$/, '')
|
|
164
|
+
const re = new RegExp(
|
|
165
|
+
'^' +
|
|
166
|
+
pattern
|
|
167
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // escape regex specials (keep * /)
|
|
168
|
+
.replace(/\*\*?/g, (m) => (m === '**' ? '.*' : '[^/]*')) + // ** -> .* , * -> one path segment
|
|
169
|
+
'$',
|
|
170
|
+
)
|
|
171
|
+
return re.test(rel)
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Accumulates pass/warn/fail results and prints a final summary. A single
|
|
176
|
+
// `fail` makes the process exit non-zero so CI / the developer notice.
|
|
177
|
+
export class Reporter {
|
|
178
|
+
constructor() {
|
|
179
|
+
this.results = []
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
record(status, label, detail) {
|
|
183
|
+
this.results.push({status, label, detail})
|
|
184
|
+
const mark =
|
|
185
|
+
status === 'pass'
|
|
186
|
+
? color.green('✓')
|
|
187
|
+
: status === 'warn'
|
|
188
|
+
? color.yellow('⚠')
|
|
189
|
+
: color.red('✗')
|
|
190
|
+
const tail = detail ? ` ${color.dim('— ' + detail)}` : ''
|
|
191
|
+
console.log(` ${mark} ${label}${tail}`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pass(label, detail) {
|
|
195
|
+
this.record('pass', label, detail)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
warn(label, detail) {
|
|
199
|
+
this.record('warn', label, detail)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fail(label, detail) {
|
|
203
|
+
this.record('fail', label, detail)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
hasFailures() {
|
|
207
|
+
return this.results.some((r) => r.status === 'fail')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Print the summary and any actionable next steps, then return the exit code.
|
|
211
|
+
summary(nextSteps = []) {
|
|
212
|
+
const counts = {pass: 0, warn: 0, fail: 0}
|
|
213
|
+
for (const r of this.results) counts[r.status]++
|
|
214
|
+
|
|
215
|
+
section('Summary')
|
|
216
|
+
info(
|
|
217
|
+
`${color.green(counts.pass + ' ok')}, ` +
|
|
218
|
+
`${color.yellow(counts.warn + ' warning(s)')}, ` +
|
|
219
|
+
`${color.red(counts.fail + ' failure(s)')}`,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const actionable = this.results.filter(
|
|
223
|
+
(r) => r.status !== 'pass' && r.detail,
|
|
224
|
+
)
|
|
225
|
+
if (actionable.length) {
|
|
226
|
+
section('Action needed')
|
|
227
|
+
for (const r of actionable) {
|
|
228
|
+
const mark = r.status === 'fail' ? color.red('✗') : color.yellow('⚠')
|
|
229
|
+
info(`${mark} ${r.label}: ${r.detail}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (nextSteps.length) {
|
|
234
|
+
section('Next steps')
|
|
235
|
+
for (const s of nextSteps) info(`• ${s}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return this.hasFailures() ? 1 : 0
|
|
239
|
+
}
|
|
240
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Acoustic, L.P., Radoslaw Serek, Aksana Naskalava",
|
|
3
|
+
"bin": {
|
|
4
|
+
"acoustic-connect": "cli/index.mjs"
|
|
5
|
+
},
|
|
3
6
|
"dependencies": {
|
|
4
7
|
"@prettier/plugin-xml": "^2.2.0",
|
|
5
8
|
"fast-xml-parser": "^4.0.8",
|
|
@@ -76,6 +79,7 @@
|
|
|
76
79
|
"plugin/build",
|
|
77
80
|
"plugin/swift",
|
|
78
81
|
"plugin/src",
|
|
82
|
+
"cli",
|
|
79
83
|
"*.podspec",
|
|
80
84
|
"README.md",
|
|
81
85
|
"CHANGELOG.md",
|
|
@@ -197,20 +201,20 @@
|
|
|
197
201
|
"build": "npm run typecheck && bob build",
|
|
198
202
|
"build:plugin": "tsc -p plugin/tsconfig.json",
|
|
199
203
|
"codegen": "nitrogen --logLevel=\"debug\" && npm run build && node post-script.js",
|
|
200
|
-
"postinstall": "
|
|
204
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
201
205
|
"prepack": "npm run build:plugin",
|
|
202
206
|
"rebuild": "npm run codegen && npm run rebuildExample --prefix ./example",
|
|
203
207
|
"release": "release-it",
|
|
204
208
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
205
209
|
"test:plugin": "npm run build:plugin && node --test plugin/__tests__/",
|
|
206
210
|
"typecheck": "tsc --noEmit",
|
|
207
|
-
"verifyConnectSetup": "node
|
|
211
|
+
"verifyConnectSetup": "node cli/index.mjs doctor $INIT_CWD",
|
|
208
212
|
"yalcPublish": "npm run rebuild && yalc publish"
|
|
209
213
|
},
|
|
210
214
|
"source": "src/index",
|
|
211
215
|
"summary": "react-native ios android tealeaf connect cxa wxca er enhanced-replay",
|
|
212
216
|
"types": "./lib/typescript/src/index.d.ts",
|
|
213
|
-
"version": "18.0.
|
|
217
|
+
"version": "18.0.38",
|
|
214
218
|
"workspaces": [
|
|
215
219
|
"example",
|
|
216
220
|
"Examples/bare-workflow"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Cross-platform postinstall (replaces the bash-only postInstallScripts.sh,
|
|
2
|
+
// which silently no-opped on Windows and violated the repo's cross-platform
|
|
3
|
+
// rule). Runs the same three helper scripts that integrate the SDK into a
|
|
4
|
+
// consumer project, from the package root, best-effort: a non-zero exit from
|
|
5
|
+
// any of them is logged but never fails `npm install`.
|
|
6
|
+
|
|
7
|
+
import {spawnSync} from 'node:child_process'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import {fileURLToPath} from 'node:url'
|
|
10
|
+
|
|
11
|
+
const scriptsDir = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const packageRoot = path.dirname(scriptsDir)
|
|
13
|
+
|
|
14
|
+
// Order matches the original postInstallScripts.sh. Note: the file is
|
|
15
|
+
// `xmlparser.js` (lowercase) — the old shell script referenced `xmlParser.js`
|
|
16
|
+
// with a capital P, which failed on case-sensitive filesystems (Linux CI).
|
|
17
|
+
const steps = ['reviewConnectConfig.js', 'xmlparser.js', 'gradleParser.js']
|
|
18
|
+
|
|
19
|
+
console.log(
|
|
20
|
+
'**Acoustic Integration***********************************************************************',
|
|
21
|
+
)
|
|
22
|
+
for (const step of steps) {
|
|
23
|
+
console.log(`Running scripts/${step} ...`)
|
|
24
|
+
const result = spawnSync(process.execPath, [path.join('scripts', step)], {
|
|
25
|
+
cwd: packageRoot,
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
})
|
|
28
|
+
if (result.status !== 0)
|
|
29
|
+
console.log(
|
|
30
|
+
`scripts/${step} exited with ${result.status ?? result.signal} — continuing (postinstall is best-effort).`,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
console.log(
|
|
34
|
+
'*********************************************************************************************',
|
|
35
|
+
)
|
package/scripts/xmlparser.js
CHANGED
|
@@ -14,7 +14,7 @@ const xmlPaserPlugin = require("@prettier/plugin-xml");
|
|
|
14
14
|
const { exit } = require('process');
|
|
15
15
|
const { isValid } = require('./util');
|
|
16
16
|
|
|
17
|
-
console.log("Run
|
|
17
|
+
console.log("Run xmlparser.js");
|
|
18
18
|
|
|
19
19
|
const directoryPath = path.join(__dirname,"..","..","..")
|
|
20
20
|
const filePath = `${directoryPath}/android/app/src/main/AndroidManifest.xml`;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
echo "**Acoustic Integration***********************************************************************"
|
|
3
|
-
echo "Current directory:"
|
|
4
|
-
pwd
|
|
5
|
-
echo "Running Node scripts..."
|
|
6
|
-
node ./scripts/reviewConnectConfig.js
|
|
7
|
-
node ./scripts/xmlParser.js
|
|
8
|
-
node ./scripts/gradleParser.js
|
|
9
|
-
echo "*********************************************************************************************"
|
|
10
|
-
echo "Running here scripts..."
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/********************************************************************************************
|
|
2
|
-
* Copyright (C) 2024 Acoustic, L.P. All rights reserved.
|
|
3
|
-
*
|
|
4
|
-
* NOTICE: This file contains material that is confidential and proprietary to
|
|
5
|
-
* Acoustic, L.P. and/or other developers. No license is granted under any intellectual or
|
|
6
|
-
* industrial property rights of Acoustic, L.P. except as may be provided in an agreement with
|
|
7
|
-
* Acoustic, L.P. Any unauthorized copying or distribution of content from this file is
|
|
8
|
-
* prohibited.
|
|
9
|
-
********************************************************************************************/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Verify and Update React Native SDK integration to capture screen tracking.
|
|
13
|
-
*
|
|
14
|
-
* cd node_modules/react-native-acoustic-connect-beta folder
|
|
15
|
-
*
|
|
16
|
-
* yarn run verifyConnectSetup
|
|
17
|
-
*
|
|
18
|
-
*/
|
|
19
|
-
const fs = require('fs');
|
|
20
|
-
|
|
21
|
-
//const filePath = "../../src/components/RootComponent.tsx"
|
|
22
|
-
const filePath = "Example/nativebase-v3-kitchensink/src/components/RootComponent.tsx"
|
|
23
|
-
|
|
24
|
-
function log(message) {
|
|
25
|
-
console.log(message);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isRootFileAvailable() {
|
|
29
|
-
return fs.existsSync(filePath);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isConnectComponentAvailable() {
|
|
33
|
-
log("Looking for Connect tag...");
|
|
34
|
-
return isComponentAvailable(/<Connect/);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isNavigationContainerAvailable() {
|
|
38
|
-
log("Looking for NavigationContainer tag...");
|
|
39
|
-
return isComponentAvailable(/<NavigationContainer/);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isComponentAvailable(componentTag) {
|
|
43
|
-
try {
|
|
44
|
-
let data = fs.readFileSync(filePath, 'utf8');
|
|
45
|
-
const re = new RegExp(componentTag, "g");
|
|
46
|
-
const found = re.test(data);
|
|
47
|
-
|
|
48
|
-
return found;
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.error(err);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (isRootFileAvailable()) {
|
|
55
|
-
if (isConnectComponentAvailable()) {
|
|
56
|
-
log("Connect component found. You are ready to go!");
|
|
57
|
-
} else if (isNavigationContainerAvailable()) {
|
|
58
|
-
log("NavigationContainer component is available but Connect component is missing.\n I'll add Connect around NavigationContainer component.");
|
|
59
|
-
AddConnectComponent();
|
|
60
|
-
} else {
|
|
61
|
-
log("Missing both Connect and NavigationContainer component.\n Please refer to SDK integration without React Navigation.");
|
|
62
|
-
};
|
|
63
|
-
} else {
|
|
64
|
-
console.error("App.js file not found in root app folder.");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Update and add Connect around NavigationContainer
|
|
68
|
-
function AddConnectComponent() {
|
|
69
|
-
const connectImport = `import { Connect } from 'react-native-acoustic-connect-beta';`
|
|
70
|
-
|
|
71
|
-
const startNavigationContainer = /<NavigationContainer/
|
|
72
|
-
const startConnectComponent = `<Connect><NavigationContainer`
|
|
73
|
-
|
|
74
|
-
const endNavigationContainer = /<\/NavigationContainer>/
|
|
75
|
-
const endConnectComponent = `</NavigationContainer></Connect>`
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
let data = fs.readFileSync(filePath, 'utf8');
|
|
79
|
-
data = data
|
|
80
|
-
.replace(startNavigationContainer, startConnectComponent)
|
|
81
|
-
.replace(endNavigationContainer, endConnectComponent);
|
|
82
|
-
fs.writeFileSync(filePath, ConnectImport + '\n' + data);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
log("Something went wrong.\n Please refer to SDK integration with React Navigation.");
|
|
85
|
-
console.error(err);
|
|
86
|
-
}
|
|
87
|
-
}
|