/** * Bridge zwischen CallKit/ConnectionService-Events und unserer Call-Store. * * Wenn der User in der nativen Call-UI Accept/Reject/Hangup tippt, kommt das * NICHT über unser React-UI rein — sondern via RNCallKeep-Events. Wir * übersetzen die in store-Actions. * * Wird einmal app-weit im _layout.tsx aufgerufen. */ import { useEffect } from 'react'; import { AppState, Platform } from 'react-native'; import { useRouter } from 'expo-router'; import RNCallKeep from 'react-native-callkeep'; import { useCallStore } from '../stores/call'; import { setupCallKeep } from '../lib/callkit'; // VoIP-PushKit (iOS only) — Payload-Empfang um peer-Info in den Store zu // hydrieren, BEVOR User in der CallKit-UI auf "Annehmen" tippt. let RNVoipPushNotification: any = null; if (Platform.OS === 'ios') { try { RNVoipPushNotification = require('react-native-voip-push-notification').default; } catch { // ungelinkt — skip } } export function useCallKeepEvents() { const router = useRouter(); useEffect(() => { void setupCallKeep(); // VoIP-Push-Payload (iOS) → Store hydrieren mit caller-Info, damit // acceptCall() den richtigen peer kennt. Der CallKit-UI-Show ist bereits // in AppDelegate.swift (reportNewIncomingCall) erfolgt. const onVoipNotification = (payload: any) => { console.log('[voip] incoming push payload', payload); const st = useCallStore.getState(); const callId = String(payload?.callId ?? ''); const from = payload?.from; if (!callId || !from?.id) return; // receiveIncoming dedupliziert via status-Check → safe doppelt zu rufen. st.receiveIncoming(callId, { id: String(from.id), nickname: String(from.nickname ?? payload?.callerName ?? 'ReBreak'), avatar: from.avatar ?? null, }); }; if (RNVoipPushNotification) { RNVoipPushNotification.addEventListener('notification', onVoipNotification); // didLoadWithEvents wird beim Cold-Start gefeuert mit den queued Events, // die vor dem ersten addEventListener-Call eingegangen sind. RNVoipPushNotification.addEventListener('didLoadWithEvents', (events: any[]) => { for (const ev of events ?? []) { if (ev?.name === 'RNVoipPushRemoteNotificationReceivedEvent') { onVoipNotification(ev?.data); } } }); } // User tippt "Annehmen" in der CallKit-/ConnectionService-UI const onAnswer = ({ callUUID }: { callUUID: string }) => { console.log('[callkeep] answer', callUUID, 'appState=', AppState.currentState); const st = useCallStore.getState(); if (st.status !== 'incoming') return; // Call-Screen \u00f6ffnen und Accept-Flow triggern. router.push('/call'); void st.acceptCall(); }; // User tippt "Ablehnen" oder "Auflegen" in der nativen UI const onEnd = ({ callUUID }: { callUUID: string }) => { const appState = AppState.currentState; const st = useCallStore.getState(); console.log('[callkeep] end', callUUID, 'appState=', appState, 'storeStatus=', st.status); if (st.status === 'idle' || st.status === 'ended') return; // iOS-Foreground: AppDelegate.didReceiveIncomingPush MUSS reportNewIncomingCall // aufrufen (Apple-Pflicht, sonst killt iOS die App). Im Foreground bedient // sich der User aber an unserem In-App /call-Screen mit Accept/Decline- // Buttons. CallKit's Auto-Dismiss-Timer (~5s im Foreground!) darf den // User-Flow NICHT unterbrechen — sonst verschwindet der /call-Screen nach // 1-5s ohne dass der User irgendwas getippt hat. // (Empirisch verifiziert: dieser early-return ist genau der Fix für das // "1-3s disappear"-Bug. Bitte nicht erneut entfernen.) if (Platform.OS === 'ios' && appState === 'active' && st.status === 'incoming') { console.log('[callkeep] endCall IGNORED (iOS foreground incoming — in-app UI is authoritative)'); return; } if (st.status === 'incoming') { st.declineCall(); } else { st.hangup('ended'); } }; // User mutet/unmutet über die native UI const onMuted = ({ muted }: { muted: boolean; callUUID: string }) => { const st = useCallStore.getState(); if (st.muted !== muted) st.toggleMute(); }; RNCallKeep.addEventListener('answerCall', onAnswer); RNCallKeep.addEventListener('endCall', onEnd); RNCallKeep.addEventListener('didPerformSetMutedCallAction', onMuted); // didActivateAudioSession kommt nach CallKit-Audio-Activation — wir nutzen // das (noch) nicht aktiv, weil WebRTC + InCallManager das selber regeln. return () => { RNCallKeep.removeEventListener('answerCall'); RNCallKeep.removeEventListener('endCall'); RNCallKeep.removeEventListener('didPerformSetMutedCallAction'); if (RNVoipPushNotification) { RNVoipPushNotification.removeEventListener('notification'); RNVoipPushNotification.removeEventListener('didLoadWithEvents'); } }; }, [router]); }