Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option for custom error page #5723

Merged
merged 25 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8a76d2d
adding android.errorUrl
theproducer Jun 23, 2022
e04b711
Renaming errorUrl to errorPath
theproducer Jun 23, 2022
878332b
Using Thomas’s minimum webview check
theproducer Jun 23, 2022
f209acd
displaying error page on webview error
theproducer Jun 24, 2022
49204f4
Adding ios support for errorPath config option
theproducer Jun 24, 2022
0ceae71
redirect to error page in case of webkit errors
theproducer Jun 24, 2022
fd75f4d
tweaking onReceivedError error page logic
theproducer Jun 27, 2022
ebcf085
docs
theproducer Jun 27, 2022
a1f3557
fmt
theproducer Jun 27, 2022
b45cd5e
improvements to `isMinimumWebViewInstalled`
theproducer Jun 30, 2022
994c77e
moving `ios|android.errorPath` to `server.errorPath`
theproducer Jun 30, 2022
4e77723
Merge branch 'main' into feat/webview-error-page
theproducer Jun 30, 2022
286d40b
fmt
theproducer Jun 30, 2022
7c5a9bd
Merge branch 'main' into feat/webview-error-page
theproducer Jun 30, 2022
791debc
using local url for building error path
theproducer Jul 5, 2022
76b269b
dont inject js on error pages
theproducer Jul 5, 2022
f448b93
using local url for building error path (iOS)
theproducer Jul 5, 2022
8aa8f11
Merge branch 'main' into feat/webview-error-page
theproducer Jul 5, 2022
378ca15
Merge branch 'main' into feat/webview-error-page
jcesarmobile Jul 11, 2022
9ff138e
making isMinimumWebViewInstalled public
theproducer Jul 11, 2022
cea08a4
fixing typo
theproducer Jul 11, 2022
0184c50
Merge branch 'main' into feat/webview-error-page
theproducer Jul 14, 2022
a901b52
checking errorUrl in shouldInterceptRequest
theproducer Jul 14, 2022
e096757
also redirect to error page from onReceivedHttpError
theproducer Jul 14, 2022
b610167
fmt
theproducer Jul 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 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")
private boolean isMinimumWebViewInstalled() {
theproducer marked this conversation as resolved.
Show resolved Hide resolved
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,20 @@ public String getServerUrl() {
return this.config.getServerUrl();
}

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

if (errorPath != null && !errorPath.trim().isEmpty()) {
return appUrl + 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,15 @@ public void onReceivedError(WebView view, WebResourceRequest request, WebResourc
listener.onReceivedError(view);
}
}

String errorPath = bridge.getErrorUrl();
String appUrl = bridge.getAppUrl();
if (appUrl.charAt(appUrl.length() - 1) != '/') {
appUrl += "/";
}
if (errorPath != null && request.getUrl().toString().equals(appUrl)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this compare if the url to load is different from the appUrl? on iOS it's not comparing that.
If it's because when loading the error page the app enters into a loop of failures because of favicon.ico not loading, I think it's best to check if the request was to the main frame
if (errorPath != null && request.isForMainFrame()) {.

Copy link
Contributor Author

@theproducer theproducer Jul 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's because when loading the error page the app enters into a loop of failures..

Yes, this was exactly why. Thanks!

view.loadUrl(errorPath);
}
}

@Override
Expand Down
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 setErrorUPath(String errorPath) {
theproducer marked this conversation as resolved.
Show resolved Hide resolved
this.errorPath = errorPath;
return this;
}

public Builder setHostname(String hostname) {
this.hostname = hostname;
return this;
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 appStartServerURL.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 @@ -132,13 +132,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