smartech-base-expo-plugin
Version:
Smartech Base Expo SDK's React Native Plugin For React Native Projects.
399 lines (322 loc) • 14.5 kB
text/typescript
import {
AndroidConfig,
ConfigPlugin,
withAndroidManifest,
withDangerousMod,
withGradleProperties
} from '@expo/config-plugins';
import { ExpoConfig } from 'expo/config';
import { promises as fs } from 'fs';
import { SmartechBaseProps } from './smartechBaseProps';
import { Helper } from './helper';
import { appBuildGradlePath, AutoFetchLocationKey, dependency, hanselTestDeviceSnippet, repositorySnippet, smartechHanselEncryptionKey, smartechInitSnippet, smartechInitSnippetJava, smartechInitSnippetKotlin, hanselTestDeviceUpdatedSnippet, smartechTestDeviceSnippet } from './androidConstants';
import path from 'path';
const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;
const modifyMainApplication = async (filePath: string, props: SmartechBaseProps) => {
const fileExtension = path.extname(filePath);
let fileContent = await fs.readFile(filePath, 'utf-8');
let isKotlinFile = props.android.isKotlinProject;
// adding the import statement
const importStatement = isKotlinFile ? smartechInitSnippet.createImportStatementKotlin(props) : smartechInitSnippet.createImportStatementJava(props);
if (!fileContent.includes(importStatement)) {
const packageIndex = fileContent.indexOf('package');
const insertIndex = fileContent.indexOf('\n', packageIndex) + 1;
fileContent = `${fileContent.slice(0, insertIndex)}\n${importStatement}\n${fileContent.slice(insertIndex)}`;
}
let onCreateMethodRegex = isKotlinFile ? smartechInitSnippet.onCreateMethodRegexKotlin : smartechInitSnippet.onCreateMethodRegexJava;
let newArchConditionRegex = isKotlinFile ? smartechInitSnippet.newArchConditionRegexKotlin : smartechInitSnippet.newArchConditionRegexJava;
const onCreateMatch = fileContent.match(onCreateMethodRegex);
if (!onCreateMatch) {
throw new Error('Could not find the onCreate method in MainApplication.');
}
let onCreateBody = onCreateMatch[0].trim();
const areLogsEnabled = props.android.isLogEnabled;
const smartechInitCodeBlock = props.android.isKotlinProject ? smartechInitSnippetKotlin(props).trim() : smartechInitSnippetJava(props).trim();
// If the flag is true, insert the custom code inside the New Architecture check.
if (props.isNewArchEnabled) {
const newArchCheckRegex = smartechInitSnippet.newArchCheckRegex;
const newArchCheckMatch = onCreateBody.match(newArchCheckRegex);
if (newArchCheckMatch) {
const newArchCheckBody = newArchCheckMatch[1].trim();
const modifiedNewArchCheckBody = `${newArchCheckBody}\n ${smartechInitCodeBlock} \n`;
onCreateBody = onCreateBody.replace(newArchCheckBody, modifiedNewArchCheckBody);
} else {
onCreateBody += `\n ${smartechInitCodeBlock} \n`;
}
} else {
onCreateBody += `\n ${smartechInitCodeBlock}\n`;
}
// Replace the original onCreate method with the modified one
fileContent = fileContent.replace(onCreateMethodRegex, onCreateBody);
// Save the modified content back to the file
await fs.writeFile(filePath, fileContent, 'utf-8');
};
// Expo Config Plugin to modify MainApplication.kt or MainApplication.java
const addSmartechInitCode: ConfigPlugin<SmartechBaseProps> = (
config,
props
) => {
return withDangerousMod(config, [
'android',
async (config) => {
const isKotlinFile = props.android.isKotlinProject;
const packageName = config.android?.package ?? '';
const mainApplicationJavaPath = `./android/app/src/main/java/${packageName.replace(/\./g, '/')}/MainApplication.java`;
const mainApplicationKotlinPath = `./android/app/src/main/java/${packageName.replace(/\./g, '/')}/MainApplication.kt`;
const mainApplicationPath = isKotlinFile ? mainApplicationKotlinPath : mainApplicationJavaPath;
await modifyMainApplication(mainApplicationPath, props);
return config;
},
]);
};
const addMetaDataToManifest: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withAndroidManifest(config, async config => {
const androidManifest = config.modResults;
const mainApplication = getMainApplicationOrThrow(androidManifest);
// Adding Smartech MetaData to Manifest
const smartechMetaData = props.android.smartechMetaData
if (smartechMetaData) {
smartechMetaData.forEach(item => {
addMetaDataItemToMainApplication(mainApplication, item.name, item.value)
});
}
// If autoFetchLocation is enabled then add it inside the manifest file,
if (props.android.autoFetchLocation != undefined) {
addMetaDataItemToMainApplication(mainApplication, AutoFetchLocationKey, props.android.autoFetchLocation ? "1" : "0")
}
/**
* Check Hansel is enabled or not, If not then we dont
* add the meta data to manifest.
*/
const isHanselEnabled = props.android.smartechNudges.smartechNudgesEnabled;
if (isHanselEnabled) {
const hanselMetaData = props.android.smartechNudges.smartechNudgesMetaData;
hanselMetaData.forEach(item => {
addMetaDataItemToMainApplication(mainApplication, item.name, item.value);
})
// check for the Encryption value from
addMetaDataItemToMainApplication(mainApplication, smartechHanselEncryptionKey, `${props.android.smartechNudges.useEncryption}`);
}
return config;
});
};
// Helper function to check if the dependency exists in the contents
function containsDependency(contents: string, dependency: string) {
const regex = new RegExp(`api\\s+"${dependency}[^"]*"`);
return regex.test(contents);
}
// Set custom Android build.gradle modifications
const addSmartechDependency: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withDangerousMod(config, [
'android',
async (config) => {
const appBuildGradleContents = await fs.readFile(appBuildGradlePath, 'utf-8');
let modifiedAppContents = appBuildGradleContents.replace(
'dependencies {',
`${repositorySnippet}\n\ndependencies {`
);
// Adding Smartech Base dependency in build.gradle
modifiedAppContents = Helper.Android.addDependency(modifiedAppContents, dependency.smartechBase, props.android.SMARTECH_BASE_SDK_VERSION)
await fs.writeFile(appBuildGradlePath, modifiedAppContents, 'utf-8');
return config;
}
]);
};
const modifyMainActivityForTestDevice = (content: string, addTestDeviceHansel: boolean, addTestDeviceSmartech: boolean, isKotlin: boolean): { modifiedContent: string, importsAdded: string[] } => {
const superOnCreate = 'super.onCreate';
const superIndex = content.indexOf(superOnCreate);
const insertIndex = content.indexOf(')', superIndex) + 1;
if (superIndex === -1) {
console.warn('super.onCreate not found in MainActivity');
return { modifiedContent: content, importsAdded: [] };
}
let modified = content;
// Implementing the AddTestDevice code
const linesToInsert: string[] = [];
const importsAdded: string[] = [];
const hanselSnippetUpdated = hanselTestDeviceUpdatedSnippet(isKotlin);
const smartechSnippet = smartechTestDeviceSnippet(isKotlin);
if (addTestDeviceHansel) {
linesToInsert.push(hanselSnippetUpdated.methodStatement);
importsAdded.push(hanselSnippetUpdated.importStatement);
}
if (addTestDeviceSmartech) {
linesToInsert.push(smartechSnippet.methodStatement);
importsAdded.push(smartechSnippet.importStatement);
}
// Add lines to onCreate if any
if (linesToInsert.length > 0) {
const indent = content.substring(0, superIndex).match(/\s*$/)?.[0] || ' ';
const formattedLines = linesToInsert
.map(line => `${indent}${line}`)
.join('\n');
modified = [
modified.slice(0, insertIndex),
`\n${formattedLines}`,
modified.slice(insertIndex),
].join('');
}
return { modifiedContent: modified, importsAdded };
}
const addTestDevice: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withDangerousMod(config, [
'android',
async (config) => {
const isKotlin = props.android.isKotlinProject;
const isHanselEnabled = props.android.smartechNudges.smartechNudgesEnabled;
const addTestDeviceForHansel = props.android.smartechNudges.addTestDevice ?? false;
const addTestDeviceForSmartech = props.android.addTestDevice ?? false;
const packageName = config.android?.package ?? '';
const mainActivityDir = path.resolve(
config.modRequest.projectRoot,
`android/app/src/main/java/${packageName.replace(/\./g, '/')}`
);
const mainActivityPath = path.join(mainActivityDir, isKotlin ? 'MainActivity.kt' : 'MainActivity.java')
// Check if the file exists
try {
let content = await fs.readFile(mainActivityPath, 'utf-8');
const { modifiedContent, importsAdded } = modifyMainActivityForTestDevice(
content,
(isHanselEnabled && addTestDeviceForHansel),
addTestDeviceForSmartech,
isKotlin
);
// Add imports at the top if any
if (importsAdded.length > 0) {
const packageIndex = modifiedContent.indexOf('package');
if (packageIndex === -1) throw new Error('Package statement not found');
const insertIndex = modifiedContent.indexOf('\n', packageIndex) + 1;
content = [
modifiedContent.slice(0, insertIndex),
importsAdded.join('\n') + '\n',
modifiedContent.slice(insertIndex),
].join('');
} else {
content = modifiedContent;
}
await fs.writeFile(mainActivityPath, content, 'utf-8');
} catch (err) {
console.error(`MainActivity file does not exist at path: ${mainActivityPath}`);
}
return config;
},
]);
};
// Main plugin function to create xml directory and copy the native.xml file
const withAndroidXMLFilesBackup: ConfigPlugin<SmartechBaseProps> = (config, props) => {
try {
const allowBackup = config.android?.allowBackup ?? false;
// If allowBackup is not true, skip the rest of the operations
if (!allowBackup) {
return config;
}
const targetSdkVersion = config.plugins?.find(
([pluginName]) => pluginName === 'expo-build-properties'
)?.[1]?.android?.targetSdkVersion || 31; // Fallback to 31 if not defined
config = withDangerousMod(config, ['android', async (config) => {
// Path to the React Native app's assets/native.xml file
const projectRoot = config.modRequest.projectRoot;
// Read the backup folder path from the config file
const backupAssetFolder = props.android.backupXMLFiles;
if (!backupAssetFolder || backupAssetFolder.length === 0) {
console.log('backupXMLFiles path is either undefined or empty');
return config;
}
// Construct the full path to the backup folder
const backupFolder = path.join(projectRoot, backupAssetFolder);
// Ensure the folder exists
await ensureDirectoryExists(backupFolder);
const sourceFile = path.join(backupAssetFolder, 'backup.xml');
const sourceFile31 = path.join(backupAssetFolder, 'backup_31.xml');
// Path to the Android res/xml directory
const xmlDir = path.join(projectRoot, 'android', 'app', 'src', 'main', 'res', 'xml');
// Ensure the xml directory exists
ensureDirectoryExists(xmlDir);
// Destination path for the native.xml file inside res/xml
const destinationFile = path.join(xmlDir, 'backup.xml');
copyFile(sourceFile, destinationFile);
if (targetSdkVersion >= 31) {
// Destination path for the native.xml file inside res/xml
const destinationFile31 = path.join(xmlDir, 'backup_31.xml');
copyFile(sourceFile31, destinationFile31);
}
return config;
}]);
// Modify the AndroidManifest.xml
config = withAndroidManifest(config, (config) => {
const application = config.modResults.manifest.application?.[0];
if (application) {
application['$']['android:fullBackupContent'] = '@xml/backup'
if (targetSdkVersion >= 31) {
application['$']['android:dataExtractionRules'] = '@xml/backup_31'
}
}
return config;
});
} catch (error) {
console.error("Error in withAndroidXMLFilesBackup function")
}
return config;
};
// Utility to ensure directory exists asynchronously
const ensureDirectoryExists = async (directory: string): Promise<void> => {
try {
await fs.access(directory);
} catch (error) {
await fs.mkdir(directory, { recursive: true });
}
};
// Utility to copy a file asynchronously if it doesn't already exist
const copyFile = async (source: string, destination: string): Promise<void> => {
try {
await fs.access(source);
try {
await fs.access(destination);
console.log(`${destination} already exists. Skipping file copy.`);
} catch (error) {
// Destination does not exist, proceed to copy
await fs.copyFile(source, destination);
}
} catch (error) {
console.warn(`Source file ${source} does not exist`);
}
};
export const withNetcoreAndroid: ConfigPlugin<SmartechBaseProps> = (config, props) => {
try {
config = withAndroidXMLFilesBackup(config, props);
config = addMetaDataToManifest(config, props);
config = updateGradlePropertise(config, props);
config = addSmartechDependency(config, props);
config = addSmartechInitCode(config, props);
config = addTestDevice(config, props);
} catch (error) {
console.error('Failed to add smartech base expo plugin custom code with error:', error);
}
return config;
};
/**
*
* This method will add the Smartech gradle dependencies to the build.gradle.
*
* @param config Expo Config
* @param props SmartechBaseProps
* @returns modified config
*/
const updateGradlePropertise = (config: ExpoConfig, props: SmartechBaseProps) => {
return withGradleProperties(config, async (config) => {
const customProperties = [
{ key: 'SMARTECH_BASE_SDK_VERSION', value: props.android.SMARTECH_BASE_SDK_VERSION }
];
// Update the modResults with custom properties
for (const property of customProperties) {
if (!config.modResults.some((item: any) => item.type === 'property' && item.key === property.key)) {
config.modResults.push({
type: 'property',
key: property.key,
value: property.value,
});
}
}
return config;
});
};