react-native-notify-kit 10.3.3 → 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/RNNotifee/NotifeeExtensionHelper.h +2 -2
- package/ios/RNNotifee/NotifeeExtensionHelper.m +1 -0
- package/package.json +15 -2
- 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,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
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_PACKAGE_PATH_FROM_IOS = '../node_modules/react-native-notify-kit';
|
|
4
|
+
const RNFB_INFO_PLIST_INPUT_PATH = '$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)';
|
|
5
|
+
const RNFB_POST_INSTALL_MARKER =
|
|
6
|
+
'NotifyKitNSE: avoid an Xcode build cycle between the embedded app extension';
|
|
7
|
+
|
|
8
|
+
function patchPodfileForNotifyKitNse(podfileText, options) {
|
|
9
|
+
let patched = podfileText;
|
|
10
|
+
let changed = false;
|
|
11
|
+
const packagePathFromIos = options.packagePathFromIos ?? DEFAULT_PACKAGE_PATH_FROM_IOS;
|
|
12
|
+
const placement = options.placement ?? 'nested';
|
|
13
|
+
const useFrameworks = options.useFrameworks ?? false;
|
|
14
|
+
|
|
15
|
+
if (!hasUncommentedTarget(patched, options.targetName)) {
|
|
16
|
+
const withNseTarget = insertNseTarget(
|
|
17
|
+
patched,
|
|
18
|
+
options.targetName,
|
|
19
|
+
packagePathFromIos,
|
|
20
|
+
placement,
|
|
21
|
+
useFrameworks,
|
|
22
|
+
);
|
|
23
|
+
if (withNseTarget === null) {
|
|
24
|
+
return { contents: patched, didChange: changed };
|
|
25
|
+
}
|
|
26
|
+
patched = withNseTarget;
|
|
27
|
+
changed = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const withRnfbPatch = ensureRnfbPostInstallPatch(patched);
|
|
31
|
+
if (withRnfbPatch !== patched) {
|
|
32
|
+
patched = withRnfbPatch;
|
|
33
|
+
changed = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { contents: patched, didChange: changed };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function insertNseTarget(content, targetName, packagePathFromIos, placement, useFrameworks) {
|
|
40
|
+
const block = buildNseTargetBlock(targetName, packagePathFromIos, placement, useFrameworks);
|
|
41
|
+
|
|
42
|
+
const targetMatch = content.match(/^target\s+['"][^'"]+['"]\s+do/m);
|
|
43
|
+
if (!targetMatch || targetMatch.index === undefined) {
|
|
44
|
+
return appendNseTarget(content, block, placement);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const targetEndIndex = findMatchingRubyBlockEnd(content, targetMatch.index);
|
|
48
|
+
|
|
49
|
+
if (targetEndIndex === -1) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Could not locate main app target's closing 'end' in Podfile. NSE insertion aborted. " +
|
|
52
|
+
'Your Podfile may use abstract_target, unusual formatting, or nested blocks - ' +
|
|
53
|
+
'add the NSE target manually per the legacy guide.',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (placement === 'topLevel') {
|
|
58
|
+
const insertIndex = findLineEnd(content, targetEndIndex);
|
|
59
|
+
const separator = insertIndex > 0 && content[insertIndex - 1] === '\n' ? '\n' : '\n\n';
|
|
60
|
+
return content.slice(0, insertIndex) + separator + block + content.slice(insertIndex);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const insertIndex = targetEndIndex;
|
|
64
|
+
return content.slice(0, insertIndex) + block + content.slice(insertIndex);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildNseTargetBlock(targetName, packagePathFromIos, placement, useFrameworks) {
|
|
68
|
+
if (placement === 'topLevel') {
|
|
69
|
+
const useFrameworksLine = buildUseFrameworksLine(useFrameworks);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
`target '${targetName}' do\n` +
|
|
73
|
+
useFrameworksLine +
|
|
74
|
+
` pod 'RNNotifeeCore', :path => '${packagePathFromIos}'\n` +
|
|
75
|
+
`end\n`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
`\n target '${targetName}' do\n` +
|
|
81
|
+
` inherit! :search_paths\n` +
|
|
82
|
+
` pod 'RNNotifeeCore', :path => '${packagePathFromIos}'\n` +
|
|
83
|
+
` end\n`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildUseFrameworksLine(useFrameworks) {
|
|
88
|
+
if (useFrameworks === 'static') {
|
|
89
|
+
return ` use_frameworks! :linkage => :static\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (useFrameworks === 'dynamic') {
|
|
93
|
+
return ` use_frameworks! :linkage => :dynamic\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (useFrameworks === true) {
|
|
97
|
+
return ` use_frameworks!\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function appendNseTarget(content, block, placement) {
|
|
104
|
+
if (placement === 'nested') {
|
|
105
|
+
return content + '\n' + block;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const separator = content.length === 0 ? '' : content.endsWith('\n') ? '\n' : '\n\n';
|
|
109
|
+
return content + separator + block;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findLineEnd(content, lineStartIndex) {
|
|
113
|
+
const newlineIndex = content.indexOf('\n', lineStartIndex);
|
|
114
|
+
return newlineIndex === -1 ? content.length : newlineIndex + 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ensureRnfbPostInstallPatch(content) {
|
|
118
|
+
if (content.includes(RNFB_POST_INSTALL_MARKER)) {
|
|
119
|
+
return content;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const postInstall = findPostInstallBlock(content);
|
|
123
|
+
if (postInstall) {
|
|
124
|
+
const snippet =
|
|
125
|
+
'\n' + buildRnfbPostInstallSnippet(postInstall.bodyIndent, postInstall.paramName);
|
|
126
|
+
return content.slice(0, postInstall.endIndex) + snippet + content.slice(postInstall.endIndex);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const separator = content.trim().length === 0 || content.endsWith('\n') ? '\n' : '\n\n';
|
|
130
|
+
return (
|
|
131
|
+
content +
|
|
132
|
+
separator +
|
|
133
|
+
'post_install do |installer|\n' +
|
|
134
|
+
buildRnfbPostInstallSnippet(' ', 'installer') +
|
|
135
|
+
'end\n'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildRnfbPostInstallSnippet(indent, installerParamName) {
|
|
140
|
+
return [
|
|
141
|
+
`${indent}# ${RNFB_POST_INSTALL_MARKER}`,
|
|
142
|
+
`${indent}# and React Native Firebase's Info.plist processing phase.`,
|
|
143
|
+
`${indent}rnfb_info_plist_input_path = '${RNFB_INFO_PLIST_INPUT_PATH}'`,
|
|
144
|
+
`${indent}rnfb_phase_names = [`,
|
|
145
|
+
`${indent} '[RNFB] Core Configuration',`,
|
|
146
|
+
`${indent} '[CP-User] [RNFB] Core Configuration',`,
|
|
147
|
+
`${indent}]`,
|
|
148
|
+
'',
|
|
149
|
+
`${indent}${installerParamName}.aggregate_targets.each do |aggregate_target|`,
|
|
150
|
+
`${indent} aggregate_target.target_definition.script_phases.each do |script_phase|`,
|
|
151
|
+
`${indent} next unless rnfb_phase_names.include?(script_phase[:name])`,
|
|
152
|
+
`${indent} next unless script_phase[:input_files]`,
|
|
153
|
+
'',
|
|
154
|
+
`${indent} script_phase[:input_files].delete(rnfb_info_plist_input_path)`,
|
|
155
|
+
`${indent} end`,
|
|
156
|
+
'',
|
|
157
|
+
`${indent} user_project = aggregate_target.user_project`,
|
|
158
|
+
`${indent} next unless user_project`,
|
|
159
|
+
'',
|
|
160
|
+
`${indent} rnfb_input_path_removed = false`,
|
|
161
|
+
'',
|
|
162
|
+
`${indent} user_project.targets.each do |target|`,
|
|
163
|
+
`${indent} target.shell_script_build_phases.each do |phase|`,
|
|
164
|
+
`${indent} next unless rnfb_phase_names.include?(phase.name)`,
|
|
165
|
+
`${indent} next unless phase.input_paths`,
|
|
166
|
+
'',
|
|
167
|
+
`${indent} if phase.input_paths.delete(rnfb_info_plist_input_path)`,
|
|
168
|
+
`${indent} rnfb_input_path_removed = true`,
|
|
169
|
+
`${indent} end`,
|
|
170
|
+
`${indent} end`,
|
|
171
|
+
`${indent} end`,
|
|
172
|
+
'',
|
|
173
|
+
`${indent} user_project.save if rnfb_input_path_removed`,
|
|
174
|
+
`${indent}end`,
|
|
175
|
+
'',
|
|
176
|
+
].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function findPostInstallBlock(content) {
|
|
180
|
+
const postInstallPattern = /^([ \t]*)post_install\s+do\s+\|([^|]+)\|\s*(?:#.*)?$/gm;
|
|
181
|
+
let match;
|
|
182
|
+
|
|
183
|
+
while ((match = postInstallPattern.exec(content)) !== null) {
|
|
184
|
+
const endIndex = findMatchingRubyBlockEnd(content, match.index);
|
|
185
|
+
if (endIndex !== -1) {
|
|
186
|
+
return {
|
|
187
|
+
bodyIndent: `${match[1]} `,
|
|
188
|
+
endIndex,
|
|
189
|
+
paramName: match[2].trim(),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findMatchingRubyBlockEnd(content, startIndex) {
|
|
198
|
+
const afterStart = content.slice(startIndex);
|
|
199
|
+
const lines = afterStart.split('\n');
|
|
200
|
+
let depth = 0;
|
|
201
|
+
let charIndex = startIndex;
|
|
202
|
+
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
const trimmed = stripRubyLineComment(line).trim();
|
|
205
|
+
|
|
206
|
+
depth += countRubyBlockOpeners(trimmed);
|
|
207
|
+
|
|
208
|
+
if (trimmed === 'end') {
|
|
209
|
+
depth--;
|
|
210
|
+
if (depth === 0) {
|
|
211
|
+
return charIndex;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
charIndex += line.length + 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return -1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function countRubyBlockOpeners(line) {
|
|
222
|
+
if (line.length === 0) {
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const startsWithKeywordBlock = /^(if|unless|case|begin|while|until|for|def|class|module)\b/.test(
|
|
227
|
+
line,
|
|
228
|
+
);
|
|
229
|
+
if (startsWithKeywordBlock) {
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (/\bdo\b(\s*\|[^|]*\|)?\s*$/.test(line)) {
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function stripRubyLineComment(line) {
|
|
241
|
+
return line.replace(/#.*$/, '');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function hasUncommentedTarget(content, targetName) {
|
|
245
|
+
const pattern = new RegExp(`target\\s+['"]${escapeRegex(targetName)}['"]`);
|
|
246
|
+
for (const line of content.split('\n')) {
|
|
247
|
+
const stripped = line.replace(/#.*$/, '');
|
|
248
|
+
if (pattern.test(stripped)) {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function escapeRegex(s) {
|
|
256
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
patchPodfileForNotifyKitNse,
|
|
261
|
+
};
|