Skip to content

Commit d5848ba

Browse files
CopilotBunsDev
andauthored
Harden local-notifications iOS patching for Capacitor 8 API drift in release builds (#482)
* Initial plan * fix: harden Capacitor local-notifications iOS patching Agent-Logs-Url: https://github.com/OpenKnots/okcode/sessions/76a7e532-d68e-4b0b-9194-62dd020d7ae3 Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> * refactor: simplify local-notifications patch validation constants Agent-Logs-Url: https://github.com/OpenKnots/okcode/sessions/76a7e532-d68e-4b0b-9194-62dd020d7ae3 Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BunsDev <68980965+BunsDev@users.noreply.github.com>
1 parent 7d560ea commit d5848ba

File tree

1 file changed

+114
-25
lines changed

1 file changed

+114
-25
lines changed

scripts/patch-capacitor-local-notifications.ts

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
import { resolve } from "node:path";
33

44
const ROOT_DIR = resolve(process.cwd());
5+
const PACKAGE_FILE = resolve(ROOT_DIR, "node_modules/@capacitor/local-notifications/package.json");
56
const TARGET_FILE = resolve(
67
ROOT_DIR,
78
"node_modules/@capacitor/local-notifications/ios/Sources/LocalNotificationsPlugin/LocalNotificationsPlugin.swift",
@@ -11,15 +12,32 @@ const HANDLER_FILE = resolve(
1112
"node_modules/@capacitor/local-notifications/ios/Sources/LocalNotificationsPlugin/LocalNotificationsHandler.swift",
1213
);
1314

14-
const REPLACEMENTS: ReadonlyArray<[string, string]> = [
15+
const SWIFT_REPLACEMENTS: ReadonlyArray<[string, string]> = [
1516
[
1617
'call.getArray("notifications", JSObject.self)',
18+
'call.getArray("notifications", []).compactMap({ $0 as? JSObject })',
19+
],
20+
[
1721
'call.getArray("notifications")?.compactMap({ $0 as? JSObject })',
22+
'call.getArray("notifications", []).compactMap({ $0 as? JSObject })',
23+
],
24+
['call.getArray("types", JSObject.self)', 'call.getArray("types", []).compactMap({ $0 as? JSObject })'],
25+
['call.getArray("types")?.compactMap({ $0 as? JSObject })', 'call.getArray("types", []).compactMap({ $0 as? JSObject })'],
26+
["call.reject(\"Must provide notifications array as notifications option\")", "call.unimplemented(\"Must provide notifications array as notifications option\")"],
27+
["call.reject(\"Notification missing identifier\")", "call.unimplemented(\"Notification missing identifier\")"],
28+
["call.reject(\"Unable to make notification\", nil, error)", "call.unimplemented(\"Unable to make notification\")"],
29+
[
30+
"call.reject(\"Unable to create notification, trigger failed\", nil, error)",
31+
"call.unimplemented(\"Unable to create notification, trigger failed\")",
1832
],
33+
["call.reject(theError.localizedDescription)", "call.unimplemented(theError.localizedDescription)"],
34+
["call.reject(error!.localizedDescription)", "call.unimplemented(error!.localizedDescription)"],
35+
["call.reject(\"Must supply notifications to cancel\")", "call.unimplemented(\"Must supply notifications to cancel\")"],
1936
[
20-
'call.getArray("types", JSObject.self)',
21-
'call.getArray("types")?.compactMap({ $0 as? JSObject })',
37+
"call.reject(\"Scheduled time must be *after* current time\")",
38+
"call.unimplemented(\"Scheduled time must be *after* current time\")",
2239
],
40+
["call.reject(\"Must supply notifications to remove\")", "call.unimplemented(\"Must supply notifications to remove\")"],
2341
[
2442
"return bridge?.localURL(fromWebURL: webURL)",
2543
[
@@ -34,6 +52,22 @@ const REPLACEMENTS: ReadonlyArray<[string, string]> = [
3452
].join("\n"),
3553
],
3654
];
55+
const LEGACY_PLUGIN_PATTERNS = [
56+
'call.getArray("notifications", JSObject.self)',
57+
'call.getArray("notifications")?.compactMap({ $0 as? JSObject })',
58+
"call.reject(",
59+
] as const;
60+
const REQUIRED_PATCHED_PATTERNS = [
61+
'call.getArray("notifications", []).compactMap({ $0 as? JSObject })',
62+
'call.getArray("types", []).compactMap({ $0 as? JSObject })',
63+
] as const;
64+
const FORBIDDEN_PATCHED_PATTERNS = [
65+
"call.reject(",
66+
'call.getArray("notifications", JSObject.self)',
67+
'call.getArray("notifications")?.compactMap({ $0 as? JSObject })',
68+
'call.getArray("types", JSObject.self)',
69+
'call.getArray("types")?.compactMap({ $0 as? JSObject })',
70+
] as const;
3771

3872
const HANDLER_PATCH = {
3973
search:
@@ -165,32 +199,87 @@ const HANDLER_PATCH = {
165199
` }\n`,
166200
};
167201

168-
if (!existsSync(TARGET_FILE)) {
169-
console.log(`Skipping Capacitor local-notifications patch; missing file: ${TARGET_FILE}`);
170-
process.exit(0);
202+
function replaceAll(source: string, search: string, replacement: string): { updated: string; count: number } {
203+
const sourceParts = source.split(search);
204+
const count = sourceParts.length - 1;
205+
if (count <= 0) {
206+
return { updated: source, count: 0 };
207+
}
208+
209+
return { updated: sourceParts.join(replacement), count };
171210
}
172211

173-
for (const targetFile of [TARGET_FILE, HANDLER_FILE]) {
174-
if (!existsSync(targetFile)) {
175-
console.log(`Skipping Capacitor local-notifications patch; missing file: ${targetFile}`);
176-
continue;
212+
function assert(condition: unknown, message: string): asserts condition {
213+
if (!condition) {
214+
throw new Error(message);
177215
}
216+
}
178217

179-
const original = readFileSync(targetFile, "utf8");
180-
let updated = original;
218+
function isPluginSwiftPatched(source: string): boolean {
219+
return (
220+
REQUIRED_PATCHED_PATTERNS.every((pattern) => source.includes(pattern)) &&
221+
FORBIDDEN_PATCHED_PATTERNS.every((pattern) => !source.includes(pattern))
222+
);
223+
}
181224

182-
if (targetFile === TARGET_FILE) {
183-
for (const [pattern, replacement] of REPLACEMENTS) {
184-
updated = updated.replace(pattern, replacement);
185-
}
186-
} else if (!updated.includes("private func makeJSObject(from")) {
187-
updated = updated.replace(HANDLER_PATCH.search, HANDLER_PATCH.replace);
188-
}
225+
assert(existsSync(PACKAGE_FILE), `Missing local-notifications package: ${PACKAGE_FILE}`);
226+
assert(existsSync(TARGET_FILE), `Missing local-notifications Swift source: ${TARGET_FILE}`);
227+
assert(existsSync(HANDLER_FILE), `Missing local-notifications Swift source: ${HANDLER_FILE}`);
189228

190-
if (updated !== original) {
191-
writeFileSync(targetFile, updated);
192-
console.log(`Patched ${targetFile}`);
193-
} else {
194-
console.log(`No patch needed for ${targetFile}`);
195-
}
229+
const pluginPackage = JSON.parse(readFileSync(PACKAGE_FILE, "utf8")) as { version?: string };
230+
const pluginVersion = pluginPackage.version ?? "unknown";
231+
const pluginMajor = Number.parseInt(pluginVersion.split(".")[0] ?? "", 10);
232+
233+
assert(Number.isFinite(pluginMajor), `Unable to parse @capacitor/local-notifications version: ${pluginVersion}`);
234+
assert(pluginMajor === 8, `Unsupported @capacitor/local-notifications major version ${pluginMajor}; expected 8.x`);
235+
236+
let pluginOriginal = readFileSync(TARGET_FILE, "utf8");
237+
let pluginUpdated = pluginOriginal;
238+
let pluginChanges = 0;
239+
for (const [search, replacement] of SWIFT_REPLACEMENTS) {
240+
const result = replaceAll(pluginUpdated, search, replacement);
241+
pluginUpdated = result.updated;
242+
pluginChanges += result.count;
196243
}
244+
245+
const pluginHadKnownLegacyPattern = LEGACY_PLUGIN_PATTERNS.some((pattern) =>
246+
pluginOriginal.includes(pattern),
247+
);
248+
const pluginLooksPatched = isPluginSwiftPatched(pluginUpdated);
249+
250+
assert(
251+
pluginHadKnownLegacyPattern || pluginLooksPatched,
252+
"Unsupported LocalNotificationsPlugin.swift layout; patch script needs to be updated for the installed plugin version.",
253+
);
254+
255+
if (pluginUpdated !== pluginOriginal) {
256+
writeFileSync(TARGET_FILE, pluginUpdated);
257+
console.log(`Patched ${TARGET_FILE} (${pluginChanges} replacements, @capacitor/local-notifications ${pluginVersion})`);
258+
} else {
259+
console.log(`No LocalNotificationsPlugin.swift changes needed (@capacitor/local-notifications ${pluginVersion})`);
260+
}
261+
262+
assert(pluginLooksPatched, "LocalNotificationsPlugin.swift patch verification failed.");
263+
264+
const handlerOriginal = readFileSync(HANDLER_FILE, "utf8");
265+
let handlerUpdated = handlerOriginal;
266+
if (!handlerUpdated.includes("private func makeJSObject(from")) {
267+
const result = replaceAll(handlerUpdated, HANDLER_PATCH.search, HANDLER_PATCH.replace);
268+
assert(
269+
result.count > 0,
270+
"Unable to patch LocalNotificationsHandler.swift; expected source pattern was not found.",
271+
);
272+
handlerUpdated = result.updated;
273+
}
274+
275+
if (handlerUpdated !== handlerOriginal) {
276+
writeFileSync(HANDLER_FILE, handlerUpdated);
277+
console.log(`Patched ${HANDLER_FILE}`);
278+
} else {
279+
console.log(`No LocalNotificationsHandler.swift changes needed`);
280+
}
281+
282+
assert(
283+
handlerUpdated.includes("private func makeJSObject(from"),
284+
"LocalNotificationsHandler.swift patch verification failed.",
285+
);

0 commit comments

Comments
 (0)