react-native-webrtc / react-native-webrtc/react-native-callkeep

Listeners not working when App is in Kill state

Open
#791 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
1.1k
Forks
511
Avg merge
9h 12m
Merged PRs (30d)
2

Description

Bug report

  • Hi
    I am facing an issue while integrating CallKeep in my React Native app. It works as expected in the foreground and background, but in the killed state, answering the call does not trigger the listener, preventing any action.

  • Reproduced on:

  • iOS

Description

This is AppDelegate.m

#import "AppDelegate.h"
#import <Firebase.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <GoogleMaps/GoogleMaps.h>
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
#import "RNSplashScreen.h"
#import <PushKit/PushKit.h>
#import <CallKit/CallKit.h>
#import "RNVoipPushNotificationManager.h"
#import "RNCallKeep.h"
#import <AVFoundation/AVFoundation.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];
}

@implementation AppDelegate {
    CXProvider *provider;
    BOOL hasActiveCall;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [FIRApp configure];
  [self checkAndHandleActiveCall];
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

  [RNCallKeep setup:@{
       @"appName": @"callApp",
       @"maximumCallGroups": @1,
       @"maximumCallsPerCallGroup": @1,
       @"supportsVideo": @YES,
  }];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"callApp" initialProperties:nil];

  if (@available(iOS 13.0, *)) {
      rootView.backgroundColor = [UIColor systemBackgroundColor];
  } else {
      rootView.backgroundColor = [UIColor whiteColor];
  }

  [RNSplashScreen show];  // here

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  
  // Define UNUserNotificationCenter
  UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
  center.delegate = self;
  return YES;
}

- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
  restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler
{
  return [RNCallKeep application:application
           continueUserActivity:userActivity
             restorationHandler:restorationHandler];
}

#pragma mark - UNUserNotificationCenterDelegate

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
  completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
}

#pragma mark - RCTBridgeDelegate

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

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
  [RNCPushNotificationIOS didFailToRegisterForRemoteNotificationsWithError:error];
}

- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
  [RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];
}

- (void)pushRegistry:(PKPushRegistry *)registry
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
             forType:(PKPushType)type
withCompletionHandler:(void (^)(void))completion {
    NSLog(@"Received incoming VoIP push notification");

    if ([type isEqualToString:PKPushTypeVoIP]) {
       NSLog(@"Received incoming VoIP push notification: %@", payload);
       NSDictionary *payloadDictionary = payload.dictionaryPayload;
       NSDictionary *data = payloadDictionary[@"data"];
       NSString *guid;
       NSString *sourceName;
       NSString *callerID;

       if (data[@"guid"]) {
           guid = data[@"guid"];
       } else if (data[@"call"] && data[@"call"][@"guid"]) {
           guid = data[@"call"][@"guid"];
       }
       if (data[@"source"] && data[@"source"][@"name"]) {
           sourceName = data[@"source"][@"name"];
       } else if (data[@"call"] && data[@"call"][@"source"] && data[@"call"][@"source"][@"name"]) {
           sourceName = data[@"call"][@"source"][@"name"];
       }
       if (data[@"source"] && data[@"source"][@"id"]) {
           callerID = data[@"source"][@"id"];
       } else if (data[@"call"] && data[@"call"][@"source"] && data[@"call"][@"source"][@"id"]) {
           callerID = data[@"call"][@"source"][@"id"];
       }
       [[SwiftClass shared] checkGuid:guid completion:^(BOOL isNewGuid) {
           if (isNewGuid) {
               [RNCallKeep reportNewIncomingCall: guid
                                            handle: sourceName
                                        handleType: @"generic"
                                          hasVideo: YES
                               localizedCallerName: sourceName
                                   supportsHolding: YES
                                      supportsDTMF: YES
                                  supportsGrouping: YES
                                supportsUngrouping: YES
                                       fromPushKit: YES
                                           payload: payloadDictionary
                             withCompletionHandler: completion];
           } else {
               NSLog(@"Duplicate call, ignoring");
               completion();
           }
       }];

       [[SwiftClass shared] processCall:guid completion:^(NSArray *response) {
           NSLog(@"Response: %@", response);
           completion();
       }];
    }
}
@end

index.js

import React, { useEffect } from "react";
import { AppRegistry, NativeModules, Platform, NativeEventEmitter } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";
import VoipPushNotification from "react-native-voip-push-notification";
import RNCallKeep from "react-native-callkeep";
import messaging from "@react-native-firebase/messaging";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { extractGuid } from "./app/utilities/extractGuid";
import BackgroundTimer from "react-native-background-timer";
import PushNotificationIOS from '@react-native-community/push-notification-ios';
import { each, compact, last, get } from "lodash";

const callKeepEmitter = new NativeEventEmitter(RNCallKeep);

messaging().setBackgroundMessageHandler(async (remoteMessage) => {
  if (remoteMessage) {
    const notificationPayload = remoteMessage?.data;
    const guid = extractGuid(notificationPayload);
    if (guid) {
      if (Platform.OS === "android") {
        NativeModules.ButterFlyMX.processCall(guid, (error, result) => {});
      }
    } else {
      console.log("GUID not found in notification payload");
    }
  } else {
    console.log("No notification found in FCM message");
  }
});

const options = {
  ios: {
    appName: "callApp",
    supportsVideo: true,
    maximumCallGroups: "1",
    maximumCallsPerCallGroup: "1",
  },
  android: {
    alertTitle: "Permissions required",
    alertDescription: "This application needs to access your phone accounts",
    cancelButton: "Cancel",
    okButton: "OK",
  },
};

const AppWithCallKeep = ({ isHeadless }) => {
  const onDidLoadWithNativeEvents = (events) => {
    const validEvents = compact(events);
    let callDataToAdd = null;
    let callDataToAnswer = null;
    let callDataToReject = null;
    let callDataToInitiateCall = null;
    each(compact(validEvents), (event) => {
      const { name, data } = event;
      if (name === "RNCallKeepDidDisplayIncomingCall") {
        callDataToAdd = data;
        recieveCall(data);
        callDataToAnswer = null;
        callDataToReject = null;
      }
      if (name === "RNCallKeepPerformAnswerCallAction") {
        callDataToReject = null;
        callDataToAnswer = data;
        answerCall(data);
      }
      if (name === "RNCallKeepPerformEndCallAction") {
        callDataToAnswer = null;
        callDataToReject = data;
       
`

Steps to Reproduce

I have a scenario I am calling call UI from swift it works when I am in foreground,
but when ever I answer call from kill state it just open the app dont call UI as answeraction listener does not triggered

Versions

- Callkeep:^4.3.13"
- React Native:0.66.3
- iOS:17
- Android:nil
- Phone model: 13 pro 

Logs

Paste here

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with AppDelegate.m and reproduce the iOS flow from a killed state, following the PushKit and RNCallKeep callbacks shown in the issue. Then trace index.js and its native event handling, using the foreground and background behavior as a comparison. Done means answering from the killed state reaches the answer-action listener and triggers the expected application behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, objective-c, react-native, swift
Domain
mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.