tinycreative / react-native-intercom

React Native wrapper for Intercom.io
MIT License
406 stars 280 forks source link

Handling deep links on iOS #421

Closed hanniedong closed 2 years ago

hanniedong commented 3 years ago

I'm using branch.io to handle deep linking and have it working with another push notification platform (One Signal).

I'm able to receive push notifications (using intercom and this library) on both iOS and Android. The only difference is the handling of the deep link. Deep links work in Android, so when I tap on the push notification, it takes me to the correct screen in the app. For iOS, tapping the push notification only opens the app and doesn't carry over the deep link.

Has anyone figured out how to handle deep links coming from Intercom push notifications?

johnryan commented 3 years ago

Have you tried following the manual setup instructions here: https://developers.intercom.com/installing-intercom/docs/ios-push-notifications

Namely setting the IntercomAutoIntegratePushNotifications to NO in Info.plist

and then seeing if the didReceiveRemoteNotification is triggered when you tap on the message?

If that is called you should be able to grab the url from userInfo object parameter of that function.

After that you can either call openUrl with that url to pass your deep link to that function:

NSString *deepLink = "extracted from userInfo";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:deepLink]] options:nil completionHandler:nil]

or just pass it to your deep link handler directly

du5rte commented 3 years ago

Also having the same issue on iOS, I have deep links working everywhere else just not from Intercom Notifications, feels like library issue when it works on android but not on iOS.

@johnryan you might be on to something but could you detail it a little more? 🤔 How would you extract the deep link URL from userInfo and pass it to for example RCTLinkingManager?

johnryan commented 3 years ago

@du5rte You probably need to be on the latest Intercom version - but what I was suggesting was to just get the value you want out of the userInfo dictionary - you'd have to take a look at that object and grab whatever key you want

du5rte commented 3 years ago

For anyone having this issue here's my fix, basically intercepts the notification, checks for a uri key value and opens it. I'm not an Objective-C developer so maybe someone else can give suggestions on how to optimise this.

Edit: Still doesn't solve opening deeplinks from notifications when the app is closed 🤔

// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
  // Required for Intercom disabled automatic handling of Intercom push notifications:
  NSDictionary *userInfo = response.notification.request.content.userInfo;
  if ([Intercom isIntercomPushNotification:userInfo]) {
    [Intercom handleIntercomPushNotification:userInfo];

    if (userInfo[@"uri"]) {
      // Get the uri from the userInfo
      NSString *uri = userInfo[@"uri"];

      // Bring application into scope
      UIApplication *application = [UIApplication sharedApplication];

      // Convert the uri string to a NSURL Type
      NSURL *URL = [NSURL URLWithString:uri];

      // Open a URL with iOS 10 
      // @see https://useyourloaf.com/blog/openurl-deprecated-in-ios10/
      [application openURL:URL options:@{}
         completionHandler:^(BOOL success) {
        NSLog(@"Open %@: %d",uri,success);
      }];
    }
  }

  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
}

Edit 2: Check new solution bellow 👇

du5rte commented 3 years ago

Update! Found a more elegant solution and that works when the app is closed.

Checklist, make sure you've done these steps before:

The Problem

The core issue of Intercom not handling deep links for me was that react-native-intercom does not pass an initial URL to an application through didFinishLaunchingWithOptions if it's opened from the background. Instead react-native-intercom calls the openURL method immediately after the application is launched and the react-native part of the application misses this event. Read more.

The Solution

Handle intercom notifications manually, intercept notifications with deep links inside it and store it to open later when the app is ready. Read more on the original solution

Steps:

  1. Set the IntercomAutoIntegratePushNotifications to NO in Info.plist like it says on Intercom

Info.plist

<key>IntercomAutoIntegratePushNotifications</key>
<string>NO</string>
  1. Modify the didReceiveNotificationResponse in you setup on Update AppDelegate.m

AppDelegate.m

// Required for localNotification event
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
  // Required for handleIntercomPushNotification function bellow
  NSDictionary *userInfo = response.notification.request.content.userInfo;

  // Required to handle Intercom push notifications manually
  if ([Intercom isIntercomPushNotification:userInfo]) {
    [Intercom handleIntercomPushNotification:userInfo];
  }

  [RNCPushNotificationIOS didReceiveNotificationResponse:response];
}
  1. In your onNotification ( see react-native-push-notification usage). Setup up a condition that when the app is not initialized, store the deep link for later else just open the link.

I know using global is considered "dirty" but I tried many variations using AppStatus, React.Context, Async.Storage, this one worked the best).

index.ts

// Must be outside of any component LifeCycle (such as `componentDidMount`).
PushNotification.configure({
  // ...
  // (required) Called when a remote is received or opened, or local notification is opened
  onNotification: function (notification) {
    console.log('[Notification]', notification)

    // If notification is interactive
    if (notification.userInteraction) {
      const url = (notification.data as any)?.uri

      if (url) {
        /**
         * When handling a notification with a deep link
         *
         * if the app is initialized just open url
         * else save it to AsyncStorage and handle it later
         */
        if (global.initialized === true) {
          Linking.openURL(url)
        } else {
          AsyncStorage.setItem('initialURL', url)
            .then(() => {
              console.log('[Notification/AsyncStorage] initial url', url)
            })
            .catch(console.error)
        }
      }
    }

    // (required) Called when a remote is received or opened, or local notification is opened
    notification.finish(PushNotificationIOS.FetchResult.NoData)
  },
  // ...
})
  1. How you handle it later is up to you, just make sure when you handle it you remove the initialURL from the AsyncStorage otherwise every time your app starts up it will continue to open that url, and set global.initialized to true, so when the app is active new notifications deep links don't keep getting stored.
await authenticate()

// set initialized, important to
global.initialized = true

// check if there was a initialURL stored
const initialURL = await AsyncStorage.getItem('initialURL')

// open and delete it
if (initialURL) {
  await Linking.openURL(initialURL)
  await AsyncStorage.removeItem('initialURL')
}
  1. (Optional) Here's the way I did it using React Native Navigation Linking with linking.getInitialURL

App.tsx (mockup)

const linking = {
  // ...
  // Custom function to get the URL which was used to open the app
  async getInitialURL() {
    console.log('[Navigation/Linking] Getting initial url')

    // Check if app was opened from a deep link
    const url = await Linking.getInitialURL()

    if (url != null) {
      console.log('[Navigation/Linking] Has initial url', url)
      return url
    }

    // Check for additional initial deep link in AsyncStorage stored by notifications
    const initialURL = await AsyncStorage.getItem('initialURL')

    if (initialURL != null) {
      console.log('[Navigation/AsyncStorage] Has initial url', initialURL)
      await AsyncStorage.removeItem('initialURL')

      return initialURL
    }
  },
  // ...
}

function App() {
  return (
    <Auth>
      <NavigationContainer linking={linking} />
         ...
      </NavigationContainer>
    </Auth>
  )
}

Important to do the authentication before the routing

Auth.tsx (mockup)

function Auth(props) {
  async function init() {
    try {
      await authenticate()

      global.initialized = true
    } catch (error) {
      console.error(error)
    }
  }

  return loading ? (
    <LoadingIndicator />
  ) : (
    props.children
  )}
}

For TypeScript users here's how you fix the property does not exist on type 'Global & typeof globalThis'.ts(2339). On the root of the app project

global.d.ts

declare module '*.png'

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV?: 'development' | 'test' | 'production'
    }
    interface Global {
      initialized: boolean
    }
  }
}

// If this file has no import/export statements (i.e. is a script)
// convert it into a module by adding an empty export statement.
export {}
Br1an-Boyle commented 3 years ago

@du5rte @hanniedong Are your links Universal Links of Custom Scheme URLs? We now have support for deep linking a universal link in version 9.3.0