mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
feat(native): the 'server not reachable' screen names the cause, small, for a report
A load that failed sent the app to the address screen with a general message only, and a tester's screenshot told nothing. The web view's own error (net::ERR_..., an HTTP status, NSURLErrorDomain and its code, a client certificate the proxy asked for) is kept natively and shown under the message once.
This commit is contained in:
@@ -156,6 +156,24 @@ final class TrekServerStore {
|
||||
}
|
||||
|
||||
/** Start the next launch on the bundled address screen, even with a server remembered. */
|
||||
private static final String KEY_FAILURE = "last_failure";
|
||||
|
||||
/**
|
||||
* Why the server's page did not load, in the WebView's own terms
|
||||
* ("net::ERR_NAME_NOT_RESOLVED (-2)", "HTTP 503"): the address screen shows
|
||||
* it small under its message, so a report carries the cause.
|
||||
*/
|
||||
static void setFailure(Context context, String failure) {
|
||||
prefs(context).edit().putString(KEY_FAILURE, failure).commit();
|
||||
}
|
||||
|
||||
/** The failure behind this address screen, once. */
|
||||
static String takeFailure(Context context) {
|
||||
String failure = prefs(context).getString(KEY_FAILURE, null);
|
||||
if (failure != null) prefs(context).edit().remove(KEY_FAILURE).commit();
|
||||
return failure;
|
||||
}
|
||||
|
||||
static void showShellOnce(Context context) {
|
||||
prefs(context).edit().putBoolean(KEY_SHELL_ONCE, true).commit();
|
||||
}
|
||||
|
||||
@@ -315,6 +315,8 @@ public class TrekShellPlugin extends Plugin {
|
||||
JSObject result = new JSObject();
|
||||
result.put("url", url == null ? JSONObject.NULL : url);
|
||||
result.put("previousUrl", previous == null ? JSONObject.NULL : previous);
|
||||
String failure = TrekServerStore.takeFailure(getContext());
|
||||
result.put("failure", failure == null ? JSONObject.NULL : failure);
|
||||
call.resolve(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,10 @@ class TrekWebViewClient extends BridgeWebViewClient {
|
||||
String server = TrekServerStore.getUrl(view.getContext());
|
||||
Uri serverUri = server == null ? null : Uri.parse(server);
|
||||
if (serverUri != null && TrekClientIdentity.matches(String.valueOf(serverUri.getHost()), serverUri.getPort() > 0 ? serverUri.getPort() : 443, request.getHost(), request.getPort())) {
|
||||
com.liketrek.trek.clientcert.ClientCertWatch.serverAsked(view.getContext(), server, onUnreachable);
|
||||
com.liketrek.trek.clientcert.ClientCertWatch.serverAsked(view.getContext(), server, () -> {
|
||||
TrekServerStore.setFailure(view.getContext(), "client certificate requested");
|
||||
onUnreachable.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +171,7 @@ class TrekWebViewClient extends BridgeWebViewClient {
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
TrekServerStore.setFailure(view.getContext(), error.getDescription() + " (" + error.getErrorCode() + ")");
|
||||
onUnreachable.run();
|
||||
return;
|
||||
}
|
||||
@@ -189,6 +193,7 @@ class TrekWebViewClient extends BridgeWebViewClient {
|
||||
if (!request.isForMainFrame()) {
|
||||
super.onReceivedHttpError(view, request, errorResponse);
|
||||
} else if (status >= 500 || (status >= 400 && wantedCertificate(request.getUrl()))) {
|
||||
TrekServerStore.setFailure(view.getContext(), "HTTP " + status);
|
||||
onUnreachable.run();
|
||||
} else if (status >= 400) {
|
||||
errorPageUrl = request.getUrl().toString();
|
||||
|
||||
@@ -399,6 +399,22 @@ enum TrekKeyCommands {
|
||||
enum TrekServerStore {
|
||||
private static let key = "TrekServerURL"
|
||||
|
||||
private static let failureKey = "TrekLastFailure"
|
||||
|
||||
/// Why the server's page did not load ("NSURLErrorDomain -1003"): the
|
||||
/// address screen shows it small under its message, so a report carries
|
||||
/// the cause. Taken once.
|
||||
static func noteFailure(_ error: Error) {
|
||||
let ns = error as NSError
|
||||
UserDefaults.standard.set("\(ns.domain) \(ns.code)", forKey: failureKey)
|
||||
}
|
||||
|
||||
static func takeFailure() -> String? {
|
||||
let failure = UserDefaults.standard.string(forKey: failureKey)
|
||||
UserDefaults.standard.removeObject(forKey: failureKey)
|
||||
return failure
|
||||
}
|
||||
|
||||
private static let previousKey = "TrekPreviousServerURL"
|
||||
|
||||
private static let checkedKey = "TrekServerCheck"
|
||||
@@ -813,7 +829,11 @@ final class TrekNavigationProxy: NSObject, WKNavigationDelegate {
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
inner.webView?(webView, didFinish: navigation)
|
||||
onPageShown?()
|
||||
if TrekSignInRedirect.onServer(server: TrekServerStore.url, url: webView.url) { serverShown = true }
|
||||
if TrekSignInRedirect.onServer(server: TrekServerStore.url, url: webView.url) {
|
||||
serverShown = true
|
||||
// The server's page is up: an earlier failure is no cause any more.
|
||||
_ = TrekServerStore.takeFailure()
|
||||
}
|
||||
if let welcome = TrekServerStore.takeWelcomeURL() {
|
||||
webView.load(URLRequest(url: welcome))
|
||||
}
|
||||
@@ -829,6 +849,7 @@ final class TrekNavigationProxy: NSObject, WKNavigationDelegate {
|
||||
TrekClientCertWatch.serverAsked(host: space.host, port: space.port) { [weak self, weak webView] in
|
||||
guard let self, let webView else { return }
|
||||
// Capacitor's own answer to a failed load: the address screen (errorPath).
|
||||
TrekServerStore.noteFailure(URLError(.clientCertificateRejected))
|
||||
self.inner.webView?(webView, didFailProvisionalNavigation: nil, withError: URLError(.clientCertificateRejected))
|
||||
}
|
||||
return
|
||||
@@ -853,6 +874,7 @@ final class TrekNavigationProxy: NSObject, WKNavigationDelegate {
|
||||
http.statusCode >= 400, http.statusCode < 500, let asked = TrekClientIdentity.lastRequest, !asked.served,
|
||||
let url = http.url, url.host?.lowercased() == asked.host, (url.port ?? 443) == asked.port {
|
||||
decisionHandler(.cancel)
|
||||
TrekServerStore.noteFailure(URLError(.clientCertificateRequired))
|
||||
inner.webView?(webView, didFailProvisionalNavigation: nil, withError: URLError(.clientCertificateRequired))
|
||||
return
|
||||
}
|
||||
@@ -870,11 +892,13 @@ final class TrekNavigationProxy: NSObject, WKNavigationDelegate {
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
if Self.isReplaced(error) { return }
|
||||
TrekServerStore.noteFailure(error)
|
||||
inner.webView?(webView, didFail: navigation, withError: error)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
if Self.isReplaced(error) { return }
|
||||
TrekServerStore.noteFailure(error)
|
||||
inner.webView?(webView, didFailProvisionalNavigation: navigation, withError: error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +352,7 @@ public class TrekShellPlugin: CAPPlugin, CAPBridgedPlugin, ASWebAuthenticationPr
|
||||
call.resolve([
|
||||
"url": TrekServerStore.url ?? NSNull(),
|
||||
"previousUrl": TrekServerStore.previousURL ?? NSNull(),
|
||||
"failure": TrekServerStore.takeFailure() ?? NSNull(),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -1546,7 +1546,7 @@ async function connected(url: string, signInHosts: string[] = [], welcome?: stri
|
||||
});
|
||||
}
|
||||
|
||||
function showUnreachable(server: string, cert?: ClientCertDemand): void {
|
||||
function showUnreachable(server: string, cert?: ClientCertDemand, failure?: string): void {
|
||||
const certPanel = el('div', { className: 'gate', role: 'alert', hidden: true });
|
||||
if (cert) fillCertPanel(certPanel, cert, () => void TrekShell.reload());
|
||||
const retry = el('button', { type: 'button', className: cert ? 'linkbtn' : 'btn' }, t('native.offline.retry'));
|
||||
@@ -1576,7 +1576,14 @@ function showUnreachable(server: string, cert?: ClientCertDemand): void {
|
||||
: t('native.offline.hint', { server: new URL(server).host })
|
||||
),
|
||||
],
|
||||
[certPanel, retry, change, footLinks()]
|
||||
[
|
||||
certPanel,
|
||||
retry,
|
||||
change,
|
||||
// The cause in the web view's own terms, small: what a report needs, not a second message.
|
||||
...(failure ? [el('p', { className: 'diag', dir: 'ltr' }, failure.slice(0, 120))] : []),
|
||||
footLinks(),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -1664,8 +1671,9 @@ async function start(): Promise<void> {
|
||||
letKeyboardClose();
|
||||
let url: string | null;
|
||||
let previousUrl: string | null | undefined;
|
||||
let failure: string | null | undefined;
|
||||
try {
|
||||
({ url, previousUrl } = await TrekShell.getServer());
|
||||
({ url, previousUrl, failure } = await TrekShell.getServer());
|
||||
} catch {
|
||||
// No native shell under us (a web preview, or a simulator without the
|
||||
// plugin): there is no remembered server, so start at the welcome screen
|
||||
@@ -1689,7 +1697,7 @@ async function start(): Promise<void> {
|
||||
// On iOS the stored copy of its start page brings the offline-capable UI up anyway.
|
||||
const { opened } = await TrekShell.openOfflineCopy();
|
||||
if (opened) return;
|
||||
showUnreachable(url);
|
||||
showUnreachable(url, undefined, failure ?? undefined);
|
||||
}
|
||||
}
|
||||
await SplashScreen.hide();
|
||||
|
||||
@@ -1556,3 +1556,14 @@ body.quiet::after {
|
||||
.plan-feats .plan-photos svg {
|
||||
color: var(--faint);
|
||||
}
|
||||
|
||||
/* The cause of a failed load, for a report: small, quiet, and selectable. */
|
||||
.diag {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: var(--faint);
|
||||
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ export interface TrekShellPlugin {
|
||||
* newer one (SERVER_CHECK in TrekServerStore). Set only while no server is,
|
||||
* so the address screen can check it again, pre-filled.
|
||||
*/
|
||||
getServer(): Promise<{ url: string | null; previousUrl?: string | null }>;
|
||||
/**
|
||||
* The server picked, the one before an update asked to check it again, and
|
||||
* why its page did not load last time (taken once; builds before 5.1.4 leave
|
||||
* it out).
|
||||
*/
|
||||
getServer(): Promise<{ url: string | null; previousUrl?: string | null; failure?: string | null }>;
|
||||
/**
|
||||
* Remembers the server and loads it. The current page is gone afterwards.
|
||||
* `signInHosts` are the hosts of a proxy's sign-in page in front of it
|
||||
|
||||
Reference in New Issue
Block a user