react-native-notify-kit 10.3.2 → 10.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -12
- package/app.plugin.js +3 -0
- package/cli/dist/commands/initNse.js +8 -9
- package/cli/dist/commands/initNse.js.map +1 -1
- package/cli/dist/lib/detectProject.d.ts +1 -4
- package/cli/dist/lib/detectProject.js +3 -32
- package/cli/dist/lib/detectProject.js.map +1 -1
- package/cli/dist/lib/initNseCore.d.ts +8 -0
- package/cli/dist/lib/initNseCore.js +197 -0
- package/cli/dist/lib/initNseCore.js.map +1 -0
- package/cli/dist/lib/patchPodfile.d.ts +9 -0
- package/cli/dist/lib/patchPodfile.js +22 -15
- package/cli/dist/lib/patchPodfile.js.map +1 -1
- package/cli/dist/lib/patchXcodeProject.d.ts +16 -0
- package/cli/dist/lib/patchXcodeProject.js +54 -26
- package/cli/dist/lib/patchXcodeProject.js.map +1 -1
- package/cli/dist/lib/writeTemplates.js +5 -14
- package/cli/dist/lib/writeTemplates.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +6 -0
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +95 -20
- package/ios/NotifeeCore/NotifeeCore.m +13 -0
- package/ios/RNNotifee/NotifeeApiModule.mm +7 -0
- package/ios/RNNotifee/NotifeeExtensionHelper.h +2 -2
- package/ios/RNNotifee/NotifeeExtensionHelper.m +1 -0
- package/package.json +16 -3
- package/plugin/build/android/withNotifyKitAndroidManifest.js +236 -0
- package/plugin/build/index.js +62 -0
- package/plugin/build/ios/withNotifyKitIosNseAppExtension.js +142 -0
- package/plugin/build/ios/withNotifyKitIosNseFiles.js +79 -0
- package/plugin/build/ios/withNotifyKitIosNsePodfile.js +130 -0
- package/plugin/build/ios/withNotifyKitIosNseXcodeProject.js +49 -0
- package/plugin/build/options.js +224 -0
- package/plugin/build/shared/nse/index.js +7 -0
- package/plugin/build/shared/nse/initNseCore.js +220 -0
- package/plugin/build/shared/nse/patchPodfile.js +261 -0
- package/plugin/build/shared/nse/patchXcodeProject.js +199 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { patchPodfileForNotifyKitNse } = require('../shared/nse/patchPodfile');
|
|
6
|
+
|
|
7
|
+
function withNotifyKitIosNsePodfile(config, nseOptions) {
|
|
8
|
+
if (!nseOptions.enabled) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { withPodfile } = requireExpoConfigPlugins();
|
|
13
|
+
const configuredUseFrameworks = detectConfiguredExpoBuildPropertiesUseFrameworks(config);
|
|
14
|
+
|
|
15
|
+
return withPodfile(config, modConfig => {
|
|
16
|
+
const { projectRoot, platformProjectRoot } = modConfig.modRequest;
|
|
17
|
+
const packagePathFromIos = resolveNotifyKitPackagePathFromIos(
|
|
18
|
+
projectRoot,
|
|
19
|
+
platformProjectRoot,
|
|
20
|
+
);
|
|
21
|
+
const hostUseFrameworks = detectPodfileUseFrameworks(modConfig.modResults.contents);
|
|
22
|
+
const useFrameworks = resolveNseUseFrameworks(hostUseFrameworks, configuredUseFrameworks);
|
|
23
|
+
const result = patchPodfileForNotifyKitNse(modConfig.modResults.contents, {
|
|
24
|
+
targetName: nseOptions.targetName,
|
|
25
|
+
packagePathFromIos,
|
|
26
|
+
placement: 'topLevel',
|
|
27
|
+
useFrameworks,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
modConfig.modResults.contents = result.contents;
|
|
31
|
+
return modConfig;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveNotifyKitPackagePathFromIos(projectRoot, platformProjectRoot) {
|
|
36
|
+
const packageJsonPath = require.resolve('react-native-notify-kit/package.json', {
|
|
37
|
+
paths: [projectRoot],
|
|
38
|
+
});
|
|
39
|
+
const packageDir = path.dirname(packageJsonPath);
|
|
40
|
+
|
|
41
|
+
return normalizePodfilePath(path.relative(platformProjectRoot, packageDir));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePodfilePath(filePath) {
|
|
45
|
+
return filePath.replace(/\\/g, '/');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectPodfileUseFrameworks(contents) {
|
|
49
|
+
let detected = false;
|
|
50
|
+
|
|
51
|
+
for (const line of contents.split('\n')) {
|
|
52
|
+
const stripped = line.replace(/#.*$/, '').trim();
|
|
53
|
+
if (!/^use_frameworks!(?:\s|$)/.test(stripped)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (/:linkage\s*=>\s*:static\b/.test(stripped) || /\blinkage:\s*:static\b/.test(stripped)) {
|
|
58
|
+
return 'static';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
/:linkage\s*=>\s*:dynamic\b/.test(stripped) ||
|
|
63
|
+
/\blinkage:\s*:dynamic\b/.test(stripped)
|
|
64
|
+
) {
|
|
65
|
+
return 'dynamic';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
detected = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return detected;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveNseUseFrameworks(hostUseFrameworks, configuredUseFrameworks) {
|
|
75
|
+
if (hostUseFrameworks === false) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (hostUseFrameworks === true && configuredUseFrameworks !== false) {
|
|
80
|
+
return configuredUseFrameworks;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return hostUseFrameworks;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectConfiguredExpoBuildPropertiesUseFrameworks(config) {
|
|
87
|
+
const plugins = Array.isArray(config.plugins) ? config.plugins : [];
|
|
88
|
+
let detected = false;
|
|
89
|
+
|
|
90
|
+
for (const plugin of plugins) {
|
|
91
|
+
if (!Array.isArray(plugin) || plugin[0] !== 'expo-build-properties') {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const options = isPlainObject(plugin[1]) ? plugin[1] : null;
|
|
96
|
+
const ios = isPlainObject(options?.ios) ? options.ios : null;
|
|
97
|
+
const useFrameworks = ios?.useFrameworks;
|
|
98
|
+
|
|
99
|
+
if (useFrameworks === 'static' || useFrameworks === 'dynamic') {
|
|
100
|
+
detected = useFrameworks;
|
|
101
|
+
} else if (useFrameworks === true || useFrameworks === false) {
|
|
102
|
+
detected = useFrameworks;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return detected;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPlainObject(value) {
|
|
110
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireExpoConfigPlugins() {
|
|
114
|
+
try {
|
|
115
|
+
return require('expo/config-plugins');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
try {
|
|
118
|
+
return require(require.resolve('expo/config-plugins', { paths: [process.cwd()] }));
|
|
119
|
+
} catch {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
withNotifyKitIosNsePodfile,
|
|
127
|
+
resolveNotifyKitPackagePathFromIos,
|
|
128
|
+
normalizePodfilePath,
|
|
129
|
+
detectPodfileUseFrameworks,
|
|
130
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
patchXcodeProjectForNotifyKitNse,
|
|
5
|
+
} = require('../shared/nse/patchXcodeProject');
|
|
6
|
+
const {
|
|
7
|
+
resolveNotifyKitIosNseBundleIdentifier,
|
|
8
|
+
} = require('./withNotifyKitIosNseAppExtension');
|
|
9
|
+
|
|
10
|
+
function withNotifyKitIosNseXcodeProject(config, nseOptions) {
|
|
11
|
+
if (!nseOptions.enabled) {
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { withXcodeProject } = requireExpoConfigPlugins();
|
|
16
|
+
const bundleIdentifier = resolveNotifyKitIosNseBundleIdentifier(config, nseOptions);
|
|
17
|
+
|
|
18
|
+
return withXcodeProject(config, modConfig => {
|
|
19
|
+
const result = patchXcodeProjectForNotifyKitNse(modConfig.modResults, {
|
|
20
|
+
targetName: nseOptions.targetName,
|
|
21
|
+
bundleIdentifier,
|
|
22
|
+
parentTargetName: modConfig.modRequest.projectName,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.didChange && !result.hostTargetUuid) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'[react-native-notify-kit] Failed to link NotifyKit NSE target to the host app target.',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return modConfig;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requireExpoConfigPlugins() {
|
|
36
|
+
try {
|
|
37
|
+
return require('expo/config-plugins');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
try {
|
|
40
|
+
return require(require.resolve('expo/config-plugins', { paths: [process.cwd()] }));
|
|
41
|
+
} catch {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
withNotifyKitIosNseXcodeProject,
|
|
49
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_IOS_NSE_TARGET_NAME = 'NotifyKitNSE';
|
|
4
|
+
const DEFAULT_IOS_NSE_BUNDLE_SUFFIX = '.NotifyKitNSE';
|
|
5
|
+
|
|
6
|
+
const ANDROID_FOREGROUND_SERVICE_TYPES = [
|
|
7
|
+
'camera',
|
|
8
|
+
'connectedDevice',
|
|
9
|
+
'dataSync',
|
|
10
|
+
'health',
|
|
11
|
+
'location',
|
|
12
|
+
'mediaPlayback',
|
|
13
|
+
'mediaProjection',
|
|
14
|
+
'microphone',
|
|
15
|
+
'phoneCall',
|
|
16
|
+
'remoteMessaging',
|
|
17
|
+
'shortService',
|
|
18
|
+
'specialUse',
|
|
19
|
+
'systemExempted',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const TARGET_NAME_PATTERN = /^[A-Za-z0-9_\-.]+$/;
|
|
23
|
+
const BUNDLE_SUFFIX_PATTERN = /^\.[A-Za-z0-9\-.]+$/;
|
|
24
|
+
|
|
25
|
+
function normalizeNotifyKitPluginOptions(options = {}) {
|
|
26
|
+
return {
|
|
27
|
+
ios: {
|
|
28
|
+
notificationServiceExtension: normalizeIosNotificationServiceExtensionOptions(
|
|
29
|
+
options.ios && options.ios.notificationServiceExtension,
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
android: {
|
|
33
|
+
foregroundService: normalizeAndroidForegroundServiceOptions(
|
|
34
|
+
options.android && options.android.foregroundService,
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeIosNotificationServiceExtensionOptions(input) {
|
|
41
|
+
if (input === undefined || input === false) {
|
|
42
|
+
return disabledIosNotificationServiceExtensionOptions();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (input === true) {
|
|
46
|
+
return validateEnabledIosNotificationServiceExtensionOptions({
|
|
47
|
+
enabled: true,
|
|
48
|
+
targetName: DEFAULT_IOS_NSE_TARGET_NAME,
|
|
49
|
+
bundleSuffix: DEFAULT_IOS_NSE_BUNDLE_SUFFIX,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!isPlainObject(input)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'[react-native-notify-kit] ios.notificationServiceExtension must be a boolean or an object.',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (input.enabled !== undefined && typeof input.enabled !== 'boolean') {
|
|
60
|
+
throw new Error(
|
|
61
|
+
'[react-native-notify-kit] ios.notificationServiceExtension.enabled must be a boolean.',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (input.enabled !== true) {
|
|
66
|
+
return disabledIosNotificationServiceExtensionOptions();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return validateEnabledIosNotificationServiceExtensionOptions({
|
|
70
|
+
enabled: true,
|
|
71
|
+
targetName: input.targetName ?? DEFAULT_IOS_NSE_TARGET_NAME,
|
|
72
|
+
bundleSuffix: input.bundleSuffix ?? DEFAULT_IOS_NSE_BUNDLE_SUFFIX,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function disabledIosNotificationServiceExtensionOptions() {
|
|
77
|
+
return {
|
|
78
|
+
enabled: false,
|
|
79
|
+
targetName: DEFAULT_IOS_NSE_TARGET_NAME,
|
|
80
|
+
bundleSuffix: DEFAULT_IOS_NSE_BUNDLE_SUFFIX,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateEnabledIosNotificationServiceExtensionOptions(options) {
|
|
85
|
+
if (typeof options.targetName !== 'string' || options.targetName.length === 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
'[react-native-notify-kit] ios.notificationServiceExtension.targetName must be a non-empty string.',
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!TARGET_NAME_PATTERN.test(options.targetName)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[react-native-notify-kit] Invalid notification service extension targetName '${options.targetName}'. ` +
|
|
94
|
+
'Use only letters, digits, underscores, hyphens, and dots.',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof options.bundleSuffix !== 'string' || options.bundleSuffix.length === 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'[react-native-notify-kit] ios.notificationServiceExtension.bundleSuffix must be a non-empty string.',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!BUNDLE_SUFFIX_PATTERN.test(options.bundleSuffix)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`[react-native-notify-kit] Invalid notification service extension bundleSuffix '${options.bundleSuffix}'. ` +
|
|
107
|
+
"It must start with '.' and contain only letters, digits, hyphens, and dots.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return options;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeAndroidForegroundServiceOptions(input) {
|
|
115
|
+
if (input === undefined) {
|
|
116
|
+
return disabledAndroidForegroundServiceOptions();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isPlainObject(input)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
'[react-native-notify-kit] android.foregroundService must be an object with a non-empty types array.',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const types = normalizeAndroidForegroundServiceTypes(input.types);
|
|
126
|
+
const specialUseSubtype = normalizeSpecialUseSubtype(input.specialUseSubtype);
|
|
127
|
+
const hasSpecialUse = types.includes('specialUse');
|
|
128
|
+
|
|
129
|
+
if (hasSpecialUse && specialUseSubtype === undefined) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
'[react-native-notify-kit] android.foregroundService.specialUseSubtype must be a non-empty string when types includes specialUse.',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!hasSpecialUse && specialUseSubtype !== undefined) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'[react-native-notify-kit] android.foregroundService.specialUseSubtype requires types to include specialUse.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
enabled: true,
|
|
143
|
+
types,
|
|
144
|
+
...(specialUseSubtype === undefined ? {} : { specialUseSubtype }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function disabledAndroidForegroundServiceOptions() {
|
|
149
|
+
return {
|
|
150
|
+
enabled: false,
|
|
151
|
+
types: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeAndroidForegroundServiceTypes(input) {
|
|
156
|
+
if (!Array.isArray(input)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
'[react-native-notify-kit] android.foregroundService.types must be a non-empty array.',
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (input.length === 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
'[react-native-notify-kit] android.foregroundService.types must be a non-empty array.',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
const types = [];
|
|
170
|
+
|
|
171
|
+
for (const value of input) {
|
|
172
|
+
if (typeof value !== 'string') {
|
|
173
|
+
throw new Error(
|
|
174
|
+
'[react-native-notify-kit] android.foregroundService.types must contain only strings.',
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const type = value.trim();
|
|
179
|
+
if (!isAndroidForegroundServiceType(type)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[react-native-notify-kit] Invalid android.foregroundService type '${value}'. ` +
|
|
182
|
+
`Allowed values: ${ANDROID_FOREGROUND_SERVICE_TYPES.join(', ')}.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!seen.has(type)) {
|
|
187
|
+
seen.add(type);
|
|
188
|
+
types.push(type);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return types;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeSpecialUseSubtype(input) {
|
|
196
|
+
if (input === undefined) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (typeof input !== 'string' || input.trim().length === 0) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
'[react-native-notify-kit] android.foregroundService.specialUseSubtype must be a non-empty string.',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return input.trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isAndroidForegroundServiceType(value) {
|
|
210
|
+
return ANDROID_FOREGROUND_SERVICE_TYPES.includes(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isPlainObject(value) {
|
|
214
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
DEFAULT_IOS_NSE_TARGET_NAME,
|
|
219
|
+
DEFAULT_IOS_NSE_BUNDLE_SUFFIX,
|
|
220
|
+
ANDROID_FOREGROUND_SERVICE_TYPES,
|
|
221
|
+
normalizeNotifyKitPluginOptions,
|
|
222
|
+
normalizeIosNotificationServiceExtensionOptions,
|
|
223
|
+
normalizeAndroidForegroundServiceOptions,
|
|
224
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const NSE_TARGET_NAME_PATTERN = /^[A-Za-z0-9_\-.]+$/;
|
|
4
|
+
const NSE_BUNDLE_SUFFIX_PATTERN = /^\.[A-Za-z0-9\-.]+$/;
|
|
5
|
+
|
|
6
|
+
const PRODUCT_BUNDLE_IDENTIFIER_PLACEHOLDER = '$(PRODUCT_BUNDLE_IDENTIFIER:default)';
|
|
7
|
+
|
|
8
|
+
const NOTIFICATION_SERVICE_SWIFT = String.raw`import Foundation
|
|
9
|
+
import UserNotifications
|
|
10
|
+
import RNNotifeeCore
|
|
11
|
+
|
|
12
|
+
private func nseLog(_ message: String) {
|
|
13
|
+
NSLog("[NotifyKitNSE] %@", message)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private func requestedAttachmentURLs(from userInfo: [AnyHashable: Any]) -> [String] {
|
|
17
|
+
guard let serializedOptions = userInfo["notifee_options"] as? String,
|
|
18
|
+
let data = serializedOptions.data(using: .utf8),
|
|
19
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
20
|
+
let ios = json["ios"] as? [String: Any],
|
|
21
|
+
let attachments = ios["attachments"] as? [[String: Any]] else {
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return attachments.compactMap { attachment in
|
|
26
|
+
attachment["url"] as? String
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class NotificationService: UNNotificationServiceExtension {
|
|
31
|
+
var contentHandler: ((UNNotificationContent) -> Void)?
|
|
32
|
+
var bestAttemptContent: UNMutableNotificationContent?
|
|
33
|
+
private let deliveryLock = NSLock()
|
|
34
|
+
private var didDeliver = false
|
|
35
|
+
|
|
36
|
+
override func didReceive(
|
|
37
|
+
_ request: UNNotificationRequest,
|
|
38
|
+
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
|
39
|
+
) {
|
|
40
|
+
let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
|
41
|
+
|
|
42
|
+
deliveryLock.lock()
|
|
43
|
+
didDeliver = false
|
|
44
|
+
self.contentHandler = contentHandler
|
|
45
|
+
self.bestAttemptContent = mutableContent
|
|
46
|
+
deliveryLock.unlock()
|
|
47
|
+
|
|
48
|
+
let requestedAttachmentUrls = requestedAttachmentURLs(from: request.content.userInfo)
|
|
49
|
+
|
|
50
|
+
nseLog(
|
|
51
|
+
"didReceive id=\(request.identifier) title=\(request.content.title) " +
|
|
52
|
+
"hasNotifeeOptions=\(request.content.userInfo["notifee_options"] != nil) " +
|
|
53
|
+
"requestedAttachments=\(requestedAttachmentUrls.count) " +
|
|
54
|
+
"urls=\(requestedAttachmentUrls.joined(separator: ","))"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
guard let bestAttemptContent = mutableContent else {
|
|
58
|
+
nseLog("mutableCopy failed for id=\(request.identifier); delivering original content")
|
|
59
|
+
deliverOnce(request.content)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
NotifeeExtensionHelper.populateNotificationContent(
|
|
64
|
+
request,
|
|
65
|
+
with: bestAttemptContent,
|
|
66
|
+
withContentHandler: { [weak self] content in
|
|
67
|
+
let deliveredAttachmentIds = content.attachments.map(\.identifier).joined(separator: ",")
|
|
68
|
+
nseLog(
|
|
69
|
+
"contentHandler id=\(request.identifier) title=\(content.title) " +
|
|
70
|
+
"deliveredAttachments=\(content.attachments.count) " +
|
|
71
|
+
"identifiers=\(deliveredAttachmentIds)"
|
|
72
|
+
)
|
|
73
|
+
self?.deliverOnce(content)
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private func deliverOnce(_ content: UNNotificationContent) {
|
|
79
|
+
var handler: ((UNNotificationContent) -> Void)?
|
|
80
|
+
|
|
81
|
+
deliveryLock.lock()
|
|
82
|
+
if !didDeliver {
|
|
83
|
+
didDeliver = true
|
|
84
|
+
handler = contentHandler
|
|
85
|
+
contentHandler = nil
|
|
86
|
+
bestAttemptContent = nil
|
|
87
|
+
}
|
|
88
|
+
deliveryLock.unlock()
|
|
89
|
+
|
|
90
|
+
handler?(content)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override func serviceExtensionTimeWillExpire() {
|
|
94
|
+
deliveryLock.lock()
|
|
95
|
+
let content = bestAttemptContent
|
|
96
|
+
deliveryLock.unlock()
|
|
97
|
+
|
|
98
|
+
if let bestAttemptContent = content {
|
|
99
|
+
nseLog(
|
|
100
|
+
"serviceExtensionTimeWillExpire id=\(bestAttemptContent.userInfo["gcm.message_id"] ?? "n/a") " +
|
|
101
|
+
"title=\(bestAttemptContent.title) " +
|
|
102
|
+
"deliveredAttachments=\(bestAttemptContent.attachments.count)"
|
|
103
|
+
)
|
|
104
|
+
deliverOnce(bestAttemptContent)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const NSE_INFO_PLIST_TEMPLATE = `<?xml version="1.0" encoding="UTF-8"?>
|
|
111
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
112
|
+
<plist version="1.0">
|
|
113
|
+
<dict>
|
|
114
|
+
\t<key>CFBundleDisplayName</key>
|
|
115
|
+
\t<string>{{TARGET_NAME}}</string>
|
|
116
|
+
\t<key>CFBundleExecutable</key>
|
|
117
|
+
\t<string>$(EXECUTABLE_NAME)</string>
|
|
118
|
+
\t<key>CFBundleIdentifier</key>
|
|
119
|
+
\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
120
|
+
\t<key>CFBundleInfoDictionaryVersion</key>
|
|
121
|
+
\t<string>6.0</string>
|
|
122
|
+
\t<key>CFBundleName</key>
|
|
123
|
+
\t<string>$(PRODUCT_NAME)</string>
|
|
124
|
+
\t<key>CFBundlePackageType</key>
|
|
125
|
+
\t<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
|
126
|
+
\t<key>CFBundleShortVersionString</key>
|
|
127
|
+
\t<string>1.0</string>
|
|
128
|
+
\t<key>CFBundleVersion</key>
|
|
129
|
+
\t<string>1</string>
|
|
130
|
+
\t<key>NSExtension</key>
|
|
131
|
+
\t<dict>
|
|
132
|
+
\t\t<key>NSExtensionPointIdentifier</key>
|
|
133
|
+
\t\t<string>com.apple.usernotifications.service</string>
|
|
134
|
+
\t\t<key>NSExtensionPrincipalClass</key>
|
|
135
|
+
\t\t<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
|
136
|
+
\t</dict>
|
|
137
|
+
</dict>
|
|
138
|
+
</plist>
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const NSE_ENTITLEMENTS_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
|
|
142
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
143
|
+
<plist version="1.0">
|
|
144
|
+
<dict>
|
|
145
|
+
</dict>
|
|
146
|
+
</plist>
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
function validateNseTargetName(targetName) {
|
|
150
|
+
if (!NSE_TARGET_NAME_PATTERN.test(targetName)) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Invalid target name '${targetName}'. Must match [A-Za-z0-9_-.]\n` +
|
|
153
|
+
' Target names can only contain letters, digits, underscores, hyphens, and dots.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateNseBundleSuffix(bundleSuffix) {
|
|
159
|
+
if (!NSE_BUNDLE_SUFFIX_PATTERN.test(bundleSuffix)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Invalid bundle suffix '${bundleSuffix}'. Must start with '.' and contain only letters, digits, hyphens, and dots.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function deriveNseBundleIdentifier(parentBundleId, suffix, parentTargetName) {
|
|
167
|
+
if (!parentBundleId) {
|
|
168
|
+
return `${PRODUCT_BUNDLE_IDENTIFIER_PLACEHOLDER}${suffix}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!parentBundleId.includes('$(') && !parentBundleId.includes('${')) {
|
|
172
|
+
return parentBundleId + suffix;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const expandedBundleId = expandKnownBundleIdVariables(parentBundleId, parentTargetName);
|
|
176
|
+
if (expandedBundleId && !expandedBundleId.includes('$(') && !expandedBundleId.includes('${')) {
|
|
177
|
+
return expandedBundleId + suffix;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `${PRODUCT_BUNDLE_IDENTIFIER_PLACEHOLDER}${suffix}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderNotificationServiceSwift() {
|
|
184
|
+
return NOTIFICATION_SERVICE_SWIFT;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderNseInfoPlist(options) {
|
|
188
|
+
return NSE_INFO_PLIST_TEMPLATE.replace(/\{\{TARGET_NAME\}\}/g, options.targetName);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function renderNseEntitlementsPlist() {
|
|
192
|
+
return NSE_ENTITLEMENTS_PLIST;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function expandKnownBundleIdVariables(bundleId, parentTargetName) {
|
|
196
|
+
if (!parentTargetName) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const normalizedTargetName = toRfc1034Identifier(parentTargetName);
|
|
201
|
+
|
|
202
|
+
return bundleId
|
|
203
|
+
.replace(/\$\((?:PRODUCT_NAME|TARGET_NAME):rfc1034identifier\)/g, normalizedTargetName)
|
|
204
|
+
.replace(/\$\{(?:PRODUCT_NAME|TARGET_NAME):rfc1034identifier\}/g, normalizedTargetName)
|
|
205
|
+
.replace(/\$\((?:PRODUCT_NAME|TARGET_NAME)\)/g, parentTargetName)
|
|
206
|
+
.replace(/\$\{(?:PRODUCT_NAME|TARGET_NAME)\}/g, parentTargetName);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function toRfc1034Identifier(value) {
|
|
210
|
+
return value.replace(/[^A-Za-z0-9.-]/g, '-');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
deriveNseBundleIdentifier,
|
|
215
|
+
renderNotificationServiceSwift,
|
|
216
|
+
renderNseEntitlementsPlist,
|
|
217
|
+
renderNseInfoPlist,
|
|
218
|
+
validateNseBundleSuffix,
|
|
219
|
+
validateNseTargetName,
|
|
220
|
+
};
|