react-native-macos icon indicating copy to clipboard operation
react-native-macos copied to clipboard

WebView's injectJavaScript() method causes immediate crash

Open shirakaba opened this issue 6 years ago • 6 comments

Description

Using the very same code from the master-branch UIExplorer's WebView examples, I found that WebView.prototype.injectJavaScript() causes an immediate crash, with the error:

undefined is not an object (evaluating 'UIManager.RCTWebView.Commands.injectJavaScript')
	 in injectJavaScript(at NetworkOverlay.js:195:6)
	 in injectJS(at VirtualizedSectionList.js:399:38)
	 in invokeGuardedCallback(at <unknown file>:0)
	 in invokeGuardedCallbackAndCatchFirstError(at View.js:139:2)
	 in executeDispatch(at <unknown file>:0)
	 in executeDispatchesInOrder(at ReactFiberScheduler.js:1179:6)
	 in executeDispatchesAndRelease(at <unknown file>:0)
	 in forEachAccumulated(at <unknown file>:0)
	 in processEventQueue(at toIterator.js:124:13)
	 in runEventQueueInBatch(at backend.js:471:48)
	 in handleTopLevel(at backend.js:478:13)
	 in <unknown>(at backend.js:422:43)
	 in perform(at backend.js:1732:17)
	 in batchedUpdatesWithControlledComponents(at ReactFiberScheduler.js:944:19)
	 in _receiveRootNodeIDEvent(at backend.js:421:16)
	 in receiveEvent(at backend.js:428:16)
	 in __callFunction(at MessageQueue.js:223:23)
	 in <unknown>(at MessageQueue.js:71:6)
	 in __guard(at MessageQueue.js:195:6)
	 in callFunctionReturnFlushedQueue(at MessageQueue.js:70:11)

WebView.macos.js

So although WebView.prototype.injectJavaScript() is present, UIManager.RCTWebView.Commands.injectJavaScript() is not.

Here's react-native-macos/Libraries/Components/WebView/WebView.macos.js.

Unfortunately, I can't figure out where it's requiring UIManager from; the closest I can get is react-native-macos/lib/UIManager.js.

Reproduction Steps and Sample Code

The UIExplorer repository can be used to reproduce this, or the following minimal example can be copied:

'use strict';

var React = require('React');
var ReactNative = require('react-native');
var {
  StyleSheet,
  Button,
  View,
  WebView,
} = ReactNative;

class InjectJS extends React.Component {
  webview = null;
  injectJS = () => {
    const script = 'document.write("Injected JS ")'; // eslint-disable-line quotes
    if (this.webview) {
      this.webview.injectJavaScript(script);
    }
  };
  render() {
    return (
      <View>
        <WebView
          ref={webview => {
            this.webview = webview;
          }}
          style={{
            backgroundColor: 'rgba(255,255,255,0.8)',
            height: 300,
          }}
          source={{ uri: 'https://en.wikipedia.org' }}
          scalesPageToFit={true}
        />
        <View style={styles.buttons}>
          <Button title="Inject JS" onPress={this.injectJS} />
        </View>
      </View>
    );
  }
}

var styles = StyleSheet.create({
  buttons: {
    flexDirection: 'row',
    height: 30,
    backgroundColor: 'black',
    alignItems: 'center',
    justifyContent: 'space-between',
  }
})

Additional Information

  • React Native version: react-native-macos v0.16.1
  • Platform: macOS
  • Development Operating System: macOS
  • Dev tools: Xcode 9.3

Comments

I notice that the "Inject JavaScript" demo is not displayed in UIExplorer (nor the "Messaging Test"); are these examples not yet fully implemented, and therefore is this behaviour to be expected?

shirakaba avatar May 07 '18 11:05 shirakaba

Potential root of problem

I see there's a module bundled into index.macos.bundle.js that corresponds to react-native-macos/Libraries/ReactNative/UIManager.js. It has some role in setting up the UIManager.RCTWebView.Commands, and therefore may explain why UIManager.RCTWebView.Commands.injectJavaScript is undefined:

/**
 * Copies the ViewManager constants and commands into UIManager. This is
 * only needed for iOS, which puts the constants in the ViewManager
 * namespace instead of UIManager, unlike Android.
 */
if (Platform.OS === 'ios') {
// ...
  defineLazyObjectProperty(viewConfig, 'Commands', {
    // ...
  }
} else if (Platform.OS === 'android' && UIManager.AndroidLazyViewManagersEnabled) {
// ...
}

Just a guess, but perhaps the if (Platform.OS === 'ios') clause should extend to macos too? Will try out.

Extending UIManager.Command initialisation to macos

I replaced the platform check with:

if (Platform.OS === 'ios' || Platform.OS === 'macos')

And indeed, it stops the crash, suggesting that I'm on the right track – but my document.write("Whatever") still isn't occurring, so I'm a bit perplexed.

shirakaba avatar May 07 '18 13:05 shirakaba

Thanks for reporting this. Hopefully, this will be fixed in latest version, which is in alpha now. I'll keep you posted.

ptmt avatar May 09 '18 18:05 ptmt

@ptmt Thank you! Do you have any estimate of when the latest version's release might be?

I found that JS injection is now mysteriously working on my end. As far as I am aware, the only thing I changed, beyond the if (Platform.OS === 'ios' || Platform.OS === 'macos') amendment, is altering my Xcode project's info.plist's ATS settings by adding NSAllowsArbitraryLoadsInWebContent: YES. Of course, I was only ever working on HTTPS websites to begin with, so it's illogical that this might be exactly what fixed it. Maybe just a restart and a rebuild helped out.

Whether it would work now even without the amendment for macos, I don't know; I shall have to investigate next time I'm touching that part of the code.

@ptmt On a similar note: Not sure how much you've experimented with the WebView implementation, but did you ever find that the related injectedJavaScript prop worked? I can't get it working on my end. May be related to the StackOverflow post injectedJavaScript is not working in Webview of react native, where a commenter suggests adding the mixedContentMode prop. Seems to also suggest that the NSAllowsArbitraryLoadsInWebContent setting really did help. If I can't get it working after adding mixedContentMode, I'll file a separate issue for it.

Edit: looks like mixedContentMode is a purely Android concept, and thus didn't produce any difference.

shirakaba avatar May 09 '18 18:05 shirakaba

I have ported the react-native-wkwebview project to macOS, including the pull request that adds injectJavaScript support based on WKUserContentController.addUserScript(). While it doesn't close this issue, it's an alternative for anyone seeking both WKWebView and a reliable injectedJavaScript prop. Being a third-party module, I think it has no dependency upon the seemingly-broken UIManager.js that I patched above.

shirakaba avatar May 18 '18 13:05 shirakaba

That is amazing news! Thanks a lot. I think it's the right choice, having this as 3rd party mode. It also might be updated later to other react-native implementations, if any.

Do you have any estimate of when the latest version's release might be?

Not yet, unfortunately. It works in my case, but a few parts such text inputs are broken.

ptmt avatar May 20 '18 12:05 ptmt

A note to myself (and anyone else heading down my rather individual path here) in future: WKWebView renames the WebView.injectJavaScript() method to WebView.evaluateJavaScript(). I discovered this simply by printing out the keys on my WKWebView ref:

console.log(`Keys of WKWebView`, Object.keys(this.webView));

There was no need for me to alter react-native-macos/Libraries/ReactNative/UIManager.js after migrating to WKWebView.

Note that there is still a prop named injectJavaScript, however, which allows one to specify which JavaScript code to execute before loading the page.

shirakaba avatar May 28 '18 19:05 shirakaba