Skip to content

Commit

Permalink
feat(Android/iOS postMessage): refactoring the old postMessage implem…
Browse files Browse the repository at this point in the history
…entation (#303)

fixes #29
fixes #272
fixes #221
fixes #105
fixes #66

BREAKING CHANGE: Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
  • Loading branch information
Titozzz committed Feb 1, 2019
1 parent 79afbd6 commit f3bdab5
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 118 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {

protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebview";

protected static final String HTTP_METHOD_POST = "POST";

Expand Down Expand Up @@ -138,8 +138,9 @@ public void onPageFinished(WebView webView, String url) {

if (!mLastLoadFailed) {
RNCWebView reactWebView = (RNCWebView) webView;

reactWebView.callInjectedJavaScript();
reactWebView.linkBridge();

emitFinishEvent(webView, url);
}
}
Expand Down Expand Up @@ -239,6 +240,10 @@ protected class RNCWebViewBridge {
mContext = c;
}

/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
mContext.onMessage(message);
Expand Down Expand Up @@ -312,11 +317,11 @@ public void setMessagingEnabled(boolean enabled) {
}

messagingEnabled = enabled;

if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), BRIDGE_NAME);
linkBridge();
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
} else {
removeJavascriptInterface(BRIDGE_NAME);
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
}
}

Expand All @@ -342,30 +347,6 @@ public void callInjectedJavaScript() {
}
}

public void linkBridge() {
if (messagingEnabled) {
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// See isNative in lodash
String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
evaluateJavascript(testPostMessageNative, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
if (value.equals("true")) {
FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
}
});
}

evaluateJavascriptWithFallback("(" +
"window.originalPostMessage = window.postMessage," +
"window.postMessage = function(data) {" +
BRIDGE_NAME + ".postMessage(String(data));" +
"}" +
")");
}
}

public void onMessage(String message) {
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
}
Expand Down
4 changes: 2 additions & 2 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ Function that is invoked when the `WebView` is loading.

### `onMessage`

A function that is invoked when the webview calls `window.postMessage`. Setting this property will inject a `postMessage` global into your webview, but will still call pre-existing values of `postMessage`.
Function that is invoked when the webview calls `window.ReactNativeWebview.postMessage`. Setting this property will inject this global into your webview.

`window.postMessage` accepts one argument, `data`, which will be available on the event object, `event.nativeEvent.data`. `data` must be a string.
`window.ReactNativeWebview.postMessage` accepts one argument, `data`, which will be available on the event object, `event.nativeEvent.data`. `data` must be a string.

| Type | Required |
| -------- | -------- |
Expand Down
60 changes: 24 additions & 36 deletions ios/RNCUIWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

NSString *const RNCJSNavigationScheme = @"react-js-navigation";

static NSString *const kPostMessageHost = @"postMessage";
static NSString *const MessageHandlerName = @"ReactNativeWebview";

@interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>

Expand Down Expand Up @@ -86,7 +86,7 @@ - (void)postMessage:(NSString *)message
@"data": message,
};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[_webView stringByEvaluatingJavaScriptFromString:source];
Expand Down Expand Up @@ -236,7 +236,7 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR
}
}

if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
if (isJSNavigation && [request.URL.host isEqualToString:MessageHandlerName]) {
NSString *data = request.URL.query;
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
Expand All @@ -246,7 +246,7 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR
@"data": data,
}];

NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
NSString *source = [NSString stringWithFormat:@"window.%@.messageReceived();", MessageHandlerName];

[_webView stringByEvaluatingJavaScriptFromString:source];

Expand Down Expand Up @@ -289,40 +289,28 @@ - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)er
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
if (_messagingEnabled) {
#if RCT_DEV
// See isNative in lodash
NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
BOOL postMessageIsNative = [
[webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
isEqualToString:@"true"
];
if (!postMessageIsNative) {
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
#endif
NSString *source = [NSString stringWithFormat:
@"(function() {"
"window.originalPostMessage = window.postMessage;"

"var messageQueue = [];"
"var messagePending = false;"

"function processQueue() {"
"if (!messageQueue.length || messagePending) return;"
"messagePending = true;"
"window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
"}"

"window.postMessage = function(data) {"
"messageQueue.push(String(data));"
"processQueue();"
"};"

"document.addEventListener('message:received', function(e) {"
"messagePending = false;"
"processQueue();"
"});"
"})();", RNCJSNavigationScheme, kPostMessageHost
" var messageQueue = [];"
" var messagePending = false;"

" function processQueue () {"
" if (!messageQueue.length || messagePending) return;"
" messagePending = true;"
" document.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
" }"

" window.%@ = {"
" postMessage: function (data) {"
" messageQueue.push(String(data));"
" processQueue();"
" },"
" messageReceived: function () {"
" messagePending = false;"
" processQueue();"
" }"
" };"
"})();", RNCJSNavigationScheme, MessageHandlerName, MessageHandlerName
];
[webView stringByEvaluatingJavaScriptFromString:source];
}
Expand Down
56 changes: 23 additions & 33 deletions ios/RNCWKWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

#import "objc/runtime.h"

static NSString *const MessageHanderName = @"ReactNative";
static NSString *const MessageHandlerName = @"ReactNativeWebview";

// runtime trick to remove WKWebView keyboard default toolbar
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
Expand Down Expand Up @@ -101,7 +101,22 @@ - (void)didMoveToWindow
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
}
wkWebViewConfig.userContentController = [WKUserContentController new];
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];

if (_messagingEnabled) {
[wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName];

NSString *source = [NSString stringWithFormat:
@"window.%@ = {"
" postMessage: function (data) {"
" window.webkit.messageHandlers.%@.postMessage(String(data));"
" }"
"};", MessageHandlerName, MessageHandlerName
];

WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:script];
}

wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
#if WEBKIT_IOS_10_APIS_AVAILABLE
wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
Expand Down Expand Up @@ -148,7 +163,7 @@ - (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigation
- (void)removeFromSuperview
{
if (_webView) {
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
[_webView removeFromSuperview];
_webView = nil;
Expand Down Expand Up @@ -184,7 +199,7 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor

/**
* This method is called whenever JavaScript running within the web view calls:
* - window.webkit.messageHandlers.[MessageHanderName].postMessage
* - window.webkit.messageHandlers[MessageHandlerName].postMessage
*/
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message
Expand Down Expand Up @@ -253,7 +268,6 @@ - (void)visitSource

-(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
{

if (_webView == nil) {
_savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
return;
Expand All @@ -264,6 +278,7 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
}

UIView* subview;

for (UIView* view in _webView.scrollView.subviews) {
if([[view.class description] hasPrefix:@"WK"])
subview = view;
Expand Down Expand Up @@ -303,10 +318,10 @@ - (void)postMessage:(NSString *)message
{
NSDictionary *eventInitDict = @{@"data": message};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[self evaluateJS: source thenCall: nil];
[self injectJavaScript: source];
}

- (void)layoutSubviews
Expand Down Expand Up @@ -520,43 +535,18 @@ - (void)evaluateJS:(NSString *)js
}];
}


/**
* Called when the navigation is complete.
* @see https://fburl.com/rtys6jlb
*/
- (void) webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation
{
if (_messagingEnabled) {
#if RCT_DEV

// Implementation inspired by Lodash.isNative.
NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))";
[self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) {
if (! [result isEqualToString:@"true"]) {
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
}];
#endif

NSString *source = [NSString stringWithFormat:
@"(function() {"
"window.originalPostMessage = window.postMessage;"

"window.postMessage = function(data) {"
"window.webkit.messageHandlers.%@.postMessage(String(data));"
"};"
"})();",
MessageHanderName
];
[self evaluateJS: source thenCall: nil];
}

if (_injectedJavaScript) {
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
NSMutableDictionary *event = [self baseEvent];
event[@"jsEvaluationValue"] = jsEvaluationValue;

if (self.onLoadingFinish) {
self.onLoadingFinish(event);
}
Expand Down
8 changes: 0 additions & 8 deletions ios/RNCWebView.xcworkspace/contents.xcworkspacedata

This file was deleted.

2 changes: 1 addition & 1 deletion js/WebView.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ class WebView extends React.Component<WebViewSharedProps, State> {
}
thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
domStorageEnabled={this.props.domStorageEnabled}
messagingEnabled={typeof this.props.onMessage === 'function'}
cacheEnabled={this.props.cacheEnabled}
onMessage={this.onMessage}
messagingEnabled={typeof this.props.onMessage === 'function'}
overScrollMode={this.props.overScrollMode}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={
Expand Down
4 changes: 1 addition & 3 deletions js/WebView.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,6 @@ class WebView extends React.Component<WebViewSharedProps, State> {
source = { uri: this.props.url };
}

const messagingEnabled = typeof this.props.onMessage === 'function';

let NativeWebView = nativeConfig.component;

if (this.props.useWebKit) {
Expand Down Expand Up @@ -268,8 +266,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
onLoadingProgress={this._onLoadingProgress}
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
messagingEnabled={typeof this.props.onMessage === 'function'}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
Expand Down
10 changes: 4 additions & 6 deletions js/WebViewTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,13 +424,11 @@ export type WebViewSharedProps = $ReadOnly<{|
onNavigationStateChange?: (event: WebViewNavigation) => mixed,

/**
* A function that is invoked when the webview calls `window.postMessage`.
* Setting this property will inject a `postMessage` global into your
* webview, but will still call pre-existing values of `postMessage`.
* Function that is invoked when the webview calls `window.ReactNativeWebview.postMessage`.
* Setting this property will inject this global into your webview.
*
* `window.postMessage` accepts one argument, `data`, which will be
* available on the event object, `event.nativeEvent.data`. `data`
* must be a string.
* `window.ReactNativeWebview.postMessage` accepts one argument, `data`, which will be
* available on the event object, `event.nativeEvent.data`. `data` must be a string.
*/
onMessage?: (event: WebViewMessageEvent) => mixed,

Expand Down

0 comments on commit f3bdab5

Please sign in to comment.