feat(iOS): WKUserScripts (e.g. injectedJavaScript) can now update upon props change; and can be configured to inject into all frames. (#1119)

BREAKING CHANGE: 
• Props updates to `injectedJavaScript` are no longer immutable.

• `injectedJavaScript` no longer attaches a `jsEvaluationValue` property to the `onLoadingFinish` event. Check out: https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-574919464 to migrate with the same behavior.
This commit is contained in:
Jamie Birch 2020-03-17 21:01:20 +00:00 committed by GitHub
parent e9ad1df51e
commit 9cb2f6e2f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 435 additions and 129 deletions

5
.gitignore vendored
View File

@ -53,4 +53,7 @@ android/gradle
android/gradlew
android/gradlew.bat
lib/
lib/
.classpath
.project
.settings/

View File

@ -293,11 +293,13 @@ export default class App extends Component {
This runs the JavaScript in the `runFirst` string once the page is loaded. In this case, you can see that both the body style was changed to red and the alert showed up after 2 seconds.
By setting `injectedJavaScriptForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform.
<img alt="screenshot of Github repo" width="200" src="https://user-images.githubusercontent.com/1479215/53609254-e5dc9c00-3b7a-11e9-9118-bc4e520ce6ca.png" />
_Under the hood_
> On iOS, `injectedJavaScript` runs a method on WebView called `evaluateJavaScript:completionHandler:`
> On iOS, ~~`injectedJavaScript` runs a method on WebView called `evaluateJavaScript:completionHandler:`~~ this is no longer true as of version `8.2.0`. Instead, we use a `WKUserScript` with injection time `WKUserScriptInjectionTimeAtDocumentEnd`. As a consequence, `injectedJavaScript` no longer returns an evaluation value nor logs a warning to the console. In the unlikely event that your app depended upon this behaviour, please see migration steps [here](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour.
> On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback`
#### The `injectedJavaScriptBeforeContentLoaded` prop
@ -332,6 +334,11 @@ export default class App extends Component {
This runs the JavaScript in the `runFirst` string before the page is loaded. In this case, the value of `window.isNativeApp` will be set to true before the web code executes.
By setting `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false`, the JavaScript injection will occur on all frames (not just the top frame) if supported for the given platform. Howver, although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended.
> On iOS, ~~`injectedJavaScriptBeforeContentLoaded` runs a method on WebView called `evaluateJavaScript:completionHandler:`~~ this is no longer true as of version `8.2.0`. Instead, we use a `WKUserScript` with injection time `WKUserScriptInjectionTimeAtDocumentStart`. As a consequence, `injectedJavaScriptBeforeContentLoaded` no longer returns an evaluation value nor logs a warning to the console. In the unlikely event that your app depended upon this behaviour, please see migration steps [here](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-574919464) to retain equivalent behaviour.
> On Android, `injectedJavaScript` runs a method on the Android WebView called `evaluateJavascriptWithFallback`
#### The `injectJavaScript` method
While convenient, the downside to the previously mentioned `injectedJavaScript` prop is that it only runs once. That's why we also expose a method on the webview ref called `injectJavaScript` (note the slightly different name!).

View File

@ -7,7 +7,9 @@ This document lays out the current public properties and methods for the React N
- [`source`](Reference.md#source)
- [`automaticallyAdjustContentInsets`](Reference.md#automaticallyadjustcontentinsets)
- [`injectedJavaScript`](Reference.md#injectedjavascript)
- [`injectedJavaScriptBeforeContentLoaded`](Reference.md#injectedJavaScriptBeforeContentLoaded)
- [`injectedJavaScriptBeforeContentLoaded`](Reference.md#injectedjavascriptbeforecontentloaded)
- [`injectedJavaScriptForMainFrameOnly`](Reference.md#injectedjavascriptformainframeonly)
- [`injectedJavaScriptBeforeContentLoadedForMainFrameOnly`](Reference.md#injectedjavascriptbeforecontentloadedformainframeonly)
- [`mediaPlaybackRequiresUserAction`](Reference.md#mediaplaybackrequiresuseraction)
- [`nativeConfig`](Reference.md#nativeconfig)
- [`onError`](Reference.md#onerror)
@ -120,11 +122,15 @@ Controls whether to adjust the content inset for web views that are placed behin
### `injectedJavaScript`
Set this to provide JavaScript that will be injected into the web page when the view loads. Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
Set this to provide JavaScript that will be injected into the web page after the document finishes loading, but before other subresources finish loading.
Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
On iOS, see [`WKUserScriptInjectionTimeAtDocumentEnd`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentend?language=objc)
| Type | Required | Platform |
| ------ | -------- | -------- |
| string | No | iOS, Andrdoid, macOS
| string | No | iOS, Android, macOS
To learn more, read the [Communicating between JS and Native](Guide.md#communicating-between-js-and-native) guide.
@ -148,18 +154,21 @@ const INJECTED_JAVASCRIPT = `(function() {
### `injectedJavaScriptBeforeContentLoaded`
Set this to provide JavaScript that will be injected into the web page after the document element is created, but before any other content is loaded. Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
On iOS, see [WKUserScriptInjectionTimeAtDocumentStart](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc)
Set this to provide JavaScript that will be injected into the web page after the document element is created, but before other subresources finish loading.
Make sure the string evaluates to a valid type (`true` works) and doesn't otherwise throw an exception.
On iOS, see [`WKUserScriptInjectionTimeAtDocumentStart`](https://developer.apple.com/documentation/webkit/wkuserscriptinjectiontime/wkuserscriptinjectiontimeatdocumentstart?language=objc)
| Type | Required | Platform |
| ------ | -------- | -------- |
| string | No | iOS, Android, macOS |
| string | No | iOS, macOS |
To learn more, read the [Communicating between JS and Native](Guide.md#communicating-between-js-and-native) guide.
Example:
Post message a JSON object of `window.location` to be handled by [`onMessage`](Reference.md#onmessage)
Post message a JSON object of `window.location` to be handled by [`onMessage`](Reference.md#onmessage). `window.ReactNativeWebView.postMessage` *will* be available at this time.
```jsx
const INJECTED_JAVASCRIPT = `(function() {
@ -175,6 +184,32 @@ const INJECTED_JAVASCRIPT = `(function() {
---
### `injectedJavaScriptForMainFrameOnly`
If `true` (default), loads the `injectedJavaScript` only into the main frame.
If `false`, loads it into all frames (e.g. iframes).
| Type | Required | Platform |
| ------ | -------- | -------- |
| bool | No | iOS, macOS |
---
### `injectedJavaScriptBeforeContentLoadedForMainFrameOnly`
If `true` (default), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
If `false`, loads it into all frames (e.g. iframes).
Warning: although support for `injectedJavaScriptBeforeContentLoadedForMainFrameOnly: false` has been implemented for iOS and macOS, [it is not clear](https://github.com/react-native-community/react-native-webview/pull/1119#issuecomment-600275750) that it is actually possible to inject JS into iframes at this point in the page lifecycle, and so relying on the expected behaviour of this prop when set to `false` is not recommended.
| Type | Required | Platform |
| ------ | -------- | -------- |
| bool | No | iOS, macOS |
---
### `mediaPlaybackRequiresUserAction`
Boolean that determines whether HTML5 audio and video requires the user to tap them before they start playing. The default value is `true`. (Android API minimum version 17).

View File

@ -14,6 +14,7 @@ import Alerts from './examples/Alerts';
import Scrolling from './examples/Scrolling';
import Background from './examples/Background';
import Uploads from './examples/Uploads';
import Injection from './examples/Injection';
const TESTS = {
Alerts: {
@ -48,6 +49,14 @@ const TESTS = {
return <Uploads />;
},
},
Injection: {
title: 'Injection',
testId: 'injection',
description: 'Injection test',
render() {
return <Injection />;
},
},
};
type Props = {};
@ -101,6 +110,11 @@ export default class App extends Component<Props, State> {
title="Background"
onPress={() => this._changeTest('Background')}
/>
<Button
testID="testType_injection"
title="Injection"
onPress={() => this._changeTest('Injection')}
/>
{Platform.OS === 'android' && <Button
testID="testType_uploads"
title="Uploads"

View File

@ -0,0 +1,160 @@
import React, {Component} from 'react';
import {Text, View, ScrollView} from 'react-native';
import WebView from 'react-native-webview';
// const HTML = `
// <!DOCTYPE html>
// <html>
// <head>
// <meta charset="utf-8">
// <meta name="viewport" content="width=device-width, initial-scale=1">
// <title>iframe test</title>
// </head>
// <body>
// <p style="">beforeContentLoaded on the top frame <span id="before_failed" style="display: inline-block;">failed</span><span id="before_succeeded" style="display: none;">succeeded</span>!</p>
// <p style="">afterContentLoaded on the top frame <span id="after_failed" style="display: inline-block;">failed</span><span id="after_succeeded" style="display: none;">succeeded</span>!</p>
// <iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe.html?v=1" name="iframe_0" style="width: 100%; height: 25px;"></iframe>
// <iframe src="https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframe2.html?v=1" name="iframe_1" style="width: 100%; height: 25px;"></iframe>
// <iframe src="https://www.ebay.co.uk" name="iframe_2" style="width: 100%; height: 25px;"></iframe>
// </body>
// </html>
// `;
type Props = {};
type State = {
backgroundColor: string,
};
export default class Injection extends Component<Props, State> {
state = {
backgroundColor: '#FF00FF00'
};
render() {
return (
<ScrollView>
<View style={{ }}>
<View style={{ height: 300 }}>
<WebView
/**
* This HTML is a copy of a multi-frame JS injection test that I had lying around.
* @see https://birchlabs.co.uk/linguabrowse/infopages/obsol/iframeTest.html
*/
// source={{ html: HTML }}
source={{ uri: "https://birchlabs.co.uk/linguabrowse/infopages/obsol/rnw_iframe_test.html" }}
automaticallyAdjustContentInsets={false}
style={{backgroundColor:'#00000000'}}
/* Must be populated in order for `messagingEnabled` to be `true` to activate the
* JS injection user scripts, consistent with current behaviour. This is undesirable,
* so needs addressing in a follow-up PR. */
onMessage={() => {}}
/* We set this property in each frame */
injectedJavaScriptBeforeContentLoaded={`
console.log("executing injectedJavaScriptBeforeContentLoaded...");
if(typeof window.top.injectedIframesBeforeContentLoaded === "undefined"){
window.top.injectedIframesBeforeContentLoaded = [];
}
window.self.colourToUse = "orange";
if(window.self === window.top){
console.log("Was window.top. window.frames.length is:", window.frames.length);
window.self.numberOfFramesAtBeforeContentLoaded = window.frames.length;
function declareSuccessOfBeforeContentLoaded(head){
var style = window.self.document.createElement('style');
style.type = 'text/css';
style.innerHTML = "#before_failed { display: none !important; }#before_succeeded { display: inline-block !important; }";
head.appendChild(style);
}
const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
if(head){
declareSuccessOfBeforeContentLoaded(head);
} else {
window.self.document.addEventListener("DOMContentLoaded", function (event) {
const head = (window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
declareSuccessOfBeforeContentLoaded(head);
});
}
} else {
window.top.injectedIframesBeforeContentLoaded.push(window.self.name);
console.log("wasn't window.top.");
console.log("wasn't window.top. Still going...");
}
`}
injectedJavaScriptForMainFrameOnly={false}
/* We read the colourToUse property in each frame to recolour each frame */
injectedJavaScript={`
console.log("executing injectedJavaScript...");
if(typeof window.top.injectedIframesAfterContentLoaded === "undefined"){
window.top.injectedIframesAfterContentLoaded = [];
}
if(window.self.colourToUse){
window.self.document.body.style.backgroundColor = window.self.colourToUse;
} else {
window.self.document.body.style.backgroundColor = "cyan";
}
if(window.self === window.top){
function declareSuccessOfAfterContentLoaded(head){
var style = window.self.document.createElement('style');
style.type = 'text/css';
style.innerHTML = "#after_failed { display: none !important; }#after_succeeded { display: inline-block !important; }";
head.appendChild(style);
}
declareSuccessOfAfterContentLoaded(window.self.document.head || window.self.document.getElementsByTagName('head')[0]);
// var numberOfFramesAtBeforeContentLoadedEle = document.createElement('p');
// numberOfFramesAtBeforeContentLoadedEle.textContent = "Number of iframes upon the main frame's beforeContentLoaded: " +
// window.self.numberOfFramesAtBeforeContentLoaded;
// var numberOfFramesAtAfterContentLoadedEle = document.createElement('p');
// numberOfFramesAtAfterContentLoadedEle.textContent = "Number of iframes upon the main frame's afterContentLoaded: " + window.frames.length;
// numberOfFramesAtAfterContentLoadedEle.id = "numberOfFramesAtAfterContentLoadedEle";
var namedFramesAtBeforeContentLoadedEle = document.createElement('p');
namedFramesAtBeforeContentLoadedEle.textContent = "Names of iframes that called beforeContentLoaded: " + JSON.stringify(window.top.injectedIframesBeforeContentLoaded);
namedFramesAtBeforeContentLoadedEle.id = "namedFramesAtBeforeContentLoadedEle";
var namedFramesAtAfterContentLoadedEle = document.createElement('p');
namedFramesAtAfterContentLoadedEle.textContent = "Names of iframes that called afterContentLoaded: " + JSON.stringify(window.top.injectedIframesAfterContentLoaded);
namedFramesAtAfterContentLoadedEle.id = "namedFramesAtAfterContentLoadedEle";
// document.body.appendChild(numberOfFramesAtBeforeContentLoadedEle);
// document.body.appendChild(numberOfFramesAtAfterContentLoadedEle);
document.body.appendChild(namedFramesAtBeforeContentLoadedEle);
document.body.appendChild(namedFramesAtAfterContentLoadedEle);
} else {
window.top.injectedIframesAfterContentLoaded.push(window.self.name);
window.top.document.getElementById('namedFramesAtAfterContentLoadedEle').textContent = "Names of iframes that called afterContentLoaded: " + JSON.stringify(window.top.injectedIframesAfterContentLoaded);
}
`}
/>
</View>
</View>
<Text>This test presents three iframes: iframe_0 (yellow); iframe_1 (pink); and iframe_2 (transparent, because its 'X-Frame-Options' is set to 'SAMEORIGIN').</Text>
<Text>Before injection, the main frame's background is the browser's default value (transparent or white) and each frame has its natural colour.</Text>
{/*<Text>1a) At injection time "beforeContentLoaded", a variable will be set in each frame to set 'orange' as the "colour to be used".</Text>*/}
{/*<Text>1b) Also upon "beforeContentLoaded", a style element to change the text "beforeContentLoaded failed" -> "beforeContentLoaded succeeded" will be applied as soon as the head has loaded.</Text>*/}
{/*<Text>2a) At injection time "afterContentLoaded", that variable will be read if present, the colour orange will be injected into all frames. Otherwise, cyan.</Text>*/}
{/*<Text>2b) Also upon "afterContentLoaded", a style element to change the text "afterContentLoaded failed" -> "afterContentLoaded succeeded" will be applied as soon as the head has loaded.</Text>*/}
<Text> If the main frame becomes orange, then top-frame injection both beforeContentLoaded and afterContentLoaded is supported.</Text>
<Text> If iframe_0, and iframe_1 become orange, then multi-frame injection beforeContentLoaded and afterContentLoaded is supported.</Text>
<Text> If the two texts say "beforeContentLoaded on the top frame succeeded!" and "afterContentLoaded on the top frame succeeded!", then both injection times are supported at least on the main frame.</Text>
<Text> If either of the two iframes become coloured cyan, then for that given frame, JS injection succeeded after the content loaded, but didn't occur before the content loaded - please note that for iframes, this may not be a test failure, as it is not clear whether we would expect iframes to support an injection time of beforeContentLoaded anyway.</Text>
<Text> If "Names of iframes that called beforeContentLoaded: " is [], then see above.</Text>
<Text> If "Names of iframes that called afterContentLoaded: " is [], then afterContentLoaded is not supported in iframes.</Text>
<Text> If the main frame becomes coloured cyan, then JS injection succeeded after the content loaded, but didn't occur before the content loaded.</Text>
<Text> If the text "beforeContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame before the content loaded.</Text>
<Text> If the text "afterContentLoaded on the top frame failed" remains unchanged, then JS injection has failed on the main frame after the content loaded.</Text>
<Text> If the iframes remain their original colours (yellow and pink), then multi-frame injection is not supported at all.</Text>
</ScrollView>
);
}
}

View File

@ -182,7 +182,7 @@ PODS:
- React-cxxreact (= 0.61.5)
- React-jsi (= 0.61.5)
- React-jsinspector (0.61.5)
- react-native-webview (8.0.6):
- react-native-webview (8.2.0):
- React
- React-RCTActionSheet (0.61.5):
- React-Core/RCTActionSheetHeaders (= 0.61.5)
@ -326,7 +326,7 @@ SPEC CHECKSUMS:
React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7
React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386
React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0
react-native-webview: 222d83c9c489e09b5d3541519110a637490ad4fa
react-native-webview: 1db33907230d0eb344964d6f3bb56b9ee77e25a4
React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72

View File

@ -31,6 +31,8 @@
@property (nonatomic, assign) BOOL messagingEnabled;
@property (nonatomic, copy) NSString * _Nullable injectedJavaScript;
@property (nonatomic, copy) NSString * _Nullable injectedJavaScriptBeforeContentLoaded;
@property (nonatomic, assign) BOOL injectedJavaScriptForMainFrameOnly;
@property (nonatomic, assign) BOOL injectedJavaScriptBeforeContentLoadedForMainFrameOnly;
@property (nonatomic, assign) BOOL scrollEnabled;
@property (nonatomic, assign) BOOL sharedCookiesEnabled;
@property (nonatomic, assign) BOOL pagingEnabled;

View File

@ -81,6 +81,9 @@ static NSDictionary* customCertificatesForHost;
#else
@property (nonatomic, copy) RNCWKWebView *webView;
#endif // !TARGET_OS_OSX
@property (nonatomic, strong) WKUserScript *postMessageScript;
@property (nonatomic, strong) WKUserScript *atStartScript;
@property (nonatomic, strong) WKUserScript *atEndScript;
@end
@implementation RNCWebView
@ -122,10 +125,14 @@ static NSDictionary* customCertificatesForHost;
_automaticallyAdjustContentInsets = YES;
_contentInset = UIEdgeInsetsZero;
_savedKeyboardDisplayRequiresUserAction = YES;
#if !TARGET_OS_OSX
#if !TARGET_OS_OSX
_savedStatusBarStyle = RCTSharedApplication().statusBarStyle;
_savedStatusBarHidden = RCTSharedApplication().statusBarHidden;
#endif // !TARGET_OS_OSX
#endif // !TARGET_OS_OSX
_injectedJavaScript = nil;
_injectedJavaScriptForMainFrameOnly = YES;
_injectedJavaScriptBeforeContentLoaded = nil;
_injectedJavaScriptBeforeContentLoadedForMainFrameOnly = YES;
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
_savedContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
@ -206,50 +213,7 @@ static NSDictionary* customCertificatesForHost;
// Shim the HTML5 history API:
[wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self]
name:HistoryShimName];
NSString *source = [NSString stringWithFormat:
@"(function(history) {\n"
" function notify(type) {\n"
" setTimeout(function() {\n"
" window.webkit.messageHandlers.%@.postMessage(type)\n"
" }, 0)\n"
" }\n"
" function shim(f) {\n"
" return function pushState() {\n"
" notify('other')\n"
" return f.apply(history, arguments)\n"
" }\n"
" }\n"
" history.pushState = shim(history.pushState)\n"
" history.replaceState = shim(history.replaceState)\n"
" window.addEventListener('popstate', function() {\n"
" notify('backforward')\n"
" })\n"
"})(window.history)\n", HistoryShimName
];
WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:script];
if (_messagingEnabled) {
[wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate: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];
if (_injectedJavaScriptBeforeContentLoaded) {
// If user has provided an injectedJavascript prop, execute it at the start of the document
WKUserScript *injectedScript = [[WKUserScript alloc] initWithSource:_injectedJavaScriptBeforeContentLoaded injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:injectedScript];
}
}
[self resetupScripts:wkWebViewConfig];
#if !TARGET_OS_OSX
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
@ -266,68 +230,6 @@ static NSDictionary* customCertificatesForHost;
if (_applicationNameForUserAgent) {
wkWebViewConfig.applicationNameForUserAgent = [NSString stringWithFormat:@"%@ %@", wkWebViewConfig.applicationNameForUserAgent, _applicationNameForUserAgent];
}
if(_sharedCookiesEnabled) {
// More info to sending cookies with WKWebView
// https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303
if (@available(iOS 11.0, *)) {
// Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies
// See also https://forums.developer.apple.com/thread/97194
// check if websiteDataStore has not been initialized before
if(!_incognito && !_cacheEnabled) {
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
}
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
[wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];
}
} else {
NSMutableString *script = [NSMutableString string];
// Clear all existing cookies in a direct called function. This ensures that no
// javascript error will break the web content javascript.
// We keep this code here, if someone requires that Cookies are also removed within the
// the WebView and want to extends the current sharedCookiesEnabled option with an
// additional property.
// Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
// for each cookie which is already available in the WebView context.
/*
[script appendString:@"(function () {\n"];
[script appendString:@" var cookies = document.cookie.split('; ');\n"];
[script appendString:@" for (var i = 0; i < cookies.length; i++) {\n"];
[script appendString:@" if (cookies[i].indexOf('=') !== -1) {\n"];
[script appendString:@" document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"];
[script appendString:@" }\n"];
[script appendString:@" }\n"];
[script appendString:@"})();\n\n"];
*/
// Set cookies in a direct called function. This ensures that no
// javascript error will break the web content javascript.
// Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;"
// for each cookie which is available in the application context.
[script appendString:@"(function () {\n"];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
[script appendFormat:@"document.cookie = %@ + '=' + %@",
RCTJSONStringify(cookie.name, NULL),
RCTJSONStringify(cookie.value, NULL)];
if (cookie.path) {
[script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)];
}
if (cookie.expiresDate) {
[script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()",
cookie.expiresDate.timeIntervalSince1970 * 1000
];
}
[script appendString:@";\n"];
}
[script appendString:@"})();\n"];
WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:cookieInScript];
}
}
return wkWebViewConfig;
}
@ -1136,16 +1038,7 @@ static NSDictionary* customCertificatesForHost;
- (void)webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation
{
if (_injectedJavaScript) {
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
NSMutableDictionary *event = [self baseEvent];
event[@"jsEvaluationValue"] = jsEvaluationValue;
if (self.onLoadingFinish) {
self.onLoadingFinish(event);
}
}];
} else if (_onLoadingFinish) {
if (_onLoadingFinish) {
_onLoadingFinish([self baseEvent]);
}
}
@ -1194,6 +1087,174 @@ static NSDictionary* customCertificatesForHost;
}
#endif // !TARGET_OS_OSX
- (void)setInjectedJavaScript:(NSString *)source {
_injectedJavaScript = source;
self.atEndScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly:_injectedJavaScriptForMainFrameOnly];
if(_webView != nil){
[self resetupScripts:_webView.configuration];
}
}
- (void)setInjectedJavaScriptBeforeContentLoaded:(NSString *)source {
_injectedJavaScriptBeforeContentLoaded = source;
self.atStartScript = source == nil ? nil : [[WKUserScript alloc] initWithSource:source
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:_injectedJavaScriptBeforeContentLoadedForMainFrameOnly];
if(_webView != nil){
[self resetupScripts:_webView.configuration];
}
}
- (void)setInjectedJavaScriptForMainFrameOnly:(BOOL)mainFrameOnly {
_injectedJavaScriptForMainFrameOnly = mainFrameOnly;
[self setInjectedJavaScript:_injectedJavaScript];
}
- (void)setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly:(BOOL)mainFrameOnly {
_injectedJavaScriptBeforeContentLoadedForMainFrameOnly = mainFrameOnly;
[self setInjectedJavaScriptBeforeContentLoaded:_injectedJavaScriptBeforeContentLoaded];
}
- (void)setMessagingEnabled:(BOOL)messagingEnabled {
_messagingEnabled = messagingEnabled;
self.postMessageScript = _messagingEnabled ?
[
[WKUserScript alloc]
initWithSource: [
NSString
stringWithFormat:
@"window.%@ = {"
" postMessage: function (data) {"
" window.webkit.messageHandlers.%@.postMessage(String(data));"
" }"
"};", MessageHandlerName, MessageHandlerName
]
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
/* TODO: For a separate (minor) PR: use logic like this (as react-native-wkwebview does) so that messaging can be used in all frames if desired.
* I am keeping it as YES for consistency with previous behaviour. */
// forMainFrameOnly:_messagingEnabledForMainFrameOnly
forMainFrameOnly:YES
] :
nil;
if(_webView != nil){
[self resetupScripts:_webView.configuration];
}
}
- (void)resetupScripts:(WKWebViewConfiguration *)wkWebViewConfig {
[wkWebViewConfig.userContentController removeAllUserScripts];
[wkWebViewConfig.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
NSString *html5HistoryAPIShimSource = [NSString stringWithFormat:
@"(function(history) {\n"
" function notify(type) {\n"
" setTimeout(function() {\n"
" window.webkit.messageHandlers.%@.postMessage(type)\n"
" }, 0)\n"
" }\n"
" function shim(f) {\n"
" return function pushState() {\n"
" notify('other')\n"
" return f.apply(history, arguments)\n"
" }\n"
" }\n"
" history.pushState = shim(history.pushState)\n"
" history.replaceState = shim(history.replaceState)\n"
" window.addEventListener('popstate', function() {\n"
" notify('backforward')\n"
" })\n"
"})(window.history)\n", HistoryShimName
];
WKUserScript *script = [[WKUserScript alloc] initWithSource:html5HistoryAPIShimSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:script];
if(_sharedCookiesEnabled) {
// More info to sending cookies with WKWebView
// https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303
if (@available(iOS 11.0, *)) {
// Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies
// See also https://forums.developer.apple.com/thread/97194
// check if websiteDataStore has not been initialized before
if(!_incognito && !_cacheEnabled) {
wkWebViewConfig.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
}
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
[wkWebViewConfig.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:nil];
}
} else {
NSMutableString *script = [NSMutableString string];
// Clear all existing cookies in a direct called function. This ensures that no
// javascript error will break the web content javascript.
// We keep this code here, if someone requires that Cookies are also removed within the
// the WebView and want to extends the current sharedCookiesEnabled option with an
// additional property.
// Generates JS: document.cookie = "key=; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
// for each cookie which is already available in the WebView context.
/*
[script appendString:@"(function () {\n"];
[script appendString:@" var cookies = document.cookie.split('; ');\n"];
[script appendString:@" for (var i = 0; i < cookies.length; i++) {\n"];
[script appendString:@" if (cookies[i].indexOf('=') !== -1) {\n"];
[script appendString:@" document.cookie = cookies[i].split('=')[0] + '=; Expires=Thu, 01 Jan 1970 00:00:01 GMT';\n"];
[script appendString:@" }\n"];
[script appendString:@" }\n"];
[script appendString:@"})();\n\n"];
*/
// Set cookies in a direct called function. This ensures that no
// javascript error will break the web content javascript.
// Generates JS: document.cookie = "key=value; Path=/; Expires=Thu, 01 Jan 20xx 00:00:01 GMT;"
// for each cookie which is available in the application context.
[script appendString:@"(function () {\n"];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
[script appendFormat:@"document.cookie = %@ + '=' + %@",
RCTJSONStringify(cookie.name, NULL),
RCTJSONStringify(cookie.value, NULL)];
if (cookie.path) {
[script appendFormat:@" + '; Path=' + %@", RCTJSONStringify(cookie.path, NULL)];
}
if (cookie.expiresDate) {
[script appendFormat:@" + '; Expires=' + new Date(%f).toUTCString()",
cookie.expiresDate.timeIntervalSince1970 * 1000
];
}
[script appendString:@";\n"];
}
[script appendString:@"})();\n"];
WKUserScript* cookieInScript = [[WKUserScript alloc] initWithSource:script
injectionTime:WKUserScriptInjectionTimeAtDocumentStart
forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:cookieInScript];
}
}
if(_messagingEnabled){
if (self.postMessageScript){
[wkWebViewConfig.userContentController addScriptMessageHandler:[[RNCWeakScriptMessageDelegate alloc] initWithDelegate:self]
name:MessageHandlerName];
[wkWebViewConfig.userContentController addUserScript:self.postMessageScript];
}
// FIXME: For a separate (minor) PR: these two shouldn't be gated by messagingEnabled; just keeping consistency with previous behaviour.
if (self.atStartScript) {
[wkWebViewConfig.userContentController addUserScript:self.atStartScript];
}
if (self.atEndScript) {
[wkWebViewConfig.userContentController addUserScript:self.atEndScript];
}
}
}
- (NSURLRequest *)requestForSource:(id)json {
NSURLRequest *request = [RCTConvert NSURLRequest:self.source];

View File

@ -43,6 +43,8 @@ RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onContentProcessDidTerminate, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoaded, NSString)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptForMainFrameOnly, BOOL)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScriptBeforeContentLoadedForMainFrameOnly, BOOL)
RCT_EXPORT_VIEW_PROPERTY(javaScriptEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(allowFileAccessFromFileURLs, BOOL)
RCT_EXPORT_VIEW_PROPERTY(allowsInlineMediaPlayback, BOOL)

View File

@ -290,6 +290,8 @@ class WebView extends React.Component<IOSWebViewProps, State> {
originWhitelist,
renderError,
renderLoading,
injectedJavaScriptForMainFrameOnly = true,
injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true,
style,
containerStyle,
...otherProps
@ -344,6 +346,10 @@ class WebView extends React.Component<IOSWebViewProps, State> {
onScroll={this.props.onScroll}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onContentProcessDidTerminate={this.onContentProcessDidTerminate}
injectedJavaScript={this.props.injectedJavaScript}
injectedJavaScriptBeforeContentLoaded={this.props.injectedJavaScriptBeforeContentLoaded}
injectedJavaScriptForMainFrameOnly={injectedJavaScriptForMainFrameOnly}
injectedJavaScriptBeforeContentLoadedForMainFrameOnly={injectedJavaScriptBeforeContentLoadedForMainFrameOnly}
ref={this.webViewRef}
// TODO: find a better way to type this.
source={resolveAssetSource(this.props.source as ImageSourcePropType)}

View File

@ -299,6 +299,8 @@ export interface IOSNativeWebViewProps extends CommonNativeWebViewProps {
scrollEnabled?: boolean;
useSharedProcessPool?: boolean;
onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
injectedJavaScriptForMainFrameOnly?: boolean;
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
}
export interface MacOSNativeWebViewProps extends CommonNativeWebViewProps {
@ -495,6 +497,20 @@ export interface IOSWebViewProps extends WebViewSharedProps {
* @platform ios
*/
onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
/**
* If `true` (default), loads the `injectedJavaScript` only into the main frame.
* If `false`, loads it into all frames (e.g. iframes).
* @platform ios
*/
injectedJavaScriptForMainFrameOnly?: boolean;
/**
* If `true` (default), loads the `injectedJavaScriptBeforeContentLoaded` only into the main frame.
* If `false`, loads it into all frames (e.g. iframes).
* @platform ios
*/
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
}
export interface MacOSWebViewProps extends WebViewSharedProps {