Skip to content

Commit

Permalink
Add support for Airship Notification Service Extension
Browse files Browse the repository at this point in the history
  • Loading branch information
Ulrico972 committed Nov 22, 2024
1 parent 4cbccbb commit 41a58f9
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// NotificationService.swift


import AirshipServiceExtension

class NotificationService: UANotificationServiceExtension {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>AirshipNotificationServiceExtension</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME)</string>
</dict>
</dict>
</plist>
8 changes: 4 additions & 4 deletions plugin/src/withAirship.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AirshipPluginProps> = (config, props) => {
Expand Down
1 change: 1 addition & 0 deletions plugin/src/withAirshipAndroid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number> = {
Expand Down
178 changes: 173 additions & 5 deletions plugin/src/withAirshipIOS.ts
Original file line number Diff line number Diff line change
@@ -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<AirshipIOSPluginProps> = (config, props) => {
return withInfoPlist(config, (plist) => {
Expand All @@ -21,13 +32,170 @@ const withCapabilities: ConfigPlugin<AirshipIOSPluginProps> = (config, props) =>

const withAPNSEnvironment: ConfigPlugin<AirshipIOSPluginProps> = (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<AirshipIOSPluginProps> = (config, props) => {
return withDangerousMod(config, [
'ios',
async config => {
await writeNotificationServiceFilesAsync(props, config.modRequest.projectRoot);
return config;
},
]);
};

const withExtensionTargetInXcodeProject: ConfigPlugin<AirshipIOSPluginProps> = (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<AirshipIOSPluginProps> = (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<AirshipIOSPluginProps> = (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;
};
28 changes: 27 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 41a58f9

Please sign in to comment.