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

Universal links callback doesn't work if launching closed app #11191

Closed
uerceg opened this issue Nov 29, 2016 · 8 comments
Closed

Universal links callback doesn't work if launching closed app #11191

uerceg opened this issue Nov 29, 2016 · 8 comments
Labels
Resolution: Locked This issue was locked by the bot.

Comments

@uerceg
Copy link

uerceg commented Nov 29, 2016

Description

I am testing how universal links are working with react native. I have set up everything for the universal links handling in native iOS Xcode project:

  1. Associated domains are properly enabled for my app and set up.
  2. applinks parameter which opens my app
  3. My react native app uses Linking project for which I have properly set up the header search paths and Xcode project compiles properly.
  4. I have edited both of my AppDelegate.m methods (I say both, because I am also testing custom URL scheme functionality in pre iOS 9 devices, but let's keep this issue tied to universal links only) and they look like this:
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
  return [RCTLinkingManager application:application openURL:url
                      sourceApplication:sourceApplication annotation:annotation];
}

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
  return [RCTLinkingManager application:application
                   continueUserActivity:userActivity
                     restorationHandler:restorationHandler];
}
  1. In my index.ios.js I am subscribing to url event to get info about the link which opened my app like this:
// ...

componentDidMount() {
    Linking.addEventListener('url', this._handleOpenURL);
}

// ...

componentWillUnmount() {
    Linking.removeEventListener('url', this._handleOpenURL);
}

// ...

_handleOpenURL(event) {
    // Do something with event.url object.
}

I have tested two scenarios in which I am opening my app with click on the universal link:

  1. Clicking on the link while app is running in the background.
  2. Clicking on the link while app is still not started.

In 1st scenario, everything works like a charm. Clicking the universal link is starting my app (pretty much bringing it back to foreground since it was backgrounded), _handleOpenURL method gets called and I nicely get info about the universal link as part of the event.url object. Lovely.

In 2nd scenario upon clicking on link app gets opened, which is fine. But, _handleOpenURL method is never triggered after app starts.

Reproduction

  1. Completely close your app which supports universal links.
  2. Open it by clicking on the universal link and check if _handleOpenURL method gets called (it doesn't and it should, or at least I guess it should).

Solution

As far as I investigated, first problem I have encountered in this scenario your RTCLinkingManager class method gets called (https://github.com/facebook/react-native/blob/master/Libraries/LinkingIOS/RCTLinkingManager.m#L52) and seems like you're posting the notification in this method before your RTCLinkingManager instance subscribes to that event in this line https://github.com/facebook/react-native/blob/master/Libraries/LinkingIOS/RCTLinkingManager.m#L24.

Since it seems that RCTLinkingManager instance exists as a singleton in your framework, I have adapted it a bit to solve this problem by adding small queueing mechanism which will gather notifications if object instance is not subscribed to RCTOpenURLNotification and post notifications later once subscription happens. That worked well, I managed to enable RCTLinkingManager not to miss these notifications but then I got into second issue.

With these changes in framework, once opening the app with the click on the universal link, RCTOpenURLNotification notification was being announced so that RCTLinkingManager receives it, but once that was tried to be propagated up to JavaScript (which happens in this line: https://github.com/facebook/react-native/blob/master/Libraries/LinkingIOS/RCTLinkingManager.m#L67), I figured out that app itself still didn't manage to subscribe to url event. I got this warning:

<Warning>: Sending `url` with no listeners registered.

which happens in here:

RCTLogWarn(@"Sending `%@` with no listeners registered.", eventName);

Further investigation guided me to the conclusion that after event towards JavaScript part is sent with this new queueing mechanism I added, startObserving gets called before _listenerCount is incremented thus making JavaScript to miss this message from native part although componentDidMount() was already executed and JavaScript callback is ready to receive this event. Because of this, I have added small changes to RCTEventEmitter.m to support this special case now.

You can check all the changes I did in here: https://github.com/uerceg/react-native/tree/deep-linking-fix

I assume that this is not the perfect solution, but this fixes the issue for me in this case. Since I am not aware of all aspects of your framework, I am not sure if this might cause issues on some other place, so looking forward to hear some comment from you on this.

Looking forward in solving this issue and enabling this feature in react native.

Cheers

Additional Information

  • React Native version: 1.2.0
  • Platform: iOS
  • Operating System: Mac OS X Sierra
@lacker
Copy link
Contributor

lacker commented Nov 30, 2016

Thanks for providing all of this detailed information! I am still somewhat confused as to what the problem is. You can't add an event listener for "url" in a componentDidMount? That seems intentional - I would expect an event that opens the whole app to occur before a component mounted. Or am I misunderstanding what's going on here?

@uerceg
Copy link
Author

uerceg commented Nov 30, 2016

No no, I can add a listener in the componentDidMount, pretty much like advised in here: https://facebook.github.io/react-native/docs/linking.html and that's fine.

When app is in the background and if I hit an universal link on some website, since app is already started and react modules loaded, app gets "opened" (not really opened but rather just foregrounded), mechanism behind it catches the URL, fires url event and I get info into my JavaScript listener.

But case in which I don't get this if I repeat this scenario, but with app completely closed (not in background, not running/killed).

How universal links are working in native iOS app is that this method from AppDelegate.m called continueUserActivity gets triggered when ever app gets opened with an universal link - regardless of whether it was in the background or not running at all. Once it is brought to foreground or opened, information about URL which was clicked and caused the app to run in foreground is being delivered to the user in this continueUserActivity method. With this URL, user has information which link caused his app to open and based on that info user can do certain stuff in the app (navigate user to some specific place in the app other than opening default start page, or stuff like that).

I was kinda expecting that this url subscribing mechanism will work the same and provide me with URL info in JavaScript in both of these cases as well. Maybe you didn't planned this thing to work in this way?

And your expectation that event that opens the whole app happens before a component is mounted is correct. That whole mechanism is controlled by iOS and end effect is that it opens the app and triggers continueUserActivity in AppDelegate.m. From that moment, starts the react native flow of handling information which was delivered to this method. In custom branch I made I tried to find (probably dummy) solution which enables URL to be delivered to JavaScript listener in case where app was started via universal link after being closed as well.

Looking forward to hear your thoughts on this and do you think that this URL info should be delivered to JavaScript url listener in both of these cases or not.

Cheers

@philipheinser
Copy link
Contributor

@uerceg We got this working in your app by using this:

componentDidMount() {
    Linking.getInitialURL()
      .then((url) => {
        if (url) {
          this. _handleOpenURL({ url });
        }
      })
}

This way you get the url the app startet initially with after that you use the listener to listen for url events while the app is running.

@uerceg
Copy link
Author

uerceg commented Dec 5, 2016

Hi @philipheinser

And thanks for the answer. This is actually what I am using for the Android app in index.android.js file. This is now really really interesting. Here's how it looks like for me now:

Android

componentDidMount() {
    const url = Linking.getInitialURL().then(url => {
        if (url) {
            // Do stuff with URL
        }
    });
}

That's the only thing I have set up for Android app in this works in both scenarios - if I open closed app with a click on a link which contains scheme which my app handles or if I "open" backgrounded app. Regardless of scenario, url is being obtained properly in this method and everything works like a charm.

iOS

componentDidMount() {
    Linking.addEventListener('url', this.handleDeepLink);
    Linking.getInitialURL().then((url) => {
        if (url) {
            this.handleDeepLink({ url });
        }
    })
}
componentWillUnmount() {
    Linking.removeEventListener('url', this.handleDeepLink);
}
handleDeepLink(url) {
    // Do stuff with URL
}

For iOS I actually need this kind of setup in order to support both scenarios.

In scenario in which I am opening closed app with the link, mechanism that gets me URL delivered once the app starts is this thing which you have suggested in your answer from above. Indeed, I am getting the URL in this method. But, if I try to open the app which is running in the background with the link, this method does not get called. In this scenario, my handleDeepLink gets called as part of this subscription mechanism to url event.

So for iOS, only combination of these two is providing me the URL in both scenarios (on different places, but it does provide).

My question now is: Is this intended to be like this or should it be exactly the same like in Android?

Thanks in advance for your answer.

Cheers

@philipheinser
Copy link
Contributor

philipheinser commented Dec 5, 2016 via email

@uerceg
Copy link
Author

uerceg commented Dec 5, 2016

Aham, you mean the android:launchMode to be set on singleTask. Lemme try this and see how Android behaves. Will ping in here soon.

@uerceg
Copy link
Author

uerceg commented Dec 5, 2016

Indeed, if I pick launching activity to have singleTask launching mode, code that I used in iOS is working in the same way in Android as well.

Okay, I guess that this solves all the issues I had with this topic. Thank you very much for your answers and help!

Cheers

@GopiKrishna10
Copy link

Hi @uerceg @philipheinser

Am facing an issue slightly similar to @uerceg .For Android, Deeplinking is working fine in both scenarios and come to IOS Not working as expected.That is if the app is not yet opened Now clicking on the link in mail app opened, getting the URL by using Linking.getInitialURL() Method in my component and if the app is running in the background calling the same Linking.getInitialURL() method but not returning any url.it gives a null.Can you guys help me out of this. here is my code

componentDidMount() {

  if(Platform.OS === 'ios'){
    Linking.addEventListener('url', this.handleOpenURL);
    Linking.getInitialURL().then((url) => {
      if (url) {
         this.handleOpenURL(url)
      }
    })
    .catch((e) => {})
}else{
  AppState.addEventListener('change', this._handleAppStateChange);
}  

}

componentWillUnmount() { // C
Linking.removeEventListener('url', this.handleOpenURL);
this.handleOpenURL('')
}

@facebook facebook locked as resolved and limited conversation to collaborators May 24, 2018
@react-native-bot react-native-bot added the Resolution: Locked This issue was locked by the bot. label Jul 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Resolution: Locked This issue was locked by the bot.
Projects
None yet
Development

No branches or pull requests

5 participants