Skip to content

Commit

Permalink
feat: Add option for custom error page (#5723)
Browse files Browse the repository at this point in the history
  • Loading branch information
theproducer committed Jul 14, 2022
1 parent df77103 commit e8bdef3
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 2 deletions.
73 changes: 73 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/Bridge.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.getcapacitor;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
Expand All @@ -10,6 +11,7 @@
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
Expand Down Expand Up @@ -73,6 +75,8 @@ public class Bridge {
private static final String BUNDLE_PLUGIN_CALL_BUNDLE_KEY = "capacitorLastPluginCallBundle";
private static final String LAST_BINARY_VERSION_CODE = "lastBinaryVersionCode";
private static final String LAST_BINARY_VERSION_NAME = "lastBinaryVersionName";
private final int MINIMUM_ANDROID_WEBVIEW_VERSION = 60;
private static final String MINIMUM_ANDROID_WEBVIEW_ERROR = "System WebView is not supported";

// The name of the directory we use to look for index.html and the rest of our web assets
public static final String DEFAULT_WEB_ASSET_DIR = "public";
Expand Down Expand Up @@ -257,10 +261,60 @@ private void loadWebView() {
setServerBasePath(path);
}
}

if (!this.isMinimumWebViewInstalled()) {
String errorUrl = this.getErrorUrl();
if (errorUrl != null) {
webView.loadUrl(errorUrl);
return;
} else {
Logger.error(MINIMUM_ANDROID_WEBVIEW_ERROR);
}
}

// Get to work
webView.loadUrl(appUrl);
}

@SuppressLint("WebViewApiAvailability")
public boolean isMinimumWebViewInstalled() {
PackageManager pm = getContext().getPackageManager();

// Check getCurrentWebViewPackage() directly if above Android 8
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PackageInfo info = WebView.getCurrentWebViewPackage();
String majorVersionStr = info.versionName.split("\\.")[0];
int majorVersion = Integer.parseInt(majorVersionStr);
return majorVersion >= MINIMUM_ANDROID_WEBVIEW_VERSION;
}

// Otherwise manually check WebView versions
try {
String webViewPackage = "com.google.android.webview";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
webViewPackage = "com.android.chrome";
}
PackageInfo info = pm.getPackageInfo(webViewPackage, 0);
String majorVersionStr = info.versionName.split("\\.")[0];
int majorVersion = Integer.parseInt(majorVersionStr);
return majorVersion >= MINIMUM_ANDROID_WEBVIEW_VERSION;
} catch (Exception ex) {
Logger.warn("Unable to get package info for 'com.google.android.webview'" + ex.toString());
}

try {
PackageInfo info = pm.getPackageInfo("com.android.webview", 0);
String majorVersionStr = info.versionName.split("\\.")[0];
int majorVersion = Integer.parseInt(majorVersionStr);
return majorVersion >= MINIMUM_ANDROID_WEBVIEW_VERSION;
} catch (Exception ex) {
Logger.warn("Unable to get package info for 'com.android.webview'" + ex.toString());
}

// Could not detect any webview, return false
return false;
}

public boolean launchIntent(Uri url) {
/*
* Give plugins the chance to handle the url
Expand Down Expand Up @@ -408,6 +462,25 @@ public String getServerUrl() {
return this.config.getServerUrl();
}

public String getErrorUrl() {
String errorPath = this.config.getErrorPath();

if (errorPath != null && !errorPath.trim().isEmpty()) {
String authority = this.getHost();
String scheme = this.getScheme();

String localUrl = scheme + "://" + authority;

return localUrl + "/" + errorPath;
}

return null;
}

public String getAppUrl() {
return appUrl;
}

public CapConfig getConfig() {
return this.config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ public void onReceivedError(WebView view, WebResourceRequest request, WebResourc
listener.onReceivedError(view);
}
}

String errorPath = bridge.getErrorUrl();
if (errorPath != null && request.isForMainFrame()) {
view.loadUrl(errorPath);
}
}

@Override
Expand All @@ -81,5 +86,10 @@ public void onReceivedHttpError(WebView view, WebResourceRequest request, WebRes
listener.onReceivedHttpError(view);
}
}

String errorPath = bridge.getErrorUrl();
if (errorPath != null && request.isForMainFrame()) {
view.loadUrl(errorPath);
}
}
}
13 changes: 13 additions & 0 deletions android/capacitor/src/main/java/com/getcapacitor/CapConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class CapConfig {
private boolean webContentsDebuggingEnabled = false;
private boolean loggingEnabled = true;
private boolean initialFocus = true;
private String errorPath;

// Embedded
private String startPath;
Expand Down Expand Up @@ -124,6 +125,7 @@ private CapConfig(Builder builder) {
this.webContentsDebuggingEnabled = builder.webContentsDebuggingEnabled;
this.loggingEnabled = builder.loggingEnabled;
this.initialFocus = builder.initialFocus;
this.errorPath = builder.errorPath;

// Embedded
this.startPath = builder.startPath;
Expand Down Expand Up @@ -156,6 +158,7 @@ private void deserializeConfig(@Nullable Context context) {
html5mode = JSONUtils.getBoolean(configJSON, "server.html5mode", html5mode);
serverUrl = JSONUtils.getString(configJSON, "server.url", null);
hostname = JSONUtils.getString(configJSON, "server.hostname", hostname);
errorPath = JSONUtils.getString(configJSON, "server.errorPath", null);

String configSchema = JSONUtils.getString(configJSON, "server.androidScheme", androidScheme);
if (this.validateScheme(configSchema)) {
Expand Down Expand Up @@ -224,6 +227,10 @@ public String getServerUrl() {
return serverUrl;
}

public String getErrorPath() {
return errorPath;
}

public String getHostname() {
return hostname;
}
Expand Down Expand Up @@ -415,6 +422,7 @@ public static class Builder {
// Server Config Values
private boolean html5mode = true;
private String serverUrl;
private String errorPath;
private String hostname = "localhost";
private String androidScheme = CAPACITOR_HTTP_SCHEME;
private String[] allowNavigation;
Expand Down Expand Up @@ -472,6 +480,11 @@ public Builder setServerUrl(String serverUrl) {
return this;
}

public Builder setErrorPath(String errorPath) {
this.errorPath = errorPath;
return this;
}

public Builder setHostname(String hostname) {
this.hostname = hostname;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
return null;
}

if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl)) {
if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl) || isErrorUrl(loadingUrl)) {
Logger.debug("Handling local request: " + request.getUrl().toString());
return handleLocalRequest(request, handler);
} else {
Expand All @@ -185,6 +185,11 @@ private boolean isLocalFile(Uri uri) {
return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart);
}

private boolean isErrorUrl(Uri uri) {
String url = uri.toString();
return url.equals(bridge.getErrorUrl());
}

private boolean isMainUrl(Uri loadingUrl) {
return (bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost()));
}
Expand Down Expand Up @@ -226,7 +231,7 @@ private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathH
);
}

if (isLocalFile(request.getUrl())) {
if (isLocalFile(request.getUrl()) || isErrorUrl(request.getUrl())) {
InputStream responseStream = new LollipopLazyInputStream(handler, request);
String mimeType = getMimeType(request.getUrl().getPath(), responseStream);
int statusCode = getStatusCode(responseStream, handler.getStatusCode());
Expand Down
8 changes: 8 additions & 0 deletions cli/src/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,14 @@ export interface CapacitorConfig {
* @default []
*/
allowNavigation?: string[];

/**
* Specify path to a local html page to display in case of errors.
*
* @since 4.0.0
* @default null
*/
errorPath?: string;
};

cordova?: {
Expand Down
1 change: 1 addition & 0 deletions ios/Capacitor/Capacitor/CAPInstanceConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ NS_SWIFT_NAME(InstanceConfiguration)
@property (nonatomic, readonly, nonnull) NSArray<NSString*> *allowedNavigationHostnames;
@property (nonatomic, readonly, nonnull) NSURL *localURL;
@property (nonatomic, readonly, nonnull) NSURL *serverURL;
@property (nonatomic, readonly, nullable) NSString *errorPath;
@property (nonatomic, readonly, nonnull) NSDictionary *pluginConfigurations;
@property (nonatomic, readonly) BOOL loggingEnabled;
@property (nonatomic, readonly) BOOL scrollingEnabled;
Expand Down
2 changes: 2 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ - (instancetype)initWithDescriptor:(CAPInstanceDescriptor *)descriptor isDebug:(
else {
_serverURL = _localURL;
}
_errorPath = descriptor.errorPath;
// extract the one value we care about from the cordova configuration
_cordovaDeployDisabled = [descriptor cordovaDeployDisabled];
}
Expand All @@ -60,6 +61,7 @@ - (instancetype)initWithConfiguration:(CAPInstanceConfiguration*)configuration a
_allowedNavigationHostnames = [[configuration allowedNavigationHostnames] copy];
_localURL = [[configuration localURL] copy];
_serverURL = [[configuration serverURL] copy];
_errorPath = [[configuration errorPath] copy];
_pluginConfigurations = [[configuration pluginConfigurations] copy];
_loggingEnabled = configuration.loggingEnabled;
_scrollingEnabled = configuration.scrollingEnabled;
Expand Down
8 changes: 8 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ extension InstanceConfiguration {
return serverURL
}

@objc var errorPathURL: URL? {
guard let errorPath = errorPath else {
return nil
}

return localURL.appendingPathComponent(errorPath)
}

@available(*, deprecated, message: "Use getPluginConfig")
@objc public func getPluginConfigValue(_ pluginId: String, _ configKey: String) -> Any? {
return (pluginConfigurations as? JSObject)?[keyPath: KeyPath("\(pluginId).\(configKey)")]
Expand Down
5 changes: 5 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceDescriptor.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ NS_SWIFT_NAME(InstanceDescriptor)
@discussion Defaults to @c capacitor. Set by @c server.iosScheme in the configuration file.
*/
@property (nonatomic, copy, nullable) NSString *urlScheme;
/**
@brief The path to a local html page to display in case of errors.
@discussion Defaults to nil.
*/
@property (nonatomic, copy, nullable) NSString *errorPath;
/**
@brief The hostname that will be used for the server URL.
@discussion Defaults to @c localhost. Set by @c server.hostname in the configuration file.
Expand Down
3 changes: 3 additions & 0 deletions ios/Capacitor/Capacitor/CAPInstanceDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ internal extension InstanceDescriptor {
if let urlString = config[keyPath: "server.url"] as? String {
serverURL = urlString
}
if let errorPathString = (config[keyPath: "server.errorPath"] as? String) {
errorPath = errorPathString
}
if let insetBehavior = config[keyPath: "ios.contentInset"] as? String {
let availableInsets: [String: UIScrollView.ContentInsetAdjustmentBehavior] = ["automatic": .automatic,
"scrollableAxes": .scrollableAxes,
Expand Down
9 changes: 9 additions & 0 deletions ios/Capacitor/Capacitor/WebViewDelegationHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,22 @@ internal class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDel
webView.isOpaque = isOpaque
webViewLoadingState = .subsequentLoad
}

if let errorURL = bridge?.config.errorPathURL {
webView.load(URLRequest(url: errorURL))
}

CAPLog.print("⚡️ WebView failed to load")
CAPLog.print("⚡️ Error: " + error.localizedDescription)
}

// The force unwrap is part of the protocol declaration, so we should keep it.
// swiftlint:disable:next implicitly_unwrapped_optional
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
if let errorURL = bridge?.config.errorPathURL {
webView.load(URLRequest(url: errorURL))
}

CAPLog.print("⚡️ WebView failed provisional navigation")
CAPLog.print("⚡️ Error: " + error.localizedDescription)
}
Expand Down

0 comments on commit e8bdef3

Please sign in to comment.