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

(iOS & Android) Add postMessage API support #362

Merged
merged 3 commits into from
Dec 13, 2018

Conversation

dpa99c
Copy link
Contributor

@dpa99c dpa99c commented Dec 3, 2018

Platforms affected

Android
iOS (both UIWebView & WKWebView implementations)

What does this PR do?

Adds support for postMessage API enabling pages loaded into the InappBrowser to post messages back to the parent Webview of the Cordova app.

For example, sending event messages associated with UI interactions such as button clicks from the wrapped page back to the parent app Webview.

What testing has been done on this change?

Automated tests have been extended to cover the message event.

Checklist

  • [x ] Commit message follows the format: "GH-3232: (android) Fix bug with resolving file paths", where CB-xxxx is the JIRA ID & "android" is the platform affected.
  • [ x] Added automated test coverage as appropriate for this change.

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 4, 2018

@janpio could you do a review of this PR when you get a minute?

Travis CI tests failed for browser-safari platform due to ECONNRESET so presumably caused by connection issues and would pass if re-run.

@@ -348,9 +358,6 @@ - (void)loadAfterBeforeload:(CDVInvokedUrlCommand*)command

- (void)injectDeferredObject:(NSString*)source withWrapper:(NSString*)jsWrapper
{
// Ensure an iframe bridge is created to communicate with the CDVUIInAppBrowserViewController
[self.inAppBrowserViewController.webView stringByEvaluatingJavaScriptFromString:@"(function(d){_cdvIframeBridge=d.getElementById('_cdvIframeBridge');if(!_cdvIframeBridge) {var e = _cdvIframeBridge = d.createElement('iframe');e.id='_cdvIframeBridge'; e.style.display='none';d.body.appendChild(e);}})(document)"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is createIframeBridge equal to this?
Is it safe to move this to where createIframeBridge is called now?

Copy link
Contributor Author

@dpa99c dpa99c Dec 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is createIframeBridge equal to this?

Yes, this code has been factorised out into createIframeBridge

Is it safe to move this to where createIframeBridge is called now?

In this original implementation, the iframe bridge is created "on demand", the first time that an injection method (such as injectScriptCode) is called. Subsequent calls find the iframe bridge already exists, so it's only created once.

In the new implementation, createIframeBridge is called after every successful page load (webViewDidFinishLoad to ensure the bridge always exists, regardless of whether an injection method has been called. This enables pages contained within the IAB Webview to make use to the bridge in order to send message events back to the app Webview, without relying on an injection method having been previously called in order to create the bridge.

Therefore, I think it's safe enough to do this.

@janpio
Copy link
Member

janpio commented Dec 5, 2018

Restarted the tests that now pass.

What would be a use case for this new functionality, what problem does it solve?

src/ios/CDVUIInAppBrowser.m Outdated Show resolved Hide resolved
@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 5, 2018

What would be a use case for this new functionality, what problem does it solve?

Currently we can inject code into the page inside the IAB Webview, using injectScriptCode for example, and return a result.
However this only works if the result of the injected code can be obtained synchronously (i.e. return foo).
If the result of the injected code is generated asynchronously, we cannot currently easily return the result without a messy workaround, such as storing the result in a temporary local variable and using another injectScriptCode call to retrieve it.

This PR enables asynchronous communication from page in the IAB Webview back to the parent Cordova app Webview via the message event.
The contained pages posts a message using the postMessage API and the Cordova app listens for and receives it.
This enables data to be sent asynchronously from the page inside the IAB Webview back to the Cordova app.
This is particularly useful when the asynchrous result is not directly created by the injected code: for example, if the injected code adds an event listener to a UI element in the IAB page and it is user-interaction with that UI element that generates the result (see below).


A real-world use case in which I have used this functionality:

I have a Cordova app which wraps a 3rd party website in the IAB. That website consists of various promotional pages which are CMS-managed and have volatile content (marketing team is constantly changing them) so cannot be placed statically inside the Cordova app. Some of those pages contain "Buy ticket" buttons which, if clicked in a desktop browser, open a popup window containing a pre-fill form to buy certain tickets.

However when that page is loaded into the IAB, pressing that button does nothing (we can't open popup windows from the IAB).
Therefore I use injectScriptCode to attach click event listeners to those buttons which, when pressed by the user, returns the parameters which would be passed to the form in the popup window back to my Cordova app Webview via the new message event.
My Cordova app is then able to listen for those message events and on receiving, close the IAB window and show its own native "Buy tickets" screen (complete with Apple Pay, Android Pay, etc.) with the fields pre-filled using the parameters sent back in that message event.

@janpio
Copy link
Member

janpio commented Dec 5, 2018

That does indeed sound like a very useful thing to do and pretty common use case.

You might want to create a demo app and write a blog post about it... might come in handy when we release a new version of cordova-plugin-inappbrowser and need something to write about and link to in the announcement post ;)

Copy link
Member

@janpio janpio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useful functionality, code looks good.
Please some more review of people that actually know Java and Obj-C or tested this in their app.

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 5, 2018

You might want to create a demo app and write a blog post about it... might come in handy when we release a new version of cordova-plugin-inappbrowser and need something to write about and link to in the announcement post ;)

Here's one I prepared earlier 😁
https://github.com/dpa99c/cordova-plugin-inappbrowser-test

This demonstrates the new message event (as well as other existing IAB functionality).

@keenan35i
Copy link

Nice! I would definitely use this feature, would allow me to remove a lot of hacky code i wrote to communicate data back and fourth from parent to child and back.

@janpio
Copy link
Member

janpio commented Dec 5, 2018

Can you test this branch @keenan35i and see if it works for you? Would be a great help.

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 5, 2018

For reference you can install my branch using:

cordova plugin add https://github.com/dpa99c/cordova-plugin-themeablebrowser#GH-362-postMessage

Some out-of-the-box test code you can execute in any Cordova app Webview which contains this branch of the InAppBrowser plugin:

var url = "https://braintree.github.io/popup-bridge-example/this_launches_in_popup.html";
iab = cordova.InAppBrowser.open(url, '_blank');
iab.addEventListener('message', function(e) {
    var msg =  JSON.stringify(e);
    iab.close();
    console.log("Message received: " +msg);
    alert(msg);
});
iab.addEventListener('loadstop', function (e) {
    iab.executeScript({
        code: "(function() { " +
            "var body = document.querySelector('body'); " +
            "var button = document.createElement('button'); " +
            "button.innerHTML = 'Click Me'; " +
            "body.appendChild(button); " +
            "button.addEventListener(\"click\",function(e){" +
                "var message = {my_message: 'Hello World!'};" +
                "webkit.messageHandlers.cordova_iab.postMessage(JSON.stringify(message));" + 
            "},false);" +
        "})(); "
    });
});

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 5, 2018

@infil00p @stevengill @agrieve @shazron @purplecabbage @jcesarmobile
Could I bug any of you guys to review the native code changes?

@keenan35i
Copy link

keenan35i commented Dec 5, 2018

IOS 12 iphone xs: So i went ahead and tested it, doesnt seem to work in the console for just a basic check am i missing anything? parent is on the right child is on the left

image

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 5, 2018

@keenan35i You have passed a simple string rather than stringified JSON to the postMessage() function: The input type for postMessage() must be an stringified JSON object, not a plain string.

The spec for the message parameter dictates that the input type must be an Object (rather than a simple String), but the reason for the Object being stringified is that in the iOS WKWebView implementation, the postMessage() function called by the page inside the InappBrowser is actually the native WebKit implementation and this itself expects a stringified JSON argument.
In order for the iOS UIWebView and Android implementations to conform to the same API, they have been implemented to expect the same.

This is in the updated documentation:

data: the message contents , only in the case of message. (Object)

So if I call postMessage() with a stringified JSON Object, it works:

app
iab

But currently if I call postMessage() with a simple String, it fails silently:

app_2
iab_2

In the case of Android and iOS UIWebView, we have custom implementations of postMessage(), so we can potentially handle this more gracefully and log an error to either the app or inappbrowser JS console if the input parameter type to postMessage() is incorrect.
But in the case of iOS WKWebView, because we are calling the native implementation of postMessage(), IIRC when I passed it an invalid argument type, it also failed silently (I will double check this and post the result).

@keenan35i
Copy link

oh i see, just tested it with an object and seems to work perfect!

@mattdsteele
Copy link

I have a use case that looks very similar to this. I'm trying to open an IAB to a login page hosted outside our Cordova app, and would like to close the IAB after the user successfully logs in (and has received their session cookies).

@keenan35i If this PR will take some time to implement, would you be willing to share the hacky workaround code, so I can continue developing in the interim? 😄

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 7, 2018

@mattdsteele

If this PR will take some time to implement, would you be willing to share the hacky workaround code, so I can continue developing in the interim?

This PR is complete (including documentation and examples) and ready for integration into the main plugin codebase - it is only awaiting review and approval. You can install and use the functionality using my branch mentioned above.

@purplecabbage
Copy link
Contributor

Thanks @dpa99c
What differences are there between this implementation and the postMessage available in the full browsers?
I am assuming that this does not work in cordova-browser, cordova-windows, ... If so, can you please document in the readme (as quirks) that the api/event is only available on iOS and Android targets?

@mattdsteele
Does straight OAuth2 not work for you?
https://medium.com/@purplecabbage/oauth2-in-apache-cordova-3a3ba059b184

@mattdsteele
Copy link

Does straight OAuth2 not work for you?

Unfortunately no - the endpoint we're using doesn't currently support OAuth 🤷‍♂️

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 7, 2018

What differences are there between this implementation and the postMessage available in the full browsers?

The postMessage API as implemented by desktop browsers is intended for cross-frame communication, hence the API reflects this:

targetWindow.postMessage(message, targetOrigin, [transfer]);

In the case of inappbrowser we are of course emulating this for cross-webview communication so the situation is different:

The targetOrigin and transfer parameters are there for cross-frame security hence aren't relevant.

The message parameter is defined by the spec as:

Data to be sent to the other window. The data is serialized using the structured clone algorithm. This means you can pass a broad variety of data objects safely to the destination window without having to serialize them yourself.

This implies that unserialized Javascript objects can be passed as the message and that the implementation will serialize them.
However I found that to make the 3 implementations work harmoniously, serialising the objects as a string before passing them as the message argument to postMessage() was necessary.
I have updated the documentation on my branch to make this more obvious.

Note that in the case of WKWebView, it is the only implemntation where the postMessage() function is native. It is implicitly available as webkit.messageHandlers.cordova_iab.postMessage() because cordova_iab is the name of the WKScriptMessageHandler registered for the WKWebView instance aand therefore the postMessage() function gets exposed in this namespace.
Since the Android and iOS UIWebViews don't make the postMessage API available natively, this PR creates emulations of it for those webviews under the same JS namespace as the native WKWebView one.

I am assuming that this does not work in cordova-browser, cordova-windows, ... If so, can you please document in the readme (as quirks) that the api/event is only available on iOS and Android targets?

Docs updated to make this clearer.

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 10, 2018

The Travis CI tests seem quite flaky: another couple of failures for no good reason 😞

@janpio
Copy link
Member

janpio commented Dec 11, 2018

I fixed CI.

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 12, 2018

@purplecabbage I've tweaked as requested - anything else that concerns you, or do you think this is ready for merging? (@keenan35i is chomping at the bit 😀 )

@purplecabbage
Copy link
Contributor

purplecabbage commented Dec 12, 2018 via email

@dpa99c
Copy link
Contributor Author

dpa99c commented Dec 13, 2018

@janpio: if @purplecabbage is happy, can this be merged or does it need further review?

@janpio janpio merged commit c54d100 into apache:master Dec 13, 2018
@janpio
Copy link
Member

janpio commented Dec 13, 2018

Thanks for pinging me ;)

@cvanem
Copy link
Contributor

cvanem commented Dec 26, 2018

@dpa99c Thanks, works great on my end. I was able to replace a localStorage polling implementation with this. Tested on both Android and iOS physical devices.

@freescout-helpdesk
Copy link

freescout-helpdesk commented Nov 4, 2019

Is it already included in 3.1.0?

    Mobie phone platform: Android 7.1.2
    Cordova version: 8.1.1

    <preference name="android-minSdkVersion" value="19" />
    <engine name="android" spec="^8.1.0" />
    <plugin name="cordova-plugin-inappbrowser" spec="~3.1.0" />

webkit seems to be undefined in 3.1.0 on Android inside of the inappbrowser page:

alert(webkit.messageHandlers.cordova_iab.postMessage); // undefined
alert(window.webkit.messageHandlers.cordova_iab.postMessage); // undefined
alert(window['webkit'].messageHandlers['cordova_iab'].postMessage); // undefined

Are any other plugins required to use webkit.messageHandlers.cordova_iab.postMessage?

@freescout-helpdesk
Copy link

freescout-helpdesk commented Nov 4, 2019

Solution.

  1. webkit.messageHandlers.cordova_iab.postMessage is never defined in the Phonegap.

  2. In the real app it takes time for InAppBrowser to define webkit object. So it's impossible to use webkit object right away in JS, it can be used only after some time.

This way it works:

setTimeout(function() {
	alert(typeof(webkit));
	webkit.messageHandlers.cordova_iab.postMessage(JSON.stringify({'data': 'value'}));
}, 2000);

@JoshuvaGeorge03
Copy link

I got an type error, when I try to do post message using

webkit.messageHandlers.cordova_iab.postMessage and the error was Type error: webkit is undefined and not an object. This only occurs in ios and not in android.

I was using cordova-plugin-inappbrowser v3.2.0

kindly advice, if i am doing something wrong.

@JoshuvaGeorge03
Copy link

After investigating, I have found out that this issue only present in cordova-plugin-inappbrowser v3.2.0... And this version not rise any issue with android, but cause issues with ios as I mentioned earlier.

So, kindly fix this issue.

@NiklasMerz
Copy link
Member

NiklasMerz commented Jan 18, 2020

@JoshuvaGeorge03 Please open a new issue with your problem description and link it here. Reporting bugs in merged or closed PRs and issue might get them lost.

Did you use WKWebView before updating to version 3.2.0?

@JoshuvaGeorge03
Copy link

@NiklasMerz Sorry, will create the new issue.

Yes, My ionic app using WKWebView engine. is it mandatory to use WKWebView Engine for version 3.2.0?

@NiklasMerz
Copy link
Member

Yes it is

@JoshuvaGeorge03
Copy link

@NiklasMerz kindly take a look at issue #613

@JoshuvaGeorge03
Copy link

JoshuvaGeorge03 commented Jan 20, 2020

Yes it is

@NiklasMerz Could you tell me is there any other requirement? and also i have created one repo for reproducing this issue.. kindly take a look at issue, which i have created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants