UNPKG

airship-expo-plugin

Version:
347 lines (300 loc) 13 kB
import { ConfigPlugin, withEntitlementsPlist, withInfoPlist, withDangerousMod, withXcodeProject, withPodfile } from '@expo/config-plugins'; import { readFile, writeFileSync, existsSync, mkdirSync } from 'fs'; import { basename, join } from 'path'; import assert from 'assert'; import { ExpoConfig } from '@expo/config-types'; import { AirshipIOSPluginProps } from './withAirship'; import { mergeContents, MergeResults } from '@expo/config-plugins/build/utils/generateCode'; const DEFAULT_NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME = "NotificationServiceExtension"; const NOTIFICATION_SERVICE_FILE_NAME = "AirshipNotificationService.swift"; const NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME = "NotificationServiceExtension-Info.plist"; const AIRSHP_PLUGIN_EXTENDER_DIR_NAME = "AirshipPluginExtender"; const withCapabilities: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { return withInfoPlist(config, (plist) => { if (!Array.isArray(plist.modResults.UIBackgroundModes)) { plist.modResults.UIBackgroundModes = []; } if (!plist.modResults.UIBackgroundModes.includes("remote-notification")) { plist.modResults.UIBackgroundModes.push("remote-notification"); } return plist; }); }; const withAPNSEnvironment: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { return withEntitlementsPlist(config, (plist) => { plist.modResults['aps-environment'] = props.mode; return plist; }); }; const withNotificationServiceExtension: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { return withDangerousMod(config, [ 'ios', async config => { await writeNotificationServiceFilesAsync(props, config.modRequest.projectRoot); return config; }, ]); }; async function writeNotificationServiceFilesAsync(props: AirshipIOSPluginProps, projectRoot: string) { if (!props.notificationService) { return; } const pluginDir = require.resolve("airship-expo-plugin/package.json"); const sourceDir = join(pluginDir, "../plugin/NotificationServiceExtension/"); const targetName = props.notificationServiceTargetName ?? DEFAULT_NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME const extensionPath = join(projectRoot, "ios", targetName); if (!existsSync(extensionPath)) { mkdirSync(extensionPath, { recursive: true }); } // Copy the NotificationService.swift file into the iOS expo project as AirshipNotificationService.swift. var notificationServiceFile = props.notificationService == "DEFAULT_AIRSHIP_SERVICE_EXTENSION" ? join(sourceDir, NOTIFICATION_SERVICE_FILE_NAME) : props.notificationService; readFile(notificationServiceFile, 'utf8', (err, data) => { if (err || !data) { console.error("Airship couldn't read file " + notificationServiceFile); console.error(err); return; } if (!props.notificationServiceInfo) { const regexp = /class [A-Za-z]+:/; const newSubStr = "class AirshipNotificationService:"; data = data.replace(regexp, newSubStr); } writeFileSync(join(extensionPath, NOTIFICATION_SERVICE_FILE_NAME), data); }); // Copy the Info.plist (default to NotificationServiceExtension-Info.plist if null) file into the iOS expo project. readFile(props.notificationServiceInfo ?? join(sourceDir, NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME), 'utf8', (err, data) => { if (err || !data) { console.error("Airship couldn't read file " + (props.notificationServiceInfo ?? join(sourceDir, NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME))); console.error(err); return; } const infoPlistFilename = props.notificationServiceTargetName ? props.notificationServiceTargetName + "-Info.plist" : NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME; writeFileSync(join(extensionPath, infoPlistFilename), data); }); }; const withExtensionTargetInXcodeProject: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { const targetName = props.notificationServiceTargetName ?? DEFAULT_NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME return withXcodeProject(config, newConfig => { const xcodeProject = newConfig.modResults; if (!!xcodeProject.pbxTargetByName(targetName)) { console.log(targetName + " already exists in project. Skipping..."); return newConfig; } const infoPlistFilename = props.notificationServiceTargetName ? props.notificationServiceTargetName + "-Info.plist" : NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME; // Create new PBXGroup for the extension const extGroup = xcodeProject.addPbxGroup( [NOTIFICATION_SERVICE_FILE_NAME, infoPlistFilename], targetName, targetName ); // Add the new PBXGroup to the top level group. This makes the // files / folder appear in the file explorer in Xcode. const groups = xcodeProject.hash.project.objects["PBXGroup"]; Object.keys(groups).forEach(function(key) { if (typeof groups[key] === "object" && groups[key].name === undefined && groups[key].path === undefined) { xcodeProject.addToPbxGroup(extGroup.uuid, key); } }); // WORK AROUND for xcodeProject.addTarget BUG (making the pod install to fail somehow) // Xcode projects don't contain these if there is only one target in the app // An upstream fix should be made to the code referenced in this link: // - https://github.com/apache/cordova-node-xcode/blob/8b98cabc5978359db88dc9ff2d4c015cba40f150/lib/pbxProject.js#L860 const projObjects = xcodeProject.hash.project.objects; projObjects['PBXTargetDependency'] = projObjects['PBXTargetDependency'] || {}; projObjects['PBXContainerItemProxy'] = projObjects['PBXTargetDependency'] || {}; // Add the Notification Service Extension Target // This adds PBXTargetDependency and PBXContainerItemProxy const notificationServiceExtensionTarget = xcodeProject.addTarget( targetName, "app_extension", targetName, `${config.ios?.bundleIdentifier}.${targetName}` ); // Add build phases to the new Target xcodeProject.addBuildPhase( [NOTIFICATION_SERVICE_FILE_NAME], "PBXSourcesBuildPhase", "Sources", notificationServiceExtensionTarget.uuid ); xcodeProject.addBuildPhase( [], "PBXResourcesBuildPhase", "Resources", notificationServiceExtensionTarget.uuid ); xcodeProject.addBuildPhase( [], "PBXFrameworksBuildPhase", "Frameworks", notificationServiceExtensionTarget.uuid ); // Edit the new Target Build Settings and Deployment info const configurations = xcodeProject.pbxXCBuildConfigurationSection(); for (const key in configurations) { if (typeof configurations[key].buildSettings !== "undefined" && configurations[key].buildSettings.PRODUCT_NAME == `"${targetName}"` ) { const buildSettingsObj = configurations[key].buildSettings; buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = props.deploymentTarget ?? "15.0"; buildSettingsObj.SWIFT_VERSION = "5.0"; buildSettingsObj.DEVELOPMENT_TEAM = props?.developmentTeamID; buildSettingsObj.CODE_SIGN_STYLE = "Automatic"; } } // Add development teams to the target xcodeProject.addTargetAttribute("DevelopmentTeam", props?.developmentTeamID, notificationServiceExtensionTarget); return newConfig; }); }; const withAirshipServiceExtensionPod: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { const targetName = props.notificationServiceTargetName ?? DEFAULT_NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME return withPodfile(config, async (config) => { const airshipServiceExtensionPodfileSnippet = ` target '${targetName}' 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 'AirshipServiceExtension' end `; let results: MergeResults; try { results = mergeContents({ tag: "AirshipServiceExtension", src: config.modResults.contents, newSrc: airshipServiceExtensionPodfileSnippet, anchor: /target .* do/, offset: 0, comment: '#' }); } catch (error: any) { if (error.code === 'ERR_NO_MATCH') { throw new Error( `Cannot add AirshipServiceExtension to the project's ios/Podfile because it's malformed. Please report this with a copy of your project Podfile.` ); } throw error; } if (results.didMerge || results.didClear) { config.modResults.contents = results.contents; } return config; }); }; const withEasManagedCredentials: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { assert(config.ios?.bundleIdentifier, "Missing 'ios.bundleIdentifier' in app config.") config.extra = getEasManagedCredentialsConfigExtra(config as ExpoConfig, props); return config; } function getEasManagedCredentialsConfigExtra(config: ExpoConfig, props: AirshipIOSPluginProps): {[k: string]: any} { return { ...config.extra, eas: { ...config.extra?.eas, build: { ...config.extra?.eas?.build, experimental: { ...config.extra?.eas?.build?.experimental, ios: { ...config.extra?.eas?.build?.experimental?.ios, appExtensions: [ ...(config.extra?.eas?.build?.experimental?.ios?.appExtensions ?? []), { // Sync up with the new target targetName: props.notificationServiceTargetName ?? DEFAULT_NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME, bundleIdentifier: `${config?.ios?.bundleIdentifier}.${props.notificationServiceTargetName ?? DEFAULT_NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME}`, } ] } } } } } } const withAirshipExtender: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { return withDangerousMod(config, [ 'ios', async config => { await writeAirshipExtenderFileAsync(props, config.modRequest.projectRoot); return config; }, ]); }; async function writeAirshipExtenderFileAsync(props: AirshipIOSPluginProps, projectRoot: string) { if (!props.airshipExtender) { return; } const fileName = basename(props.airshipExtender) const extenderDestinationPath = join(projectRoot, "ios", AIRSHP_PLUGIN_EXTENDER_DIR_NAME); if (!existsSync(extenderDestinationPath)) { mkdirSync(extenderDestinationPath, { recursive: true }); } // Copy the Airship Extender file into the iOS expo project. readFile(props.airshipExtender, 'utf8', (err, data) => { if (err || !data) { console.error("Airship couldn't read file " + (props.airshipExtender)); console.error(err); return; } writeFileSync(join(extenderDestinationPath, fileName), data); }); }; const withAirshipExtenderInMainTarget: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { return withXcodeProject(config, newConfig => { const xcodeProject = newConfig.modResults; if (!props.airshipExtender) { return newConfig; } if (!!xcodeProject.pbxGroupByName(AIRSHP_PLUGIN_EXTENDER_DIR_NAME)) { console.log(AIRSHP_PLUGIN_EXTENDER_DIR_NAME + " already exists in project. Skipping..."); return newConfig; } const extenderFileName = basename(props.airshipExtender); const mainAppTarget = xcodeProject.getFirstTarget(); // Create new PBXGroup for the Airship Extender const extGroup = xcodeProject.addPbxGroup( [], AIRSHP_PLUGIN_EXTENDER_DIR_NAME, AIRSHP_PLUGIN_EXTENDER_DIR_NAME ); // Add the new PBXGroup to the top level group. This makes the // files / folder appear in the file explorer in Xcode. const groups = xcodeProject.hash.project.objects["PBXGroup"]; Object.keys(groups).forEach(function(key) { if (typeof groups[key] === "object" && groups[key].name === undefined && groups[key].path === undefined) { xcodeProject.addToPbxGroup(extGroup.uuid, key); } }); // Add the AirshipExtender source file to the Main Target Build Phases xcodeProject.addSourceFile( extenderFileName, { target: mainAppTarget.uuid }, extGroup.uuid ); return newConfig; }); }; export const withAirshipIOS: ConfigPlugin<AirshipIOSPluginProps> = (config, props) => { config = withCapabilities(config, props); config = withAPNSEnvironment(config, props); if (props.notificationService) { config = withNotificationServiceExtension(config, props); config = withExtensionTargetInXcodeProject(config, props); config = withAirshipServiceExtensionPod(config, props); config = withEasManagedCredentials(config, props); } if (props.airshipExtender) { config = withAirshipExtender(config, props); config = withAirshipExtenderInMainTarget(config, props); } return config; };