ptmt / react-native-macos

[deprecated in favor of https://microsoft.github.io/react-native-windows/] React Native for macOS is an experimental fork for writing desktop apps using Cocoa
MIT License
11.25k stars 429 forks source link

WebView's injectJavaScript() method causes immediate crash #195

Open shirakaba opened 6 years ago

shirakaba commented 6 years ago

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

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 commented 6 years ago

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.

ptmt commented 6 years ago

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

shirakaba commented 6 years ago

@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 commented 6 years ago

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.

ptmt commented 6 years ago

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.

shirakaba commented 6 years ago

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.