0

(Regarding this question)

I'd like to create a component in React Native that has exactly the same functionality that the native WebView, just overriding some of its methods.

I've followed the tutorials in React Native page, with modules and UI Views (which I think is the correct approach), but I'm not able to get what I'd like.

What I have been able to do:

// .../com/project/permissionwebview/    
public class PermissionWebviewView extends LinearLayout{

    private Context context;

    public PermissionWebviewView(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public void init() {
        inflate(this.context, R.layout.permissionwebview, this);
    }
}

And my View manager:

// .../com/project/permissionwebview
public class PermissionWebviewViewManager extends SimpleViewManager<PermissionWebviewView> {

    public static final String REACT_CLASS = "PermissionWebviewViewManager";

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    public PermissionWebviewView createViewInstance(ThemedReactContext context) {
        return new PermissionWebviewView(context); //If your customview has more constructor parameters pass it from here.
    }
}

The XML of the layout:

// .../res/layout/permissionwebview.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    <WebView android:id="@+id/permission_webview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
    />
</LinearLayout>

I am able to call it from React Native, but nothing is shown and nothing happens.

What I want is simply a custom element which calls the native one (in this case, WebView), so it renders exactly the same (same props, same methods), just overriding some of its methods when necessary (in my case, onPermissionRequest()).

Which would be the correct approach? How (and where) do I invoke the parent/super WebView?

EDIT 1

I think I've made it run, but I don't know if it's the correct approach, as I still don't know how to pass all the parameters from React Native component (<PermissionWebview />) to the native WebView element:

// .../com/project/permissionwebview/
public class PermissionWebviewView extends LinearLayout{

    private Context context;
    private WebView myWebView;

    public PermissionWebviewView(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public void init() {
        inflate(this.context, R.layout.permissionwebview, this);

        myWebView = (WebView)findViewById(R.id.permission_webview);

        myWebView.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onPermissionRequest(final PermissionRequest request) {
                request.grant(request.getResources());
            }
        });

        myWebView.loadUrl("https://staticWebSite.Until/Parameters/Are/Passed");
    }
}

Thank you!

Unapedra
  • 2,043
  • 4
  • 25
  • 42

1 Answers1

2

I was able to ultimately get an extended WebView working on Android by starting with this information in the react-native-website repo on GitHub. A lot of this code comes from both the sample at the link as well as the actual React Native implementation of WebView.

I plan to submit a pull request with some changes that I needed to do in order to actually build the Java, and also to retrieve the needed reference to the actual WebView that I'm wrapping (not everyone will need this).

Note that some of the classes are nested and the names need to be further qualified than in the example from GitHub:

Here is what I had to do. In my case I needed to add App Cache Manifest functionality to the WebView. You probably won't need to have all of these import statements, and may need to add others:

CustomWebViewManager.java:

import android.os.Build;
import android.view.ViewGroup.LayoutParams;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.CookieManager;

import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.webview.WebViewConfig;
import com.facebook.react.views.webview.ReactWebViewManager;

@ReactModule(name = CustomWebViewManager.REACT_CLASS)
public class CustomWebViewManager extends ReactWebViewManager {
  protected static final String REACT_CLASS = "MyCustomWebView";

  protected static class CustomReactWebViewClient extends ReactWebViewManager.ReactWebViewClient { }

  protected static class CustomReactWebView extends ReactWebViewManager.ReactWebView {
    public CustomReactWebView(ThemedReactContext reactContext) {
      super(reactContext);
    }
  }

  public CustomWebViewManager() {
    mWebViewConfig = new WebViewConfig() {
      public void configWebView(WebView webView) {
      }
    };
  }

  public CustomWebViewManager(WebViewConfig webViewConfig) {
    mWebViewConfig = webViewConfig;
  }

  @Override
  public String getName() {
    return REACT_CLASS;
  }

  @Override
  protected ReactWebView createReactWebViewInstance(ThemedReactContext reactContext) {
    return new CustomReactWebView(reactContext);
  }

  // I had to override this in order to enable my needed WebView functionality
  @Override
  protected WebView createViewInstance(ThemedReactContext reactContext) {
    ReactWebView webView = createReactWebViewInstance(reactContext);

    webView.setWebChromeClient(new WebChromeClient() {
      @Override
      public boolean onConsoleMessage(ConsoleMessage message) {
        if (ReactBuildConfig.DEBUG) {
          return super.onConsoleMessage(message);
        }
        // Ignore console logs in non debug builds.
        return true;
      }

      @Override
      public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
        callback.invoke(origin, true, false);
      }
    });
    reactContext.addLifecycleEventListener(webView);
    mWebViewConfig.configWebView(webView);
    webView.getSettings().setBuiltInZoomControls(true);
    webView.getSettings().setDisplayZoomControls(false);
    webView.getSettings().setDomStorageEnabled(true);

    webView.getSettings().setAppCacheMaxSize(1024*1024*8);
    webView.getSettings().setAllowFileAccess(true);
    webView.getSettings().setAppCacheEnabled(true);
    // not sure if the below is needed, but at least one post indicated that it was so I did this with my package name to be safe
    webView.getSettings().setAppCachePath("data/data/<your package name here>/cache");
    webView.getSettings().setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
    webView.getSettings().setAllowFileAccessFromFileURLs(true);

    // Fixes broken full-screen modals/galleries due to body height being 0.
    webView.setLayoutParams(
            new LayoutParams(LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT));

    if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      WebView.setWebContentsDebuggingEnabled(true);
    }

    return webView;
  }

  @Override
  protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
    // Do not register default touch emitter and let WebView implementation handle touches
    view.setWebViewClient(new CustomReactWebViewClient());
  }
}

CustomWebViewPackage.java

  import java.util.Arrays;
  import java.util.Collections;
  import java.util.ArrayList;
  import java.util.List;

  import com.facebook.react.ReactPackage;
  import com.facebook.react.bridge.NativeModule;
  import com.facebook.react.bridge.ReactApplicationContext;
  import com.facebook.react.uimanager.ViewManager;

  public class CustomWebViewPackage implements ReactPackage {
      @Override
      public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Collections.emptyList();
      }

      @Override
      public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(new CustomWebViewManager());
      }
  }

CustomWebView.jsx

  'use strict';

  import * as PropTypes from 'prop-types';
  import * as React from 'react';
  import {
    requireNativeComponent,
    WebView
  } from 'react-native';

  /**
   * Renders a native WebView via our custom wrapper. 
   * Necessary because the RN WebView does not turn on the App Cache functionality by default. 
   * Can't make this a .tsx file due to TS problems around spreading the this.props into the child WebView.
   */
  class CustomWebView extends React.Component {
    static propTypes = WebView.propTypes;

    render() {
      return (
        <WebView 
          {...this.props}
          ref={(view) => {
            this.props.wrappedRef(view);
          }}
          nativeConfig={{component: MyCustomWebView}} 
        />
      );
    }
  }

  const MyCustomWebView = requireNativeComponent('MyCustomWebView', CustomWebView, 
    WebView.extraNativeComponentConfig);

  export default CustomWebView;

Note the wrappedRef property. I added this because the consumer of my CustomWebView needs a reference to the WebView that's actually being rendered. This was due to it needing to reference functionality there such as postMessage(). The consumer passes an arrow function which sets the reference to a property in the consuming class. That function is invoked in what is passed to ref as you can see.

bcr
  • 1,983
  • 27
  • 30