react-native-expo-moengage
Version:
MoEngage Expo plugin for integrating MoEngage React-Native SDK
419 lines (378 loc) • 16.6 kB
text/typescript
import { ConfigPlugin, ExportedConfigWithProps, XcodeProject, withXcodeProject } from "@expo/config-plugins";
import { MoEngagePluginProps } from "../types";
import * as fs from 'fs';
import * as path from 'path';
import {
MOENGAGE_IOS_RICH_PUSH_TARGET,
MOENGAGE_IOS_PUSH_TEMPLATE_TARGET,
MOENGAGE_IOS_LIVE_ACTIVITY_TARGET,
MOENGAGE_IOS_RICH_PUSH_FILES,
MOENGAGE_IOS_PUSH_TEMPLATE_FILES
} from './constants';
const plist = require('plist');
/**
* MoEngage Expo plugin for iOS - Xcode project modifications
*
* This plugin configures the Xcode project to include MoEngage extensions for rich notifications,
* push templates, and LiveActivity. It reads configuration from the MoEngage plist file and sets up
* the necessary targets, PBX groups, and build phases.
*
* @param config - The Expo config object
* @param props - The MoEngage plugin properties
* @returns The updated config object with Xcode project modifications
*/
export const withMoEngageXcodeProject: ConfigPlugin<MoEngagePluginProps> = (config, props) => {
return withXcodeProject(config, (config) => {
if (process.env['EXPO_TV']) {
// Skip modifications for tvOS
console.log(`Skipping extension targets setup for tvOS`);
return config;
}
const { apple } = props;
const shouldAddRichPushExtension = apple.richPushNotificationEnabled || apple.pushNotificationImpressionTrackingEnabled || apple.pushTemplatesEnabled;
// Get app group from plist file if specified
let appGroupValue = ''
try {
const configFilePath = path.join(config.modRequest.projectRoot, props.apple.configFilePath);
if (fs.existsSync(configFilePath)) {
const configPlist = plist.parse(fs.readFileSync(configFilePath, 'utf8')) as { [key: string]: any };
appGroupValue = configPlist['AppGroupName'] as string;
if (!appGroupValue) {
const message = `Missing AppGroupName key in MoEngage configuration`;
console.error(message);
throw new Error(message);
}
} else {
const message = `MoEngage configuration does not exist`;
console.error(message);
throw new Error(message);
}
} catch (e) {
const message = `Could not import MoEngage configuration: ${e}`;
console.error(message);
throw new Error(message);
}
// Initialize with an empty object if these top-level objects are non-existent.
// This guarantees that the extension targets will have a destination.
const objects = config.modResults.hash.project.objects;
objects['PBXTargetDependency'] = objects['PBXTargetDependency'] || {};
objects['PBXContainerItemProxy'] = objects['PBXContainerItemProxy'] || {};
const groups = objects['PBXGroup'];
const xcconfigs = objects['XCBuildConfiguration'];
// Retrieve Swift version and code signing settings from main target to apply to dependency targets.
let swiftVersion;
let codeSignStyle;
let codeSignIdentity;
let otherCodeSigningFlags;
let developmentTeam;
let provisioningProfile;
for (const configUUID of Object.keys(xcconfigs)) {
const buildSettings = xcconfigs[configUUID].buildSettings;
if (!swiftVersion && buildSettings && buildSettings.SWIFT_VERSION) {
swiftVersion = buildSettings.SWIFT_VERSION;
codeSignStyle = buildSettings.CODE_SIGN_STYLE;
codeSignIdentity = buildSettings.CODE_SIGN_IDENTITY;
otherCodeSigningFlags = buildSettings.OTHER_CODE_SIGN_FLAGS;
developmentTeam = buildSettings.DEVELOPMENT_TEAM;
provisioningProfile = buildSettings.PROVISIONING_PROFILE_SPECIFIER;
break;
}
}
// Rich Push Notification Service Extension
if (shouldAddRichPushExtension && !config.modResults.pbxGroupByName(MOENGAGE_IOS_RICH_PUSH_TARGET)) {
// Add the Notification Service Extension target.
const richPushTarget = config.modResults.addTarget(
MOENGAGE_IOS_RICH_PUSH_TARGET,
'app_extension',
MOENGAGE_IOS_RICH_PUSH_TARGET,
`${config.ios?.bundleIdentifier}.${MOENGAGE_IOS_RICH_PUSH_TARGET}`,
);
// Add the relevant files to the PBX group.
const moengageNotificationServiceGroup = config.modResults.addPbxGroup(
MOENGAGE_IOS_RICH_PUSH_FILES,
MOENGAGE_IOS_RICH_PUSH_TARGET,
MOENGAGE_IOS_RICH_PUSH_TARGET,
);
for (const groupUUID of Object.keys(groups)) {
if (typeof groups[groupUUID] === 'object'
&& groups[groupUUID].name === undefined
&& groups[groupUUID].path === undefined) {
config.modResults.addToPbxGroup(moengageNotificationServiceGroup.uuid, groupUUID);
}
};
for (const configUUID of Object.keys(xcconfigs)) {
const buildSettings = xcconfigs[configUUID].buildSettings;
if (buildSettings && buildSettings.PRODUCT_NAME === `"${MOENGAGE_IOS_RICH_PUSH_TARGET}"`) {
buildSettings.MOENGAGE_APP_GROUP = appGroupValue;
buildSettings.SWIFT_VERSION = swiftVersion;
buildSettings.CODE_SIGN_ENTITLEMENTS = `${MOENGAGE_IOS_RICH_PUSH_TARGET}/${MOENGAGE_IOS_RICH_PUSH_TARGET}.entitlements`;
if (codeSignStyle) { buildSettings.CODE_SIGN_STYLE = codeSignStyle; }
if (codeSignIdentity) { buildSettings.CODE_SIGN_IDENTITY = codeSignIdentity; }
if (otherCodeSigningFlags) { buildSettings.OTHER_CODE_SIGN_FLAGS = otherCodeSigningFlags; }
if (developmentTeam) { buildSettings.DEVELOPMENT_TEAM = developmentTeam; }
if (provisioningProfile) { buildSettings.PROVISIONING_PROFILE_SPECIFIER = provisioningProfile; }
}
}
// Set up target build phase scripts.
config.modResults.addBuildPhase(
[
'NotificationService.swift',
],
'PBXSourcesBuildPhase',
'Sources',
richPushTarget.uuid
);
config.modResults.addBuildPhase(
['UserNotifications.framework'],
'PBXFrameworksBuildPhase',
'Frameworks',
richPushTarget.uuid
);
}
// Push Templates Notification Content Extension
if (apple.pushTemplatesEnabled && !config.modResults.pbxGroupByName(MOENGAGE_IOS_PUSH_TEMPLATE_TARGET)) {
// Add the Notification Content Extension target.
const pushTemplateTarget = config.modResults.addTarget(
MOENGAGE_IOS_PUSH_TEMPLATE_TARGET,
'app_extension',
MOENGAGE_IOS_PUSH_TEMPLATE_TARGET,
`${config.ios?.bundleIdentifier}.${MOENGAGE_IOS_PUSH_TEMPLATE_TARGET}`,
);
// Add the relevant files to the PBX group.
const moengageNotificationContentGroup = config.modResults.addPbxGroup(
MOENGAGE_IOS_PUSH_TEMPLATE_FILES,
MOENGAGE_IOS_PUSH_TEMPLATE_TARGET,
MOENGAGE_IOS_PUSH_TEMPLATE_TARGET,
);
for (const groupUUID of Object.keys(groups)) {
if (typeof groups[groupUUID] === 'object'
&& groups[groupUUID].name === undefined
&& groups[groupUUID].path === undefined) {
config.modResults.addToPbxGroup(moengageNotificationContentGroup.uuid, groupUUID);
}
};
for (const configUUID of Object.keys(xcconfigs)) {
const buildSettings = xcconfigs[configUUID].buildSettings;
if (buildSettings && buildSettings.PRODUCT_NAME === `"${MOENGAGE_IOS_PUSH_TEMPLATE_TARGET}"`) {
buildSettings.MOENGAGE_APP_GROUP = appGroupValue;
buildSettings.SWIFT_VERSION = swiftVersion;
buildSettings.CODE_SIGN_ENTITLEMENTS = `${MOENGAGE_IOS_PUSH_TEMPLATE_TARGET}/${MOENGAGE_IOS_PUSH_TEMPLATE_TARGET}.entitlements`;
if (codeSignStyle) { buildSettings.CODE_SIGN_STYLE = codeSignStyle; }
if (codeSignIdentity) { buildSettings.CODE_SIGN_IDENTITY = codeSignIdentity; }
if (otherCodeSigningFlags) { buildSettings.OTHER_CODE_SIGN_FLAGS = otherCodeSigningFlags; }
if (developmentTeam) { buildSettings.DEVELOPMENT_TEAM = developmentTeam; }
if (provisioningProfile) { buildSettings.PROVISIONING_PROFILE_SPECIFIER = provisioningProfile; }
}
}
// Set up target build phase scripts.
config.modResults.addBuildPhase(
[
'NotificationViewController.swift',
],
'PBXSourcesBuildPhase',
'Sources',
pushTemplateTarget.uuid
);
config.modResults.addBuildPhase(
[
'MainInterface.storyboard',
],
'PBXResourcesBuildPhase',
'Resources',
pushTemplateTarget.uuid
);
config.modResults.addBuildPhase(
[
'UserNotifications.framework',
'UserNotificationsUI.framework'
],
'PBXFrameworksBuildPhase',
'Frameworks',
pushTemplateTarget.uuid
);
}
// Handle Live Activity configuration if targetPath is provided
if (apple.liveActivityTargetPath && apple.liveActivityTargetPath.length && !config.modResults.pbxGroupByName(MOENGAGE_IOS_LIVE_ACTIVITY_TARGET)) {
config = withLiveActivity(config, apple.liveActivityTargetPath, {
swiftVersion,
codeSignStyle,
codeSignIdentity,
otherCodeSigningFlags,
developmentTeam,
provisioningProfile
});
}
return config;
});
};
/**
* Interface for code signing settings
* These settings are extracted from the main target and applied to extension targets
* to ensure consistent code signing across all targets in the project
*/
interface CodeSignSettings {
/** Swift version used in the project (e.g. '5.0') */
swiftVersion?: string;
/** Code signing style ('Automatic' or 'Manual') */
codeSignStyle?: string;
/** Code signing identity (e.g. 'Apple Developer') */
codeSignIdentity?: string;
/** Additional code signing flags */
otherCodeSigningFlags?: string;
/** Development team identifier */
developmentTeam?: string;
/** Provisioning profile specifier */
provisioningProfile?: string;
}
/**
* Adds Live Activity extension target to the Xcode project
*
* This function configures a LiveActivity extension by:
* 1. Creating the target and PBX groups
* 2. Scanning the provided directory for source files, resources, and configuration files
* 3. Adding all files to appropriate build phases
* 4. Setting up build settings including code signing and entitlements
*
* @param config - Expo config with Xcode project
* @param liveActivityTargetPath - Path to the LiveActivity source files
* @param codeSignSettings - Code signing settings to apply to the target
* @returns The updated config object
*/
export function withLiveActivity(config: ExportedConfigWithProps<XcodeProject>, liveActivityTargetPath: string, codeSignSettings: CodeSignSettings) {
const liveActivityPath = path.join(config.modRequest.projectRoot, liveActivityTargetPath);
const objects = config.modResults.hash.project.objects;
const xcconfigs = objects['XCBuildConfiguration'];
const groups = objects['PBXGroup'];
// Add the Live Activity target
const liveActivityTarget = config.modResults.addTarget(
MOENGAGE_IOS_LIVE_ACTIVITY_TARGET,
'app_extension',
MOENGAGE_IOS_LIVE_ACTIVITY_TARGET,
`${config.ios?.bundleIdentifier}.${MOENGAGE_IOS_LIVE_ACTIVITY_TARGET}`,
);
// Add the relevant files to the PBX group.
const moengageLiveActivityPathContentGroup = config.modResults.addPbxGroup(
[], MOENGAGE_IOS_LIVE_ACTIVITY_TARGET, liveActivityPath,
);
for (const groupUUID of Object.keys(groups)) {
if (typeof groups[groupUUID] === 'object'
&& groups[groupUUID].name === undefined
&& groups[groupUUID].path === undefined) {
config.modResults.addToPbxGroup(moengageLiveActivityPathContentGroup.uuid, groupUUID);
}
};
// Find all .xcassets files, source files, header files,
// info plist file and entitlements file in liveActivityTargetPath
let entitlementsFile = '';
let infoPlistFile = '';
let resourcesFiles: string[] = [];
let sourceFiles: string[] = [];
let headerFiles: string[] = [];
try {
const findFilesRecursively = (directory: string, group: any) => {
let items = fs.readdirSync(directory, { withFileTypes: true });
for (const item of items) {
const itemPath = path.join(directory, item.name);
const ext = path.extname(item.name).toLowerCase();
if (item.isDirectory()) {
if (['.xcassets'].includes(ext)) {
resourcesFiles.push(itemPath);
console.log(`Found ${itemPath} as resource for LiveActivity`);
} else {
// Recursively read directories
const group = config.modResults.addPbxGroup([], item.name, itemPath);
config.modResults.addToPbxGroup(group.uuid, moengageLiveActivityPathContentGroup.uuid);
findFilesRecursively(itemPath, group);
continue;
}
} else if (['.swift', '.m', '.c', '.cpp'].includes(ext)) {
sourceFiles.push(itemPath);
console.log(`Found ${itemPath} as source file for LiveActivity`);
} else if (['.h'].includes(ext)) {
headerFiles.push(itemPath);
console.log(`Found ${itemPath} as header file for LiveActivity`);
} else if (['.entitlements'].includes(ext)) {
entitlementsFile = itemPath;
console.log(`Found ${itemPath} as entitlements file for LiveActivity`);
} else if (item.name === 'Info.plist' ||
item.name === `${MOENGAGE_IOS_LIVE_ACTIVITY_TARGET}-Info.plist` ||
item.name === `${path.basename(path.dirname(liveActivityTargetPath ?? ''))}-Info.plist`) {
infoPlistFile = itemPath;
console.log(`Found ${itemPath} as Info.plist file for LiveActivity`);
} else {
resourcesFiles.push(itemPath);
console.log(`Found ${itemPath} as resource for LiveActivity`);
}
// Add file to project
config.modResults.addFile(itemPath, group.uuid);
}
};
// Find all files
findFilesRecursively(liveActivityPath, moengageLiveActivityPathContentGroup);
} catch (e) {
const message = `Error finding files in Live Activity path: ${e}`;
console.error(message);
throw new Error(message);
}
// Set up build settings for the Live Activity target
for (const configUUID of Object.keys(xcconfigs)) {
const buildSettings = xcconfigs[configUUID].buildSettings;
if (buildSettings && buildSettings.PRODUCT_NAME === `"${MOENGAGE_IOS_LIVE_ACTIVITY_TARGET}"`) {
buildSettings.SWIFT_VERSION = codeSignSettings.swiftVersion;
buildSettings.IPHONEOS_DEPLOYMENT_TARGET = '18.0'; // Set minimum iOS version for Live Activity
// Add entitlements file if found
if (entitlementsFile && entitlementsFile.length) {
buildSettings.CODE_SIGN_ENTITLEMENTS = `${entitlementsFile}`;
}
// Add Info.plist file if found
if (infoPlistFile && infoPlistFile.length) {
buildSettings.INFOPLIST_FILE = `${infoPlistFile}`;
}
// Copy code signing settings from main target
if (codeSignSettings.codeSignStyle) { buildSettings.CODE_SIGN_STYLE = codeSignSettings.codeSignStyle; }
if (codeSignSettings.codeSignIdentity) { buildSettings.CODE_SIGN_IDENTITY = codeSignSettings.codeSignIdentity; }
if (codeSignSettings.otherCodeSigningFlags) { buildSettings.OTHER_CODE_SIGN_FLAGS = codeSignSettings.otherCodeSigningFlags; }
if (codeSignSettings.developmentTeam) { buildSettings.DEVELOPMENT_TEAM = codeSignSettings.developmentTeam; }
if (codeSignSettings.provisioningProfile) { buildSettings.PROVISIONING_PROFILE_SPECIFIER = codeSignSettings.provisioningProfile; }
}
}
// Add resources (xcassets) to the target
if (resourcesFiles.length) {
config.modResults.addBuildPhase(
resourcesFiles,
'PBXResourcesBuildPhase',
'Resources',
liveActivityTarget.uuid
);
}
// Add source files to the target
if (sourceFiles.length) {
config.modResults.addBuildPhase(
sourceFiles,
'PBXSourcesBuildPhase',
'Sources',
liveActivityTarget.uuid
);
}
// Add header files to the target
if (headerFiles.length) {
config.modResults.addBuildPhase(
headerFiles,
'PBXHeadersBuildPhase',
'Headers',
liveActivityTarget.uuid
);
}
// Add required frameworks for Live Activity
config.modResults.addBuildPhase(
[
'SwiftUI.framework',
'WidgetKit.framework',
'ActivityKit.framework'
],
'PBXFrameworksBuildPhase',
'Frameworks',
liveActivityTarget.uuid
);
return config;
}