airship-expo-plugin
Version:
Airship Expo config plugin
347 lines (300 loc) • 13 kB
text/typescript
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;
};