Skip to content

[ios-13] add new class method to report incoming call when receiving voip push #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 25, 2019

Conversation

r0b0t3d
Copy link
Contributor

@r0b0t3d r0b0t3d commented Aug 26, 2019

With ios 13, apple has changed the flow we handle voip incoming push notification.

Important
On iOS 13.0 and later, incoming Voice over IP calls must be reported when they are received and before this method finishes execution using the CallKit framework, or the system will terminate your app. Repeatedly failing to report calls may prevent your app from receiving any more incoming call notifications.

pushRegistry(_:didReceiveIncomingPushWith:for:completion:)

I added new class method to report incoming call when there is new voip push

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];

  NSString *uuid = [payload.dictionaryPayload valueForKey:@"uuid"];
  NSString *callerName = [payload.dictionaryPayload valueForKey:@"fromUserName"];
  [RNCallKeep reportNewIncomingCall:uuid handle:@"hadle" handleType:@"generic" hasVideo:true localizedCallerName:callerName];
}

@ajnozari
Copy link
Contributor

I believe you're the one who answered my question in the other repository, thank you so much for this!!!!!

@danjenkins
Copy link
Collaborator

danjenkins commented Aug 30, 2019 via email

@ajnozari
Copy link
Contributor

I can load iOS 13 on my test device later and test it for you if you want?

@kylekurz
Copy link
Contributor

This needs to be prioritized and tested. My other non-react-native app is getting killed b/c I wasn't calling CallKit's reportIncomingCall method fast enough. It's going to bite RN apps built against Xcode 11 too.

@Prussich
Copy link

+1. Please increase priority. Incoming Calls not working with CallKeep on iOS 13.

@ajnozari
Copy link
Contributor

So the fix that @r0b0t3d suggested seems to work fine in my app.

@manuquentin
Copy link
Contributor

I'll update my phone and test on my side also. I'll merge and publish a version if it's ok.

@kylekurz
Copy link
Contributor

Thanks @manuquentin. @bhuangy is working on it here today too.

@danjenkins
Copy link
Collaborator

Currently buying a phone capable of ios 13. Yes. I am delayed!

@manuquentin
Copy link
Contributor

manuquentin commented Sep 20, 2019

Hi @r0b0t3d, since we're testing on ios 13 we didn't receive any notification, due an error :

Bad Request for url: https://api.sandbox.push.apple.com/3/device/... : {'reason': 'InvalidPushType'}

Even if we send apns-push-type: background and apns-priority: 10 in the requests headers.

How did you manage to send push since ios 13 ?

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Sep 20, 2019

@manuquentin I used node-apn to send push from firebase cloud functions. Below are my code

const apnConfig = {
    production: config.isProduction,
    token: {
         ...
    },
};

const apnProvider = new apn.Provider(apnConfig);

async function sendApnMessage(token: string, payload: any, appBundleId: string) {
    console.log('sendApnMessage', token);

    const notification = new apn.Notification();

    const recepients: string[] = [];
    recepients.push(apn.token(token));

    notification.topic = `${appBundleId}.voip`; // you have to add the .voip here!!
    notification.payload = payload;

    try {
        const response = await apnProvider.send(notification, token);
        console.log(JSON.stringify(response, null, 2));
    } catch (error) {
        console.log(error.message);
    }
}

@ajnozari
Copy link
Contributor

Hi @r0b0t3d, since we're testing on ios 13 we didn't receive any notification, due an error :

Bad Request for url: https://api.sandbox.push.apple.com/3/device/... : {'reason': 'InvalidPushType'}

Even if we send apns-push-type: background and apns-priority: 10 in the requests headers.

How did you manage to send push since ios 13 ?

Check your certificate and that you're actually using the right device ID.

@ajnozari
Copy link
Contributor

Ok so I can confirm that your changes work, you have to do some fiddling with the Caller Name and UUID if you send it as part of the extraPayload or not in the aps, but

NSString *uuid = [payload.dictionaryPayload valueForKeyPath:@"extraPayLoad.call_id"];

should work as it lets you specify the path to the key that you want. Same thing for callerName.

@manuquentin
Copy link
Contributor

manuquentin commented Sep 23, 2019

Thanks @r0b0t3d for the PR, it's working 👌. The issue with the push is that you shouldn't send apns-push-type header for VoIP pushes on iOS.

During my tests, I've experienced this error with JS debug activated :

Killing VoIP app <private> because it failed to post an incoming call in time.

I guess that's because in debug mode slows down the application and the reportNewIncomingCall is called too late. So avoid to test it with JS debug enabled :)

I've made a little refactoring, as reportNewIncomingCall has the same body than displayIncomingCall. Can you sherry pick this commit into your branch ? I don't have the credential on your repo.

This will also send the didDisplayIncomingCall event. This event should be used to check that there's already an incoming call and we shouldn't display it twice in our application.

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Sep 24, 2019

@manuquentin I did add your commit

@ajnozari
Copy link
Contributor

This is brilliant guys, I’ll pull it and test out this fork later today, and will report back on success.

@manuquentin
Copy link
Contributor

Hi @ajnozari did you have time to check it today ?

@ajnozari
Copy link
Contributor

Yep and it seems all is good so far, I used this fork as well as the Webrtc’s master branch and both are working. This one is ringing on an iPhone 7 and XS max.

Additionally the webrtc’s master branch fixed the random blank video shown on iOS.

@Chaitanya-Suryadevara
Copy link

@danjenkins @manuquentin ,
Please do this in priority. Incoming Calls stopped working in background and killed state on iOS 13.

@manuquentin manuquentin merged commit 030e48f into react-native-webrtc:master Sep 25, 2019
@manuquentin
Copy link
Contributor

manuquentin commented Sep 25, 2019

Merged and deployed in 3.0.3. Thanks everyone.

@Chaitanya-Suryadevara
Copy link

Chaitanya-Suryadevara commented Sep 25, 2019

@manuquentin , Thanks. I updated to version 3.0.3, but I am facing weird issue now. As soon as I accept the call my "endCall" method also get called first and then "answerCall" method get called. So my call gets end as soon I accept the call.

RNCallKeep.addEventListener("answerCall", this.onAnswerCallAction);
RNCallKeep.addEventListener("endCall", this.onEndCallAction);
onAnswerCallAction = ({ callUUID }) => {
    console.log("onAnswerCallAction===");
//other code to accept the code
}
onEndCallAction = ({ callUUID }) => {
    console.log("onEndCallAction===", callUUID);
//other code to end call
}

And as soon as I accept the call, here's the log what I get while debugging.

onIncomingCallDisplayed error== {callUUID: "0c1533ef-5ea5-4c25-95f0-d0249552e217", fromPushKit: "1", localizedCallerName: "Anonymous", error: "", handle: "+1760705XXXX"}
onIncomingCallDisplayed error== {callUUID: "4e02f645-b6e8-4203-ace9-44e26e3642c5", fromPushKit: "0", localizedCallerName: "Anonymous", error: "", handle: "+17607058888"}

onEndCallAction=== 4e02f645-b6e8-4203-ace9-44e26e3642c5
onAnswerCallAction===

@manuquentin
Copy link
Contributor

Hi @Chaitu4190
Beware that with this behaviour, callkeep will call onIncomingCallDisplayed once (with the fromPushKit = 1.).
You shouldn't call displayIncomingCall again because callkeep has already displayed the call.

@Chaitanya-Suryadevara
Copy link

Chaitanya-Suryadevara commented Sep 25, 2019

Oh yes @manuquentin , my bad. I was calling RNCallKeep.displayIncomingCall even though when I call reportNewIncomingCall from AppDelegate file.
So this works fine after removing displayIncomingCall but only when my app is in foreground mode. But Voip doesn't receive when app is in background or killed state. Before ios 13 it was working fine with ios 12 on background or killed state.
And even from recent call, my app doesn't get open now in ios 13 to make a outgoing call from app.

@ahmadworks
Copy link

Oh yes @manuquentin , my bad. I was calling RNCallKeep.displayIncomingCall even though when I call reportNewIncomingCall from AppDelegate file.
So this works fine after removing displayIncomingCall but only when my app is in foreground mode. But Voip doesn't receive when app is in background or killed state. Before ios 13 it was working fine with ios 12 on background or killed state.
And even from recent call, my app doesn't get open now in ios 13 to make a outgoing call from app.

I spend all to day to make it working on foreground and background on iOS 13.
If it worked on foreground then it should work on background. try remove your app and reinstall and see if it worked.

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Sep 26, 2019

@Chaitu4190 As the doc said

Important
On iOS 13.0 and later, if you fail to report a call to CallKit, the system will terminate your app. Repeatedly failing to report calls may cause the system to stop delivering any more VoIP push notifications to your app.

You can try to remove your app then reinstall as @ahmadworks suggest

@r0b0t3d r0b0t3d deleted the ios-13 branch September 26, 2019 02:21
@Chaitanya-Suryadevara
Copy link

Chaitanya-Suryadevara commented Sep 26, 2019

@r0b0t3d @ahmadworks After uninstalling and installing, call works fine now.
But when I go to my call logs and tap on my app's phone call it doesn't open my app, which was working fine on ios12. So can anyone suggest why my app doesn't get open when I tap from call logs.
And also while receiving call, speakers starts by default. How can I change that to normal mode?
@manuquentin , Any Idea about this 2 issues?

@ahmadworks

This comment has been minimized.

@danjenkins
Copy link
Collaborator

@ahmadworks theres another PR open for that - #71

@manuquentin
Copy link
Contributor

Hi, if you have issues with something different that this PR, can you open an issue ?

@Chaitanya-Suryadevara
Copy link

@manuquentin , Generated new issue for outgoing call from call logs. Here's the link.

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Oct 12, 2019

For now, there is value voip for apns-push-type 😄
https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/sending_notification_requests_to_apns

The apns-push-type header field has six valid values. The descriptions below describe when and how to use these values.

alert
Use the alert push type for notifications that trigger a user interaction—for example, an alert, badge, or sound. If you set this push type, the apns-topic header field must use your app’s bundle ID as the topic. For more information, see Generating a Remote Notification.

The alert push type is required on watchOS 6 and later. It is recommended on macOS, iOS, tvOS, and iPadOS.

background
Use the background push type for notifications that deliver content in the background, and don’t trigger any user interactions. If you set this push type, the apns-topic header field must use your app’s bundle ID as the topic. For more information, see Pushing Background Updates to Your App.

The background push type is required on watchOS 6 and later. It is recommended on macOS, iOS, tvOS, and iPadOS.

voip
Use the voip push type for notifications that provide information about an incoming Voice-over-IP (VoIP) call. For more information, see Responding to VoIP Notifications from PushKit.

If you set this push type, the apns-topic header field must use your app’s bundle ID with .voip appended to the end. If you’re using certificate-based authentication, you must also register the certificate for VoIP services. The topic is then part of the 1.2.840.113635.100.6.3.4 or 1.2.840.113635.100.6.3.6 extension.

The voip push type is not available on watchOS. It is recommended on macOS, iOS, tvOS, and iPadOS.

complication
Use the complication push type for notifications that contain update information for a watchOS app’s complications. For more information, see Updating Your Timeline.

If you set this push type, the apns-topic header field must use your app’s bundle ID with .complication appended to the end. If you’re using certificate-based authentication, you must also register the certificate for WatchKit services. The topic is then part of the 1.2.840.113635.100.6.3.6 extension.

The complication push type is recommended for watchOS and iOS. It is not available on macOS, tvOS, and iPadOS.

fileprovider
Use the fileprovider push type to signal changes to a File Provider extension. If you set this push type, the apns-topic header field must use your app’s bundle ID with .pushkit.fileprovider appended to the end. For more information, see Using Push Notifications to Signal Changes.

The fileprovider push type is not available on watchOS. It is recommended on macOS, iOS, tvOS, and iPadOS.

mdm
Use the mdm push type for notifications that tell managed devices to contact the MDM server. If you set this push type, you must use the topic from the UID attribute in the subject of your MDM push certificate. For more information, see Device Management.

The mdm push type is not available on watchOS. It is recommended on macOS, iOS, tvOS, and iPadOS.

@manhquyen
Copy link

I try but have error "Use of undeclared identifier 'RNCallKeep'"

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Oct 17, 2019

@manhquyen
Add

#import "RNCallKeep.h"

on top of AppDelegate.m

@rcidt
Copy link

rcidt commented Nov 12, 2019

@r0b0t3d Thanks for the work you did here! I am wondering what your thoughts are on handling a remote cancel-type event.

For example: VoIP push comes in notifying app of incoming call, phone starts ringing, but what if the caller hangs up before the callee answers the call?

I am thinking of a second push notification, but now that iOS is getting stricter with VoIP push, I am not sure on the best approach (especially in scenarios where the app is not running). What are your thoughts?

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Nov 12, 2019

@rcidt
You sure can use second push to notify the callee for hangs up event. But voip push is not an option 😄
You could use normal remote notification or background notification

  • Background notification (silent push) seem to be the best approach because it is not require user interactions. But there is limitation that this notification always treated as low-priority, it means the system doesn’t guarantee that it is delivered. Note: In this case, your app already woke up, so it worth to give this a try
  • In case of remote notification, if your app is in foreground, you can decide to present the notification or not via userNotificationCenter(_:willPresent:withCompletionHandler:)
func userNotificationCenter(_ center: UNUserNotificationCenter, 
                         willPresent notification: UNNotification, 
               withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    // Handle notification
    // ...
    // Show alert
    completionHandler([.alert, .badge, .sound])
    // Or hide alert
    completionHandler([.none])
}

Or another approach is websoket or a platform that provide realtime updates like Firebase Database, Firestore.

@danjenkins
Copy link
Collaborator

@r0b0t3d why can't you use Pushkit in this instance? Pushkit isn't just for declaring a phone call is inbound; it's meant to be there for all voip necessities - I don't see any reason not to send another pushkit push informing of the cancel if you don't have an active websocket/messaging flow open to wherever.

@r0b0t3d
Copy link
Contributor Author

r0b0t3d commented Nov 12, 2019

@danjenkins I haven't tried to use PushKit for hangs up event.
In the document, there are 3 notification types

static let complication: PKPushType 
    A push type for watchOS complications.
static let fileProvider: PKPushType
    A push type for file provider updates.
static let voIP: PKPushType
    A push type for Voice-over-IP (VoIP) call invitations.

For voiIP, they said

Use this type of notification to initiate live voice calls over the network. Apps receiving VoIP push notifications must report the call quickly to CallKit, so it can alert the user to the presence of the incoming call

So I don't think we can use it for hangs up event (just my opinion).

@danjenkins
Copy link
Collaborator

Yeah I can understand your reasoning now. But I'd also argue that their change in behaviour for callkit has now changed what we deem to be classed as data that "must report quickly to callkit" because a hangup/cancel is just as important as an invite/initiation. I'd just use pushkit with a cancel key in the payload. The worst that's going to happen is they change it in the future and you have to move to silent push.

@rcidt
Copy link

rcidt commented Nov 12, 2019

@r0b0t3d @danjenkins in the past we were using VoIP Push to send "status" update notifications like when a call has been ended or answered elsewhere, etc.

Ever since applying the changes mentioned in this thread this functionality stopped working because of the fact that we start up an "incoming call" UI anytime a VoIP Push is received.

So what I did was the following and our code now works as it used to. For now it seems that VoIP push will work for this type of functionality.

- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  // Process the received push
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
  NSString *uuid = [[NSUUID UUID] UUIDString];
  NSString *callerName = [payload.dictionaryPayload valueForKeyPath:@"custom.a.ani"];
  NSString *status = [payload.dictionaryPayload valueForKeyPath:@"custom.a.status"];
  if(status == nil || status.length < 1){
    [RNCallKeep reportNewIncomingCall:uuid handle:@"generic" handleType:@"generic" hasVideo:false localizedCallerName:callerName fromPushKit: YES];
    completion();
  } // Else we let the CallKeep event listeners pick this up and process the status update
}

@kylekurz
Copy link
Contributor

kylekurz commented Nov 12, 2019

To be very clear, the only thing you can use VoIP PushKit notifications for now is incoming call INVITES. The documentation is here:

After sending the initial push notification, don’t send additional push notifications to cancel the call or communicate new details to your app. Instead, communicate with the app directly over the network connection you established between it and your server. Using an existing network connection is generally faster than sending a push notification, and if network conditions are poor, APNs may be unable to deliver push notifications to the device anyway.
https://developer.apple.com/documentation/pushkit/responding_to_voip_notifications_from_pushkit

and

On iOS 13.0 and later, if you fail to report a call to CallKit, the system will terminate your app. Repeatedly failing to report calls may cause the system to stop delivering any more VoIP push notifications to your app. If you want to initiate a VoIP call without using CallKit, register for push notifications using the UserNotifications framework instead of PushKit. For more information, see UserNotifications.
https://developer.apple.com/documentation/pushkit/pkpushregistrydelegate/2875784-pushregistry

This sucks, as it implies Apple's thoughts are the only valuable ones, but it's the way you have to do it if you want to continue operating within the Apple ecosystem. Using VoIP for CANCEL will mostly work, but you end up with a race condition where your app can be killed for not reporting an incoming call during the PushKit callback.

@danjenkins
Copy link
Collaborator

Ah yes I was getting confused which way around the relationship was. I actually just experienced killing of apps myself as some code I'd written wasn't able to call callkit and so my app got punished. no pushkit token given when asked for etc. Thanks @kylekurz

So long and short. VoIP Pushkit for starting a call and that's it.
Silent Notifications for Cancelling (if you want to do it before whatever means of transport you have up the a server in your app)

@rcidt
Copy link

rcidt commented Nov 14, 2019

@danjenkins @r0b0t3d @kylekurz yes seems like you are all correct, after a few tests my app no longer receives the "silent" VoIP Push.

I switched over to using regular silent push, but I am seeing some odd behavior: silent pushes without content or title are being ignored completely while the CallKit Incoming Call UI is present. Note, silent pushes work fine if I provide a content/title OR the CallKit Incoming Call UI is not present.

Do you think Apple has prohibited silent push in this scenario?

UPDATE

For some reason the react-native-onesignal library I was using to listen for the push was ignoring the silent push while CallKit Incoming Call UI was running. I switched to using react-native-push-notification-ios and all is good now 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants