diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts new file mode 100644 index 000000000..75b8e53c0 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts @@ -0,0 +1,263 @@ +/* + 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, + codecForSyncTermsOfServiceResponse, +} from "@gnu-taler/taler-util"; +import { 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; + + async function checkURL() { + if (!constHref) { + return; + } + 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 }); + } + } + + useDebounceEffect( + 500, + constHref == undefined ? undefined : checkURL, + [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"; + } +} |