/* 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. Als Property gehalten (self.voipRegistry, siehe Property-Deklaration // unter dem class-Header) damit sie nicht out-of-scope dealloziert wird — // sonst kommt die Token-Registration noch durch, aber didReceiveIncomingPush // feuert NIE → iPhone wacht im Background nicht auf. let registry = PKPushRegistry(queue: .main) registry.delegate = self registry.desiredPushTypes = [.voIP] self.voipRegistry = registry ${MARKER} `; // Class-Property für den Registry-Halter — muss innerhalb der AppDelegate-Klasse // stehen. Wird direkt nach `var reactNativeFactory:` eingefügt. const CLASS_PROPERTY = ` ${MARKER} var voipRegistry: PKPushRegistry? ${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 ) { let hex = pushCredentials.token.map { String(format: "%02hhx", $0) }.joined() NSLog("[VoIP] didUpdate token (len=%d) prefix=%@", pushCredentials.token.count, String(hex.prefix(16))) 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 ) { NSLog("[VoIP] didInvalidatePushTokenFor type=%@", type.rawValue) } // 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 // callId (z.B. "1780632453911-8jeln4xg") ist KEIN valides UUID. CXProvider. // reportNewIncomingCall parst den String via NSUUID → nil → NSInvalidArgument // Exception → App-Crash → iOS-VoIP-Drossel. Deterministisch konvertieren wie // der JS-Layer (lib/callkit.ts:callIdToUuid), damit CallKit + JS dieselbe UUID // nutzen (accept/end matchen sonst nicht). let callUUID = AppDelegate.callIdToUuid(callId) NSLog("[VoIP] didReceiveIncomingPush callId=%@ uuid=%@ callerName=%@", callId, callUUID, 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( callUUID, 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) } // Deterministische callId→UUID-Abbildung. MUSS exakt lib/callkit.ts:callIdToUuid // entsprechen, sonst nutzen CallKit (nativ) und der JS-Layer verschiedene UUIDs // und accept/end greifen ins Leere. static func callIdToUuid(_ callId: String) -> String { var hex = "" for scalar in callId.unicodeScalars { if hex.count >= 32 { break } let code = Int(scalar.value) hex.append(String((code >> 4) & 0xf, radix: 16)) if hex.count < 32 { hex.append(String(code & 0xf, radix: 16)) } } while hex.count < 32 { hex.append("0") } let c = Array(hex.prefix(32)) func sub(_ a: Int, _ b: Int) -> String { String(c[a.. 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}`); } // 2a) Class-Property `var voipRegistry: PKPushRegistry?` einfügen \u2014 muss // gehalten werden, sonst dealloziert iOS die Registry nach // didFinishLaunching und Pushes erreichen den Delegate nie. const classPropRe = /(\n\s+var reactNativeFactory:[^\n]+\n)/; if (classPropRe.test(src) && !src.includes('var voipRegistry')) { src = src.replace(classPropRe, `$1${CLASS_PROPERTY}`); } // 2b) 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; }, ]); };