summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddBackupProvider/state.ts263
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";
+ }
+}