Skip to content

Commit f3bdab5

Browse files
authored
feat(Android/iOS postMessage): refactoring the old postMessage implementation (#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!
1 parent 79afbd6 commit f3bdab5

File tree

8 files changed

+65
-118
lines changed

8 files changed

+65
-118
lines changed

android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java

+10-29
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
109109

110110
protected static final String HTML_ENCODING = "UTF-8";
111111
protected static final String HTML_MIME_TYPE = "text/html";
112-
protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
112+
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebview";
113113

114114
protected static final String HTTP_METHOD_POST = "POST";
115115

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

139139
if (!mLastLoadFailed) {
140140
RNCWebView reactWebView = (RNCWebView) webView;
141+
141142
reactWebView.callInjectedJavaScript();
142-
reactWebView.linkBridge();
143+
143144
emitFinishEvent(webView, url);
144145
}
145146
}
@@ -239,6 +240,10 @@ protected class RNCWebViewBridge {
239240
mContext = c;
240241
}
241242

243+
/**
244+
* This method is called whenever JavaScript running within the web view calls:
245+
* - window[JAVASCRIPT_INTERFACE].postMessage
246+
*/
242247
@JavascriptInterface
243248
public void postMessage(String message) {
244249
mContext.onMessage(message);
@@ -312,11 +317,11 @@ public void setMessagingEnabled(boolean enabled) {
312317
}
313318

314319
messagingEnabled = enabled;
320+
315321
if (enabled) {
316-
addJavascriptInterface(createRNCWebViewBridge(this), BRIDGE_NAME);
317-
linkBridge();
322+
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
318323
} else {
319-
removeJavascriptInterface(BRIDGE_NAME);
324+
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
320325
}
321326
}
322327

@@ -342,30 +347,6 @@ public void callInjectedJavaScript() {
342347
}
343348
}
344349

345-
public void linkBridge() {
346-
if (messagingEnabled) {
347-
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
348-
// See isNative in lodash
349-
String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
350-
evaluateJavascript(testPostMessageNative, new ValueCallback<String>() {
351-
@Override
352-
public void onReceiveValue(String value) {
353-
if (value.equals("true")) {
354-
FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
355-
}
356-
}
357-
});
358-
}
359-
360-
evaluateJavascriptWithFallback("(" +
361-
"window.originalPostMessage = window.postMessage," +
362-
"window.postMessage = function(data) {" +
363-
BRIDGE_NAME + ".postMessage(String(data));" +
364-
"}" +
365-
")");
366-
}
367-
}
368-
369350
public void onMessage(String message) {
370351
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
371352
}

docs/Reference.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ Function that is invoked when the `WebView` is loading.
196196

197197
### `onMessage`
198198

199-
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`.
199+
Function that is invoked when the webview calls `window.ReactNativeWebview.postMessage`. Setting this property will inject this global into your webview.
200200

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

203203
| Type | Required |
204204
| -------- | -------- |

ios/RNCUIWebView.m

+24-36
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

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

14-
static NSString *const kPostMessageHost = @"postMessage";
14+
static NSString *const MessageHandlerName = @"ReactNativeWebview";
1515

1616
@interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
1717

@@ -86,7 +86,7 @@ - (void)postMessage:(NSString *)message
8686
@"data": message,
8787
};
8888
NSString *source = [NSString
89-
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
89+
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
9090
RCTJSONStringify(eventInitDict, NULL)
9191
];
9292
[_webView stringByEvaluatingJavaScriptFromString:source];
@@ -236,7 +236,7 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR
236236
}
237237
}
238238

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

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

251251
[_webView stringByEvaluatingJavaScriptFromString:source];
252252

@@ -289,40 +289,28 @@ - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)er
289289
- (void)webViewDidFinishLoad:(UIWebView *)webView
290290
{
291291
if (_messagingEnabled) {
292-
#if RCT_DEV
293-
// See isNative in lodash
294-
NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
295-
BOOL postMessageIsNative = [
296-
[webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
297-
isEqualToString:@"true"
298-
];
299-
if (!postMessageIsNative) {
300-
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
301-
}
302-
#endif
303292
NSString *source = [NSString stringWithFormat:
304293
@"(function() {"
305-
"window.originalPostMessage = window.postMessage;"
306-
307-
"var messageQueue = [];"
308-
"var messagePending = false;"
309-
310-
"function processQueue() {"
311-
"if (!messageQueue.length || messagePending) return;"
312-
"messagePending = true;"
313-
"window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
314-
"}"
315-
316-
"window.postMessage = function(data) {"
317-
"messageQueue.push(String(data));"
318-
"processQueue();"
319-
"};"
320-
321-
"document.addEventListener('message:received', function(e) {"
322-
"messagePending = false;"
323-
"processQueue();"
324-
"});"
325-
"})();", RNCJSNavigationScheme, kPostMessageHost
294+
" var messageQueue = [];"
295+
" var messagePending = false;"
296+
297+
" function processQueue () {"
298+
" if (!messageQueue.length || messagePending) return;"
299+
" messagePending = true;"
300+
" document.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
301+
" }"
302+
303+
" window.%@ = {"
304+
" postMessage: function (data) {"
305+
" messageQueue.push(String(data));"
306+
" processQueue();"
307+
" },"
308+
" messageReceived: function () {"
309+
" messagePending = false;"
310+
" processQueue();"
311+
" }"
312+
" };"
313+
"})();", RNCJSNavigationScheme, MessageHandlerName, MessageHandlerName
326314
];
327315
[webView stringByEvaluatingJavaScriptFromString:source];
328316
}

ios/RNCWKWebView.m

+23-33
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
#import "objc/runtime.h"
1515

16-
static NSString *const MessageHanderName = @"ReactNative";
16+
static NSString *const MessageHandlerName = @"ReactNativeWebview";
1717

1818
// runtime trick to remove WKWebView keyboard default toolbar
1919
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
@@ -101,7 +101,22 @@ - (void)didMoveToWindow
101101
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
102102
}
103103
wkWebViewConfig.userContentController = [WKUserContentController new];
104-
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
104+
105+
if (_messagingEnabled) {
106+
[wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName];
107+
108+
NSString *source = [NSString stringWithFormat:
109+
@"window.%@ = {"
110+
" postMessage: function (data) {"
111+
" window.webkit.messageHandlers.%@.postMessage(String(data));"
112+
" }"
113+
"};", MessageHandlerName, MessageHandlerName
114+
];
115+
116+
WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
117+
[wkWebViewConfig.userContentController addUserScript:script];
118+
}
119+
105120
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
106121
#if WEBKIT_IOS_10_APIS_AVAILABLE
107122
wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
@@ -148,7 +163,7 @@ - (void)setAllowsBackForwardNavigationGestures:(BOOL)allowsBackForwardNavigation
148163
- (void)removeFromSuperview
149164
{
150165
if (_webView) {
151-
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
166+
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
152167
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
153168
[_webView removeFromSuperview];
154169
_webView = nil;
@@ -184,7 +199,7 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor
184199

185200
/**
186201
* This method is called whenever JavaScript running within the web view calls:
187-
* - window.webkit.messageHandlers.[MessageHanderName].postMessage
202+
* - window.webkit.messageHandlers[MessageHandlerName].postMessage
188203
*/
189204
- (void)userContentController:(WKUserContentController *)userContentController
190205
didReceiveScriptMessage:(WKScriptMessage *)message
@@ -253,7 +268,6 @@ - (void)visitSource
253268

254269
-(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
255270
{
256-
257271
if (_webView == nil) {
258272
_savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
259273
return;
@@ -264,6 +278,7 @@ -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
264278
}
265279

266280
UIView* subview;
281+
267282
for (UIView* view in _webView.scrollView.subviews) {
268283
if([[view.class description] hasPrefix:@"WK"])
269284
subview = view;
@@ -303,10 +318,10 @@ - (void)postMessage:(NSString *)message
303318
{
304319
NSDictionary *eventInitDict = @{@"data": message};
305320
NSString *source = [NSString
306-
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
321+
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
307322
RCTJSONStringify(eventInitDict, NULL)
308323
];
309-
[self evaluateJS: source thenCall: nil];
324+
[self injectJavaScript: source];
310325
}
311326

312327
- (void)layoutSubviews
@@ -520,43 +535,18 @@ - (void)evaluateJS:(NSString *)js
520535
}];
521536
}
522537

523-
524538
/**
525539
* Called when the navigation is complete.
526540
* @see https://fburl.com/rtys6jlb
527541
*/
528542
- (void) webView:(WKWebView *)webView
529543
didFinishNavigation:(WKNavigation *)navigation
530544
{
531-
if (_messagingEnabled) {
532-
#if RCT_DEV
533-
534-
// Implementation inspired by Lodash.isNative.
535-
NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))";
536-
[self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) {
537-
if (! [result isEqualToString:@"true"]) {
538-
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
539-
}
540-
}];
541-
#endif
542-
543-
NSString *source = [NSString stringWithFormat:
544-
@"(function() {"
545-
"window.originalPostMessage = window.postMessage;"
546-
547-
"window.postMessage = function(data) {"
548-
"window.webkit.messageHandlers.%@.postMessage(String(data));"
549-
"};"
550-
"})();",
551-
MessageHanderName
552-
];
553-
[self evaluateJS: source thenCall: nil];
554-
}
555-
556545
if (_injectedJavaScript) {
557546
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
558547
NSMutableDictionary *event = [self baseEvent];
559548
event[@"jsEvaluationValue"] = jsEvaluationValue;
549+
560550
if (self.onLoadingFinish) {
561551
self.onLoadingFinish(event);
562552
}

ios/RNCWebView.xcworkspace/contents.xcworkspacedata

-8
This file was deleted.

js/WebView.android.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ class WebView extends React.Component<WebViewSharedProps, State> {
157157
}
158158
thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
159159
domStorageEnabled={this.props.domStorageEnabled}
160-
messagingEnabled={typeof this.props.onMessage === 'function'}
161160
cacheEnabled={this.props.cacheEnabled}
162161
onMessage={this.onMessage}
162+
messagingEnabled={typeof this.props.onMessage === 'function'}
163163
overScrollMode={this.props.overScrollMode}
164164
contentInset={this.props.contentInset}
165165
automaticallyAdjustContentInsets={

js/WebView.ios.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,6 @@ class WebView extends React.Component<WebViewSharedProps, State> {
232232
source = { uri: this.props.url };
233233
}
234234

235-
const messagingEnabled = typeof this.props.onMessage === 'function';
236-
237235
let NativeWebView = nativeConfig.component;
238236

239237
if (this.props.useWebKit) {
@@ -268,8 +266,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
268266
onLoadingFinish={this._onLoadingFinish}
269267
onLoadingError={this._onLoadingError}
270268
onLoadingProgress={this._onLoadingProgress}
271-
messagingEnabled={messagingEnabled}
272269
onMessage={this._onMessage}
270+
messagingEnabled={typeof this.props.onMessage === 'function'}
273271
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
274272
scalesPageToFit={scalesPageToFit}
275273
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}

js/WebViewTypes.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,11 @@ export type WebViewSharedProps = $ReadOnly<{|
424424
onNavigationStateChange?: (event: WebViewNavigation) => mixed,
425425

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

0 commit comments

Comments
 (0)