iosreactjsobjective-creact-nativevoip

React native iOS application crashed with voip notification when app is in the background(not in foreground or killed)


I've written an react-native ios application that should be able to receive voip calls. I've used react-native-callkeep, react-native-voip-push-notification, react-native-notification to implement voip notifications. I've handled notifications both inside the app with registerPushKitNotificationReceived and for when app is killed(didReceiveStartCallAction and didReceiveIncomingPushWithPayload). I've also implemented didLoadWithEvents to ensure all events are captured in the js thread. The app seems to work fine when in the foreground or killed. After the voip notification is received, iOS shows the call screen and navigates to the correct screen when the user responds.

However when app is in the background, the app shows the call screen and then crashes when a notification is received because completion method isn't called

Code from App.js

const doInitialCallKeepSetup = async () => {
  console.log('Initial callkeep setup');
  RNCallKeep.setup({
    ios: {
      appName: '...',
      supportsVideo: true,
    },
    android: {
      alertTitle: 'Permissions required',
      alertDescription: 'This application needs to make phone calls',
      cancelButton: 'Cancel',
      okButton: 'Ok',...
    },
  });

};
const PushkitNotificationSetup = (props) => {

  if (!isIOS) return <></>

  const user = useSelector((state) => state.authReducer?.user);
  const apnsToken = useSelector((state) => state?.deviceReducer?.apns_token);
  const navigation = useNavigation();
  const dispatch = useDispatch();

  const logger = useLogger('IOSNotificationManager');

  const registerDevice = async () => {
    ...
  };

  const processCallNotification = (event) => {
    logger.info('Pushkit event: new chat', { event });
    // AsyncStorage.setItem(`call-${event.call_uuid}`, JSON.stringify(event));
    dispatch(
              startVideoChat({
                              ...event,
              token: event.token,
              room_name: event.room_name,
              call_id: event.call_id,
              friend: event.friend,
              callUUID: event.call_uuid,
              call_uuid: event.call_uuid,
                  // location: JSON.parse(data.location ?? '{}'),
              })
              );
    logger.info('Pushkit event: showing calll ui', { event });
      RNCallKeep.displayIncomingCall(
        event.call_uuid,
        'New call',
        event.caller || 'Caller id',
        'generic',
        true
      );
  }

  useEffect(() => {
    Notifications.ios.registerPushKit();
    Notifications.ios.events().registerPushKitRegistered((event) => {
      dispatch(setIosToken({
        token: event.pushKitToken,
      }))
    })
    Notifications.ios.events().registerPushKitNotificationReceived((event, completion) => {
      logger.info("Pushkit notification recieved", {event_payload: event})

      if (event.type == 'new_chat') {
        processCallNotification(event)
      }

      completion()
    })
    Notifications.events().registerNotificationReceivedForeground((event, completion) => {
      logger.info("Foreground notification recieved", {event_payload: event.payload, event_body: event.body})

      if (event.type == 'new_chat') {
        processCallNotification(event) 
      }
      
      completion()
    })
    Notifications.events().registerNotificationReceivedBackground((event, completion) => {
      logger.info("Bacckground notification recieved", {event_payload: event.payload, event_body: event.body})

      if (event.type == 'new_chat') {
        processCallNotification(event)
      }
      
      completion()
    })
  }, [])

  useEffect(() => {
    logger.info('Checking registration status', apnsToken, user, user?.id);
    if (apnsToken && user && user.id) {
      logger.info('Registering device', apnsToken, user, user.id);
      registerDevice();
    }
  }, [user, apnsToken]);


  return <></>

}

doInitialCallKeepSetup();

const CallKeepSetup = () => {
  const logger = useLogger('CallKeep');
  const navigation = useNavigation();
  const dispatch = useDispatch();

  const answerCall = ({ callUUID }) => {
    logger.info("Call recieved, answering call...")
    AsyncStorage.setItem('currentCallUUID', callUUID);

    try {
      logger.info("Starting Callkeep call")
      RNCallKeep.startCall(callUUID, 'Caller', "caller", 'generic', true);
      logger.info("Call started successfully")
    } catch(e) {
      logger.error("Failed to start callkeep call")
    }

    setTimeout(() => {
      logger.info("Setting current active call...")
      try {
        RNCallKeep.setCurrentCallActive(callUUID);
        logger.info("Current active call set");
      } catch(e) {
        logger.error("Failed to set current call", {error: e});

      }
    }, 1000);


    logger.info("Call started successfully, setting active call")


    // On Android display the app when answering a video call
    if (!isIOS) {
      console.log('bringing app to foreground');
      RNCallKeep.backToForeground();
    }

    logger.info("Setting current call UUID")
    dispatch(setCurrentCallUUID(callUUID))

    logger.info("Navigating to call screen")

    setTimeout(() => {
      navigation.navigate('Video Call');
    }, 250);
  };

  const endCall = ({ callUUID }) => {
    logger.info("Ending call", {callUUID})
    if (!RNCallKeep.isCallActive(callUUID)) return;
    
    try {


    } catch(e) {
      logger.error("Failed to end call", { error: e})
    }
  };

  const didDisplayIncomingCall = async ({ callUUID, payload, handle, }) => {
    try {
      logger.info('Recieved call with data', { payload, callUUID, handle });
      if(payload && payload.token) {
        dispatch(
          startVideoChatWithouActivating({
            token: payload.token,
            room_name: payload.room_name,
            call_id: payload.call_id,
            friend: payload.friend,
            callUUID,
            call_uuid: callUUID,
            // location: JSON.parse(data.location ?? '{}'),
          })
        );
      }
      logger.info('Call displayed with data', { payload, callUUID, handle });
    } catch(e) {
      logger.error('Failed to save call data', { payload, callUUID, handle });
    }
  }

  const handlePreJSEvents = (events) => {
    logger.info('PreJS events', { events });
    for (let event of events) {
      if (event.name == "RNCallKeepDidDisplayIncomingCall") {
        logger.info('PreJS events: didDisplayIncomingCall', { event });
        didDisplayIncomingCall(event.data)
      } else if (event.name == 'RNCallKeepAnswerCall') {
        logger.info('PreJS events: answerCall', { event });
        answerCall(event.data)
      } else if (event.name == 'RNCallKeepEndCall') {
        logger.info('PreJS events: endCall', { event });
        endCall(event.data)
      }
    }
  }

  const initializeCallKeep = () => {
    try {
      logger.info('Call keep initiated successfully');
      RNCallKeep.setAvailable(true);

      RNCallKeep.addEventListener('answerCall', answerCall);
      RNCallKeep.addEventListener('didReceiveStartCallAction', answerCall);
      RNCallKeep.addEventListener('endCall', endCall);
      RNCallKeep.addEventListener('didDisplayIncomingCall', didDisplayIncomingCall);
      
      if (isIOS) {
        RNCallKeep.addEventListener('didLoadWithEvents', handlePreJSEvents);
      }
    } catch (err) {
      logger.error('Failed to initialize call keep', {
        error: err,
        msg: err.message,
      });
      console.error('initializeCallKeep error:', err.message);
    }
  };


  const voipNotificationRecieved = (notification) => {
    console.log(notification)
    logger.info("Notification recieved from rn-voip-notification", {notification_data: notification});
    try {
      logger.info("Saving notification data...")
      if (RNCallKeep.isCallActive(notification.call_uuid)) return ;
      dispatch(
         startVideoChatWithouActivating({
          ...notification,
            callUUID: notification.call_uuid,
        })
      );
      RNCallKeep.displayIncomingCall(
        call_uuid,
        notification?.caller,
        notification?.caller,
        'generic',
        true
      );
        // navigation.navigate('Video Call');
      logger.info("Notification info saved");
        VoipPushNotification.onVoipNotificationCompleted(notification.call_uuid)
        // logger.info("Sending finish state");
    } catch(e) {
      logger.error("Failed to save notificaion data", { error: e, error_str: "" + e});
    }
  }

  useEffect(() => {
    initializeCallKeep();

    if (isIOS) {
      VoipPushNotification.addEventListener('notification', voipNotificationRecieved)
    }
    return () => {
      RNCallKeep.removeEventListener('answerCall', answerCall);
      RNCallKeep.removeEventListener('didReceiveStartCallAction', answerCall);
      RNCallKeep.removeEventListener('endCall', endCall);
      RNCallKeep.removeEventListener('didDisplayIncomingCall', didDisplayIncomingCall);
      if (isIOS) {
        RNCallKeep.removeEventListener('didLoadWithEvents', handlePreJSEvents);
        VoipPushNotification.removeEventListener('notification');
      }
    };
  }, []);

  return <></>;
};

Code from AppDelegate.m

#import "AppDelegate.h"

#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTConvert.h>

#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>

#import <Firebase.h>

#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
#import <FlipperKit/FlipperClient.h>
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
#import <FlipperKitNetworkPlugin/FlipperKitNetworkPlugin.h>
#import <SKIOSNetworkPlugin/SKIOSNetworkAdapter.h>
#import <FlipperKitReactPlugin/FlipperKitReactPlugin.h>

static void InitializeFlipper(UIApplication *application) {
  FlipperClient *client = [FlipperClient sharedClient];
  SKDescriptorMapper *layoutDescriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
  [client addPlugin:[[FlipperKitLayoutPlugin alloc] initWithRootNode:application withDescriptorMapper:layoutDescriptorMapper]];
  [client addPlugin:[[FKUserDefaultsPlugin alloc] initWithSuiteName:nil]];
  [client addPlugin:[FlipperKitReactPlugin new]];
  [client addPlugin:[[FlipperKitNetworkPlugin alloc] initWithNetworkAdapter:[SKIOSNetworkAdapter new]]];
  [client start];
}
#endif

#import <PushKit/PushKit.h>
#import "RNVoipPushNotificationManager.h"
#import "RNFBMessagingModule.h"
#import "RNNotifications.h"


@implementation AppDelegate



// IOS push notificaion
// Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
  [RNCPushNotificationIOS didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
  [RNNotifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];

}
// Required for the notification event. You must call the completion handler after handling the remote notification.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
  NSLog(@"Native notificatoin recieved");

  // [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
  [RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler];

}
// Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
//  [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error];
  [RNNotifications didFailToRegisterForRemoteNotificationsWithError:error];

}
// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler
{
  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  [FIRApp configure];

#if defined(FB_SONARKIT_ENABLED) && __has_include(<FlipperKit/FlipperClient.h>)
  InitializeFlipper(application);
#endif
  
  RCTBridge *bridge = [self.reactDelegate createBridgeWithDelegate:self launchOptions:launchOptions];
  // RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

  [RNVoipPushNotificationManager voipRegistration];

// RN firebase headless
  NSDictionary *appProperties = [RNFBMessagingModule addCustomPropsToUserProps:nil withLaunchOptions:launchOptions];


  // RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"main" initialProperties:nil];
  RCTRootView *rootView = [self.reactDelegate createRootViewWithBridge:bridge moduleName:@"main" initialProperties:appProperties];
  rootView.backgroundColor = [UIColor whiteColor];
  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [self.reactDelegate createRootViewController];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];

  [super application:application didFinishLaunchingWithOptions:launchOptions];

  // Define UNUserNotificationCenter
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  center.delegate = self;

  [RNNotifications startMonitorNotifications];
  
  return YES;
 }

- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
  return @[];
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
 #ifdef DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
 #else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
 #endif
}



//Called when a notification is delivered to a foreground app.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}

/* Add PushKit delegate method */

// --- Handle updated push credentials
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
  // Register VoIP push token (a property of PKPushCredentials) with server
  [RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];
}

- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type
{
  // --- The system calls this method when a previously provided push token is no longer valid for use. No action is necessary on your part to reregister the push type. Instead, use this method to notify your server not to send push notifications using the matching push token.
}

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  // Process the received push
  

  NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:%@",
      payload.dictionaryPayload);
  
  
  NSString *uuid = [payload.dictionaryPayload valueForKey:@"call_uuid"];
  NSString *callerName = [payload.dictionaryPayload valueForKey:@"user"];
  NSString *handle = [payload.dictionaryPayload valueForKey:@"user"];
  
  [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];

  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];

  
  [RNCallKeep reportNewIncomingCall: uuid
                             handle: handle
                         handleType: @"generic"
                           hasVideo: YES
                localizedCallerName: callerName
                    supportsHolding: YES
                       supportsDTMF: YES
                   supportsGrouping: YES
                 supportsUngrouping: YES
                        fromPushKit: YES
                            payload: payload.dictionaryPayload
              withCompletionHandler: completion];

  completion();
  
}


@end



Solution

  • So turns out the issue for me was that I was using react-native-notifications and react-native-voip-notifications at the same time, and I used RNN to get PushKit token, So RNN registered a different listener and caused the RNVN code not to run, and since RNN only registered the listeners on JS side, it worked fine in killed state, but didn't work when app was in the background.(I setup a listener in JS side for forground through RNN).

    You can fix this issue by just using one library for handling PushKit notifications.