smartech-base-expo-plugin
Version:
Smartech Base Expo SDK's React Native Plugin For React Native Projects.
509 lines (447 loc) • 23.6 kB
text/typescript
import {
ConfigPlugin, withInfoPlist,
withAppDelegate,
withEntitlementsPlist,
withDangerousMod
} from "@expo/config-plugins";
import { promises as fs } from 'fs';
import path from 'path';
import fSystem from 'fs';
import { ExpoConfig } from "@expo/config-types";
import { SmartechBaseProps } from "./smartechBaseProps";
import { appdelegateSnippet, smartechBaseSnippet, swiftImports, smartechBaseSwiftSnippet, deepLinkHandlerSwiftSnippet } from "./iosConstants";
import { Helper } from "./helper";
type URLSchemeEntry = {
CFBundleTypeRole: string;
CFBundleURLName: string;
CFBundleURLSchemes: string[];
};
const addURLSchemeEntry = (
existingURLTypes: URLSchemeEntry[],
urlName: string,
urlSchema: string
): URLSchemeEntry[] => {
const newEntry: URLSchemeEntry = {
CFBundleTypeRole: "Editor",
CFBundleURLName: urlName,
CFBundleURLSchemes: [urlSchema],
};
const isNewEntryAlreadyAdded = existingURLTypes.some((entry) => (
entry.CFBundleURLName === newEntry.CFBundleURLName &&
entry.CFBundleURLSchemes.includes(newEntry.CFBundleURLSchemes[0])
));
if (!isNewEntryAlreadyAdded) {
existingURLTypes.push(newEntry);
}
return existingURLTypes;
};
// Set custom entitlements plist
const modifyEntitlementsPlist: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withEntitlementsPlist(config, async (newConfig) => {
newConfig.modResults = {
...newConfig.modResults,
"com.apple.security.application-groups": [`group.${props.ios.groupIdentifier}`],
};
return newConfig;
});
};
// Set custom info plist
const modifyInfoPlist: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withInfoPlist(config, (newConfig) => {
newConfig.modResults = {
...newConfig.modResults,
SmartechKeys: {
SmartechAppGroup: `group.${props.ios.groupIdentifier}`,
SmartechAppId: props.ios.appId,
SmartechAutoFetchLocation: props.ios.autoFetchLocation,
SmartechUseAdvId: props.ios.useAdvID,
},
...(props.ios.smartechNudges.smartechNudgesEnabled && {
HanselKeys: {
HanselAppId: props.ios.smartechNudges.appId,
HanselAppKey: props.ios.smartechNudges.appKey,
}
})
};
return newConfig;
});
};
const addSmartechURLScheme: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withInfoPlist(config, (newConfig) => {
if (!props.ios.addTestDevice) {
return newConfig;
}
if (props.ios.testDeviceURLName == undefined || props.ios.testDeviceURLSchema == undefined) {
console.log(`Smartech Url scheme ${props.ios.testDeviceURLName} and ${props.ios.testDeviceURLSchema}`);
return newConfig;
}
const modResults = newConfig.modResults;
const existingURLTypes = (modResults.CFBundleURLTypes || []) as URLSchemeEntry[];
modResults.CFBundleURLTypes = addURLSchemeEntry(
existingURLTypes,
props.ios.testDeviceURLName,
props.ios.testDeviceURLSchema
);
return newConfig;
});
};
const addHanselURLScheme: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withInfoPlist(config, (newConfig) => {
if (!props.ios.smartechNudges.smartechNudgesEnabled || !props.ios.smartechNudges.addTestDevice) {
return newConfig;
}
if (props.ios.smartechNudges?.testDeviceURLName == undefined || props.ios.smartechNudges?.testDeviceURLSchema == undefined) {
console.log(`Smartech Nudge Url scheme ${props.ios.smartechNudges?.testDeviceURLName} and ${props.ios.smartechNudges?.testDeviceURLSchema}`);
return newConfig;
}
const modResults = newConfig.modResults;
const existingURLTypes = (modResults.CFBundleURLTypes || []) as URLSchemeEntry[];
modResults.CFBundleURLTypes = addURLSchemeEntry(
existingURLTypes,
props.ios.smartechNudges.testDeviceURLName,
props.ios.smartechNudges.testDeviceURLSchema
);
return newConfig;
});
};
const addSmartechHeaderMod: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withDangerousMod(config, [
'ios',
async (config) => {
const { platformProjectRoot, projectName } = config.modRequest;
if (projectName && platformProjectRoot) {
const isSwift = await Helper.Ios.isSwiftProject(platformProjectRoot, projectName);
if (isSwift) {
// For Swift projects, add imports to AppDelegate.swift
try {
const appDelegateSwiftPath = path.join(platformProjectRoot, projectName, 'AppDelegate.swift');
const appDelegateSwiftContents = await fs.readFile(appDelegateSwiftPath, 'utf-8');
//console.log(`appdelegate content before is ${appDelegateSwiftContents}`);
console.log(`appdelegate path is ${appDelegateSwiftPath}`);
// Add Smartech import if not already present (check for exact import line)
const lines = appDelegateSwiftContents.split('\n');
const hasSmartechImport = lines.some(line => line.trim() === swiftImports.smartechImport);
if (!hasSmartechImport) {
let insertIndex = 0;
// Find the last import statement
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('import ')) {
insertIndex = i + 1;
} else if (lines[i].trim() !== '' && !lines[i].trim().startsWith('import ')) {
break;
}
}
// Insert the Smartech import
lines.splice(insertIndex, 0, swiftImports.smartechImport);
const modifiedContents = lines.join('\n');
//console.log(`appdelegate content is ${modifiedContents}`);
await fs.writeFile(appDelegateSwiftPath, modifiedContents, 'utf-8');
console.log(`${swiftImports.smartechImport} added to AppDelegate.swift at line ${insertIndex + 1}`);
} else {
console.log(`${swiftImports.smartechImport} is already added to AppDelegate.swift`);
}
} catch (error) {
console.error('Error modifying AppDelegate.swift:', error);
}
} else {
// For Objective-C projects (existing logic)
try {
const appDelegateHeaderPath = path.join(platformProjectRoot, projectName, appdelegateSnippet.fileName);
const appDelegateHeaderContents = await fs.readFile(appDelegateHeaderPath, 'utf-8');
// Add Smartech header snipper if not already present.
if (!appDelegateHeaderContents.includes(appdelegateSnippet.smartechHeaderSnippet)) {
const modifiedAppDelegateHeaderContents = `${appDelegateHeaderContents}\n${appdelegateSnippet.smartechHeaderSnippet}`;
await fs.writeFile(appDelegateHeaderPath, modifiedAppDelegateHeaderContents, 'utf-8');
} else {
console.log(`${appdelegateSnippet.smartechHeaderSnippet} is already added.`)
}
} catch (error) {
console.error('Error modifying AppDelegate.h:', error);
}
}
}
return config;
}
]);
};
const addSmartechHanseleHeaderMod: ConfigPlugin<SmartechBaseProps> = (config, props) => {
return withDangerousMod(config, [
'ios',
async (config) => {
const { platformProjectRoot, projectName } = config.modRequest;
if (platformProjectRoot && projectName) {
const isSwift = await Helper.Ios.isSwiftProject(platformProjectRoot, projectName);
if (isSwift) {
// For Swift projects, add imports to AppDelegate.swift
try {
const appDelegateSwiftPath = path.join(platformProjectRoot, projectName, 'AppDelegate.swift');
// Re-read the file to get the current state (including any changes from previous functions)
const appDelegateSwiftContents = await fs.readFile(appDelegateSwiftPath, 'utf-8');
// console.log(`appdelegate content hansel before is ${appDelegateSwiftContents}`);
// Add Hansel import if Hansel is enabled and not already present (check for exact import line)
const lines = appDelegateSwiftContents.split('\n');
const hasHanselImport = lines.some(line => line.trim() === swiftImports.hanselImport);
if (props.ios.smartechNudges.smartechNudgesEnabled && !hasHanselImport) {
let insertIndex = 0;
// Find the last import statement
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('import ')) {
insertIndex = i + 1;
} else if (lines[i].trim() !== '' && !lines[i].trim().startsWith('import ')) {
break;
}
}
// Insert the Hansel import
lines.splice(insertIndex, 0, swiftImports.hanselImport);
const modifiedContents = lines.join('\n');
// console.log(`appdelegate content hansel after is ${modifiedContents}`);
await fs.writeFile(appDelegateSwiftPath, modifiedContents, 'utf-8');
console.log(`${swiftImports.hanselImport} added to AppDelegate.swift at line ${insertIndex + 1}`);
} else {
console.log(`${swiftImports.hanselImport} is already added or SmartechNudges disabled: ${props.ios.smartechNudges.smartechNudgesEnabled}`);
}
} catch (error) {
console.error('Error modifying AppDelegate.swift:', error);
}
} else {
// For Objective-C projects (existing logic)
try {
const appDelegateHeaderPath = path.join(platformProjectRoot, projectName, appdelegateSnippet.fileName);
const appDelegateHeaderContents = await fs.readFile(appDelegateHeaderPath, 'utf-8');
// Add Hansel header snippet if Hansel is enabled and not already present
if (props.ios.smartechNudges.smartechNudgesEnabled && !appDelegateHeaderContents.includes(appdelegateSnippet.hanselHeaderSnippet)) {
const modifiedAppDelegateHeaderContents = `${appDelegateHeaderContents}\n${appdelegateSnippet.hanselHeaderSnippet}`;
await fs.writeFile(appDelegateHeaderPath, modifiedAppDelegateHeaderContents, 'utf-8');
} else {
console.log(`${appdelegateSnippet.hanselHeaderSnippet} is already added or status of SmartechNudge: ${props.ios.smartechNudges.smartechNudgesEnabled}`);
}
} catch (error) {
console.error('Error modifying AppDelegate.h:', error);
}
}
}
return config;
}
]);
};
// Set custom AppDelegate contents modifications
const setCustomAppDelegateContentsMod: ConfigPlugin<SmartechBaseProps> = (config: ExpoConfig, props) => {
return withDangerousMod(config, [
'ios',
async (config) => {
const { platformProjectRoot, projectName } = config.modRequest;
if (platformProjectRoot && projectName) {
const isSwift = await Helper.Ios.isSwiftProject(platformProjectRoot, projectName);
if (isSwift) {
// Handle Swift AppDelegate
const appDelegateSwiftPath = path.join(platformProjectRoot, projectName, 'AppDelegate.swift');
try {
let contents = await fs.readFile(appDelegateSwiftPath, 'utf-8');
contents = addSmartechInitCodeSwift(contents, props);
contents = addDeepLinkHandlerSwift(contents, props);
await fs.writeFile(appDelegateSwiftPath, contents, 'utf-8');
console.log('Swift AppDelegate contents updated successfully');
} catch (error) {
console.error('Error modifying AppDelegate.swift:', error);
}
} else {
// Handle Objective-C AppDelegate (existing logic)
const appDelegatePath = await Helper.Ios.getObjCAppDelegatePath(platformProjectRoot, projectName);
try {
let contents = await fs.readFile(appDelegatePath, 'utf-8');
const smartechInitContent = addSmartechInitCode(contents, props);
const updatedContent = Helper.Ios.addDeepLinkHandler(smartechInitContent, props);
await fs.writeFile(appDelegatePath, updatedContent, 'utf-8');
console.log(`Objective-C AppDelegate contents updated successfully: ${appDelegatePath}`);
} catch (error) {
console.error(`Error modifying AppDelegate file (${appDelegatePath}):`, error);
}
}
}
return config;
}
]);
};
// Helper function to modify AppDelegate contents
const addSmartechInitCode = (contents: string, props: SmartechBaseProps): string => {
const lines = contents.split("\n");
const didLaunchIndex = lines.findIndex(line => smartechBaseSnippet.regexPattern.test(line));
if (didLaunchIndex === -1) {
return contents;
}
// Prepare the lines to insert
const sdkLines: string[] = [
smartechBaseSnippet.smartechInit,
...(props.ios.isLogEnabled ? [smartechBaseSnippet.debugLevel] : []),
smartechBaseSnippet.trackAppInstallUpdateBySmartech,
...((props.ios.smartechNudges.smartechNudgesEnabled && props.ios.smartechNudges.isLogEnabled) ? [smartechBaseSnippet.hanselLogSnippet] : []),
];
// Add the snippets before and after the `didFinishLaunching` index
const resultLines = [
...lines.slice(0, didLaunchIndex + 2),
...sdkLines,
...lines.slice(didLaunchIndex + 2),
];
return resultLines.join("\n");
};
// Helper function to modify Swift AppDelegate contents
const addSmartechInitCodeSwift = (contents: string, props: SmartechBaseProps): string => {
const lines = contents.split("\n");
// Find didFinishLaunchingWithOptions method by looking for the specific method name
let didLaunchIndex = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Look for func application and then check if didFinishLaunchingWithOptions appears in the next few lines
if (smartechBaseSwiftSnippet.didFinishLaunchingRegex.test(line)) {
// Check the next 5 lines for didFinishLaunchingWithOptions
for (let j = i; j < Math.min(i + 5, lines.length); j++) {
if (lines[j].includes('didFinishLaunchingWithOptions')) {
didLaunchIndex = i;
break;
}
}
if (didLaunchIndex !== -1) break;
}
}
if (didLaunchIndex === -1) {
console.log('Could not find didFinishLaunchingWithOptions method in Swift AppDelegate');
return contents;
}
// Find the opening brace of the method
let methodStartIndex = didLaunchIndex;
for (let i = didLaunchIndex; i < lines.length; i++) {
if (lines[i].includes('{')) {
methodStartIndex = i;
break;
}
}
// Prepare the lines to insert
const sdkLines: string[] = [
` ${smartechBaseSwiftSnippet.smartechInit}`,
...(props.ios.isLogEnabled ? [` ${smartechBaseSwiftSnippet.debugLevel}`] : []),
` ${smartechBaseSwiftSnippet.trackAppInstallUpdateBySmartech}`,
...((props.ios.smartechNudges.smartechNudgesEnabled && props.ios.smartechNudges.isLogEnabled) ? [` ${smartechBaseSwiftSnippet.hanselLogSnippet}`] : []),
];
// Add the snippets after the method opening brace
const resultLines = [
...lines.slice(0, methodStartIndex + 1),
...sdkLines,
...lines.slice(methodStartIndex + 1),
];
console.log('Swift SDK initialization code added to didFinishLaunchingWithOptions');
return resultLines.join("\n");
};
// Helper function to add Swift deeplink handler
const addDeepLinkHandlerSwift = (contents: string, props: SmartechBaseProps): string => {
const lines = contents.split("\n");
// Find the end of the main AppDelegate class (before the last closing brace, but not the ReactNativeDelegate class)
let classEndIndex = -1;
let insideAppDelegate = false;
let braceCount = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Look for the AppDelegate class declaration
if (line.includes('class') && line.includes('AppDelegate') && line.includes(':')) {
insideAppDelegate = true;
braceCount = 0;
}
if (insideAppDelegate) {
// Count braces to find the end of the AppDelegate class
const openBraces = (line.match(/\{/g) || []).length;
const closeBraces = (line.match(/\}/g) || []).length;
braceCount += openBraces - closeBraces;
// When we reach the closing brace of AppDelegate class
if (braceCount === 0 && line.includes('}') && openBraces === 0) {
classEndIndex = i;
break;
}
}
}
if (classEndIndex === -1) {
console.log('Could not find AppDelegate class end in Swift file');
return contents;
}
const deepLinkHandlerLines = deepLinkHandlerSwiftSnippet(props.deepLinkDelay);
const resultLines = [
...lines.slice(0, classEndIndex),
deepLinkHandlerLines,
...lines.slice(classEndIndex),
];
return resultLines.join("\n");
};
// Helper function to modify Swift openURL method
const modifySwiftOpenURLMethod = (contents: string): string => {
const swiftCodeBlock = `let handleBySmartech = Smartech.sharedInstance().application(app, open: url, options: options)
if !handleBySmartech {
return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)
}
return true`;
if (contents.includes("open url:")) {
const modifiedContents = contents.replace(
"return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options)", swiftCodeBlock);
return modifiedContents;
}
console.log('Could not find Swift openURL method to modify');
return contents;
};
// Set custom AppDelegate contents modifications
const modifyAppDelegateWithLinker: ConfigPlugin<SmartechBaseProps> = (config: ExpoConfig, props) => {
const addTestDeviceForSmartech = props.ios.addTestDevice ?? false;
const addTestDeviceForHansel =
props.ios.smartechNudges.smartechNudgesEnabled &&
(props.ios.smartechNudges.addTestDevice ?? false);
if (!addTestDeviceForSmartech && !addTestDeviceForHansel) {
return config;
}
return withDangerousMod(config, [
'ios',
async (config) => {
const { platformProjectRoot, projectName } = config.modRequest;
if (platformProjectRoot && projectName) {
const isSwift = await Helper.Ios.isSwiftProject(platformProjectRoot, projectName);
if (isSwift) {
// Handle Swift AppDelegate
const appDelegateSwiftPath = path.join(platformProjectRoot, projectName, 'AppDelegate.swift');
try {
let contents = await fs.readFile(appDelegateSwiftPath, 'utf-8');
contents = modifySwiftOpenURLMethod(contents);
await fs.writeFile(appDelegateSwiftPath, contents, 'utf-8');
console.log('Swift AppDelegate openURL method updated successfully');
} catch (error) {
console.error('Error modifying Swift AppDelegate openURL method:', error);
}
} else {
// Handle Objective-C AppDelegate (existing logic)
const appDelegatePath = await Helper.Ios.getObjCAppDelegatePath(platformProjectRoot, projectName);
try {
let contents = await fs.readFile(appDelegatePath, 'utf-8');
const codeBlock = `BOOL handleBySmartech = [[Smartech sharedInstance] application:application openURL:url options:options];
if(!handleBySmartech) {
return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];
}
return YES;`;
if (contents.includes("openURL:")) {
const modifiedContents = contents.replace(
"return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options];", `${codeBlock}`);
await fs.writeFile(appDelegatePath, modifiedContents, 'utf-8');
}
} catch (error) {
console.error('Error modifying Objective-C AppDelegate openURL method:', error);
}
}
}
return config;
}
]);
};
export const withNetcoreiOS: ConfigPlugin<SmartechBaseProps> = (config, props) => {
config = modifyEntitlementsPlist(config, props);
config = modifyInfoPlist(config, props);
config = addSmartechURLScheme(config, props);
config = addHanselURLScheme(config, props);
config = addSmartechHeaderMod(config, props);
config = addSmartechHanseleHeaderMod(config, props);
config = setCustomAppDelegateContentsMod(config, props);
config = modifyAppDelegateWithLinker(config, props);
return config;
}