From 41a58f9f28395bd8f39eff18c9763eaa83b12227 Mon Sep 17 00:00:00 2001 From: Ulrich GIBERNE Date: Fri, 22 Nov 2024 13:47:24 -0400 Subject: [PATCH] Add support for Airship Notification Service Extension --- .../AirshipNotificationService.swift | 8 + ...hipNotificationServiceExtension-Info.plist | 27 +++ plugin/src/withAirship.ts | 8 +- plugin/src/withAirshipAndroid.ts | 1 + plugin/src/withAirshipIOS.ts | 178 +++++++++++++++++- yarn.lock | 28 ++- 6 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 plugin/NotificationServiceExtension/AirshipNotificationService.swift create mode 100644 plugin/NotificationServiceExtension/AirshipNotificationServiceExtension-Info.plist diff --git a/plugin/NotificationServiceExtension/AirshipNotificationService.swift b/plugin/NotificationServiceExtension/AirshipNotificationService.swift new file mode 100644 index 0000000..bd2e90b --- /dev/null +++ b/plugin/NotificationServiceExtension/AirshipNotificationService.swift @@ -0,0 +1,8 @@ +// NotificationService.swift + + +import AirshipServiceExtension + +class NotificationService: UANotificationServiceExtension { + +} diff --git a/plugin/NotificationServiceExtension/AirshipNotificationServiceExtension-Info.plist b/plugin/NotificationServiceExtension/AirshipNotificationServiceExtension-Info.plist new file mode 100644 index 0000000..2e99a2e --- /dev/null +++ b/plugin/NotificationServiceExtension/AirshipNotificationServiceExtension-Info.plist @@ -0,0 +1,27 @@ + + + + + CFBundleDisplayName + AirshipNotificationServiceExtension + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME) + + + diff --git a/plugin/src/withAirship.ts b/plugin/src/withAirship.ts index 4d711a5..de21764 100644 --- a/plugin/src/withAirship.ts +++ b/plugin/src/withAirship.ts @@ -6,16 +6,16 @@ import { withAirshipIOS } from './withAirshipIOS'; const pkg = require('airship-expo-plugin/package.json'); export type AirshipAndroidPluginProps = { - icon: string; + icon: string; }; export type AirshipIOSPluginProps = { - mode: 'development' | 'production'; + mode: 'development' | 'production'; } export type AirshipPluginProps = { - android?: AirshipAndroidPluginProps; - ios?: AirshipIOSPluginProps; + android?: AirshipAndroidPluginProps; + ios?: AirshipIOSPluginProps; }; const withAirship: ConfigPlugin = (config, props) => { diff --git a/plugin/src/withAirshipAndroid.ts b/plugin/src/withAirshipAndroid.ts index 87f2664..eba9004 100644 --- a/plugin/src/withAirshipAndroid.ts +++ b/plugin/src/withAirshipAndroid.ts @@ -7,6 +7,7 @@ import { import { generateImageAsync, ImageOptions } from '@expo/image-utils'; import { writeFileSync, existsSync, mkdirSync } from 'fs'; import { resolve, basename } from 'path'; + import { AirshipAndroidPluginProps } from './withAirship'; const iconSizeMap: Record = { diff --git a/plugin/src/withAirshipIOS.ts b/plugin/src/withAirshipIOS.ts index 8b8a6de..d656be8 100644 --- a/plugin/src/withAirshipIOS.ts +++ b/plugin/src/withAirshipIOS.ts @@ -1,10 +1,21 @@ import { ConfigPlugin, withEntitlementsPlist, - withInfoPlist + withInfoPlist, + withDangerousMod, + withXcodeProject, + withPodfile } from '@expo/config-plugins'; -import { AirshipIOSPluginProps } from './withAirship'; +import { readFile, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; + +import { AirshipIOSPluginProps } from './withAirship'; +import { mergeContents, MergeResults } from '@expo/config-plugins/build/utils/generateCode'; + +const NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME = "AirshipNotificationServiceExtension"; +const NOTIFICATION_SERVICE_FILE_NAME = "AirshipNotificationService.swift"; +const NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME = "AirshipNotificationServiceExtension-Info.plist"; const withCapabilities: ConfigPlugin = (config, props) => { return withInfoPlist(config, (plist) => { @@ -21,13 +32,170 @@ const withCapabilities: ConfigPlugin = (config, props) => const withAPNSEnvironment: ConfigPlugin = (config, props) => { return withEntitlementsPlist(config, (plist) => { - plist.modResults['aps-environment'] = props.mode + plist.modResults['aps-environment'] = props.mode; return plist; }); }; +async function writeNotificationServiceFilesAsync(props: AirshipIOSPluginProps, projectRoot: string) { + const pluginDir = require.resolve("airship-expo-plugin/package.json"); + const sourceDir = join(pluginDir, "../plugin/NotificationServiceExtension/"); + + const extensionPath = join(projectRoot, "ios", NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME); + + if (!existsSync(extensionPath)) { + mkdirSync(extensionPath, { recursive: true }); + } + + // Copy the AirshipNotificationService.swift file into the iOS expo project. + readFile(join(sourceDir, NOTIFICATION_SERVICE_FILE_NAME), 'utf8', (err, data) => { + if (err || !data) { + console.error("Airship couldn't read file " + join(sourceDir, NOTIFICATION_SERVICE_FILE_NAME)); + console.error(err); + return; + } + writeFileSync(join(extensionPath, NOTIFICATION_SERVICE_FILE_NAME), data); + }); + + // Copy the AirshipNotificationServiceExtension-Info.plist file into the iOS expo project. + readFile(join(sourceDir, NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME), 'utf8', (err, data) => { + if (err || !data) { + console.error("Airship couldn't read file " + join(sourceDir, NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME)); + console.error(err); + return; + } + writeFileSync(join(extensionPath, NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME), data); + }); +}; + +const withNotificationServiceExtension: ConfigPlugin = (config, props) => { + return withDangerousMod(config, [ + 'ios', + async config => { + await writeNotificationServiceFilesAsync(props, config.modRequest.projectRoot); + return config; + }, + ]); +}; + +const withExtensionTargetInXcodeProject: ConfigPlugin = (config, props) => { + return withXcodeProject(config, newConfig => { + const xcodeProject = newConfig.modResults; + + if (!!xcodeProject.pbxTargetByName(NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME)) { + console.log(NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME + " already exists in project. Skipping..."); + return newConfig; + } + + // Create new PBXGroup for the extension + const extGroup = xcodeProject.addPbxGroup( + [NOTIFICATION_SERVICE_FILE_NAME, NOTIFICATION_SERVICE_INFO_PLIST_FILE_NAME], + NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME, + NOTIFICATION_SERVICE_EXTENSION_TARGET_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); + } + }); + + // WORK AROUND for codeProject.addTarget BUG + // Xcode projects don't contain these if there is only one target + // 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( + NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME, + "app_extension", + NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME, + `${config.ios?.bundleIdentifier}.${NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME}` + ); + + // 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 == `"${NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME}"` + ) { + const buildSettingsObj = configurations[key].buildSettings; + buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = 14; + buildSettingsObj.SWIFT_VERSION = 5; + } + } + + return newConfig; + }); +}; + +const withAirshipServiceExtensionPod: ConfigPlugin = (config, props) => { + return withPodfile(config, async (config) => { + const airshipServiceExtensionPodfileSnippet = ` + target '${NOTIFICATION_SERVICE_EXTENSION_TARGET_NAME}' do + 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; + }); +}; + export const withAirshipIOS: ConfigPlugin = (config, props) => { - config = withCapabilities(config, props) - config = withAPNSEnvironment(config, props) + config = withCapabilities(config, props); + config = withAPNSEnvironment(config, props); + config = withNotificationServiceExtension(config, props); + config = withExtensionTargetInXcodeProject(config, props); + config = withAirshipServiceExtensionPod(config, props); return config; }; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index cc413bd..928e374 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1226,7 +1226,28 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@expo/config-plugins@^7.2.5", "@expo/config-plugins@~7.8.2": +"@expo/config-plugins@^8.0.4": + version "8.0.11" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-8.0.11.tgz#b814395a910f4c8b7cc95d9719dccb6ca53ea4c5" + integrity sha512-oALE1HwnLFthrobAcC9ocnR9KXLzfWEjgIe4CPe+rDsfC6GDs8dGYCXfRFoCEzoLN4TGYs9RdZ8r0KoCcNrm2A== + dependencies: + "@expo/config-types" "^51.0.3" + "@expo/json-file" "~8.3.0" + "@expo/plist" "^0.1.0" + "@expo/sdk-runtime-versions" "^1.0.0" + chalk "^4.1.2" + debug "^4.3.1" + find-up "~5.0.0" + getenv "^1.0.0" + glob "7.1.6" + resolve-from "^5.0.0" + semver "^7.5.4" + slash "^3.0.0" + slugify "^1.6.6" + xcode "^3.0.1" + xml2js "0.6.0" + +"@expo/config-plugins@~7.8.2": version "7.8.4" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.4.tgz#533b5d536c1dc8b5544d64878b51bda28f2e1a1f" integrity sha512-hv03HYxb/5kX8Gxv/BTI8TLc9L06WzqAfHRRXdbar4zkLcP2oTzvsLEF4/L/TIpD3rsnYa0KU42d0gWRxzPCJg== @@ -1254,6 +1275,11 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b" integrity sha512-0kkhIwXRT6EdFDwn+zTg9R2MZIAEYGn1MVkyRohAd+C9cXOb5RA8WLQi7vuxKF9m1SMtNAUrf0pO+ENK0+/KSw== +"@expo/config-types@^51.0.3": + version "51.0.3" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-51.0.3.tgz#520bdce5fd75f9d234fd81bd0347443086419450" + integrity sha512-hMfuq++b8VySb+m9uNNrlpbvGxYc8OcFCUX9yTmi9tlx6A4k8SDabWFBgmnr4ao3wEArvWrtUQIfQCVtPRdpKA== + "@expo/config@~8.5.0": version "8.5.4" resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.4.tgz#bb5eb06caa36e4e35dc8c7647fae63e147b830ca"