Skip to content
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

Refactor / Simplify Codes, support didLoadWithEvents #69

Merged
merged 6 commits into from
Dec 2, 2020

Conversation

zxcpoiu
Copy link
Member

@zxcpoiu zxcpoiu commented Nov 17, 2020

Breaking Change

This PR does the following:

  • remove unused codes, keep this lib simple, lots functionalities should be easily implemented in the modern RN eco system

    • remove requestPermission ( use PushNotificationIOS instead )
    • remove getting background state ( use AppState instead )
  • use NativeEventEmitter instead the deprecated DeviceEventEmitter

  • tweak event handler logic

  • support caching events before js initialize
    you can subscribe didLoadWithEvents when app ready

About didLoadWithEvents

This is aim to fix #59 ( voip push received and the event fired before js initialized )

@JJMoon proposed a workaround like in PR #65 and a proper fix in PR #61
Thanks!

But then I tend to use a different way which is the same with react-native-webrtc/react-native-callkeep/pull/205

The reason:

  • Instead using a promise method to get events directly, using event based with startObserving, we can make sure the bridge is up and READY for events, and this may prevent edge case between get initial events and subsequently fired events.

  • We cache events if js is not up, this can be work together with, if you would like to use voipRegistration directly in didFnishLaunchingWithOptions in AppDelegate.m. This is described in didReceiveIncomingPushWithPayload not called in background/terminated #59 (comment) by @chevonc . Not sure but I believe that helps in some situation. (We don't know how exactly PushKit works and when will it DELIVER voip push to us happily.)

Side question and discussion

The thing I can not figure out is, when app killed, and the app can be wake up by the remote push and enter the didFnishLaunchingWithOptions, doesn't that mean we already have a valid voip token to RECEIVE voip push?

Does PushKit invoke didReceiveIncomingPushWithPayload only when we have registered voip token?
Do we recommend to move voipRegistration in didFnishLaunchingWithOptions in AppDelegate.m?

* remove unused codes, keep this lib simple, lots functionalities
  should be easily implemented in the modern RN eco system
  - remove requestPermission ( ican use PushNotificationIOS )
  - remove getting background state ( can use AppState )

* use NativeEventEmitter instead the deprecated DeviceEventEmitter

* support caching events before js initialize
  you can subscribe `didLoadWithEvents` when app ready
@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 17, 2020

Usage:

We have 2 API:

  • VoipPushNotification.registerVoipToken()
  • VoipPushNotification.onVoipNotificationCompleted(notification.uuid);

And 3 Events:

  • 'register'
  • 'notification'
  • 'didLoadWithEvents'
...

import VoipPushNotification from 'react-native-voip-push-notification';

...

class MyComponent extends React.Component {

...

    // --- or anywhere which is most comfortable and appropriate for you, usually ASAP
    componentDidMount() {
        VoipPushNotification.registerVoipToken(); // --- register token

        VoipPushNotification.addEventListener('didLoadWithEvents', (events) => {
            // --- this will fire when there are events occured before js bridge initialized
            // --- use this event to execute your event handler manually by event type

            if (!events || !Array.isArray(events) || events.length < 1) {
                return;
            }
            for (let voipPushEvent of events) {
                let { name, data } = voipPushEvent;
                if (name === VoipPushNotification.RNVoipPushRemoteNotificationsRegisteredEvent) {
                    this.onVoipPushNotificationRegistered(data);
                } else if (name === VoipPushNotification.RNVoipPushRemoteNotificationReceivedEvent) {
                    this.onVoipPushNotificationiReceived(data);
                }
            }
        });
      
        // --- onVoipPushNotificationRegistered
        VoipPushNotification.addEventListener('register', (token) => {
            // --- send token to your apn provider server
        });

        // --- onVoipPushNotificationiReceived
        VoipPushNotification.addEventListener('notification', (notification) => {
            // --- when receive remote voip push, register your VoIP client, show local notification ... etc
            this.doSomething();
          
            // --- optionally, if you `addCompletionHandler` from the native side, once you have done the js jobs to initiate a call, call `completion()`
            VoipPushNotification.onVoipNotificationCompleted(notification.getData().uuid);
        });
    }

    // --- unsubscribe event listeners
    componentWillUnmount() {
        VoipPushNotification.removeEventListener('didLoadWithEvents');
        VoipPushNotification.removeEventListener('register');
        VoipPushNotification.removeEventListener('notification');
    }
...
}

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 18, 2020

The PushKit require us to REGISTER delegate first, then the PushKit obj is responsible to pass credential and voip push to our delegate.

Without register delegate, we will not receive any callback from PushKit, so it is recommended to register voip ASAP in AppDelegate.m

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 18, 2020

I have exposed voipRegistration as a static method and a lock _isVoipRegisterd = YES / NO checks to prevent duplicated delegate register.

readme updated as well.

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 18, 2020

Usage to register voip push in AppDelegate.m ASAP
thanks to @chevonc pointed this out in #59 (comment)

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


  // ===== (THIS IS OPTIONAL) =====
  // --- register VoipPushNotification here ASAP rather than in JS. Doing this from the JS side may be too slow for some use cases
  // --- see: https://github.com/react-native-webrtc/react-native-voip-push-notification/issues/59#issuecomment-691685841
  [RNVoipPushNotificationManager voipRegistration];
  // ===== (THIS IS OPTIONAL) =====



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

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 18, 2020

I will leave it here few days for folks to test and feedback. Then merge into master with 3.0 version.

@Jerome91410
Copy link

@zxcpoiu
I tested with the optional [RNVoipPushNotificationManager voipRegistration] in appDelegate.m.
Sound good, CallKeep was called even the app was killed or in background.

Just 2 remarks, don't know if it's the expected behavior:

  • about the event didLoadWithEvents with token, I received it only a the first start, after an app restart (force kill) it is not reached.
  • when app is killed and receiving a voip push notif, the app is started but i did not trigger the event didLoadWithEvents with RNVoipPushRemoteNotificationReceivedEvent

env:
ios: 14
rn: 0.61.5

thanks

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 24, 2020

HI @Jerome91410 , thanks for the feedback.

didLoadWithEvents is more like a helper function which let you can get the events fired BEFORE you have subscribed event listener.

So it does NOT mean the didLoadWithEvents will fire each time the app started, instead, it only fires IF there are events occurred in the native too early and BEFORE JS bridge is initialized.

If the JS bridge is initialized earlier, the event will fires through the normal event handler.

Personally I would suggest:

  • subscribe VoipPush events ASAP, like in your app.js or your main component's constructor or at the global scope ( outside of your main component ), register events in componentDidMount may be too late sometime. ( but it will work with didLoadWithEvents eventually though. )

  • I personally subscribe didLoadWithEvents first, then subscribe the rest.

All in all

Each event should fire once, either via didLoadWithEvents or normal register / notification, and you should subscribe them ASAP and handle all the event listeners.

@Jerome91410
Copy link

Jerome91410 commented Nov 24, 2020

Thanks for the explanation.

It's exactly what I did. The package is initialized in app.js at the really beginning of the app.

I'll double check, but as the app is started (previously killed) due to a push voip received, the event should be automatically fired before the JS initialization.

@Jerome91410
Copy link

Jerome91410 commented Nov 24, 2020

@zxcpoiu I doubled check.
You was right, the listener initialization of didLoadWithEvents takes some time to be called. I tried to improve my code, but was able to listen the events at least 600ms after the JS initialization.

I did some changes; and now I am more confident to get the token (if requested from native part) and the previous notification with this kind of code:

diff --git a/node_modules/react-native-voip-push-notification/index.js b/node_modules/react-native-voip-push-notification/index.js
index 88949f1..c5f730b 100644
--- a/node_modules/react-native-voip-push-notification/index.js
+++ b/node_modules/react-native-voip-push-notification/index.js
@@ -95,6 +95,31 @@ export default class RNVoipPushNotification {
         RNVoipPushNotificationManager.registerVoipToken();
     }
 
+    /**
+     * usefull to get the last register token if your JS takes time to initialize the listener on `didLoadWithEvents`
+     * Should be call before events initialization
+     *
+     * @static
+     * @memberof RNVoipPushNotification
+     * 
+     *
+     */
+    static getLastVoipToken() {
+        return RNVoipPushNotificationManager.getLastVoipToken();
+    }    
+    
+    /**
+     * usefull to get the last notification if your JS takes time to initialize the listener on `didLoadWithEvents`
+     * Should be call before events initialization
+     *
+     * @static
+     * @memberof RNVoipPushNotification
+     * 
+     */
+    static getLastNotification() {
+        return RNVoipPushNotificationManager.getLastNotification();
+    }
+
     /**
      * When you have processed necessary initialization for voip push, tell ios completed.
      * This is mainly for ios 11+, which apple required us to execute `complete()` when we finished.
diff --git a/node_modules/react-native-voip-push-notification/ios/RNVoipPushNotification/RNVoipPushNotificationManager.m b/node_modules/react-native-voip-push-notification/ios/RNVoipPushNotification/RNVoipPushNotificationManager.m
index 2699a77..4ee9130 100644
--- a/node_modules/react-native-voip-push-notification/ios/RNVoipPushNotification/RNVoipPushNotificationManager.m
+++ b/node_modules/react-native-voip-push-notification/ios/RNVoipPushNotification/RNVoipPushNotificationManager.m
@@ -30,7 +30,8 @@ @implementation RNVoipPushNotificationManager
 
 static bool _isVoipRegistered = NO;
 static NSMutableDictionary<NSString *, RNVoipPushNotificationCompletion> *completionHandlers = nil;
-
+static NSString *_lastVoipToken = nil;
+static NSDictionary *_lastNotification = nil;
 
 // =====
 // ===== RN Module Configure and Override =====
@@ -160,9 +161,10 @@ + (void)didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSStr
     for (NSUInteger i = 0; i < voipTokenLength; i++) {
         [hexString appendFormat:@"%02x", bytes[i]];
     }
-
     RNVoipPushNotificationManager *voipPushManager = [RNVoipPushNotificationManager allocWithZone: nil];
     [voipPushManager sendEventWithNameWrapper:RNVoipPushRemoteNotificationsRegisteredEvent body:[hexString copy]];
+    _lastVoipToken =[hexString copy];
+    
 }
 
 // --- should be called from `AppDelegate.didReceiveIncomingPushWithPayload`
@@ -171,9 +173,9 @@ + (void)didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSSt
 #ifdef DEBUG
     RCTLog(@"[RNVoipPushNotificationManager] didReceiveIncomingPushWithPayload payload.dictionaryPayload = %@, type = %@", payload.dictionaryPayload, type);
 #endif
-
     RNVoipPushNotificationManager *voipPushManager = [RNVoipPushNotificationManager allocWithZone: nil];
     [voipPushManager sendEventWithNameWrapper:RNVoipPushRemoteNotificationReceivedEvent body:payload.dictionaryPayload];
+    _lastNotification =payload.dictionaryPayload;
 }
 
 // --- getter for completionHandlers
@@ -212,6 +214,28 @@ + (void)removeCompletionHandler:(NSString *)uuid
     });
 }
 
+
+// --- get last voip push token
+RCT_REMAP_METHOD(getLastVoipToken,
+                 resolverToken:(RCTPromiseResolveBlock)resolve
+                 rejecterToken:(RCTPromiseRejectBlock)reject)
+{
+#ifdef DEBUG
+        RCTLog(@"[RNVoipPushNotificationManager] getLastVoipToken() _lastVoipToken = %@", _lastVoipToken);
+#endif
+    resolve(_lastVoipToken);
+    _lastVoipToken = nil;
+}
+
+// --- get last voip push token
+RCT_REMAP_METHOD(getLastNotification,
+                 resolverNotif:(RCTPromiseResolveBlock)resolve
+                 rejecterNotif:(RCTPromiseRejectBlock)reject)
+{
+    resolve(_lastNotification);
+    _lastNotification = nil;
+}
+
 // --- called from js when finished to process incoming voip push
 RCT_EXPORT_METHOD(onVoipNotificationCompleted:(NSString *)uuid)
 {

how i use it:

            const lastNotification = await VoipPushNotification.getLastNotification();
            if (lastNotification) { 
                ... // Do what you want with
            }
            VoipPushNotification.addEventListener('didLoadWithEvents', events => {
                ...
            })

            VoipPushNotification.addEventListener('notification', notification => {
                ...
            })

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 24, 2020

Thanks @Jerome91410

Yeh, I got your point and have considered it while I made this PR. This is more like a confidence level to the js event bridge.

The principle of this lib is to propagate native events to the js reliably and brainlessly, said, even if it is a duplicated register event from PushKit or when multiple VoipPush received at the same time, we should propagate it to the js for user to handle.

Honestly, I personally like the reliable feel of your solution ( invoke and get response right away instead waiting events without any indicator ), but it can't handle the multiple voip notification arrived at the same time since you only have on variable to store it.

Another potential race condition ( or duplicate events confusion ) of your solution may occur like below:

const lastNotification = await VoipPushNotification.getLastNotification();
if (lastNotification) { 
    ... // Do what you want with
}
// =====
// Race Condition and risk for duplicated events here
// 
// after we check the initial event, whether it is exist or not, 
// at this moment, we received another event,
// but while native `startObserving` is not being called yet and `_hasListeners` is still `NO`,
//
// We will not only save the last event to `_lastNotification` / `_lastVoipToken` but also add it to the cached `_delayedEvents`.
//
// So eventually, we still relied on `didLoadWithEvents`
// or we can call `getLastNotification()` / `getLastVoipToken()`
// again after we've subscribed event listeners. ( But we won't
// know when `startObserving` is actually being called in js side
// until we got the first event )
//
// And we have to check / distinguish whether the events from `didLoadWithEvents` are duplicated with `getLastNotification` / `getLastVoipToken`
//
// To harmonize the above issue, we will increase complexity to this lib or our app
//
// =====
VoipPushNotification.addEventListener('didLoadWithEvents', events => {
...
})

So if your use case is really that time sensitive which can't wait for the event bridge, you can made another PR on top of this one to add the method, I am happy to merge it but would not recommend it in the doc.

Thanks for sharing it. 👍

@danj565
Copy link

danj565 commented Nov 24, 2020

@zxcpoiu .. We tested this branch with iOS and everything is working flawlessly on our end. Specifically, it has successfully fixed VOIP notifications showing while the app is in background/killed states and successfully fires the 'notification' callback so we can register our websocket.

Thank you for your contribution!

@stephanoparaskeva
Copy link

stephanoparaskeva commented Nov 28, 2020

Does this allow both Android + IOS open app when terminated, or just IOS?

Is it possible for Android too?

Also how do you register that the incoming call has hanged up?

E.g: Call user -> user phone is off -> cancel call -> user turns phone on -> user opens app -> incoming call (already been cancelled on other side). How do you fix this?

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Nov 29, 2020

@stephanoparaskeva

Does this allow both Android + IOS open app when terminated, or just IOS?

Is it possible for Android too?

This lib is for iOS only, entirely nothing to do with android.

Also how do you register that the incoming call has hanged up?

E.g: Call user -> user phone is off -> cancel call -> user turns phone on -> user opens app -> incoming call (already been cancelled on other side). How do you fix this?

There is nothing to fix but how your app use Voip Push

For cancel a expired voip push, usually:

  • set expiry = 0 or some short value to push notification on your server side when sending voip push to apn
  • send timestamp with voip push and ignore voip push which is expired in your app based your own algorithm
  • use Remote Push to notify callee to cancel a voip push or let callee know what voip push uuid has been canceled.

@stephanoparaskeva
Copy link

stephanoparaskeva commented Dec 1, 2020

@zxcpoiu

Hi for some reason my registration callback has stopped firing. I have tried with both

[RNVoipPushNotificationManager voipRegistration];

and

  RNVoipPushNotificationManager.registerVoipToken();

My version is:

    "react-native-voip-push-notification": "github:react-native-webrtc/react-native-voip-push-notification#pull/69/head",

Here's some logs:
Screenshot 2020-12-01 at 16 23 48

It was working before, but now 'runs 3' never gets console.logged:

  console.warn('runs');
  if (Platform.OS === 'ios') {
    console.warn('runs2');
    // VoipPushNotification.registerVoipToken();
    VoipPushNotification.addEventListener('register', (token: string) => {
      console.warn('runs3');
      PushNotification.configure({
        onRegister: onRegister(token),
        onNotification,
        popInitialNotification: true,
        requestPermissions: true,
        alert: true,
        badge: true,
        sound: true,
      } as any);
    });

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Dec 2, 2020

@stephanoparaskeva

You can do a simple experiment:

  1. add register event listener first
  2. implement a button which calls VoipPushNotification.registerVoipToken()
  3. when app loaded and everything is rendered, press the button and check the event

If you see the register event, then the issue you mentioned is likely a event fired before bridge start
You should both check: didLoadWithEvents and your normal register events. That's the issue that this PR is trying to solve.

@zxcpoiu zxcpoiu merged commit 739b149 into react-native-webrtc:master Dec 2, 2020
@stephanoparaskeva
Copy link

stephanoparaskeva commented Dec 2, 2020

@zxcpoiu

When I press the button I don't get 'register' callback. But this This is what I get in Xcode Console when I press the button:

Screenshot 2020-12-02 at 19 31 39

So 'register' event never runs in my JS code, and I don't know what the APNS_VOIP token is

AppDelegate.m for reference.
AppDelegate.h for reference.

Also, thank you for helping and this library. It was amazing when it worked for me. I'm just not sure why it's stopped.

These are some logs of when the app first loads:
Screenshot 2020-12-02 at 19 57 48

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Dec 2, 2020

You have called [RNVoipPushNotificationManager voipRegistration] In your AppDelegate.m line 54. So when you call again in JS side, the log says: already registered, this is to prevent duplicate registered delegate for PushKit. ( #69 (comment) )

You should use BOTH: didLoadWithEvents and your normal register events.

Please check this example out:
#69 (comment) and echo log inside didLoadWithEvents

@stephanoparaskeva
Copy link

You have called [RNVoipPushNotificationManager voipRegistration] In your AppDelegate.m line 54. So when you call again in JS side, the log says: already registered, this is to prevent duplicate registered delegate for PushKit. ( #69 (comment) )

You should use BOTH: didLoadWithEvents and your normal register events.

Please check this example out:
#69 (comment) and echo log inside didLoadWithEvents

This worked, thank you!

@Romick2005
Copy link
Contributor

@zxcpoiu, I like your way of solving voip push notifications - clean and nice.
I have only one question/observation there are didLoadWithEvents event that are used to fire delayed push notification. Would it have no consequences to remove it, but when we will have js listeners added, we can fire only “register” or “notification” events from the delayed events queue. Is there any specific reason that we would need didLoadWithEvents? Because I think it is a good idea to hide unnecessary event for the end user and simplify the lib setup. What do you think?

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Dec 9, 2020

@Romick2005

First, this solution is originally provided from @konradkierus at react-native-webrtc/react-native-callkeep#169, there are some discussion similar your question react-native-webrtc/react-native-callkeep#169 (comment)

My thinking path when I'm writing this PR was simply:

  1. This lib is tighten to callkeep, so the api is better to align with it.
  2. Does register and notification event arrived order is matter? ( it is matter in callkeep based on the discussion )
    I'm not sure if some people, for example, use register and notification on different places and different time.
    • If yes, then we must store different event in different queue, and flush them each time once the specific corresponded event listener had subscribed. But it lost the order between two events.
  3. Are there any use cases I missed? I'm not sure.

Personally, I prefer to hide didLoadWithEvent and just fired cached event when specific listener had subscribed ( like you mentioned ), but from the unknown use cases point of view, I choose to align with callkeep's api and let user to decide it.

@Romick2005
Copy link
Contributor

I don't see any contradiction with @konradkierus. When we will remove didLoadWithEvents, we will keep the order of events based on the index in queue. So that will also quarantie the order and we would have full control over events.
2. If some people add listenters in different places than would it be a surprise for them that events come in different order?

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Dec 9, 2020

May be I didn't understand what is your point.
Do you mean We don't need didLoadWithEvents entirely right?

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Dec 9, 2020

on native side, track each event on it's own queue, when the event subscribed, fire and flush the corresponding queue. If user does not subscribe for a event, every event fired in native side would stored into a quere?

Or use one queue, when specific event subscribed, for each the queue, fire and flush for the specific event?

Is above what you mean?

@Romick2005
Copy link
Contributor

Yep, I suggest to remove didLoadWithEvents entirely as it is unnecessary and simplifies the code.
I would stick with two queues and fire only after subscribe to specific event and in order to have last event probably we can emit even specific queue as an array, where q[q.length-1] would be the latest event. What do you think of having event timestamp?

@stephanoparaskeva
Copy link

stephanoparaskeva commented Dec 9, 2020

Yep, I suggest to remove didLoadWithEvents entirely as it is unnecessary and simplifies the code.
I would stick with two queues and fire only after subscribe to specific event and in order to have last event probably we can emit even specific queue as an array, where q[q.length-1] would be the latest event. What do you think of having event timestamp?

Disagree entirely. I use 'didLoadWithEvents' And I don't even need 'register'. Register event gets called in Native code and my app never sees event as it always runs first (in AppDelegate.m). So my app Always Loads with event using 'didLoadWithEvent' and this is used to set the APNS VOIP token to async storage and the app then uses token to send to VOIP Backend to find and call phone.

'didLoadWithEvents' is very useful.

@Romick2005
Copy link
Contributor

@stephanoparaskeva Do you know that didLoadWithEvents fire both register and notification events in one array? If you don't need 'register' than you don't have to subscribe to it. Probably you do not get the idea here. So you will just have to listen 'register' in your app and when get one - use received voip token. That would be possible only after suggested changes. And just remove unneeded 'didLoadWithEvent'. By the way how do you handle voip token change event?

@stephanoparaskeva
Copy link

@stephanoparaskeva Do you know that didLoadWithEvents fire both register and notification events in one array? If you don't need 'register' than you don't have to subscribe to it. Probably you do not get the idea here. So you will just have to listen 'register' in your app and when get one - use received voip token. That would be possible only after suggested changes. And just remove unneeded 'didLoadWithEvent'. By the way how do you handle voip token change event?

I honestly think it's fine as is. 'didLoadWithEvents' gives you all events executed before JS code runs, so you can have events first before everything else. It's quite clever.

By the way how do you handle voip token change event?

I don't handle this event, should I, and what for?

@zxcpoiu
Copy link
Member Author

zxcpoiu commented Jan 18, 2021

Hey, sorry for late reply, and happy new year.

Actually @Romick2005 is right, we can of course remove didLoadWithEvents entirely, and cache all events separately in separate native event queue then fires them when corresponding event listener registered from js. This is more intuitive, but the cons are what I mentioned above:

  • This can not handle the priority between different events. Unless sort them again with additional added timestamp.
  • Most user use this libs also uses CallKeep, and the api currently is align with CallKeep, whether method we use, user eventually has to use didLoadWithEvent api on CallKeep libs.

So I prefer to not introduce another breaking change again, unless it's necessary and the change can compatible with both method ( ex: while implementing multiple separated queue for more intuitive api like @Romick2005 said and support didLoadWithEvents as well for user which require the exact event order )

@nero2009
Copy link

nero2009 commented Jul 8, 2021

addEventListener

@zxcpoiu Please should this be used alongside the native configurations in the AppDelegate.m. I have been trying to get this library to receive voip notiifcation and then call react native callkeep but it does not work.
below is my code:

AppDelegate.m


#import "AppDelegate.h"

#import "RNCallKeep.h"
#import <UserNotifications/UserNotifications.h>
#import <RNCPushNotificationIOS.h>
#import <PushKit/PushKit.h> 
#import "RNVoipPushNotificationManager.h" 
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <CodePush/CodePush.h>

#import <UMCore/UMModuleRegistry.h>
#import <UMReactNativeAdapter/UMNativeModulesProxy.h>
#import <UMReactNativeAdapter/UMModuleRegistryAdapter.h>

#import <React/RCTLinkingManager.h>

@interface AppDelegate () <RCTBridgeDelegate>
 
@property (nonatomic, strong) UMModuleRegistryAdapter *moduleRegistryAdapter;
 
@end

@implementation AppDelegate

// Required for deeplinking
- (BOOL)application:(UIApplication *)application
   openURL:(NSURL *)url
   options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
  return [RCTLinkingManager application:application openURL:url options:options];
}

// Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
 [RNCPushNotificationIOS 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
{
  [RNCPushNotificationIOS didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
}
// Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
 [RNCPushNotificationIOS 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
{
  self.moduleRegistryAdapter = [[UMModuleRegistryAdapter alloc] initWithModuleRegistryProvider:[[UMModuleRegistryProvider alloc] init]];

  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];

    // ===== (THIS IS OPTIONAL BUT RECOMMENDED) =====
  // --- register VoipPushNotification here ASAP rather than in JS. Doing this from the JS side may be too slow for some use cases
  // --- see: https://github.com/react-native-webrtc/react-native-voip-push-notification/issues/59#issuecomment-691685841
 [RNVoipPushNotificationManager voipRegistration];
  // ===== (THIS IS OPTIONAL BUT RECOMMENDED) =====

  [RNCallKeep setup:@{
    @"appName": @"SmartHealthMobile",
    @"maximumCallGroups": @3,
    @"maximumCallsPerCallGroup": @1,
    @"supportsVideo": @NO,
  }];

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

  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  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;
  
  [super application:application didFinishLaunchingWithOptions:launchOptions];

  return YES;
}

/* 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.
}

// --- Handle incoming pushes
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
  

  // --- NOTE: apple forced us to invoke callkit ASAP when we receive voip push
  // --- see: react-native-callkeep

  // --- Retrieve information from your voip push payload
  // NSString *uuid = payload.dictionaryPayload[@"uuid"];
  // NSString *callerName = [NSString stringWithFormat:@"%@ (Connecting...)", payload.dictionaryPayload[@"callerName"]];
  // NSString *handle = payload.dictionaryPayload[@"handle"];

  NSString *uuid = @"44713e33-fa78-4ff5-8ec5-983e0832d1c6";
  NSString *callerName = @"Test";
  NSString *handle = @"handle";

  // --- this is optional, only required if you want to call `completion()` on the js side
  [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion];

  // --- Process the received push
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];

  // --- You should make sure to report to callkit BEFORE execute `completion()`
  [RNCallKeep reportNewIncomingCall: uuid
                             handle: handle
                         handleType: @"generic"
                           hasVideo: NO
                localizedCallerName: callerName
                    supportsHolding: YES
                       supportsDTMF: YES
                   supportsGrouping: YES
                 supportsUngrouping: YES
                        fromPushKit: YES
                            payload: nil
              withCompletionHandler: completion];
  // --- You don't need to call it if you stored `completion()` and will call it on the js side.
  completion();
}

- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
    NSArray<id<RCTBridgeModule>> *extraModules = [_moduleRegistryAdapter extraModulesForBridge:bridge];
    // If you'd like to export some custom RCTBridgeModules that are not Expo modules, add them here!
    return extraModules;
}

//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);
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
  return [CodePush bundleURL];
#endif
}

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

@end

then is my class that exports voip listeners

export class VoipNotificationInterface {
  constructor() {
    VoipPushNotification.registerVoipToken()
    this.setupEventListeners()
  }

  static instance = null

  static getInstance() {
    if (this.instance == null) this.instance = new VoipNotificationInterface()
    return this.instance
  }

  setupEventListeners = () => {
    VoipPushNotification.addEventListener('didLoadWithEvents', async events => {
      // --- this will fire when there are events occured before js bridge initialized
      // --- use this event to execute your event handler manually by event type

      if (!events || !Array.isArray(events) || events.length < 1) {
        return
      }
      for (let voipPushEvent of events) {
        let {name, data} = voipPushEvent
        if (name === VoipPushNotification.RNVoipPushRemoteNotificationsRegisteredEvent) {
          console.log('adding voip token in didLoadWithEvents', data)
          await this.addVoipTokenToServer(data)
        } else if (name === VoipPushNotification.RNVoipPushRemoteNotificationReceivedEvent) {
          console.log('onVoipPushNotificationiReceived :>> ', data)
          // RNCallKeep.displayIncomingCall(data?.reference, 'patientID', '34747')
        }
      }
    })
    VoipPushNotification.addEventListener('register', async token => {
      // --- send token to your apn provider server
      console.log('VoipPushNotification register event token:>> ', token)
      await this.addVoipTokenToServer(token)
    })

    VoipPushNotification.addEventListener('notification', notification => {
      // --- when receive remote voip push, register your VoIP client, show local notification ... etc
      // --- optionally, if you `addCompletionHandler` from the native side, once you have done the js jobs to initiate a call, call `completion()`
      // RNCallKeep.displayIncomingCall('44713e33-fa78-4ff5-8ec5-983e0832d1c6', 'Test', 'handle')
    })
  }

  removeEventListeners = () => {}

  addVoipTokenToServer = async token => {
    const patientID = await getUserID()
    let res = await addVoipDeviceToken(patientID, token)

    console.log('res  addVoipTokenToServer:>> ', res)
  }
}

then in my App.js I import VoipNotificationInterface and instantiate it

App.js

VoipNotificationInterface.getInstance()

Please I can get this to work.

My expected behaviour

  • send voip notification
  • app should should native call screen when app is in background/killed/quit/locked state.
  • on answer call, app should navigate to call screen where I initialize the voip client(Agora in my case) and start webrtc call

I cant seem to when this to work. please an help will do

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.

didReceiveIncomingPushWithPayload not called in background/terminated
6 participants