react-native-acoustic-connect-beta
Version:
BETA: React native plugin for Acoustic Connect
157 lines (144 loc) • 6.8 kB
text/typescript
// Copyright (C) 2026 Acoustic, L.P. All rights reserved.
//
// NOTICE: This file contains material that is confidential and proprietary to
// Acoustic, L.P. and/or other developers. No license is granted under any
// intellectual or industrial property rights of Acoustic, L.P. except as may
// be provided in an agreement with Acoustic, L.P. Any unauthorized copying or
// distribution of content from this file is prohibited.
import {
withAppBuildGradle,
withDangerousMod,
type ConfigPlugin,
} from '@expo/config-plugins'
import * as fs from 'fs'
import * as path from 'path'
import type { ConnectPluginProps } from './withConnectNSE'
// The Gradle snippet that runs the SDK's config.gradle. config.gradle reads
// the consumer app's ConnectConfig.json (`$project.rootDir/../ConnectConfig.json`)
// and writes AppKey / PostMessageUrl / KillSwitchUrl (and the Tealeaf + EOCore
// configs) into the SDK module's `src/main/assets/*` at Gradle configuration
// time — so the collector the app reports to is driven by ConnectConfig.json.
//
// This mirrors the line `scripts/gradleParser.js` appends for the bare-workflow
// sample (Examples/bare-workflow/android/app/build.gradle). The Expo Android
// project is gitignored and regenerated by `expo prebuild`, so the line cannot
// be committed there — this mod re-injects it on every prebuild instead.
//
// The ':react-native-acoustic-connect-beta' Gradle project is provided by
// React Native autolinking and resolves in the Expo app the same way it does
// in bare-workflow.
export const CONFIG_GRADLE_APPLY =
`apply from: project(':react-native-acoustic-connect-beta')` +
`.projectDir.getPath() + "/config.gradle"`
/**
* Append the config.gradle `apply from:` line to an app `build.gradle` body.
*
* Idempotent: a no-op when the line is already present, so repeated prebuilds
* (and a prebuild over an already-patched, non-`--clean` project) don't stack
* duplicate applies.
*/
export function appendConfigGradleApply(contents: string): string {
if (contents.includes('/config.gradle')) {
return contents
}
return `${contents}\n\n${CONFIG_GRADLE_APPLY}\n`
}
/**
* Expo config mod: wire the SDK's config.gradle into the generated Android
* app build so ConnectConfig.json values reach the native assets at build
* time. Without it, the Android SDK ships its committed default collector
* config and the app reports to the wrong endpoint (iOS is unaffected — it
* uses AcousticConnectRNConfig.json via a separate flow).
*/
export const withConnectAndroidConfig: ConfigPlugin<ConnectPluginProps> = (
config
) =>
withAppBuildGradle(config, (cfg) => {
// The RN/Expo template app build.gradle is Groovy. If a future template
// switches to the Kotlin DSL the append heuristic no longer applies —
// warn and skip rather than corrupt the file.
if (cfg.modResults.language !== 'groovy') {
console.warn(
`[react-native-acoustic-connect-beta] Skipping Android config ` +
`propagation: app/build.gradle is '${cfg.modResults.language}', ` +
`expected 'groovy'. Add the following line manually:\n` +
` ${CONFIG_GRADLE_APPLY}`
)
return cfg
}
cfg.modResults.contents = appendConfigGradleApply(cfg.modResults.contents)
return cfg
})
/** True when ConnectConfig.json at `projectRoot` has Connect.PushEnabled === true. */
function isPushEnabled(projectRoot: string): boolean {
const configPath = path.join(projectRoot, 'ConnectConfig.json')
if (!fs.existsSync(configPath)) return false
try {
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8')) as {
Connect?: { PushEnabled?: unknown }
}
return parsed.Connect?.PushEnabled === true
} catch {
return false
}
}
/**
* Fail fast at prebuild when push is enabled but `android.package` is absent
* from the configured `google-services.json`. FCM matches its client by package
* name, so a mismatch makes Gradle fail much later at
* `:app:processDebugGoogleServices` with the opaque "No matching client found
* for package name …" — and only after a full prebuild + Gradle config. Surface
* it here, at the moment the native project is (re)generated, with the exact fix
* (CA-144135 §10b). `acoustic-connect doctor` performs the same check up front;
* this is the guard for a developer who runs `expo run:android` directly.
*
* Only the genuine MISMATCH throws. A missing package / googleServicesFile is
* left to `doctor` and the build itself (this mod doesn't duplicate those).
*/
export const withConnectAndroidGoogleServicesMatch: ConfigPlugin<
ConnectPluginProps
> = (config) =>
withDangerousMod(config, [
'android',
async (cfg) => {
const projectRoot = cfg._internal?.projectRoot
if (!projectRoot || !isPushEnabled(projectRoot)) return cfg
const androidPackage = cfg.android?.package
if (!androidPackage) return cfg
// Most Expo + FCM consumers omit android.googleServicesFile and rely on
// the default ./google-services.json at the project root. Mirror that
// default (and `acoustic-connect doctor`'s) so the match check isn't
// silently skipped for the common case.
const gsFile = cfg.android?.googleServicesFile ?? 'google-services.json'
const gsPath = path.isAbsolute(gsFile)
? gsFile
: path.join(projectRoot, gsFile)
if (!fs.existsSync(gsPath)) return cfg // missing file is doctor's / the build's concern
let parsed: {
client?: Array<{
client_info?: { android_client_info?: { package_name?: string } }
}>
}
try {
parsed = JSON.parse(fs.readFileSync(gsPath, 'utf8'))
} catch {
return cfg // malformed google-services.json — doctor/build will report it
}
const packages = (parsed.client ?? [])
.map((c) => c.client_info?.android_client_info?.package_name)
.filter((p): p is string => typeof p === 'string')
if (packages.length > 0 && !packages.includes(androidPackage)) {
throw new Error(
`[react-native-acoustic-connect-beta] android.package "${androidPackage}" ` +
`has no matching client in ${gsFile} (it has: ${packages.join(', ')}).\n\n` +
`FCM matches its client by package name, so the Android build would ` +
`fail at :app:processDebugGoogleServices. Register "${androidPackage}" in ` +
`the same Firebase project and re-download google-services.json, or set ` +
`app.json android.package to one of the registered packages.\n` +
`Note: changing android.package requires a clean prebuild — ` +
`\`npx expo prebuild --platform android --clean\`.`
)
}
return cfg
},
])