import { ConfigPlugin, withInfoPlist, withXcodeProject, withDangerousMod, withEntitlementsPlist } from "expo/config-plugins"; import { ConfigProps } from "./types"; const fs = require('fs'); const path = require('path'); const BRAZE_IOS_RICH_PUSH_TARGET = 'BrazeExpoRichPush'; const BRAZE_IOS_PUSH_STORY_TARGET = 'BrazeExpoPushStories'; const BRAZE_IOS_RICH_PUSH_FILES = [ 'NotificationService.swift', `${BRAZE_IOS_RICH_PUSH_TARGET}-Info.plist`, `${BRAZE_IOS_RICH_PUSH_TARGET}.entitlements` ]; const BRAZE_IOS_PUSH_STORY_FILES = [ 'NotificationViewController.swift', `${BRAZE_IOS_PUSH_STORY_TARGET}-Info.plist`, `${BRAZE_IOS_PUSH_STORY_TARGET}.entitlements` ]; const BRAZE_IOS_NOTIFICATION_SERVICE_POD = 'BrazeNotificationService'; const BRAZE_IOS_PUSH_STORY_POD = 'BrazePushStory'; const withBrazeInfoPlist: ConfigPlugin = (config, props) => { return withInfoPlist(config, (config) => { delete config.modResults.Braze; const { iosApiKey, baseUrl } = props; // Always create the Braze plist dictionary so non-credential settings (push, // geofence, etc.) are available to BrazeAppDelegate for both the legacy and // delayed initialization paths. config.modResults.Braze = {}; if (iosApiKey) { config.modResults.Braze.ApiKey = iosApiKey; } if (baseUrl) { config.modResults.Braze.Endpoint = baseUrl; } if (props.sessionTimeout != null) { config.modResults.Braze.SessionTimeout = props.sessionTimeout; } if (props.enableSdkAuthentication != null) { config.modResults.Braze.EnableSDKAuth = props.enableSdkAuthentication; } if (props.logLevel != null) { config.modResults.Braze.LogLevel = props.logLevel; } if (props.enableGeofence != null) { config.modResults.Braze.EnableGeofence = props.enableGeofence; } if (props.minimumTriggerIntervalInSeconds != null) { config.modResults.Braze.TriggerInterval = props.minimumTriggerIntervalInSeconds; } if (props.enableAutomaticLocationCollection != null) { config.modResults.Braze.EnableAutomaticLocationCollection = props.enableAutomaticLocationCollection; } if (props.enableAutomaticGeofenceRequests != null) { config.modResults.Braze.EnableAutomaticGeofenceRequests = props.enableAutomaticGeofenceRequests; } if (props.dismissModalOnOutsideTap != null) { config.modResults.Braze.DismissModalOnOutsideTap = props.dismissModalOnOutsideTap; } if (props.enableBrazeIosPush != null) { config.modResults.Braze.UseBrazePush = props.enableBrazeIosPush; } if (props.iosRequestPushPermissionsAutomatically != null) { config.modResults.Braze.RequestPushPermissionsAutomatically = props.iosRequestPushPermissionsAutomatically; } if (props.iosPushStoryAppGroup != null) { config.modResults.Braze.BrazePushStoryAppGroup = props.iosPushStoryAppGroup; } if (props.iosUseUUIDAsDeviceId != null) { config.modResults.Braze.UseUUIDAsDeviceId = props.iosUseUUIDAsDeviceId; } if (props.iosForwardUniversalLinks != null) { config.modResults.Braze.ForwardUniversalLinks = props.iosForwardUniversalLinks; } return config; }); } const withBrazeEntitlements: ConfigPlugin = (config, props) => { return withEntitlementsPlist(config, (config) => { // Add the app group to the main application target's entitlements. if (props.enableBrazeIosPushStories === true && props.iosPushStoryAppGroup != null) { const appGroupsKey = 'com.apple.security.application-groups'; const existingAppGroups = config.modResults[appGroupsKey]; if (Array.isArray(existingAppGroups) && !existingAppGroups.includes(props.iosPushStoryAppGroup)) { config.modResults[appGroupsKey] = existingAppGroups.concat([props.iosPushStoryAppGroup]); } else { config.modResults[appGroupsKey] = [props.iosPushStoryAppGroup]; } } return config; }); }; // Modify the Xcode project to include the Notification Service Extension and its relevant files. const withBrazeXcodeProject: ConfigPlugin = (config, props) => { return withXcodeProject(config, (config) => { if (props.enableBrazeIosRichPush === true || props.enableBrazeIosPushStories === true) { // 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 (props.enableBrazeIosRichPush === true && !config.modResults.pbxGroupByName(BRAZE_IOS_RICH_PUSH_TARGET)) { // Add the Notification Service Extension target. const richPushTarget = config.modResults.addTarget( BRAZE_IOS_RICH_PUSH_TARGET, 'app_extension', BRAZE_IOS_RICH_PUSH_TARGET, `${config.ios?.bundleIdentifier}.${BRAZE_IOS_RICH_PUSH_TARGET}`, ); // Add the relevant files to the PBX group. const brazeNotificationServiceGroup = config.modResults.addPbxGroup( BRAZE_IOS_RICH_PUSH_FILES, BRAZE_IOS_RICH_PUSH_TARGET, BRAZE_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(brazeNotificationServiceGroup.uuid, groupUUID); } }; for (const configUUID of Object.keys(xcconfigs)) { const buildSettings = xcconfigs[configUUID].buildSettings; if (buildSettings && buildSettings.PRODUCT_NAME === `"${BRAZE_IOS_RICH_PUSH_TARGET}"`) { buildSettings.SWIFT_VERSION = swiftVersion; buildSettings.CODE_SIGN_ENTITLEMENTS = `${BRAZE_IOS_RICH_PUSH_TARGET}/${BRAZE_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 Stories Notification Content Extension if (props.enableBrazeIosPushStories === true && props.iosPushStoryAppGroup != null && !config.modResults.pbxGroupByName(BRAZE_IOS_PUSH_STORY_TARGET)) { // Add the Notification Content Extension target. const pushStoriesTarget = config.modResults.addTarget( BRAZE_IOS_PUSH_STORY_TARGET, 'app_extension', BRAZE_IOS_PUSH_STORY_TARGET, `${config.ios?.bundleIdentifier}.${BRAZE_IOS_PUSH_STORY_TARGET}`, ); // Add the relevant files to the PBX group. const brazeNotificationServiceGroup = config.modResults.addPbxGroup( BRAZE_IOS_PUSH_STORY_FILES, BRAZE_IOS_PUSH_STORY_TARGET, BRAZE_IOS_PUSH_STORY_TARGET, ); for (const groupUUID of Object.keys(groups)) { if (typeof groups[groupUUID] === 'object' && groups[groupUUID].name === undefined && groups[groupUUID].path === undefined) { config.modResults.addToPbxGroup(brazeNotificationServiceGroup.uuid, groupUUID); } }; for (const configUUID of Object.keys(xcconfigs)) { const buildSettings = xcconfigs[configUUID].buildSettings; if (buildSettings && buildSettings.PRODUCT_NAME === `"${BRAZE_IOS_PUSH_STORY_TARGET}"`) { buildSettings.BRAZE_PUSH_STORY_APP_GROUP = props.iosPushStoryAppGroup; buildSettings.SWIFT_VERSION = swiftVersion; buildSettings.CODE_SIGN_ENTITLEMENTS = `${BRAZE_IOS_PUSH_STORY_TARGET}/${BRAZE_IOS_PUSH_STORY_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', pushStoriesTarget.uuid ); config.modResults.addBuildPhase( [ 'UserNotifications.framework', 'UserNotificationsUI.framework' ], 'PBXFrameworksBuildPhase', 'Frameworks', pushStoriesTarget.uuid ); } } return config; }); }; // Direct modifications to the project files. // Used for any operations that can't be contained within direct manipulation of the Xcode project or properties. const withBrazeDangerousMod: ConfigPlugin = (config, props) => { return withDangerousMod(config, [ 'ios', (config) => { const projectRoot = config.modRequest.projectRoot; // Modify the Podfile for rich push. if (props.enableBrazeIosRichPush === true) { // Copy Rich Push files to project path. const absoluteSource = require.resolve('@braze/expo-plugin/ios/ExpoAdapterBraze/RichPush/NotificationService.swift'); const sourcePath = path.dirname(absoluteSource); const destinationPath = `${projectRoot}/ios/${BRAZE_IOS_RICH_PUSH_TARGET}`; if (!fs.existsSync(`${destinationPath}`)) { fs.mkdirSync(`${destinationPath}`); } for (const file of BRAZE_IOS_RICH_PUSH_FILES) { fs.copyFileSync(`${sourcePath}/${file}`, `${destinationPath}/${file}`); } // Modify Podfile to include `BrazeNotificationService`. const podfilePath = `${projectRoot}/ios/Podfile`; const podfile = fs.readFileSync(podfilePath); if (!podfile.includes(BRAZE_IOS_NOTIFICATION_SERVICE_POD)) { const notificationServiceTarget = ` target '${BRAZE_IOS_RICH_PUSH_TARGET}' do use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] pod '${BRAZE_IOS_NOTIFICATION_SERVICE_POD}' end ` fs.appendFileSync(podfilePath, notificationServiceTarget); } } // Modify the Podfile for Push Stories. if (props.enableBrazeIosPushStories === true) { // Copy Push Stories files to project path. const absoluteSource = require.resolve('@braze/expo-plugin/ios/ExpoAdapterBraze/PushStories/NotificationViewController.swift'); const sourcePath = path.dirname(absoluteSource); const destinationPath = `${projectRoot}/ios/${BRAZE_IOS_PUSH_STORY_TARGET}`; if (!fs.existsSync(`${destinationPath}`)) { fs.mkdirSync(`${destinationPath}`); } for (const file of BRAZE_IOS_PUSH_STORY_FILES) { fs.copyFileSync(`${sourcePath}/${file}`, `${destinationPath}/${file}`); } // Modify Podfile to include `BrazePushStory`. const podfilePath = `${projectRoot}/ios/Podfile`; const podfile = fs.readFileSync(podfilePath); if (!podfile.includes(BRAZE_IOS_PUSH_STORY_POD)) { const notificationServiceTarget = ` target '${BRAZE_IOS_PUSH_STORY_TARGET}' do use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] pod '${BRAZE_IOS_PUSH_STORY_POD}' end ` fs.appendFileSync(podfilePath, notificationServiceTarget); } } return config; }, ]); }; export const withIOSBrazeSdk: ConfigPlugin = (config, props) => { config = withBrazeInfoPlist(config, props); config = withBrazeEntitlements(config, props); config = withBrazeXcodeProject(config, props); config = withBrazeDangerousMod(config, props); return config; };