react-native-webrtc / react-native-voip-push-notification

React Native VoIP Push Notification - Currently iOS only
ISC License
211 stars 82 forks source link

Integrating with Expo #106

Open frozencap opened 1 year ago

frozencap commented 1 year ago

Has anybody managed to get an Expo Module/Config Plugin working for this lib ?

This is the only lib under react-native-webrtc/ organization that doesn't provide this

Yeasiin commented 7 months ago

Did you get any solution?

disco-panda commented 5 months ago

I have used it successfully with Expo Plugins to generate the delegate.

You can use this in your app config to give the proper permissions

    ios: {
...
      infoPlist: {
        UIBackgroundModes: ["voip"],
      }
    },
frozencap commented 5 months ago

I have used it successfully with Expo Plugins to generate the delegate.

@disco-panda awesome! mind sharing your config plugin ?

AlanGreyjoy commented 4 months ago

I have used it successfully with Expo Plugins to generate the delegate.

You can use this in your app config to give the proper permissions

    ios: {
...
      infoPlist: {
        UIBackgroundModes: ["voip"],
      }
    },

Very nice. Share that bad boy?

ramezrafla commented 3 months ago

@disco-panda well done! Could you share the plugin? I don't mind cleaning up and helping PR this repo I would save a lot of people a lot of headaches.

jexodusmercado commented 3 months ago

@ramezrafla forked the repo and created my own plugin. We dont have any contribution guide here, I don't know if I should request a PR.

Note: I implemented the RNCallKeep in the AppDelegate too. This is the exact requirements of our app.

https://github.com/jexodusmercado/react-native-voip-push-notification

AlanGreyjoy commented 3 months ago

@ramezrafla forked the repo and created my own plugin. We dont have any contribution guide here, I don't know if I should request a PR.

Note: I implemented the RNCallKeep in the AppDelegate too. This is the exact requirements of our app.

https://github.com/jexodusmercado/react-native-voip-push-notification

Holy mac-n-cheese. There isn't a number that would accurately represent the amount of people you are helping with this. You could request a PR here, but the main dev is in and out alot.

jexodusmercado commented 3 months ago

@AlanGreyjoy Let me clean my code first then request a PR in a few days. I need sleep. The amount of trial and error to make it work locally and in the eas-build is too much.

AlanGreyjoy commented 3 months ago

@AlanGreyjoy Let me clean my code first then request a PR in a few days. I need sleep. The amount of trial and error to make it work locally and in the eas-build is too much.

I know the pain... been developing a node-red plugin. Things like this make you worship whoever came up with HMR ha.

jexodusmercado commented 3 months ago

I created the PR. Lets hope the main dev review this

ramezrafla commented 3 months ago

Dear @jexodusmercado -- well done! We worked in parallel. We used app.config.js in our app to do the same. I am sharing here to help people:

Notes:

  1. Save this file as app.config.js in your project root
  2. Edit the obj-c to align with your own notification payload
  3. You also need to add react-native-call-keep package -- this code handles both
  4. Run npx expo prebuild and inspect the output AppDelegate.mm file (don't forget to remove folders ios and android before you build on expo online)
  5. During build if there is an error, look for it on the expo.dev build console and adjust
  6. No need to call completion from JS side -- it's handled here already
  7. Don't add self.bridge stuff from the README -- it's for pure React-Native. Expo handles for you
// app.config.js
// https://github.com/invertase/react-native-firebase/discussions/5386#discussioncomment-860295
// https://www.videosdk.live/blog/react-native-ios-video-calling-app-with-callkeep
// https://sendbird.com/docs/calls/sdk/v1/react-native/direct-call/receiving-a-call/receive-a-call-in-the-background

import { withAppDelegate } from '@expo/config-plugins'
import { addObjcImports, insertContentsInsideObjcFunctionBlock, findObjcFunctionCodeBlock } from '@expo/config-plugins/build/ios/codeMod'

const DID_FINISH_LAUNCHING = 'application:didFinishLaunchingWithOptions:'
const DID_UPDATE_PUSH_CREDENTIALS = 'pushRegistry:didUpdatePushCredentials:forType:'
const DID_RECEIVE_INCOMING_PUSH = 'pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:'
const CONTINUE_USER_ACTIVITY = 'application:continueUserActivity:restorationHandler:'

function addContinueUserActivity(contents) {
  // call the setup of RNCallKeep
  // https://github.com/react-native-webrtc/react-native-callkeep
  // call the setup of voip push notification
  // https://github.com/react-native-webrtc/react-native-voip-push-notification
  const setupContinue = 'BOOL resultRNCall = [RNCallKeep application:application continueUserActivity:userActivity restorationHandler:restorationHandler];'
  if (!contents.includes(setupContinue)) {
    contents = insertContentsInsideObjcFunctionBlock(
      contents,
      CONTINUE_USER_ACTIVITY,
      setupContinue,
      { position: 'head' }
    )
    contents = contents.replace('[super application:application continueUserActivity:userActivity restorationHandler:restorationHandler]', '[super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || resultRNCall')
  }
  return contents
}

// https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/expo-config-plugin/src/withPushAppDelegate.ts#L30
function addImports(contents) {
  return addObjcImports(
    contents,
    [
      '<PushKit/PushKit.h>',
      '"RNVoipPushNotificationManager.h"',
      '"RNCallKeep.h"'
    ]
  )
}

// https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/expo-config-plugin/src/common/addNewLinesToAppDelegate.ts
function addNewLinesToAppDelegate(content, toAdd) {
  const lines = content.split('\n')
  let lineIndex = lines.findIndex((line) => line.match('@end'))
  if (lineIndex < 0) throw Error('Malformed app delegate')
  toAdd.unshift('')
  lineIndex -= 1
  for (const newLine of toAdd) {
    lines.splice(lineIndex, 0, newLine)
    lineIndex++
  }
  return lines.join('\n')
}

function addDidFinishLaunching(contents) {
  // call the setup of RNCallKeep
  // https://github.com/react-native-webrtc/react-native-callkeep
  // call the setup of voip push notification
  // https://github.com/react-native-webrtc/react-native-voip-push-notification
  const setupCallKeep = `
  NSString *appName = [[[NSBundle mainBundle] infoDictionary]objectForKey :@"CFBundleDisplayName"];
  [RNCallKeep setup:@{
    @"appName": appName,
    @"supportsVideo": @NO,
    @"includesCallsInRecents": @YES,
    @"maximumCallGroups": @1,
    @"maximumCallsPerCallGroup": @1
  }];
  [RNVoipPushNotificationManager voipRegistration];
  `
  if (!contents.includes('[RNCallKeep setup:@')) {
    contents = insertContentsInsideObjcFunctionBlock(
      contents,
      DID_FINISH_LAUNCHING,
      setupCallKeep,
      { position: 'head' }
    )
  }
  return contents
}

function addDidUpdatePushCredentials(contents) {
  if (!contents.includes('didInvalidatePushTokenForType:(PKPushType)type')) {
    contents = addNewLinesToAppDelegate(contents, [
`
- (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.
}
`
    ])
  }

  const updatedPushCredentialsMethod = '[RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];'
  if (!contents.includes(updatedPushCredentialsMethod)) {
    if (!findObjcFunctionCodeBlock(contents, DID_UPDATE_PUSH_CREDENTIALS)) {
      contents = addNewLinesToAppDelegate(contents, [
`- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
    ${updatedPushCredentialsMethod}
}`
      ])
    }
    else {
      contents = insertContentsInsideObjcFunctionBlock(
        contents,
        DID_UPDATE_PUSH_CREDENTIALS,
        updatedPushCredentialsMethod,
        { position: 'tail' }
      )
    }
  }
  return contents
}

function addDidReceiveIncomingPush(contents) {
  // https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/expo-config-plugin/src/withPushAppDelegate.ts#L30
  // https://github.com/react-native-webrtc/react-native-callkeep#pushkit
  const onIncomingPush = `
  [RNCallKeep reportNewIncomingCall: [[NSUUID UUID] UUIDString]
    handle: payload.dictionaryPayload[@"handle"]
    handleType: @"generic"
    hasVideo: NO
    localizedCallerName: payload.dictionaryPayload[@"caller"]
    supportsHolding: YES
    supportsDTMF: YES
    supportsGrouping: YES
    supportsUngrouping: YES
    fromPushKit: YES
    payload: nil
    withCompletionHandler: completion];  
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
`
  if (!contents.includes('[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload')) {
    if (!findObjcFunctionCodeBlock(contents, DID_RECEIVE_INCOMING_PUSH)) {
      contents = addNewLinesToAppDelegate(contents, [
`- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
${onIncomingPush}
}`
      ])
    }
    else {
      contents = insertContentsInsideObjcFunctionBlock(
        contents,
        DID_RECEIVE_INCOMING_PUSH,
        onIncomingPush,
        { position: 'tail' }
      )
    }
  }
  return contents
}

// https://www.sitepen.com/blog/doing-more-with-expo-using-custom-native-code
// https://docs.expo.dev/config-plugins/plugins-and-mods/
// https://github.com/invertase/react-native-firebase/discussions/5386
function withVoipAppDelegate(config) {
  return withAppDelegate(config, (cfg) => {
    const { modResults } = cfg
    if (['objc', 'objcpp'].includes(modResults.language)) {
      modResults.contents = addImports(modResults.contents)
      modResults.contents = addDidFinishLaunching(modResults.contents)
      modResults.contents = addDidUpdatePushCredentials(modResults.contents)
      modResults.contents = addDidReceiveIncomingPush(modResults.contents)
      modResults.contents = addContinueUserActivity(modResults.contents)
      // console.log(modResults.contents)
    }
    return cfg
  })
}

module.exports = ({ config }) => {
  return withVoipAppDelegate(config)
}
grondo89 commented 2 months ago

I created the PR. Lets hope the main dev review this

Hey, sorry for the silly question but: How do I apply your approach? I create the plugin file, add the plugin to the config and make a build / prebuild? Or should I manually apply the modifications you outline in the AppDelegate.m modification? Or both? Thanks a lot for the help!

wilkinsonj commented 2 months ago

I created the PR. Lets hope the main dev review this

Hey, sorry for the silly question but: How do I apply your approach? I create the plugin file, add the plugin to the config and make a build / prebuild? Or should I manually apply the modifications you outline in the AppDelegate.m modification? Or both? Thanks a lot for the help!

I created the PR. Lets hope the main dev review this

Hey, sorry for the silly question but: How do I apply your approach? I create the plugin file, add the plugin to the config and make a build / prebuild? Or should I manually apply the modifications you outline in the AppDelegate.m modification? Or both? Thanks a lot for the help!

Don't manually make the modifications, that would be overwritten when you next build.

In your root folder (where you find package.json etc) create a file called voip.js, and paste the following into it (credit to @ramezrafla above):

// https://github.com/invertase/react-native-firebase/discussions/5386#discussioncomment-860295
// https://www.videosdk.live/blog/react-native-ios-video-calling-app-with-callkeep
// https://sendbird.com/docs/calls/sdk/v1/react-native/direct-call/receiving-a-call/receive-a-call-in-the-background

const { addObjcImports, insertContentsInsideObjcFunctionBlock, findObjcFunctionCodeBlock } = require('@expo/config-plugins/build/ios/codeMod')
const { withAppDelegate } = require('@expo/config-plugins')

const DID_FINISH_LAUNCHING = 'application:didFinishLaunchingWithOptions:'
const DID_UPDATE_PUSH_CREDENTIALS = 'pushRegistry:didUpdatePushCredentials:forType:'
const DID_RECEIVE_INCOMING_PUSH = 'pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:'
const CONTINUE_USER_ACTIVITY = 'application:continueUserActivity:restorationHandler:'

function addContinueUserActivity(contents) {
    // call the setup of RNCallKeep
    // https://github.com/react-native-webrtc/react-native-callkeep
    // call the setup of voip push notification
    // https://github.com/react-native-webrtc/react-native-voip-push-notification
    const setupContinue = 'BOOL resultRNCall = [RNCallKeep application:application continueUserActivity:userActivity restorationHandler:restorationHandler];'
    if (!contents.includes(setupContinue)) {
        contents = insertContentsInsideObjcFunctionBlock(
            contents,
            CONTINUE_USER_ACTIVITY,
            setupContinue,
            { position: 'head' }
        )
        contents = contents.replace('[super application:application continueUserActivity:userActivity restorationHandler:restorationHandler]', '[super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || resultRNCall')
    }
    return contents
}

// https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/expo-config-plugin/src/withPushAppDelegate.ts#L30
function addImports(contents) {
    return addObjcImports(
        contents,
        [
            '<PushKit/PushKit.h>',
            '"RNVoipPushNotificationManager.h"',
            '"RNCallKeep.h"'
        ]
    )
}

// https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/expo-config-plugin/src/common/addNewLinesToAppDelegate.ts
function addNewLinesToAppDelegate(content, toAdd) {
    const lines = content.split('\n')
    let lineIndex = lines.findIndex((line) => line.match('@end'))
    if (lineIndex < 0) throw Error('Malformed app delegate')
    toAdd.unshift('')
    lineIndex -= 1
    for (const newLine of toAdd) {
        lines.splice(lineIndex, 0, newLine)
        lineIndex++
    }
    return lines.join('\n')
}

function addDidFinishLaunching(contents) {
    // call the setup of RNCallKeep
    // https://github.com/react-native-webrtc/react-native-callkeep
    // call the setup of voip push notification
    // https://github.com/react-native-webrtc/react-native-voip-push-notification
    const setupCallKeep = `
  NSString *appName = [[[NSBundle mainBundle] infoDictionary]objectForKey :@"CFBundleDisplayName"];
  [RNCallKeep setup:@{
    @"appName": appName,
    @"supportsVideo": @YES,
    @"includesCallsInRecents": @YES,
    @"maximumCallGroups": @1,
    @"maximumCallsPerCallGroup": @1
  }];
  [RNVoipPushNotificationManager voipRegistration];
  `
    if (!contents.includes('[RNCallKeep setup:@')) {
        contents = insertContentsInsideObjcFunctionBlock(
            contents,
            DID_FINISH_LAUNCHING,
            setupCallKeep,
            { position: 'head' }
        )
    }
    return contents
}

function addDidUpdatePushCredentials(contents) {
    if (!contents.includes('didInvalidatePushTokenForType:(PKPushType)type')) {
        contents = addNewLinesToAppDelegate(contents, [
            `
- (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.
}
`
        ])
    }

    const updatedPushCredentialsMethod = '[RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];'
    if (!contents.includes(updatedPushCredentialsMethod)) {
        if (!findObjcFunctionCodeBlock(contents, DID_UPDATE_PUSH_CREDENTIALS)) {
            contents = addNewLinesToAppDelegate(contents, [
                `- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type {
    ${updatedPushCredentialsMethod}
}`
            ])
        }
        else {
            contents = insertContentsInsideObjcFunctionBlock(
                contents,
                DID_UPDATE_PUSH_CREDENTIALS,
                updatedPushCredentialsMethod,
                { position: 'tail' }
            )
        }
    }
    return contents
}

function addDidReceiveIncomingPush(contents) {
    // https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/expo-config-plugin/src/withPushAppDelegate.ts#L30
    // https://github.com/react-native-webrtc/react-native-callkeep#pushkit
    const onIncomingPush = `
  [RNCallKeep reportNewIncomingCall: [[NSUUID UUID] UUIDString]
    handle: payload.dictionaryPayload[@"handle"]
    handleType: @"generic"
    hasVideo: YES
    localizedCallerName: payload.dictionaryPayload[@"caller"]
    supportsHolding: YES
    supportsDTMF: YES
    supportsGrouping: YES
    supportsUngrouping: YES
    fromPushKit: YES
    payload: nil
    withCompletionHandler: completion];  
  [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
`
    if (!contents.includes('[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload')) {
        if (!findObjcFunctionCodeBlock(contents, DID_RECEIVE_INCOMING_PUSH)) {
            contents = addNewLinesToAppDelegate(contents, [
                `- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
${onIncomingPush}
}`
            ])
        }
        else {
            contents = insertContentsInsideObjcFunctionBlock(
                contents,
                DID_RECEIVE_INCOMING_PUSH,
                onIncomingPush,
                { position: 'tail' }
            )
        }
    }
    return contents
}

// https://www.sitepen.com/blog/doing-more-with-expo-using-custom-native-code
// https://docs.expo.dev/config-plugins/plugins-and-mods/
// https://github.com/invertase/react-native-firebase/discussions/5386
module.exports = function withPrefixedName(config, prefix) {
    //return config
    return withAppDelegate(config, (cfg) => {
        const { modResults } = cfg
        if (['objc', 'objcpp'].includes(modResults.language)) {
            modResults.contents = addImports(modResults.contents)
            modResults.contents = addDidFinishLaunching(modResults.contents)
            modResults.contents = addDidUpdatePushCredentials(modResults.contents)
            modResults.contents = addDidReceiveIncomingPush(modResults.contents)
            modResults.contents = addContinueUserActivity(modResults.contents)
            // console.log(modResults.contents)
        }
        return cfg
    })
}

Then in your app.config.js, add the following:

"plugins": [
    "./voip"
]

And the following:

"ios": {
    "infoPlist": {
      "UIBackgroundModes": [
        "voip",
        "remote-notification"
      ]
    }
  },

Then just build normally.

A word of warning, this plugin assumes your VoIP notification contains a data dictionary with the properties handle, callerName and uuid. Without those, your app will crash when you receive a notification. If you want to change that, find them in voip.js and amend as needed.

Example push notification payload:

// Notification payload
const notificationPayload = {
  aps: {
    alert: {
      // add custom data here
    },
    sound: 'default',
    'content-available': 1 // Indicates a VoIP notification
  },
  type: 'voip',
  data: {
    handle: 'handle',
    callerName: 'Caller Name',
    uuid: uuid
  }
};

Final point - I changed the video settings to 'YES' - as this allows a user to automatically go to your app even if their phone was locked prior to receiving the call