react-native-acoustic-mobile-push-beta
Version:
BETA: Acoustic Mobile Push Plugin
532 lines (457 loc) • 21.7 kB
JavaScript
/*
* Copyright (C) 2025 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.
*/
const fs = require('fs');
const plist = require('plist');
const path = require('path');
const ncp = require('ncp');
const xml2js = require('xml2js');
const chalk = require('chalk');
const { execSync } = require('child_process');
function findInstallDirectory() {
if(process.env.MCE_RN_DIRECTORY) {
console.log(chalk.yellow.bold("Using MCE_RN_DIRECTORY override instead of finding the application source directory."))
return process.env.MCE_RN_DIRECTORY;
}
// Mac
var currentDirectory = process.argv[ process.argv.length-1 ];
if(currentDirectory != "$INIT_CWD") {
return currentDirectory;
}
// Windows
currentDirectory = process.cwd();
while(!fs.existsSync(path.join(currentDirectory, "app.json"))) {
var parentDirectory = path.dirname(currentDirectory);
console.log("cwd: ", currentDirectory, ", parent: ", parentDirectory);
if(parentDirectory == currentDirectory) {
console.error(chalk.red("Could not find installation directory!"));
return;
}
currentDirectory = parentDirectory;
}
console.log("Install Directory Found:", currentDirectory);
return currentDirectory;
}
function containsStanza(array, stanza, type) {
for(var i = 0; i < array.length; i++) {
if(array[i]['$']['android:name'] == stanza[type]['$']['android:name']) {
return true
}
}
return false;
}
function verifyStanza(array, stanzaString) {
if(typeof array == "undefined") {
array = [];
}
new xml2js.Parser().parseString(stanzaString, function (err, stanza) {
const types = Object.getOwnPropertyNames(stanza);
const type = types[0];
if( !containsStanza(array, stanza, type) ) {
console.log("Adding required " + type + " stanza to AndroidManifest.xml");
array.push( stanza[type] )
}
});
return array;
}
function modifyManifest(installDirectory) {
let manifestPath = path.join(installDirectory, "android", "app", "src", "main", "AndroidManifest.xml");
new xml2js.Parser().parseString(fs.readFileSync(manifestPath), function (err, document) {
console.log("Adding required xmlns:tools schemas, tools:replace attributes to AndroidManifest.xml");
// Add xmlns:tools attribute to manifest if not exists
if (!document.manifest.$['xmlns:tools']) {
document.manifest.$['xmlns:tools'] = "http://schemas.android.com/tools";
}
// Add tools:replace="android:name" to application if not exists
if (!document.manifest.application[0].$['tools:replace']) {
document.manifest.application[0].$['tools:replace'] = "android:name";
}
console.log("Adding required receivers to AndroidManifest.xml");
var receivers = document.manifest.application[0].receiver;
[
'<receiver android:name="co.acoustic.mobile.push.sdk.wi.AlarmReceiver" tools:replace="android:exported" android:exported="true"><intent-filter><action android:name="android.intent.action.BOOT_COMPLETED" /></intent-filter><intent-filter><action android:name="android.intent.action.TIMEZONE_CHANGED" /></intent-filter><intent-filter><action android:name="android.intent.action.PACKAGE_REPLACED" /><data android:scheme="package" /></intent-filter><intent-filter><action android:name="android.intent.action.LOCALE_CHANGED" /></intent-filter></receiver>',
'<receiver android:name="co.acoustic.mobile.push.RNAcousticMobilePushBroadcastReceiver" android:exported="true"><intent-filter><action android:name="co.acoustic.mobile.push.sdk.NOTIFIER" /></intent-filter></receiver>',
'<receiver android:name="co.acoustic.mobile.push.sdk.notification.NotifActionReceiver" />',
'<receiver android:name="co.acoustic.mobile.push.sdk.location.LocationBroadcastReceiver"/>'
].forEach((receiver) => {
receivers = verifyStanza(receivers, receiver);
});
document.manifest.application[0].receiver = receivers;
console.log("Adding required providers to AndroidManifest.xml");
var providers = document.manifest.application[0].provider;
var provider = '<provider android:name="co.acoustic.mobile.push.sdk.db.Provider" android:authorities="${applicationId}.MCE_PROVIDER" android:exported="false" />';
document.manifest.application[0].provider = verifyStanza(providers, provider);
console.log("Adding required services to AndroidManifest.xml");
var services = document.manifest.application[0].service;
[
'<service android:name="co.acoustic.mobile.push.sdk.session.SessionTrackingIntentService"/>',
'<service android:name="co.acoustic.mobile.push.sdk.events.EventsAlarmListener" />',
'<service android:name="co.acoustic.mobile.push.sdk.registration.PhoneHomeIntentService" />',
'<service android:name="co.acoustic.mobile.push.sdk.registration.RegistrationIntentService" />',
'<service android:name="co.acoustic.mobile.push.sdk.attributes.AttributesQueueConsumer" />',
'<service android:name="co.acoustic.mobile.push.sdk.job.MceJobService" android:permission="android.permission.BIND_JOB_SERVICE"/>',
'<service android:name="co.acoustic.mobile.push.sdk.messaging.fcm.FcmMessagingService"><intent-filter><action android:name="com.google.firebase.MESSAGING_EVENT"/></intent-filter></service>'
].forEach((service) => {
services = verifyStanza(services, service);
});
document.manifest.application[0].service = services;
console.log("Adding internet, wake lock, boot, vibrate and call_phone permisssions to AndroidManifest.xml");
var permissions = document.manifest['uses-permission'];
[
'<uses-permission android:name="android.permission.INTERNET"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.CALL_PHONE"/>'
].forEach((permission) => {
permissions = verifyStanza(permissions, permission);
});
document.manifest['uses-permission'] = permissions;
var output = new xml2js.Builder().buildObject(document);
fs.writeFileSync(manifestPath, output);
});
}
function modifyInfoPlist(mainAppPath) {
if(!fs.existsSync(mainAppPath) || !fs.lstatSync(mainAppPath).isDirectory()) {
console.error("Incorrect main app path: " + mainAppPath);
return;
}
const infoPath = path.join(mainAppPath, "Info.plist");
if(!fs.existsSync(infoPath) || !fs.lstatSync(infoPath).isFile()) {
console.error("Couldn't locate Info.plist.");
return;
}
var infoPlist = plist.parse(fs.readFileSync(infoPath, 'utf8'));
if(typeof infoPlist.UIBackgroundModes == "undefined") {
infoPlist.UIBackgroundModes = [];
}
var backgroundSet = new Set(infoPlist.UIBackgroundModes);
console.log("Adding remote-notification and fetch to UIBackgroundModes of info.plist");
backgroundSet.add("remote-notification");
backgroundSet.add("fetch");
infoPlist.UIBackgroundModes = Array.from(backgroundSet);
fs.writeFileSync(infoPath, plist.build(infoPlist), "utf8");
}
function findMainPath(installDirectory) {
if(!fs.existsSync(installDirectory)) {
console.error("Couldn't locate install directory.");
return;
}
const iosDirectory = path.join(installDirectory, 'ios');
var directory;
fs.readdirSync(iosDirectory).forEach((basename) => {
const mainPath = path.join(iosDirectory, basename);
if(fs.lstatSync(mainPath).isDirectory()) {
const filename = path.join(mainPath, "main.m");
if(fs.existsSync(filename)) {
directory = mainPath;
}
}
});
return directory;
}
function replaceMain(mainAppPath) {
if(!fs.existsSync(mainAppPath) || !fs.lstatSync(mainAppPath).isDirectory()) {
console.error(chalk.red("Incorrect main app path: " + mainAppPath));
return;
}
const mainPath = path.join(mainAppPath, "main.m");
if(!fs.existsSync(mainPath) || !fs.lstatSync(mainPath).isFile()) {
console.error(chalk.red("Couldn't locate main.m."));
return;
}
const backupMainPath = mainPath + "-backup";
if(!fs.existsSync(backupMainPath)) {
console.log("Backing up main.m as main.m-backup");
fs.renameSync(mainPath, backupMainPath);
console.log("Replacing main.m with SDK provided code");
fs.copyFileSync(path.join("postinstall", "ios", "main.m"), mainPath);
}
}
function stringExists(name, strings) {
for(var i=0; i<strings.resources.string.length; i++) {
if(strings.resources.string[i]['$'].name == name) {
return true;
}
}
return false;
}
function verifyString(name, strings) {
if(!stringExists(name, strings)) {
new xml2js.Parser().parseString('<string name="' + name + '">REPLACE THIS PLACEHOLDER</string>', function (err, string) {
strings.resources.string.push( string.string );
});
}
return strings;
}
function modifyStrings(installDirectory) {
if(process.env.MCE_RN_NOSTRINGS) {
console.log(chalk.yellow.bold("Android strings.xml will not be modified because MCE_RN_NOSTRINGS environment flag detected."));
return;
}
let stringsPath = path.join(installDirectory, "android", "app", "src", "main", "res", "values", "strings.xml");
console.log("Modifying strings.xml in Android project");
new xml2js.Parser().parseString(fs.readFileSync(stringsPath), function (err, strings) {
// TODO: it seems that both strings cannot be added to strings.xml as build will
// fail with information about duplicated entries
return;
["google_api_key", "google_app_id"].forEach((name) => {
verifyString(name, strings);
});
var output = new xml2js.Builder().buildObject(strings);
fs.writeFileSync(stringsPath, output);
});
}
if(process.env.MCE_RN_NOCONFIG) {
console.log(chalk.yellow.bold("Acoustic Mobile Push SDK installed, but will not be auto configured because MCE_RN_NOCONFIG environment flag detected."));
return;
}
/**
* Copies the CampaignConfig.json file from the plugin directory to the project directory
* if it does not exist. It then reads this configuration file to apply specific configurations
* for iOS and Android.
*/
function addOrReplaceMobilePushConfigFile(installDirectory) {
const configName = 'CampaignConfig.json';
const pluginPath = path.resolve(__dirname, '');
const configPath = path.join(pluginPath, configName);
const appConfigPath = path.join(installDirectory, configName);
console.log("Add or Replace CampaignConfig.json file into the App - " + configPath);
if(!fs.existsSync(appConfigPath)) {
console.log("Copying CampaignConfig.json file into project - " + appConfigPath);
fs.copyFileSync(configPath, appConfigPath);
} else {
console.log("CampaignConfig.json already exists at " + appConfigPath);
}
// Read and save cooresponding ios/android MceConfig.json sections from CampaignConfig.json
readAndSaveMceConfig(installDirectory, pluginPath, appConfigPath);
}
/**
* Reads the provided mobile push configuration file, extracts platform-specific configurations,
* and saves these configurations to respective directories. Optionally updates Android's build.gradle.
*
* @param {string} installDirectory - The path to the Sample app's directory.
* @param {string} pluginPath - The path to the plugin directory.
* @param {string} campaignConfigFilePath - The path to the CampaignConfig.json file.
*/
function readAndSaveMceConfig(installDirectory, pluginPath, campaignConfigFilePath) {
try {
// Read the file synchronously
const fileData = fs.readFileSync(campaignConfigFilePath, 'utf8');
const jsonData = JSON.parse(fileData);
if (jsonData.android && jsonData.iOS) {
if (jsonData.android) {
const androidConfig = JSON.stringify(jsonData.android, null, 2);
const androidDestinationPath = path.join(pluginPath, 'postinstall/android/MceConfig.json');
saveConfig(androidConfig, androidDestinationPath);
updateAndroidBuildGradle(jsonData.useRelease);
const gradlePropertiesPath = path.join(campaignConfigFilePath, '../android/gradle.properties');
updateCampaignSDKVersionInProperties(gradlePropertiesPath, jsonData.androidVersion);
const destinationDirectory = path.join(installDirectory, "android", "app", "src", "main", "assets");
// Create assets directory if it doesn't exist
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirSync(destinationDirectory, { recursive: true });
console.log(`Created assets directory at: ${destinationDirectory}`);
}
const androidAppPath = path.join(destinationDirectory, "MceConfig.json");
saveConfig(androidConfig, androidAppPath);
}
if (jsonData.iOS) {
const iosConfig = JSON.stringify(jsonData.iOS, null, 2);
const iosDestinationPath = path.join(pluginPath, 'postinstall/ios/MceConfig.json');
saveConfig(iosConfig, iosDestinationPath);
const iosAppPath = path.join(mainAppPath, "MceConfig.json");
saveConfig(iosConfig, iosAppPath);
}
} else {
console.error('No "android/ios" object found in the JSON file.');
}
managePlugins(jsonData);
} catch (error) {
if (error.code === 'ENOENT') { // Handle "file not found" error specifically
console.error(`File not found: ${campaignConfigFilePath}`);
} else {
console.error('Error reading or parsing JSON file:', error);
}
}
}
/**
* Saves the specified configuration data to a given destination path.
*
* @param {string} configData - The configuration data to be saved.
* @param {string} destinationPath - The file path where the configuration data should be saved.
*/
function saveConfig(configData, destinationPath) {
try {
fs.writeFileSync(destinationPath, configData, { flag: 'w' });
console.log(`${destinationPath} saved successfully!`);
} catch (error) {
console.error(`Error saving ${destinationPath}:`, error);
}
}
/**
* Checks if the specified plugin is installed by looking into the project's package.json.
*
* @param {string} pluginName - The name of the plugin to check.
* @returns {boolean} True if the plugin is installed, false otherwise.
*/
function isPluginInstalled(pluginName) {
const packageJsonPath = 'package.json';
const packageJsonData = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonData);
return packageJson.dependencies && packageJson.dependencies[pluginName] ||
packageJson.devDependencies && packageJson.devDependencies[pluginName];
}
/**
* Manages the installation or removal of plugins based on the specified configuration object.
* The choice of package manager (npm or yarn) is determined by the presence of their respective lock files.
*
* @param {Object} config - An object containing the configuration for plugins.
*/
function managePlugins(config) {
// Determine the package manager by checking for lock files
const hasYarnLock = fs.existsSync(path.join(process.cwd(), 'yarn.lock'));
const hasNpmLock = fs.existsSync(path.join(process.cwd(), 'package-lock.json'));
const packageManager = hasYarnLock ? 'yarn' : (hasNpmLock ? 'npm' : null);
if (!packageManager) {
console.error('No lock file found. Please ensure you are in the correct directory and try again.');
return;
}
Object.entries(config.plugins).forEach(([plugin, isEnabled]) => {
if (plugin.includes("plugins")) return;
const installed = isPluginInstalled(plugin);
try {
if (isEnabled && !installed) {
console.log(`Adding ${plugin}...`);
if (packageManager === 'yarn') {
execSync(`yarn add ${plugin}`, { stdio: 'inherit', cwd: process.cwd() });
} else {
execSync(`npm install ${plugin}`, { stdio: 'inherit', cwd: process.cwd() });
}
} else if (!isEnabled && installed) {
console.log(`Removing ${plugin}...`);
if (packageManager === 'yarn') {
execSync(`yarn remove ${plugin}`, { stdio: 'inherit', cwd: process.cwd() });
} else {
execSync(`npm uninstall ${plugin}`, { stdio: 'inherit', cwd: process.cwd() });
}
}
} catch (error) {
console.error(`Failed to manage plugin ${plugin}:`, error);
}
});
}
/**
* Updates build.gradle file for Android project to remove Maven URL if useRelease is true and it exists.
* Adds Maven URL if useRelease is true and it doesn't already exist.
* Logs a message indicating whether changes were made or not.
*
*
* @param {Object} useRelease - True/False for release version of SDK.
*/
function updateAndroidBuildGradle(useRelease) {
const buildGradlePath = path.join(process.cwd(), "android", "build.gradle");
try {
let buildGradleContent = fs.readFileSync(buildGradlePath, 'utf8');
const mavenUrlRegex = /maven\s*{\s*url\s*"https:\/\/s01\.oss\.sonatype\.org\/content\/groups\/staging"\s*}/;
if (useRelease && mavenUrlRegex.test(buildGradleContent)) {
// Remove Maven URL if useRelease is true and it exists
buildGradleContent = buildGradleContent.replace(mavenUrlRegex, '');
fs.writeFileSync(buildGradlePath, buildGradleContent, 'utf8');
console.log('Maven URL removed from build.gradle');
} else if (!useRelease && !mavenUrlRegex.test(buildGradleContent)) {
// Add Maven URL if useRelease is true and it doesn't already exist
const repositoriesIndex = buildGradleContent.lastIndexOf('repositories {');
const closingBracketIndex = buildGradleContent.indexOf('}', repositoriesIndex);
const mavenUrlString = ' maven { url "https://s01.oss.sonatype.org/content/groups/staging" }\n';
buildGradleContent = buildGradleContent.slice(0, closingBracketIndex) + mavenUrlString + buildGradleContent.slice(closingBracketIndex);
fs.writeFileSync(buildGradlePath, buildGradleContent, 'utf8');
console.log('Maven URL added to build.gradle');
} else {
console.log('No changes needed in build.gradle');
}
} catch (error) {
console.error('Error updating build.gradle:', error);
}
}
/**
* Updates or inserts the 'campaignSDKVersion' property in the gradle.properties file.
* @param {string} propertiesFilePath - The path to the gradle.properties file.
* @param {string} version - The version value to set for 'campaignSDKVersion'.
*/
function updateCampaignSDKVersionInProperties(propertiesFilePath, version) {
fs.readFile(propertiesFilePath, { encoding: 'utf-8' }, (err, data) => {
if (err) {
console.error('Error reading gradle.properties:', err);
return;
}
const propName = 'campaignSDKVersion';
const versionPattern = /^\d+\.\d+\.\d+$/; // Pattern to match x.x.x version format
const propRegex = new RegExp(`(^${propName}=).*`, 'm');
let updatedData;
// Determine the value to set for campaignSDKVersion
let versionValue = (version === '+' || version.match(versionPattern)) ? version : '+';
// Check if 'campaignSDKVersion' exists and update or insert it
if (data.match(propRegex)) {
// Replace the existing value
console.log('Version value invalid, default to use latest build.');
updatedData = data.replace(propRegex, `$1${versionValue}`);
} else {
// Insert the new property
updatedData = data.trim() + `\n${propName}=${versionValue}\n`;
}
// Write the updated content back to the gradle.properties file
fs.writeFile(propertiesFilePath, updatedData, (err) => {
if (err) {
console.error('Error writing gradle.properties:', err);
return;
}
console.log('gradle.properties has been updated successfully.');
});
});
}
/**
* Start of the script
*/
console.log(chalk.green.bold("Setting up Acoustic Campaign SDK"));
// Get plugin's recommended Node version
let recommendedNodeVersion;
try {
// First check our direct package.json
const packageJsonPath = path.join(__dirname, 'package.json');
// Then check React Native's package.json
const rnPackageJsonPath = path.join(__dirname, '..', 'react-native', 'package.json');
if (fs.existsSync(rnPackageJsonPath)) {
const rnPackageJson = JSON.parse(fs.readFileSync(rnPackageJsonPath, 'utf8'));
if (rnPackageJson.engines && rnPackageJson.engines.node) {
recommendedNodeVersion = rnPackageJson.engines.node;
}
}
console.log(chalk.blue(`This script recommends Node.js version ${recommendedNodeVersion || 'unknown'} (based on React Native requirements)`));
console.log(chalk.blue("Your current Node.js version is: " + process.version));
} catch (error) {
console.log(chalk.yellow("Could not determine recommended Node.js version"));
console.log(chalk.blue("Your current Node.js version is: " + process.version));
}
const installDirectory = findInstallDirectory();
const mainAppPath = findMainPath(installDirectory);
addOrReplaceMobilePushConfigFile(installDirectory);
replaceMain(mainAppPath);
modifyInfoPlist(mainAppPath);
modifyManifest(installDirectory);
modifyStrings(installDirectory);
console.log(chalk.green("Installation Complete!"));
console.log(chalk.blue.bold("\nPost Installation Steps\n"));
console.log(chalk.blue('iOS Support:'));
console.log("1. Open the iOS project in Xcode.");
console.log("2. In the `Capabilities` tab of the main app target, enable push notifications by turning the switch to the on position");
console.log("3. Then add a new `Notification Service Extension` target");
console.log("4. Replace the contents of `NotificationService.m` and `NotificationService.h` with the ones provided in the `react-native-acoustic-mobile-push Notification Service` folder");
console.log("5. Add the `MceConfig.json` file in the project directory to the xcode project to **Application** AND **Notification Service** targets");
console.log("6. Adjust the `baseUrl` and `appKey`s provided by your account team");