From 6667f2ea1d3654c058c167bb0927b21134e76ca7 Mon Sep 17 00:00:00 2001 From: Sean Holbert Date: Mon, 18 Aug 2025 21:13:38 -0700 Subject: [PATCH] Adds moengage targets to the eas build appExtensions. This notifies EAS to generate provisioning profiles for these targets --- sdk/expo/src/apple/index.ts | 17 +- .../injectAppExtensionIntoEasManagedExtra.ts | 45 +++++ sdk/expo/src/apple/withEASManagedProject.ts | 170 ++++++++++++++++++ sdk/expo/src/types.ts | 20 ++- 4 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 sdk/expo/src/apple/injectAppExtensionIntoEasManagedExtra.ts create mode 100644 sdk/expo/src/apple/withEASManagedProject.ts diff --git a/sdk/expo/src/apple/index.ts b/sdk/expo/src/apple/index.ts index 8e58ab05..17326a27 100644 --- a/sdk/expo/src/apple/index.ts +++ b/sdk/expo/src/apple/index.ts @@ -4,7 +4,8 @@ import { withMoEngageInfoPlist } from "./withInfoPlist"; import { withMoEngageEntitlements } from "./withEntitlements"; import { withMoEngageXcodeProject } from "./withXcodeProject"; import { withMoEngageDangerousMod } from "./withDangerousMod"; -import * as constants from './constants'; +import * as constants from "./constants"; +import { withEASManagedProject } from "./withEasManagedProject"; /** * The main iOS plugin that applies all modifiers to set up MoEngage iOS integration @@ -17,22 +18,32 @@ import * as constants from './constants'; * 2. withMoEngageEntitlements - Sets up app groups and other entitlements * 3. withMoEngageXcodeProject - Configures the Xcode project with extension targets * 4. withMoEngageDangerousMod - Performs file operations and updates Podfile + * 5. withEASManagedProject - Adds any targets to config.extra.eas.build.experimental.ios.appExtensions, to assist with automatic provisioning profile generation via EAS build. * * @param config - The Expo config object * @param props - The MoEngage plugin properties * @returns The fully configured Expo config */ -export const withMoEngageIos: ConfigPlugin = (config, props) => { +export const withMoEngageIos: ConfigPlugin = ( + config, + props +) => { config = withMoEngageInfoPlist(config, props); config = withMoEngageEntitlements(config, props); config = withMoEngageXcodeProject(config, props); config = withMoEngageDangerousMod(config, props); + config = withEASManagedProject(config, props); return config; }; // Export all constants and individual modifiers for advanced usage export { constants }; -export { withMoEngageInfoPlist, withMoEngageEntitlements, withMoEngageXcodeProject, withMoEngageDangerousMod }; +export { + withMoEngageInfoPlist, + withMoEngageEntitlements, + withMoEngageXcodeProject, + withMoEngageDangerousMod, +}; // Default export export default withMoEngageIos; diff --git a/sdk/expo/src/apple/injectAppExtensionIntoEasManagedExtra.ts b/sdk/expo/src/apple/injectAppExtensionIntoEasManagedExtra.ts new file mode 100644 index 00000000..cf79665d --- /dev/null +++ b/sdk/expo/src/apple/injectAppExtensionIntoEasManagedExtra.ts @@ -0,0 +1,45 @@ +import { ExpoConfig } from "@expo/config-types"; +import { EasManagedExtra } from "../types"; + +/** + * Taps into the EAS build process, exposing what targets are being built + * so EAS build an automatically generate/maintain the appropriate provisioning profiles for the app + * More info here: https://docs.expo.dev/build-reference/app-extensions/ + */ +export default function injectAppExtensionIntoEasManagedExtra( + config: ExpoConfig, + injectedValue: { + targetName: string; + bundleIdentifier: string; + entitlements: Record; + } +): ExpoConfig["extra"] { + const easExtra = (config.extra ?? {}) as EasManagedExtra; + easExtra.eas ??= {}; + easExtra.eas.build ??= {}; + easExtra.eas.build.experimental ??= {}; + easExtra.eas.build.experimental.ios ??= {}; + easExtra.eas.build.experimental.ios.appExtensions = []; + + config.extra = easExtra; + + return { + ...easExtra, + eas: { + ...easExtra?.eas, + build: { + ...easExtra?.eas?.build, + experimental: { + ...easExtra.eas?.build?.experimental, + ios: { + ...easExtra.eas?.build?.experimental?.ios, + appExtensions: [ + ...(easExtra.eas?.build?.experimental?.ios?.appExtensions ?? []), + injectedValue, + ], + }, + }, + }, + }, + }; +} diff --git a/sdk/expo/src/apple/withEASManagedProject.ts b/sdk/expo/src/apple/withEASManagedProject.ts new file mode 100644 index 00000000..1d21767e --- /dev/null +++ b/sdk/expo/src/apple/withEASManagedProject.ts @@ -0,0 +1,170 @@ +import { + ConfigPlugin, + ExportedConfigWithProps, + withDangerousMod, +} from "@expo/config-plugins"; +import { AppExtension, MoEngagePluginProps } from "../types"; +import * as fs from "fs"; +import * as path from "path"; +import { ExpoConfig } from "@expo/config-types"; +import { EasManagedExtra } from "../types"; +import { + MOENGAGE_IOS_LIVE_ACTIVITY_TARGET, + MOENGAGE_IOS_PUSH_TEMPLATE_TARGET, + MOENGAGE_IOS_RICH_PUSH_TARGET, +} from "./constants"; +const plist = require("plist"); + +const isTargetAlreadyPresent = (extra: EasManagedExtra, target: string) => { + const extension = extra.eas?.build?.experimental?.ios?.appExtensions?.find( + (extension: { targetName?: string }) => extension.targetName === target + ); + if (extension) { + console.log( + `Target '${target}' already present in eas appExtensions. Skipping appExtension autogeneration....` + ); + } + return Boolean(extension); +}; + +/** + * Taps into the EAS build process, exposing what targets are being built + * so EAS build an automatically generate/maintain the appropriate provisioning profiles for the app + * More info here: https://docs.expo.dev/build-reference/app-extensions/ + */ +export default function injectAppExtensionIntoEasManagedExtra( + config: ExpoConfig, + injectedValue: AppExtension +): EasManagedExtra { + assertTargetNotPresent( + config.extra as EasManagedExtra, + injectedValue.bundleIdentifier + ); + + const easExtra = (config.extra ?? {}) as EasManagedExtra; + easExtra.eas ??= {}; + easExtra.eas.build ??= {}; + easExtra.eas.build.experimental ??= {}; + easExtra.eas.build.experimental.ios ??= {}; + easExtra.eas.build.experimental.ios.appExtensions = []; + + config.extra = easExtra; + + return { + ...easExtra, + eas: { + ...easExtra?.eas, + build: { + ...easExtra?.eas?.build, + experimental: { + ...easExtra.eas?.build?.experimental, + ios: { + ...easExtra.eas?.build?.experimental?.ios, + appExtensions: [ + ...(easExtra.eas?.build?.experimental?.ios?.appExtensions ?? []), + injectedValue, + ], + }, + }, + }, + }, + }; +} + +const getAppGroupName = ( + exportedConfig: ExportedConfigWithProps, + filePath: string +) => { + try { + const configFilePath = path.join( + exportedConfig.modRequest.projectRoot, + filePath + ); + if (fs.existsSync(configFilePath)) { + const configPlist = plist.parse( + fs.readFileSync(configFilePath, "utf8") + ) as { [key: string]: any }; + return configPlist["AppGroupName"] as string; + } 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); + } +}; + +export const withEASManagedProject: ConfigPlugin = ( + config, + props +) => { + return withDangerousMod(config, [ + "ios", + (exportedConfig) => { + const { apple } = props; + let easExtra = (exportedConfig.extra ?? {}) as EasManagedExtra; + easExtra.eas ??= {}; + easExtra.eas.build ??= {}; + easExtra.eas.build.experimental ??= {}; + easExtra.eas.build.experimental.ios ??= {}; + easExtra.eas.build.experimental.ios.appExtensions ??= []; + + const shouldAddRichPushExtension = + apple.richPushNotificationEnabled || + apple.pushNotificationImpressionTrackingEnabled || + apple.pushTemplatesEnabled; + + const appGroupName = getAppGroupName( + exportedConfig, + apple.configFilePath + ); + if (!exportedConfig.ios?.bundleIdentifier) { + const message = `Could not find bundle identifier under 'ios' key`; + console.error(message); + throw new Error(message); + } + if ( + shouldAddRichPushExtension && + !isTargetAlreadyPresent(easExtra, MOENGAGE_IOS_RICH_PUSH_TARGET) + ) { + const richPushExtensionBundleID = `${exportedConfig.ios.bundleIdentifier}.${MOENGAGE_IOS_RICH_PUSH_TARGET}`; + easExtra = injectAppExtensionIntoEasManagedExtra(config, { + targetName: MOENGAGE_IOS_RICH_PUSH_TARGET, + bundleIdentifier: richPushExtensionBundleID, + entitlements: { + "com.apple.security.application-groups": [appGroupName], + }, + }); + } + if ( + apple.pushTemplatesEnabled && + !isTargetAlreadyPresent(easExtra, MOENGAGE_IOS_PUSH_TEMPLATE_TARGET) + ) { + const templatePushBundleId = `${exportedConfig.ios.bundleIdentifier}.${MOENGAGE_IOS_PUSH_TEMPLATE_TARGET}`; + easExtra = injectAppExtensionIntoEasManagedExtra(config, { + targetName: MOENGAGE_IOS_PUSH_TEMPLATE_TARGET, + bundleIdentifier: templatePushBundleId, + entitlements: { + "com.apple.security.application-groups": [appGroupName], + }, + }); + } + if ( + apple.liveActivityTargetPath && + apple.liveActivityTargetPath.length && + !isTargetAlreadyPresent(easExtra, MOENGAGE_IOS_LIVE_ACTIVITY_TARGET) + ) { + easExtra = injectAppExtensionIntoEasManagedExtra(config, { + targetName: MOENGAGE_IOS_LIVE_ACTIVITY_TARGET, + bundleIdentifier: exportedConfig.ios.bundleIdentifier, + entitlements: {}, + }); + } + exportedConfig.extra = easExtra; + return exportedConfig; + }, + ]); +}; diff --git a/sdk/expo/src/types.ts b/sdk/expo/src/types.ts index 098a90b2..85c472dd 100644 --- a/sdk/expo/src/types.ts +++ b/sdk/expo/src/types.ts @@ -97,4 +97,22 @@ export interface MoEngagePluginProps { * iOS-specific configuration options */ apple: MoEngageIosConfig; -} \ No newline at end of file +} + +export type AppExtension = { + targetName: string; + bundleIdentifier: string; + entitlements: Record; +}; + +export type EasManagedExtra = { + eas?: { + build?: { + experimental?: { + ios?: { + appExtensions?: AppExtension[]; + }; + }; + }; + }; +};