UNPKG

smartech-base-expo-plugin

Version:

Smartech Base Expo SDK's React Native Plugin For React Native Projects.

399 lines (322 loc) 14.5 kB
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; }); };