apache / cordova-ios

Apache Cordova iOS
https://cordova.apache.org/
Apache License 2.0
2.16k stars 988 forks source link

Pass Launch Parameters to Cordova App #723

Open driesdriessen opened 4 years ago

driesdriessen commented 4 years ago

Feature Request

Motivation Behind Feature

It is possible to launch an app with start-up parameters. This is useful for opening an app in a specific context. An example is opening an editor with a specific file. Unfortunately startup parameters currently are not passed to the Cordova app.

Feature Description

An app can be launched with parameters: myapp?var1=foo

In the Cordova app, the parameters can be picked up: var url = new URL(globalThis.location;); var var1 = url.searchParams.get("var1");

Alternatives or Workarounds

For IOS I currently copy this code in xCode manually to didFinishLaunchingWithOptions in CDVAppDelegate.m

// Set your app's start page by setting the <content src='foo.html' /> tag in config.xml.
// If necessary, uncomment the line below to override it.
NSURL *url = launchOptions[UIApplicationLaunchOptionsURLKey];
if (url)
{
    self.viewController.startPage = [@"index.html?" stringByAppendingString:url.query];
} else {
    self.viewController.startPage = @"index.html";
}

It would be nice if this can become a part of the standard IOS implementation, and if a similar solution can be created for other platforms.

brodycj commented 4 years ago

Thanks, this would be an enhancement that may need additional testing and support efforts if added. Unfortunately the Cordova maintainers are already overloaded, see apache/cordova#163.

HarelM commented 3 years ago

I have an issue in my app that is similar to this one and I think is described here: https://stackoverflow.com/questions/29515700/phonegap-handleopenurl Basically what happens is that I associated my app with some file types (GPX, KML) and the app opens the files if the app is already open. This doesn't work well when the app is not open in the background. Since I'm using cordova in my production app I have no problems testing this to make sure this works. I also don't mind sending a PR to solve this, except the other PR I sent was not merged (and was not closed too), so I'm not sure how to properly help... I'm not sure the solution suggested above is the right solution as there's already a solution using handleOpenUrl method which I think is the right one, there's only a need to call this function/raise the relevant event in the didFinishLaunchingWithOptions method, I think. In any case, I'd love to help move this enhancement forward, just direct me to the right direction/approach...

HarelM commented 3 years ago

I stand corrected. TL;RD: there's a need to add setTimeout(..., 0) to the following line: https://github.com/apache/cordova-ios/blob/7e3402c565c2e34eae2bb954c65c989f71e20df1/CordovaLib/Classes/Private/Plugins/CDVHandleOpenURL/CDVHandleOpenURL.m#L62 Explanation The handleOpenUrl is called from the objective-c code when the app starts, the problem is that this code can run before the "main" deviceready event of the rest of the app, and cause some kind of race condition. I think that in order to solve this the above suggestion is required. Anther way to solve this using a hack is to add a script to the index.html to handle the case where the method is called before it is initialized (for example if the device ready method is async and can switch away from execution before the registration) and store the value inside a global member and later check that member to see if we "missed" the invocation. i.e:

    <!-- this code should be before the cordova.js script include-->
    <script>
        window.handleOpenURL = (url) => {
            if (typeof(window.handleExternalUrl) === "function") {
                window.handleExternalUrl(url);
            } else {
                window.externalUrl = url;
            }
        }
    </script>

And after that instead of defining window.handleOpenUrl check if externalUrl is defined and use and delete it and register for the new method defined above:

       if (window.externalUrl) {
            doSomething(window.externalUrl);
            delete window.externalUrl;
        }
        window.handleExternalUrl = (url: string) => doSomething(url);

Hope this will help someone else in the future. Please let me know if you want a PR for the addition of the setTimeout that is required above...

breautek commented 3 years ago

So if I understand correctly, the race condition is deviceready may fire and as a result, may attempt to call handleOpenURL, before the application has a chance to set handleOpenURL.

I don't think setTimeout is the proper solution here.

NSString stringWithFormat:@"document.addEventListener('deviceready',function(){if (typeof h the javascript being invoked from native adds a listener to deviceready. This means it will either wait until the deviceready event is fired, or if it's already fired then deviceready will be fired immediately. Adding a setTimeout to that logic will force execution to be delayed until the next JS callstack, but I don't think that will guarantee to solve the race condition.

The document parses the script tags synchronously by default. As long as you don't have the async or defer attributes set and you're not using Javascript Modules (which are async by default). So for example if you had the following html:

<script src="cordova.js"></script>
<script src="myapp.js"></script>

This should work as expected, as long as myapp.js sets handleOpenURL synchronously. The webview will parse and execute cordova.js then myapp.js. It should never fire deviceready before myapp.js is finish executing because it parses scripts synchronously. However, myapp.js should ensure it sets handleOpenURL in a synchronously way. Ie don't wait to set it in any callbacks.

If you want to have reassurance, you could reverse the order and execute myapp.js before cordova.js, like you suggested but you'd have to keep in mind that the cordova API hasn't been loaded yet at all, thus you can't use document.addEventListener('deviceready', ...);

HarelM commented 3 years ago

Thanks for getting back to me so quickly! :-) As far as I understand, and I might be wrong here using document.addEventListener('deviceready, ...) is not deterministic and we want to make sure that when someone writes a handler for the handleOpenURL inside the deviceready event this call is not "missed". As far as I know, the only way to ensure it is by adding a setTimeout(..., 0) to make sure that the handleOpenURL that is called in the objective-c code is not executed before the deviceready that the cordova developer wirtes.

Am I missing something here? I tested my assumption yesterday by playing with the code I'm writing and with the code in the objective-c code, and adding setTimeout was the solution that seems to be the "right" one when looking at frameworks like ionic and angular as the js code that is being executed in the client side.

breautek commented 3 years ago

As far as I know, the only way to ensure it is by adding a setTimeout(..., 0) to make sure that the handleOpenURL that is called in the objective-c code is not executed before the deviceready that the cordova developer wirtes.

The objective-c code uses deviceready:

document.addEventListener('deviceready', function() {
    if (typeof handleOpenURL === 'function') {
        handleOpenURL(\"%@\");
    }
});

Where %@ is a native string placeholder to be replaced with another value.

So handleOpenURL will never be called before deviceready because it's inside the deviceready event. But you do still need to make sure handleOpenURL is defined before this occurs.

So using my both example where you may have something like this in your html:

<script src="cordova.js"></script>
<script src="myapp.js"></script>

And if you have myapp.js: :heavy_check_mark:

var handleOpenURL = function(url) {
    // do stuff
};

document.addEventListener('deviceready', function() {
    // initialize app
});

I'm like 99% sure that would work just fine, as long as you declare the handleOpenURL where it is assigned as myapp.js file is parsed by the browser.

If you do the following: :x:

document.addEventListener('deviceready', function() {
    var handleOpenURL = function(url) {
        // This will definitely cause a race condition
    };
    // initialize app
});

setTimeout(function() {
    var handleOpenURL = function(url) {
        // This will also cause a race condition, because handleOpenURL is being set asynchronously.
    };
}, 0);
HarelM commented 3 years ago

I agree with what you wrote. Consider the following scenario:

document.addEventListener('deviceready', function() {
    window.handleOpenURL = function(url) {
       // I want to be inside deviceready to make sure I have everything needed to run the code inside this callback...
    };
});

As a cordova developer, everything written about cordova says - do this and that after deviceready so I, as a Cordova developer, am used to the concept of placing everything inside this function and thus the problem... If you can think of a better way than setTimeout feel free to suggest. :-) A possible solution for this would be to change the code in the objective-c to be (store the url in case someone "missed" the opportunity to register the handleOpenURL callback):

document.addEventListener('deviceready', function() {
    if (typeof handleOpenURL === 'function') {
        handleOpenURL(\"%@\");
    } else {
        _handleOpenURL = \"%@\"
    }
});

This will allow this kind of scenario to be handled inside the deviceready function... I have a similar code for the android written in the same manor, see here (register for an intent and also check the current intent to see how the app started): https://github.com/IsraelHikingMap/Site/blob/90330c431add8adbbce7b84cde7cac7a14b17b11/IsraelHiking.Web/sources/application/services/open-with.service.ts#L79L89 In any case, I have added the relevant code to workaround this temporarily, but I feel other may benefit from the insight I got... The relevant commit to solve this is here: https://github.com/IsraelHikingMap/Site/commit/1932f23b1cd71f6a021005e2975260e3a2e4d47b#diff-c69e535eea05a4913a7ff20510cbffd1da88417599f190e072bf9a503fdd69ec Also there's a question here if this shouldn't be the same for both Android and iOS to allow people like me to write the code once for both platforms, but this is a different question I guess...

dpogue commented 2 weeks ago

Putting aside for a moment the reliability issues with the JavaScript handleOpenURL implementation, the original issue here appears to be that CDVAppDelegate's barebones application:didFinishLaunchingWithOptions: does not do anything with the URL used to launch the app. application:openURL:options: is not called in that case, so no CDVPluginHandleOpenURLNotification is ever dispatched.

This issue seems fixable (albeit with a risk of double notifications in the case where plugins have registered their own handling based on UIApplicationDidFinishLaunchingNotification notifications, but there is no way to detect that).