wallet-core/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
Sebastian 4a781bd0dd
fix #7153: more error handling
if handler do not trap error then fail at compile time,
all safe handlers push alert on error
errors are typed so they render good information
2023-01-09 20:20:09 -03:00

260 lines
6.6 KiB
TypeScript

/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { canonicalizeBaseUrl, Codec } from "@gnu-taler/taler-util";
import {
codecForSyncTermsOfServiceResponse,
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import { useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { assertUnreachable } from "../../utils/index.js";
import { Props, State } from "./index.js";
type UrlState<T> = UrlOk<T> | UrlError;
interface UrlOk<T> {
status: "ok";
result: T;
}
type UrlError =
| UrlNetworkError
| UrlClientError
| UrlServerError
| UrlParsingError
| UrlReadError;
interface UrlNetworkError {
status: "network-error";
href: string;
}
interface UrlClientError {
status: "client-error";
code: number;
}
interface UrlServerError {
status: "server-error";
code: number;
}
interface UrlParsingError {
status: "parsing-error";
json: any;
}
interface UrlReadError {
status: "url-error";
}
function useDebounceEffect(
time: number,
cb: undefined | (() => Promise<void>),
deps: Array<any>,
): void {
const [currentTimer, setCurrentTimer] = useState<any>();
useEffect(() => {
if (currentTimer !== undefined) clearTimeout(currentTimer);
if (cb !== undefined) {
const tid = setTimeout(cb, time);
setCurrentTimer(tid);
}
}, deps);
}
function useUrlState<T>(
host: string | undefined,
path: string,
codec: Codec<T>,
): UrlState<T> | undefined {
const [state, setState] = useState<UrlState<T> | undefined>();
let href: string | undefined;
try {
if (host) {
const isHttps =
host.startsWith("https://") && host.length > "https://".length;
const isHttp =
host.startsWith("http://") && host.length > "http://".length;
const withProto = isHttp || isHttps ? host : `https://${host}`;
const baseUrl = canonicalizeBaseUrl(withProto);
href = new URL(path, baseUrl).href;
}
} catch (e) {
setState({
status: "url-error",
});
}
const constHref = href;
useDebounceEffect(
500,
constHref == undefined
? undefined
: async () => {
const req = await fetch(constHref).catch((e) => {
return setState({
status: "network-error",
href: constHref,
});
});
if (!req) return;
if (req.status >= 400 && req.status < 500) {
setState({
status: "client-error",
code: req.status,
});
return;
}
if (req.status > 500) {
setState({
status: "server-error",
code: req.status,
});
return;
}
const json = await req.json();
try {
const result = codec.decode(json);
setState({ status: "ok", result });
} catch (e: any) {
setState({ status: "parsing-error", json });
}
},
[host, path],
);
return state;
}
export function useComponentState({
onBack,
onComplete,
onPaymentRequired,
}: Props): State {
const api = useBackendContext();
const [url, setHost] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [tos, setTos] = useState(false);
const { pushAlertOnError } = useAlertContext();
const urlState = useUrlState(
url,
"config",
codecForSyncTermsOfServiceResponse(),
);
const [showConfirm, setShowConfirm] = useState(false);
async function addBackupProvider(): Promise<void> {
if (!url || !name) return;
const resp = await api.wallet.call(WalletApiOperation.AddBackupProvider, {
backupProviderBaseUrl: url,
name: name,
activate: true,
});
switch (resp.status) {
case "payment-required":
if (resp.talerUri) {
return onPaymentRequired(resp.talerUri);
} else {
return onComplete(url);
}
case "ok":
return onComplete(url);
default:
assertUnreachable(resp);
}
}
if (showConfirm && urlState && urlState.status === "ok") {
return {
status: "confirm-provider",
error: undefined,
onAccept: {
onClick: !tos ? undefined : pushAlertOnError(addBackupProvider),
},
onCancel: {
onClick: pushAlertOnError(onBack),
},
provider: urlState.result,
tos: {
value: tos,
button: {
onClick: pushAlertOnError(async () => setTos(!tos)),
},
},
url: url ?? "",
};
}
return {
status: "select-provider",
error: undefined,
name: {
value: name || "",
onInput: pushAlertOnError(async (e) => setName(e)),
error:
name === undefined ? undefined : !name ? "Can't be empty" : undefined,
},
onCancel: {
onClick: pushAlertOnError(onBack),
},
onConfirm: {
onClick:
!urlState || urlState.status !== "ok" || !name
? undefined
: pushAlertOnError(async () => {
setShowConfirm(true);
}),
},
urlOk: urlState?.status === "ok",
url: {
value: url || "",
onInput: pushAlertOnError(async (e) => setHost(e)),
error: errorString(urlState),
},
};
}
function errorString(state: undefined | UrlState<any>): string | undefined {
if (!state) return state;
switch (state.status) {
case "ok":
return undefined;
case "client-error": {
switch (state.code) {
case 404:
return "Not found";
case 401:
return "Unauthorized";
case 403:
return "Forbidden";
default:
return `Server says it a client error: ${state.code}.`;
}
}
case "server-error":
return `Server had a problem ${state.code}.`;
case "parsing-error":
return `Server response doesn't have the right format.`;
case "network-error":
return `Unable to connect to ${state.href}.`;
case "url-error":
return "URL is not complete";
}
}