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

Implement a postMessage function and an onMessage event for webviews … #9762

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Examples/UIExplorer/js/WebViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,53 @@ class ScaledWebView extends React.Component {
}
}

class MessagingTest extends React.Component {
webview = null

Choose a reason for hiding this comment

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

curly: Expected { after 'if' condition.


state = {
messagesReceivedFromWebView: 0,
message: '',
}

onMessage = e => this.setState({
messagesReceivedFromWebView: this.state.messagesReceivedFromWebView + 1,
message: e.nativeEvent.data,
})

postMessage = () => {
if (this.webview) {
this.webview.postMessage('"Hello" from React Native!');
}
}

render(): ReactElement<any> {
const {messagesReceivedFromWebView, message} = this.state;

return (
<View style={[styles.container, { height: 200 }]}>
<View style={styles.container}>
<Text>Messages received from web view: {messagesReceivedFromWebView}</Text>

Choose a reason for hiding this comment

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

react/no-string-refs: Using this.refs is deprecated.

<Text>{message || '(No message)'}</Text>
<View style={styles.buttons}>
<Button text="Send Message to Web View" enabled onPress={this.postMessage} />

Choose a reason for hiding this comment

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

property postMessage Property not found in MessagingTest

</View>
</View>
<View style={styles.container}>
<WebView
ref={webview => { this.webview = webview; }}
style={{
backgroundColor: BGWASH,
height: 100,
}}
source={require('./messagingtest.html')}
onMessage={this.onMessage}

Choose a reason for hiding this comment

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

property onMessage Property not found in MessagingTest

/>
</View>
</View>
);
}
}

var styles = StyleSheet.create({
container: {
flex: 1,
Expand Down Expand Up @@ -391,5 +438,9 @@ exports.examples = [
/>
);
}
},
{
title: 'Mesaging Test',
render(): ReactElement<any> { return <MessagingTest />; }
}
];
28 changes: 28 additions & 0 deletions Examples/UIExplorer/js/messagingtest.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<title>Messaging Test</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=320, user-scalable=no">
</head>
<body>
<p>Messages recieved from React Native: 0</p>
<p>(No messages)</p>
<button type="button">
Send message to React Native
</button>
</body>
<script>
var messagesReceivedFromReactNative = 0;
document.addEventListener('message', function(e) {
messagesReceivedFromReactNative += 1;
document.getElementsByTagName('p')[0].innerHTML =
'Messages recieved from React Native: ' + messagesReceivedFromReactNative;
document.getElementsByTagName('p')[1].innerHTML = e.data;
});

document.getElementsByTagName('button')[0].addEventListener('click', function() {
window.postMessage('"Hello" from the web view');
});
</script>
</html>
22 changes: 21 additions & 1 deletion Libraries/Components/WebView/WebView.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -218,6 +219,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}
Expand Down Expand Up @@ -268,6 +271,14 @@ class WebView extends React.Component {
);
};

postMessage = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.postMessage,
[String(data)]
);
};

/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
Expand Down Expand Up @@ -310,9 +321,18 @@ class WebView extends React.Component {
});
this.updateNavigationState(event);
};

onMessage = (event: Event) => {
var {onMessage} = this.props;
onMessage && onMessage(event);
}
}

var RCTWebView = requireNativeComponent('RCTWebView', WebView);
var RCTWebView = requireNativeComponent('RCTWebView', WebView, {
nativeOnly: {
messagingEnabled: PropTypes.bool,
},
});

var styles = StyleSheet.create({
container: {
Expand Down
39 changes: 39 additions & 0 deletions Libraries/Components/WebView/WebView.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,16 @@ class WebView extends React.Component {
* Function that is invoked when the `WebView` loading starts or ends.
*/
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, but will still call pre-existing values of `postMessage`.
*
* `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,
/**
* Boolean value that forces the `WebView` to show the loading view
* on the first load.
Expand Down Expand Up @@ -382,6 +392,8 @@ class WebView extends React.Component {
source.uri = this.props.url;
}

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

var webView =
<RCTWebView
ref={RCT_WEBVIEW_REF}
Expand All @@ -397,6 +409,8 @@ class WebView extends React.Component {
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={this.props.scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
Expand Down Expand Up @@ -457,6 +471,24 @@ class WebView extends React.Component {
);
};

/**
* Posts a message to the web view, which will emit a `message` event.
* Accepts one argument, `data`, which must be a string.
*
* In your webview, you'll need to something like the following.
*
* ```js
* document.addEventListener('message', e => { document.title = e.data; });
* ```
*/
postMessage = (data) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.postMessage,
[String(data)]
);
};

/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
Expand Down Expand Up @@ -502,13 +534,20 @@ class WebView extends React.Component {
});
this._updateNavigationState(event);
};

_onMessage = (event: Event) => {
var {onMessage} = this.props;
onMessage && onMessage(event);
}
}

var RCTWebView = requireNativeComponent('RCTWebView', WebView, {
nativeOnly: {
onLoadingStart: true,
onLoadingError: true,
onLoadingFinish: true,
onMessage: true,
messagingEnabled: PropTypes.bool,
},
});

Expand Down
2 changes: 2 additions & 0 deletions React/Views/RCTWebView.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)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;

- (void)goForward;
- (void)goBack;
- (void)reload;
- (void)stopLoading;
- (void)postMessage:(NSString *)message;

@end
46 changes: 46 additions & 0 deletions React/Views/RCTWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
#import "UIView+React.h"

NSString *const RCTJSNavigationScheme = @"react-js-navigation";
NSString *const RCTJSPostMessageHost = @"postMessage";

@interface RCTWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>

@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
@property (nonatomic, copy) RCTDirectEventBlock onMessage;

@end

Expand Down Expand Up @@ -82,6 +84,18 @@ - (void)stopLoading
[_webView stopLoading];
}

- (void)postMessage:(NSString *)message
{
NSDictionary *eventInitDict = @{
@"data": message,
};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[_webView stringByEvaluatingJavaScriptFromString:source];
}

- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
Expand Down Expand Up @@ -221,6 +235,18 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR
}
}

if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) {
NSString *data = request.URL.query;
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
@"data": data,
}];
_onMessage(event);
}

// JS Navigation handler
return !isJSNavigation;
}
Expand Down Expand Up @@ -248,6 +274,26 @@ - (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:
@"window.originalPostMessage = window.postMessage;"
"window.postMessage = function(data) {"
"window.location = '%@://%@?' + encodeURIComponent(String(data));"
"};", RCTJSNavigationScheme, RCTJSPostMessageHost
];
[webView stringByEvaluatingJavaScriptFromString:source];
}
if (_injectedJavaScript != nil) {
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];

Expand Down
14 changes: 14 additions & 0 deletions React/Views/RCTWebViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -97,6 +99,18 @@ - (UIView *)view
}];
}

RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
RCTWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view postMessage:message];
}
}];
}

#pragma mark - Exported synchronous methods

- (BOOL)webView:(__unused RCTWebView *)webView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ android_library(
name = 'webview',
srcs = glob(['**/*.java']),
deps = [
react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'),
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),
Expand Down
Loading