Search code examples
androidreact-nativesslreact-native-webview

React native custom webview prevent SSL errors while developing


Issue
Trying to connect to my local development server using react-native-webview on Android.
You can use http://10.0.0.2 as the default gateway to connect to your local machine on the emulator. Alternatively, I ran the build on my phone and connected to the IPV4 address of my machine.
I need the Crypto API and that's only available on HTTPS, which meant I needed to connect to https://10.0.0.2.
I didn't get the self signed certificate to work.

Quick & dirty solution
Call handler.proceed() on the first line of the onReceivedSslError method of the com.reactnativecommunity.webview package.
Although this might work, it's not the preferred way because it's not under source control and needs to be removed each time a release build is made or the project is freshly installed.

Solution
See answers below
Create a custom webview on development builds to circumvent the SSL error (or any other webview method).


Solution

  • My solution

    1. Create a debug only class CustomWebviewManager.java
    We don't want our custom webview manager ending up in release builds.

    package nl.myapp;
    
    import android.webkit.WebView;
    import android.webkit.SslErrorHandler;
    import android.net.http.SslError;
    
    import com.facebook.react.module.annotations.ReactModule;
    import com.facebook.react.uimanager.ThemedReactContext;
    import com.reactnativecommunity.webview.RNCWebViewManager;
    
    @ReactModule(name = CustomWebViewManager.REACT_CLASS)
    public class CustomWebViewManager extends RNCWebViewManager {
        protected static final String REACT_CLASS = "RNCCustomWebView"; // React native import name
    
        protected static class CustomWebviewClient extends RNCWebViewClient {
            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error){
                // Prevent SSL errors
                handler.proceed();
            }
        }
    
        @Override
        public String getName() {
            return REACT_CLASS;
        }
    
        @Override
        protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
            // Set our custom client as webview client
            view.setWebViewClient(new CustomWebviewClient());
        }
    }
    

    2. Create a new class to register React Packages MyappAppPackage.java
    We should register our debug class only on debug mode and as a ViewManager.
    React Native will call createViewManagers automatically after registering our package in the next step
    Use BuildConfig.DEBUG to only execute in debug builds.
    We cannot instantiate our class directly, as it will cause errors in productions builds. That's why we use Class.forName

    package nl.myapp;
    
    import com.facebook.react.ReactPackage;
    import com.facebook.react.bridge.NativeModule;
    import com.facebook.react.bridge.ReactApplicationContext;
    import com.facebook.react.uimanager.ViewManager;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class MyappAppPackage implements ReactPackage {
    
        @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            List<ViewManager> viewManagers = new ArrayList<>();
    
            if (BuildConfig.DEBUG) {
                // Add custom webview manager to circumvent SSL errors
                try {
                    Class<ViewManager> c = (Class<ViewManager>) Class.forName("nl.myapp.CustomWebViewManager");
                    viewManagers.add(c.newInstance());
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
    
            return viewManagers;
        }
    
        @Override
        public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
            return Collections.emptyList();
        }
    }
    

    3. Register our package

    package nl.myapp;
    
    import android.app.Application;
    
    import com.facebook.react.PackageList;
    import com.facebook.react.ReactApplication;
    import com.facebook.react.ReactNativeHost;
    import com.facebook.react.ReactPackage;
    import java.util.List;
    
    public class MainApplication extends Application implements ReactApplication {
        private final ReactNativeHost mReactNativeHost =
            new ReactNativeHost(this) {
              @Override
              public boolean getUseDeveloperSupport() {
                  return BuildConfig.DEBUG;
              }
    
              @Override
              protected List<ReactPackage> getPackages() {
                  @SuppressWarnings("UnnecessaryLocalVariable")
                  List<ReactPackage> packages = new PackageList(this).getPackages();
    
                  packages.add(new MyappAppPackage());
    
                  return packages;
              }
    
              @Override
              protected String getJSMainModuleName() {
                  return "index";
              }
            };
    
        @Override
        public ReactNativeHost getReactNativeHost() {
            return mReactNativeHost;
        }
    
        ...
    }
    

    4. And finally use our custom webview while developing
    Put it in separate file, otherwise hot-reloading won't work because requireNativeComponent would try to register the ViewManager again
    src/components/native/RNCCustomWebView.js

    import {requireNativeComponent} from 'react-native';
    
    module.exports = requireNativeComponent('RNCCustomWebView');
    

    src/components/webview.js

    import React from 'react';
    import { WebView } from 'react-native-webview';
    import RNCCustomWebView from './native/RNCCustomWebView';
    
    // ...
    
    const renderWebView = () => {
      // ...
    
      const nativeConfig = {};
      if (__DEV__) { // __DEV__ is set by RN on debug builds
        // Set custom component to circumvent SSL errors
        nativeConfig.component = RNCCustomWebView;
      }
    
      return (
        <WebView
          nativeConfig={nativeConfig}
          source="https://10.0.0.1"
        />
      );
    };