From 426b2f8835abb033d764bc71490b8fe603674a8c Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 6 Sep 2016 10:27:56 +0100 Subject: [PATCH 01/13] Implement a postMessage function and an onMessage event for webviews in ios --- Examples/UIExplorer/js/WebViewExample.js | 49 ++++++++++++++++++++ Examples/UIExplorer/js/messagingtest.html | 25 +++++++++++ Libraries/Components/WebView/WebView.ios.js | 50 +++++++++++++++++++++ React/Views/RCTWebView.h | 2 + React/Views/RCTWebView.m | 29 ++++++++++++ React/Views/RCTWebViewManager.m | 14 ++++++ 6 files changed, 169 insertions(+) create mode 100644 Examples/UIExplorer/js/messagingtest.html diff --git a/Examples/UIExplorer/js/WebViewExample.js b/Examples/UIExplorer/js/WebViewExample.js index 469c062620368f..37b24f1863834c 100644 --- a/Examples/UIExplorer/js/WebViewExample.js +++ b/Examples/UIExplorer/js/WebViewExample.js @@ -217,6 +217,51 @@ class ScaledWebView extends React.Component { } } +class MessagingTest extends React.Component { + webview = null + + state = { + messagesReceivedFromWebView: 0, + } + + onMessage = () => this.setState(state => ({ + messagesReceivedFromWebView: state.messagesReceivedFromWebView + 1, + })) + + postMessage = () => { + if (this.webview) { + this.webview.postMessage({ message: 'Hello from React Native!' }); + } + } + + render(): ReactElement { + const {messagesReceivedFromWebView} = this.state; + + return ( + + + Messages received from web view: {messagesReceivedFromWebView} + + + + + diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 8c3920771bb402..790277ae80639f 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -235,6 +235,20 @@ class WebView extends React.Component { * Function that is invoked when the `WebView` loading starts or ends. */ onNavigationStateChange: PropTypes.func, + /** + * @ios + * + * A function that is invoked when the webview calls `window.postMessage`. + * Requires `messagingEnabled` to be true. + * + * The argument passed to postMessage is available on `event.nativeEvent.message`. + * + * Note that if you pass an object to postMessage, an object is received in the + * event handler. However, this object has been passed through JSON.stringify, so + * undefined values are missing from objects, and NaN and Infinity values are + * converted to null. + */ + onMessage: PropTypes.func, /** * Boolean value that forces the `WebView` to show the loading view * on the first load. @@ -267,6 +281,13 @@ class WebView extends React.Component { PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), ]), + /** + * @ios + * + * Injects a postMessage global into the webview. Required to use onMessage. + */ + messagingEnabled: PropTypes.bool, + /** * Boolean value to enable JavaScript in the `WebView`. Used on Android only * as JavaScript is enabled by default on iOS. The default value is `true`. @@ -397,6 +418,8 @@ class WebView extends React.Component { onLoadingStart={this._onLoadingStart} onLoadingFinish={this._onLoadingFinish} onLoadingError={this._onLoadingError} + messagingEnabled={this.props.messagingEnabled} + onMessage={this._onMessage} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} scalesPageToFit={this.props.scalesPageToFit} allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback} @@ -457,6 +480,27 @@ class WebView extends React.Component { ); }; + /** + * @ios + * + * Posts a message to the web view, which will invoke the global `onmessage` function. + * + * See note on object conversion in onMessage. + * + * In your webview, you'll need to something like the following. + * + * ``` + * window.onmessage = function(message) { ... } + * ``` + */ + postMessage = (message) => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWebView.Commands.postMessage, + [message] + ); + }; + /** * We return an event with a bunch of fields including: * url, title, loading, canGoBack, canGoForward @@ -502,6 +546,11 @@ class WebView extends React.Component { }); this._updateNavigationState(event); }; + + _onMessage = (event: Event) => { + var {onMessage} = this.props; + onMessage && onMessage(event); + } } var RCTWebView = requireNativeComponent('RCTWebView', WebView, { @@ -509,6 +558,7 @@ var RCTWebView = requireNativeComponent('RCTWebView', WebView, { onLoadingStart: true, onLoadingError: true, onLoadingFinish: true, + onMessage: true, }, }); diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h index f8bd2608cc0bf3..39a4dd5b7f6eb3 100644 --- a/React/Views/RCTWebView.h +++ b/React/Views/RCTWebView.h @@ -34,6 +34,7 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request @property (nonatomic, copy) NSDictionary *source; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, assign) BOOL messagingEnabled; @property (nonatomic, copy) NSString *injectedJavaScript; @property (nonatomic, assign) BOOL scalesPageToFit; @@ -41,5 +42,6 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request - (void)goBack; - (void)reload; - (void)stopLoading; +- (void)postMessage:(id)message; @end diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 76c3f096003d01..a01c623a4edd8c 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -20,6 +20,7 @@ #import "UIView+React.h" NSString *const RCTJSNavigationScheme = @"react-js-navigation"; +NSString *const RCTJSPostMessageHost = @"postMessage"; @interface RCTWebView () @@ -27,6 +28,7 @@ @interface RCTWebView () @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError; @property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest; +@property (nonatomic, copy) RCTDirectEventBlock onMessage; @end @@ -82,6 +84,13 @@ - (void)stopLoading [_webView stopLoading]; } +- (void)postMessage:(id)json +{ + NSString *serialized = RCTJSONStringify(json, NULL); + NSString *source = [NSString stringWithFormat:@"if (typeof onmessage === 'function') window.onmessage(%@);", serialized]; + [_webView stringByEvaluatingJavaScriptFromString:source]; +} + - (void)setSource:(NSDictionary *)source { if (![_source isEqualToDictionary:source]) { @@ -221,6 +230,19 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR } } + if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) { + NSString *query = request.URL.query; + query = [query stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + query = [query stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + id message = RCTJSONParse(query, NULL); + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"message": message, + }]; + _onMessage(event); + } + // JS Navigation handler return !isJSNavigation; } @@ -248,6 +270,13 @@ - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)er - (void)webViewDidFinishLoad:(UIWebView *)webView { + if (_messagingEnabled) { + NSString *source = [NSString stringWithFormat: + @"window.postMessage = function(message) {" + " window.location = '%@://%@?' + encodeURIComponent(JSON.stringify(message));" + "};", RCTJSNavigationScheme, RCTJSPostMessageHost]; + [webView stringByEvaluatingJavaScriptFromString:source]; + } if (_injectedJavaScript != nil) { NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript]; diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index af5f5b03b8a027..43745e15fd4c5d 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -38,12 +38,14 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL) RCT_REMAP_VIEW_PROPERTY(decelerationRate, _webView.scrollView.decelerationRate, CGFloat) RCT_EXPORT_VIEW_PROPERTY(scalesPageToFit, BOOL) +RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL) RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(allowsInlineMediaPlayback, _webView.allowsInlineMediaPlayback, BOOL) RCT_REMAP_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, _webView.mediaPlaybackRequiresUserAction, BOOL) @@ -97,6 +99,18 @@ - (UIView *)view }]; } +RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag json:(id)json) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RCTWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + } else { + [view postMessage:json]; + } + }]; +} + #pragma mark - Exported synchronous methods - (BOOL)webView:(__unused RCTWebView *)webView From dec7c42c588889691794d3fdd4706f928ec65ec2 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Thu, 8 Sep 2016 18:27:02 +0100 Subject: [PATCH 02/13] Merge messagingEnabled prop with onMessage --- Examples/UIExplorer/js/WebViewExample.js | 1 - Libraries/Components/WebView/WebView.ios.js | 15 +++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Examples/UIExplorer/js/WebViewExample.js b/Examples/UIExplorer/js/WebViewExample.js index 37b24f1863834c..a380ab83e19897 100644 --- a/Examples/UIExplorer/js/WebViewExample.js +++ b/Examples/UIExplorer/js/WebViewExample.js @@ -254,7 +254,6 @@ class MessagingTest extends React.Component { }} source={require('./messagingtest.html')} onMessage={this.onMessage} - messagingEnabled /> diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 790277ae80639f..0da1bd05ed2175 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -238,8 +238,8 @@ class WebView extends React.Component { /** * @ios * - * A function that is invoked when the webview calls `window.postMessage`. - * Requires `messagingEnabled` to be true. + * A function that is invoked when the webview calls `window.postMessage`. Setting + * this property will inject a `postMessage` global into your webview. * * The argument passed to postMessage is available on `event.nativeEvent.message`. * @@ -281,13 +281,6 @@ class WebView extends React.Component { PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), ]), - /** - * @ios - * - * Injects a postMessage global into the webview. Required to use onMessage. - */ - messagingEnabled: PropTypes.bool, - /** * Boolean value to enable JavaScript in the `WebView`. Used on Android only * as JavaScript is enabled by default on iOS. The default value is `true`. @@ -403,6 +396,8 @@ class WebView extends React.Component { source.uri = this.props.url; } + const messagingEnabled = typeof this.props.onMessage === 'function'; + var webView = Date: Sat, 10 Sep 2016 10:16:47 +0100 Subject: [PATCH 03/13] WIP android branch --- .../Components/WebView/WebView.android.js | 14 ++++++++ Libraries/Components/WebView/WebView.ios.js | 2 -- React/Views/RCTWebView.m | 5 ++- .../views/webview/ReactWebViewBridge.java | 12 +++++++ .../views/webview/ReactWebViewManager.java | 34 +++++++++++++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 2c843de46a567a..65803632f47c42 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -58,6 +58,7 @@ class WebView extends React.Component { automaticallyAdjustContentInsets: PropTypes.bool, contentInset: EdgeInsetsPropType, onNavigationStateChange: PropTypes.func, + onMessage: PropTypes.func, onContentSizeChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, @@ -268,6 +269,14 @@ class WebView extends React.Component { ); }; + postMessage = (message) => { + UIManager.dispatchViewManagerCommand( + this.getWebViewHandle(), + UIManager.RCTWebView.Commands.postMessage, + [JSON.stringify(message)] + ); + }; + /** * We return an event with a bunch of fields including: * url, title, loading, canGoBack, canGoForward @@ -310,6 +319,11 @@ class WebView extends React.Component { }); this.updateNavigationState(event); }; + + onMessage = (event: Event) => { + var {onMessage} = this.props; + onMessage && onMessage(event); + } } var RCTWebView = requireNativeComponent('RCTWebView', WebView); diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 0da1bd05ed2175..a8251509a280a0 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -236,8 +236,6 @@ class WebView extends React.Component { */ onNavigationStateChange: PropTypes.func, /** - * @ios - * * A function that is invoked when the webview calls `window.postMessage`. Setting * this property will inject a `postMessage` global into your webview. * diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index a01c623a4edd8c..0293dac75f712d 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -87,7 +87,10 @@ - (void)stopLoading - (void)postMessage:(id)json { NSString *serialized = RCTJSONStringify(json, NULL); - NSString *source = [NSString stringWithFormat:@"if (typeof onmessage === 'function') window.onmessage(%@);", serialized]; + NSString *source = [NSString + stringWithFormat:@"if (typeof window.onmessage === 'function') window.onmessage(%@);", + serialized + ]; [_webView stringByEvaluatingJavaScriptFromString:source]; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java new file mode 100644 index 00000000000000..19418cf39dbeff --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java @@ -0,0 +1,12 @@ +class ReactWebViewBridge { + Context mContext; + + RecatWebViewBridge(Context c) { + mContext = c; + } + + @JavascriptInterface + public void postMessage(message) { + mContext.postMessage(message); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index 6e15091850ef13..c8e5345d286d3e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -46,6 +46,7 @@ import com.facebook.react.views.webview.events.TopLoadingErrorEvent; import com.facebook.react.views.webview.events.TopLoadingFinishEvent; import com.facebook.react.views.webview.events.TopLoadingStartEvent; +import com.facebook.react.views.webview.ReactWebViewBridge; /** * Manages instances of {@link WebView} @@ -74,6 +75,7 @@ public class ReactWebViewManager extends SimpleViewManager { private static final String HTML_ENCODING = "UTF-8"; private static final String HTML_MIME_TYPE = "text/html; charset=utf-8"; + private static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE"; private static final String HTTP_METHOD_POST = "POST"; @@ -81,11 +83,14 @@ public class ReactWebViewManager extends SimpleViewManager { public static final int COMMAND_GO_FORWARD = 2; public static final int COMMAND_RELOAD = 3; public static final int COMMAND_STOP_LOADING = 4; + public static final int COMMAND_POST_MESSAGE = 5; // Use `webView.loadUrl("about:blank")` to reliably reset the view // state and release page resources (including any running JavaScript). private static final String BLANK_URL = "about:blank"; + private boolean hasJavascriptInterface = false; + private WebViewConfig mWebViewConfig; private @Nullable WebView.PictureListener mPictureListener; @@ -100,6 +105,7 @@ public void onPageFinished(WebView webView, String url) { if (!mLastLoadFailed) { ReactWebView reactWebView = (ReactWebView) webView; reactWebView.callInjectedJavaScript(); + reactWebView.linkBridge(); emitFinishEvent(webView, url); } } @@ -229,6 +235,14 @@ public void callInjectedJavaScript() { } } + private void linkBridge() { + if (getSettings().getMessagingEnabled()) { + loadUrl("javascript:(window.postMessage = function(message) {" + + BRIDGE_NAME + ".postMessage(message);" + + "})", null); + } + } + private void cleanupCallbacksAndDestroy() { setWebViewClient(null); destroy(); @@ -310,6 +324,21 @@ public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScr ((ReactWebView) view).setInjectedJavaScript(injectedJavaScript); } + @ReactProp(name = "messagingEnabled") + public void setMessagingEnabled(WebView view, boolean enabled) { + if (hasJavascriptInterface == enabled) { + return; + } + + hasJavascriptInterface = enabled; + if (enabled) { + view.addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME); + linkBridge(); + } else { + view.removeJavascriptInterface(BRIDGE_NAME); + } + } + @ReactProp(name = "source") public void setSource(WebView view, @Nullable ReadableMap source) { if (source != null) { @@ -403,6 +432,11 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray case COMMAND_STOP_LOADING: root.stopLoading(); break; + case COMMAND_POST_MESSAGE: + root.evaluateJavascript("if (typeof window.onload === 'function') {" + + "window.onload(" + args[0] ")" + + "}") + break; } } From efce5f1394358566874169c3da3046ebc123ca94 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Mon, 12 Sep 2016 16:57:48 +0100 Subject: [PATCH 04/13] WIP web view messaging --- .../Components/WebView/WebView.android.js | 4 + .../uimanager/UIManagerModuleConstants.java | 1 + .../views/webview/ReactWebViewBridge.java | 12 --- .../views/webview/ReactWebViewManager.java | 74 +++++++++++++------ .../views/webview/events/TopMessageEvent.java | 49 ++++++++++++ 5 files changed, 105 insertions(+), 35 deletions(-) delete mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 65803632f47c42..7d90d2271ff7a5 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -58,6 +58,7 @@ class WebView extends React.Component { automaticallyAdjustContentInsets: PropTypes.bool, contentInset: EdgeInsetsPropType, onNavigationStateChange: PropTypes.func, + messagingEnabled: PropTypes.bool, onMessage: PropTypes.func, onContentSizeChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load @@ -219,6 +220,8 @@ class WebView extends React.Component { userAgent={this.props.userAgent} javaScriptEnabled={this.props.javaScriptEnabled} domStorageEnabled={this.props.domStorageEnabled} + messagingEnabled={typeof this.props.onMessage === 'function'} + onMessage={this.onMessage} contentInset={this.props.contentInset} automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets} onContentSizeChange={this.props.onContentSizeChange} @@ -322,6 +325,7 @@ class WebView extends React.Component { onMessage = (event: Event) => { var {onMessage} = this.props; + event.message = JSON.parse(event.nativeEvent.message); onMessage && onMessage(event); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java index 7b5528bf20daee..506c0063acabed 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java @@ -77,6 +77,7 @@ .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish")) .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart")) .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange")) + .put("topMessage", MapBuilder.of("registrationName", "onMessage")) .build(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java deleted file mode 100644 index 19418cf39dbeff..00000000000000 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewBridge.java +++ /dev/null @@ -1,12 +0,0 @@ -class ReactWebViewBridge { - Context mContext; - - RecatWebViewBridge(Context c) { - mContext = c; - } - - @JavascriptInterface - public void postMessage(message) { - mContext.postMessage(message); - } -} diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index c8e5345d286d3e..0bbe32fb592d6f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -26,6 +26,7 @@ import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.webkit.JavascriptInterface; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; @@ -46,7 +47,7 @@ import com.facebook.react.views.webview.events.TopLoadingErrorEvent; import com.facebook.react.views.webview.events.TopLoadingFinishEvent; import com.facebook.react.views.webview.events.TopLoadingStartEvent; -import com.facebook.react.views.webview.ReactWebViewBridge; +import com.facebook.react.views.webview.events.TopMessageEvent; /** * Manages instances of {@link WebView} @@ -89,8 +90,6 @@ public class ReactWebViewManager extends SimpleViewManager { // state and release page resources (including any running JavaScript). private static final String BLANK_URL = "about:blank"; - private boolean hasJavascriptInterface = false; - private WebViewConfig mWebViewConfig; private @Nullable WebView.PictureListener mPictureListener; @@ -171,7 +170,7 @@ public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload private void emitFinishEvent(WebView webView, String url) { dispatchEvent( webView, - new TopLoadingFinishEvent( + new TopMessageEvent( webView.getId(), createWebViewEvent(webView, url))); } @@ -196,6 +195,20 @@ private WritableMap createWebViewEvent(WebView webView, String url) { */ private static class ReactWebView extends WebView implements LifecycleEventListener { private @Nullable String injectedJS; + private boolean messagingEnabled = false; + + private class ReactWebViewBridge { + ReactWebView mContext; + + ReactWebViewBridge(ReactWebView c) { + mContext = c; + } + + @JavascriptInterface + public void postMessage(String message) { + mContext.onMessage(message); + } + } /** * WebView must be created with an context of the current activity @@ -227,6 +240,20 @@ public void setInjectedJavaScript(@Nullable String js) { injectedJS = js; } + public void setMessagingEnabled(boolean enabled) { + if (messagingEnabled == enabled) { + return; + } + + messagingEnabled = enabled; + if (enabled) { + addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME); + linkBridge(); + } else { + removeJavascriptInterface(BRIDGE_NAME); + } + } + public void callInjectedJavaScript() { if (getSettings().getJavaScriptEnabled() && injectedJS != null && @@ -235,14 +262,24 @@ public void callInjectedJavaScript() { } } - private void linkBridge() { - if (getSettings().getMessagingEnabled()) { + public void linkBridge() { + if (messagingEnabled) { loadUrl("javascript:(window.postMessage = function(message) {" + - BRIDGE_NAME + ".postMessage(message);" + - "})", null); + BRIDGE_NAME + ".postMessage(JSON.stringify(message));" + + "})"); } } + public void onMessage(String message) { + WritableMap event = Arguments.createMap(); + event.putDouble("target", getId()); + // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks + // like onPageFinished + event.putString("message", message); + + dispatchEvent(this, new TopMessageEvent(this.getId(), event)); + } + private void cleanupCallbacksAndDestroy() { setWebViewClient(null); destroy(); @@ -326,17 +363,7 @@ public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScr @ReactProp(name = "messagingEnabled") public void setMessagingEnabled(WebView view, boolean enabled) { - if (hasJavascriptInterface == enabled) { - return; - } - - hasJavascriptInterface = enabled; - if (enabled) { - view.addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME); - linkBridge(); - } else { - view.removeJavascriptInterface(BRIDGE_NAME); - } + ((ReactWebView) view).setMessagingEnabled(enabled); } @ReactProp(name = "source") @@ -414,7 +441,8 @@ protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { "goBack", COMMAND_GO_BACK, "goForward", COMMAND_GO_FORWARD, "reload", COMMAND_RELOAD, - "stopLoading", COMMAND_STOP_LOADING); + "stopLoading", COMMAND_STOP_LOADING, + "postMessage", COMMAND_POST_MESSAGE); } @Override @@ -433,9 +461,9 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray root.stopLoading(); break; case COMMAND_POST_MESSAGE: - root.evaluateJavascript("if (typeof window.onload === 'function') {" + - "window.onload(" + args[0] ")" + - "}") + root.evaluateJavascript("if (typeof window.onmessage === 'function') {" + + "window.onmessage(" + args.getString(0) + ")" + + "}", null); break; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java new file mode 100644 index 00000000000000..a0bf6cc5384dde --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.webview.events; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted when there is an error in loading. + */ +public class TopMessageEvent extends Event { + + public static final String EVENT_NAME = "topMessage"; + private WritableMap mEventData; + + public TopMessageEvent(int viewId, WritableMap eventData) { + super(viewId); + mEventData = eventData; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData); + } +} From 62edd7737c0274dceb3ca830d60e3ae0b91b6098 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 13 Sep 2016 09:17:52 +0100 Subject: [PATCH 05/13] Implement webview communication on Android --- Libraries/Components/WebView/WebView.android.js | 2 +- .../react/views/webview/ReactWebViewManager.java | 10 ++-------- .../react/views/webview/events/TopMessageEvent.java | 11 +++++++---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 7d90d2271ff7a5..d9d642dfdd0e10 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -325,7 +325,7 @@ class WebView extends React.Component { onMessage = (event: Event) => { var {onMessage} = this.props; - event.message = JSON.parse(event.nativeEvent.message); + event.nativeEvent.message = JSON.parse(event.nativeEvent.message); onMessage && onMessage(event); } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index 0bbe32fb592d6f..3da57a45942505 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -170,7 +170,7 @@ public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload private void emitFinishEvent(WebView webView, String url) { dispatchEvent( webView, - new TopMessageEvent( + new TopLoadingFinishEvent( webView.getId(), createWebViewEvent(webView, url))); } @@ -271,13 +271,7 @@ public void linkBridge() { } public void onMessage(String message) { - WritableMap event = Arguments.createMap(); - event.putDouble("target", getId()); - // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks - // like onPageFinished - event.putString("message", message); - - dispatchEvent(this, new TopMessageEvent(this.getId(), event)); + dispatchEvent(this, new TopMessageEvent(this.getId(), message)); } private void cleanupCallbacksAndDestroy() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java index a0bf6cc5384dde..4349fc4d1aece6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java @@ -10,6 +10,7 @@ package com.facebook.react.views.webview.events; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.Arguments; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.RCTEventEmitter; @@ -19,11 +20,11 @@ public class TopMessageEvent extends Event { public static final String EVENT_NAME = "topMessage"; - private WritableMap mEventData; + private final String mMessage; - public TopMessageEvent(int viewId, WritableMap eventData) { + public TopMessageEvent(int viewId, String message) { super(viewId); - mEventData = eventData; + mMessage = message; } @Override @@ -44,6 +45,8 @@ public short getCoalescingKey() { @Override public void dispatch(RCTEventEmitter rctEventEmitter) { - rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData); + WritableMap data = Arguments.createMap(); + data.putString("message", mMessage); + rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data); } } From d0f0341d3f2eddd20cff81de4c6e97ed4fd3f644 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 13 Sep 2016 09:49:32 +0100 Subject: [PATCH 06/13] Move native-only props to requireNativeComponent --- Libraries/Components/WebView/WebView.android.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index d9d642dfdd0e10..7de267574fc8fe 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -58,7 +58,6 @@ class WebView extends React.Component { automaticallyAdjustContentInsets: PropTypes.bool, contentInset: EdgeInsetsPropType, onNavigationStateChange: PropTypes.func, - messagingEnabled: PropTypes.bool, onMessage: PropTypes.func, onContentSizeChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load @@ -330,7 +329,11 @@ class WebView extends React.Component { } } -var RCTWebView = requireNativeComponent('RCTWebView', WebView); +var RCTWebView = requireNativeComponent('RCTWebView', WebView, { + nativeOnly: { + messagingEnabled: PropTypes.bool, + }, +}); var styles = StyleSheet.create({ container: { From dfd4db2056ee620f4c5595c1b41772fbb001e02f Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Tue, 13 Sep 2016 09:58:51 +0100 Subject: [PATCH 07/13] Remove incorrect ios annotation from documentation --- Libraries/Components/WebView/WebView.ios.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index a8251509a280a0..ea4fbe2478f414 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -474,8 +474,6 @@ class WebView extends React.Component { }; /** - * @ios - * * Posts a message to the web view, which will invoke the global `onmessage` function. * * See note on object conversion in onMessage. From a508f46d62638040851dc70a6c4f24b358a1839a Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Wed, 14 Sep 2016 08:48:39 +0100 Subject: [PATCH 08/13] Don't implicitly serialise data passed to onmessage/postMessage: only pass strings instead --- Examples/UIExplorer/js/WebViewExample.js | 2 +- Examples/UIExplorer/js/messagingtest.html | 2 +- .../Components/WebView/WebView.android.js | 3 +-- Libraries/Components/WebView/WebView.ios.js | 22 +++++++++---------- React/Views/RCTWebView.h | 2 +- React/Views/RCTWebView.m | 16 ++++++-------- React/Views/RCTWebViewManager.m | 4 ++-- .../views/webview/ReactWebViewManager.java | 4 ++-- 8 files changed, 25 insertions(+), 30 deletions(-) diff --git a/Examples/UIExplorer/js/WebViewExample.js b/Examples/UIExplorer/js/WebViewExample.js index a380ab83e19897..9b397193a93617 100644 --- a/Examples/UIExplorer/js/WebViewExample.js +++ b/Examples/UIExplorer/js/WebViewExample.js @@ -230,7 +230,7 @@ class MessagingTest extends React.Component { postMessage = () => { if (this.webview) { - this.webview.postMessage({ message: 'Hello from React Native!' }); + this.webview.postMessage('Hello from React Native!'); } } diff --git a/Examples/UIExplorer/js/messagingtest.html b/Examples/UIExplorer/js/messagingtest.html index e2f92d7f49ebcb..100cbba2614a55 100644 --- a/Examples/UIExplorer/js/messagingtest.html +++ b/Examples/UIExplorer/js/messagingtest.html @@ -19,7 +19,7 @@ 'Messages recieved from React Native: ' + messagesReceivedFromReactNative; }; document.getElementsByTagName('button')[0].addEventListener('click', function() { - window.postMessage({ message: 'Hello from the web view' }); + window.postMessage('Hello from the web view'); }); diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 7de267574fc8fe..ac88f5f8bb1e67 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -275,7 +275,7 @@ class WebView extends React.Component { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.postMessage, - [JSON.stringify(message)] + [String(message)] ); }; @@ -324,7 +324,6 @@ class WebView extends React.Component { onMessage = (event: Event) => { var {onMessage} = this.props; - event.nativeEvent.message = JSON.parse(event.nativeEvent.message); onMessage && onMessage(event); } } diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index ea4fbe2478f414..3c9084c7b99508 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -236,15 +236,13 @@ class WebView extends React.Component { */ onNavigationStateChange: PropTypes.func, /** - * A function that is invoked when the webview calls `window.postMessage`. Setting - * this property will inject a `postMessage` global into your webview. + * A function that is invoked when the webview calls `window.postMessage`. + * Setting this property will inject a `postMessage` global into your + * webview, overriding any previous values. * - * The argument passed to postMessage is available on `event.nativeEvent.message`. - * - * Note that if you pass an object to postMessage, an object is received in the - * event handler. However, this object has been passed through JSON.stringify, so - * undefined values are missing from objects, and NaN and Infinity values are - * converted to null. + * `window.postMessage` accepts one argument, `message`, which will be + * available on the event object, `event.nativeEvent.message`. `message` + * must be a string. */ onMessage: PropTypes.func, /** @@ -474,9 +472,8 @@ class WebView extends React.Component { }; /** - * Posts a message to the web view, which will invoke the global `onmessage` function. - * - * See note on object conversion in onMessage. + * Posts a message to the web view, which will invoke the global `onmessage` + * function. Accepts one argument, `message`, which must be a string. * * In your webview, you'll need to something like the following. * @@ -488,7 +485,7 @@ class WebView extends React.Component { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.postMessage, - [message] + [String(message)] ); }; @@ -550,6 +547,7 @@ var RCTWebView = requireNativeComponent('RCTWebView', WebView, { onLoadingError: true, onLoadingFinish: true, onMessage: true, + messagingEnabled: PropTypes.bool, }, }); diff --git a/React/Views/RCTWebView.h b/React/Views/RCTWebView.h index 39a4dd5b7f6eb3..c2c41431f632d2 100644 --- a/React/Views/RCTWebView.h +++ b/React/Views/RCTWebView.h @@ -42,6 +42,6 @@ shouldStartLoadForRequest:(NSMutableDictionary *)request - (void)goBack; - (void)reload; - (void)stopLoading; -- (void)postMessage:(id)message; +- (void)postMessage:(NSString *)message; @end diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 0293dac75f712d..07f72aeefd42d5 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -84,12 +84,11 @@ - (void)stopLoading [_webView stopLoading]; } -- (void)postMessage:(id)json +- (void)postMessage:(NSString *)message { - NSString *serialized = RCTJSONStringify(json, NULL); NSString *source = [NSString - stringWithFormat:@"if (typeof window.onmessage === 'function') window.onmessage(%@);", - serialized + stringWithFormat:@"if (typeof window.onmessage === 'function') window.onmessage('%@');", + message ]; [_webView stringByEvaluatingJavaScriptFromString:source]; } @@ -234,11 +233,10 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR } if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) { - NSString *query = request.URL.query; - query = [query stringByReplacingOccurrencesOfString:@"+" withString:@" "]; - query = [query stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *message = request.URL.query; + message = [message stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + message = [message stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; - id message = RCTJSONParse(query, NULL); NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ @"message": message, @@ -276,7 +274,7 @@ - (void)webViewDidFinishLoad:(UIWebView *)webView if (_messagingEnabled) { NSString *source = [NSString stringWithFormat: @"window.postMessage = function(message) {" - " window.location = '%@://%@?' + encodeURIComponent(JSON.stringify(message));" + " window.location = '%@://%@?' + encodeURIComponent(String(message));" "};", RCTJSNavigationScheme, RCTJSPostMessageHost]; [webView stringByEvaluatingJavaScriptFromString:source]; } diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index 43745e15fd4c5d..2730124fffa281 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -99,14 +99,14 @@ - (UIView *)view }]; } -RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag json:(id)json) +RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message) { [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { RCTWebView *view = viewRegistry[reactTag]; if (![view isKindOfClass:[RCTWebView class]]) { RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); } else { - [view postMessage:json]; + [view postMessage:message]; } }]; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index 3da57a45942505..93c1399b1d35e1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -265,7 +265,7 @@ public void callInjectedJavaScript() { public void linkBridge() { if (messagingEnabled) { loadUrl("javascript:(window.postMessage = function(message) {" + - BRIDGE_NAME + ".postMessage(JSON.stringify(message));" + + BRIDGE_NAME + ".postMessage(String(message));" + "})"); } } @@ -456,7 +456,7 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray break; case COMMAND_POST_MESSAGE: root.evaluateJavascript("if (typeof window.onmessage === 'function') {" + - "window.onmessage(" + args.getString(0) + ")" + + "window.onmessage('" + args.getString(0) + "')" + "}", null); break; } From 4e4b27c05bb66d42210fb8c06ebc980616471a05 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Fri, 23 Sep 2016 09:27:01 +0100 Subject: [PATCH 09/13] Call window.postMessage with an event-like object rather than the data directly to align better with the spec, rename message to data in all onMessage/onmessage calls to better align with the spec, correct the escaping of strings that may contain single quotation marks --- Examples/UIExplorer/js/WebViewExample.js | 13 ++++++++----- Examples/UIExplorer/js/messagingtest.html | 6 ++++-- .../Components/WebView/WebView.android.js | 4 ++-- Libraries/Components/WebView/WebView.ios.js | 12 ++++++------ React/Views/RCTWebView.m | 13 +++++++------ .../views/webview/ReactWebViewManager.java | 19 ++++++++++++++----- .../views/webview/events/TopMessageEvent.java | 8 ++++---- 7 files changed, 45 insertions(+), 30 deletions(-) diff --git a/Examples/UIExplorer/js/WebViewExample.js b/Examples/UIExplorer/js/WebViewExample.js index 9b397193a93617..d307bb6445364a 100644 --- a/Examples/UIExplorer/js/WebViewExample.js +++ b/Examples/UIExplorer/js/WebViewExample.js @@ -222,25 +222,28 @@ class MessagingTest extends React.Component { state = { messagesReceivedFromWebView: 0, + message: '', } - onMessage = () => this.setState(state => ({ - messagesReceivedFromWebView: state.messagesReceivedFromWebView + 1, - })) + onMessage = e => this.setState({ + messagesReceivedFromWebView: this.state.messagesReceivedFromWebView + 1, + message: e.nativeEvent.data, + }) postMessage = () => { if (this.webview) { - this.webview.postMessage('Hello from React Native!'); + this.webview.postMessage('"Hello" from React Native!'); } } render(): ReactElement { - const {messagesReceivedFromWebView} = this.state; + const {messagesReceivedFromWebView, message} = this.state; return ( Messages received from web view: {messagesReceivedFromWebView} + {message || '(No message)'} diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index ac88f5f8bb1e67..60c01162ea377e 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -271,11 +271,11 @@ class WebView extends React.Component { ); }; - postMessage = (message) => { + postMessage = (data) => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.postMessage, - [String(message)] + [String(data)] ); }; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 3c9084c7b99508..9a8624d8cb4d23 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -240,8 +240,8 @@ class WebView extends React.Component { * Setting this property will inject a `postMessage` global into your * webview, overriding any previous values. * - * `window.postMessage` accepts one argument, `message`, which will be - * available on the event object, `event.nativeEvent.message`. `message` + * `window.postMessage` accepts one argument, `data`, which will be + * available on the event object, `event.nativeEvent.data`. `data` * must be a string. */ onMessage: PropTypes.func, @@ -473,19 +473,19 @@ class WebView extends React.Component { /** * Posts a message to the web view, which will invoke the global `onmessage` - * function. Accepts one argument, `message`, which must be a string. + * function. Accepts one argument, `data`, which must be a string. * * In your webview, you'll need to something like the following. * * ``` - * window.onmessage = function(message) { ... } + * window.onmessage = function(e) { document.title = e.data; } * ``` */ - postMessage = (message) => { + postMessage = (data) => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RCTWebView.Commands.postMessage, - [String(message)] + [String(data)] ); }; diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 07f72aeefd42d5..00338a76af9c8d 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -86,9 +86,10 @@ - (void)stopLoading - (void)postMessage:(NSString *)message { + NSString *escapedString = RCTJSONStringify(message, NULL); NSString *source = [NSString - stringWithFormat:@"if (typeof window.onmessage === 'function') window.onmessage('%@');", - message + stringWithFormat:@"if (typeof window.onmessage === 'function') window.onmessage({ data: %@ });", + escapedString ]; [_webView stringByEvaluatingJavaScriptFromString:source]; } @@ -233,13 +234,13 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR } if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) { - NSString *message = request.URL.query; - message = [message stringByReplacingOccurrencesOfString:@"+" withString:@" "]; - message = [message stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + NSString *data = request.URL.query; + data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "]; + data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ - @"message": message, + @"data": data, }]; _onMessage(event); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index 93c1399b1d35e1..3497f103290f44 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -49,6 +49,9 @@ import com.facebook.react.views.webview.events.TopLoadingStartEvent; import com.facebook.react.views.webview.events.TopMessageEvent; +import org.json.JSONObject; +import org.json.JSONException; + /** * Manages instances of {@link WebView} * @@ -264,8 +267,8 @@ public void callInjectedJavaScript() { public void linkBridge() { if (messagingEnabled) { - loadUrl("javascript:(window.postMessage = function(message) {" + - BRIDGE_NAME + ".postMessage(String(message));" + + loadUrl("javascript:(window.postMessage = function(data) {" + + BRIDGE_NAME + ".postMessage(String(data));" + "})"); } } @@ -455,9 +458,15 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray root.stopLoading(); break; case COMMAND_POST_MESSAGE: - root.evaluateJavascript("if (typeof window.onmessage === 'function') {" + - "window.onmessage('" + args.getString(0) + "')" + - "}", null); + try { + JSONObject eventJson = new JSONObject(); + eventJson.put("data", args.getString(0)); + root.evaluateJavascript("if (typeof window.onmessage === 'function') {" + + "window.onmessage(" + eventJson.toString() + ")" + + "}", null); + } catch (JSONException e) { + throw new RuntimeException(e); + } break; } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java index 4349fc4d1aece6..db5a4200d7fd6e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/TopMessageEvent.java @@ -20,11 +20,11 @@ public class TopMessageEvent extends Event { public static final String EVENT_NAME = "topMessage"; - private final String mMessage; + private final String mData; - public TopMessageEvent(int viewId, String message) { + public TopMessageEvent(int viewId, String data) { super(viewId); - mMessage = message; + mData = data; } @Override @@ -46,7 +46,7 @@ public short getCoalescingKey() { @Override public void dispatch(RCTEventEmitter rctEventEmitter) { WritableMap data = Arguments.createMap(); - data.putString("message", mMessage); + data.putString("data", mData); rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data); } } From 762b708d2b6e44ba0870532e007a938f076b5d64 Mon Sep 17 00:00:00 2001 From: Jacob Parker Date: Mon, 26 Sep 2016 12:05:12 +0100 Subject: [PATCH 10/13] Change onmessage handler to the event-based equivalent --- Examples/UIExplorer/js/messagingtest.html | 5 +++-- Libraries/Components/WebView/WebView.ios.js | 8 ++++---- React/Views/RCTWebView.m | 8 +++++--- .../react/views/webview/ReactWebViewManager.java | 11 ++++++----- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Examples/UIExplorer/js/messagingtest.html b/Examples/UIExplorer/js/messagingtest.html index 229f75d8f41edd..c9258dbf5104a9 100644 --- a/Examples/UIExplorer/js/messagingtest.html +++ b/Examples/UIExplorer/js/messagingtest.html @@ -14,12 +14,13 @@