rebreak-monorepo/apps/rebreak-native/plugins/with-voip-pushkit-ios.js

206 lines
7.1 KiB
JavaScript

/* eslint-disable @typescript-eslint/no-var-requires */
/**
* VoIP-PushKit + CallKit Bridge im iOS-AppDelegate.
*
* Patcht beim `expo prebuild`:
* 1) ReBreak-Bridging-Header.h
* → exposed RNVoipPushNotificationManager + RNCallKeep an Swift.
* 2) AppDelegate.swift
* → `import PushKit`
* → PKPushRegistry init in didFinishLaunchingWithOptions
* → PKPushRegistryDelegate-Extension: didUpdatePushCredentials forwarded an
* RNVoipPushNotificationManager; didReceiveIncomingPushWithPayload meldet
* sofort eingehenden Anruf an RNCallKeep (iOS 13+ Anforderung: MUSS in
* derselben run-loop CXProvider.reportNewIncomingCall aufrufen, sonst
* killt iOS die App).
*
* react-native-voip-push-notification hat KEIN config-plugin → ohne diesen
* Patch greift `pnpm expo prebuild --clean` jedes Mal die Edits ab.
*
* Idempotent via Marker. Läuft als withDangerousMod('ios'), nach prebuild,
* vor pod install.
*/
const { withDangerousMod } = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
const MARKER = '// REBREAK_VOIP_PUSHKIT';
// ───────────────────────────────────────────────────── Bridging-Header
const BRIDGING_PATCH = `
${MARKER}
#import "RNVoipPushNotificationManager.h"
#import "RNCallKeep.h"
${MARKER}
`;
// ───────────────────────────────────────────────────── AppDelegate.swift
const IMPORT_PATCH = `${MARKER}
import PushKit
${MARKER}
`;
// In didFinishLaunchingWithOptions — direkt vor `return super.application(...)`.
const REGISTRY_INIT = `
${MARKER}
// PushKit-Registry für VoIP-Push (CallKit). MUSS in didFinishLaunching
// initialisiert werden, sonst kommt der erste Push nach App-Cold-Start nicht
// an.
let voipRegistry = PKPushRegistry(queue: nil)
voipRegistry.desiredPushTypes = [.voIP]
voipRegistry.delegate = self
${MARKER}
`;
// PKPushRegistryDelegate-Extension am Ende der Datei.
const DELEGATE_EXTENSION = `
${MARKER}
// MARK: - VoIP PushKit + CallKit Bridge
extension AppDelegate: PKPushRegistryDelegate {
// Device-Token vom System nach voIP-Registrierung.
public func pushRegistry(
_ registry: PKPushRegistry,
didUpdate pushCredentials: PKPushCredentials,
for type: PKPushType
) {
RNVoipPushNotificationManager.didUpdate(pushCredentials, forType: type.rawValue)
}
// Token-Invalidierung — aktuell no-op; Client refresht beim nächsten Boot.
public func pushRegistry(
_ registry: PKPushRegistry,
didInvalidatePushTokenFor type: PKPushType
) {
// nothing
}
// Eingehender VoIP-Push. MUSS auf iOS 13+ in derselben run-loop
// CXProvider.reportNewIncomingCall aufrufen, sonst killt iOS die App
// ("VoIP push received but app didn't post incoming call").
public func pushRegistry(
_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType,
completion: @escaping () -> Void
) {
let dict = payload.dictionaryPayload
let callId = (dict["callId"] as? String) ?? UUID().uuidString
let callerName = (dict["callerName"] as? String) ?? "ReBreak"
let handle = (dict["handle"] as? String) ?? callerName
// 1) Sofort an CallKit melden (iOS-Pflicht). uuid MUSS deterministisch sein,
// damit der JS-Layer den Call mit demselben UUID antworten/auflegen kann.
RNCallKeep.reportNewIncomingCall(
callId,
handle: handle,
handleType: "generic",
hasVideo: false,
localizedCallerName: callerName,
supportsHolding: false,
supportsDTMF: false,
supportsGrouping: false,
supportsUngrouping: false,
fromPushKit: true,
payload: dict,
withCompletionHandler: completion
)
// 2) Forward an JS — wenn die App schon im Vordergrund war, kann sie auf
// didLoadWithEvents reagieren (Auto-Navigation /call).
RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue)
}
}
${MARKER}
`;
// ─────────────────────────────────────────────────────────────────────
function patchBridgingHeader(iosRoot) {
// Bridging-Header heißt analog zum Target — bei uns "ReBreak-Bridging-Header.h".
const candidates = fs
.readdirSync(iosRoot, { withFileTypes: true })
.filter((d) => d.isDirectory())
.flatMap((d) => {
const p = path.join(iosRoot, d.name);
return fs.existsSync(p)
? fs
.readdirSync(p)
.filter((f) => f.endsWith('-Bridging-Header.h'))
.map((f) => path.join(p, f))
: [];
});
if (candidates.length === 0) {
console.warn('[with-voip-pushkit-ios] no *-Bridging-Header.h found — skipping bridging patch');
return;
}
for (const headerPath of candidates) {
let header = fs.readFileSync(headerPath, 'utf-8');
if (header.includes(MARKER)) continue;
header += BRIDGING_PATCH;
fs.writeFileSync(headerPath, header);
console.log('[with-voip-pushkit-ios] patched bridging header:', path.basename(headerPath));
}
}
function patchAppDelegate(iosRoot) {
const candidates = fs
.readdirSync(iosRoot, { withFileTypes: true })
.filter((d) => d.isDirectory())
.flatMap((d) => {
const p = path.join(iosRoot, d.name);
const file = path.join(p, 'AppDelegate.swift');
return fs.existsSync(file) ? [file] : [];
});
if (candidates.length === 0) {
console.warn('[with-voip-pushkit-ios] AppDelegate.swift not found — skipping');
return;
}
for (const appDelegatePath of candidates) {
let src = fs.readFileSync(appDelegatePath, 'utf-8');
if (src.includes(MARKER)) continue;
// 1) Top-of-file: import PushKit (nach `import React` einfügen).
if (!src.includes('import PushKit')) {
src = src.replace(/(import React\n)/, `$1${IMPORT_PATCH}`);
}
// 2) didFinishLaunching: PKPushRegistry direkt vor dem return-super injizieren.
const returnSuperRe = /(\n\s+return super\.application\(application, didFinishLaunchingWithOptions: launchOptions\))/;
if (returnSuperRe.test(src)) {
src = src.replace(returnSuperRe, `${REGISTRY_INIT}$1`);
} else {
console.warn('[with-voip-pushkit-ios] kein didFinishLaunching-return gefunden — Registry-Init NICHT eingefügt');
}
// 3) Delegate-Extension am Ende der Datei anhängen.
src += DELEGATE_EXTENSION;
fs.writeFileSync(appDelegatePath, src);
console.log('[with-voip-pushkit-ios] patched AppDelegate.swift');
}
}
module.exports = function withVoipPushKitIOS(config) {
return withDangerousMod(config, [
'ios',
async (cfg) => {
const iosRoot = cfg.modRequest.platformProjectRoot;
try {
patchBridgingHeader(iosRoot);
patchAppDelegate(iosRoot);
} catch (e) {
console.error('[with-voip-pushkit-ios] patch failed:', e);
}
return cfg;
},
]);
};