/* 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 */ import { ExchangeEntryStatus, OperationFailWithBody, OperationOk, TalerExchangeApi, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { useCallback, useEffect, useState } from "preact/hooks"; import { useBackendContext } from "../../context/backend.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { withSafe } from "../../mui/handlers.js"; import { RecursiveState } from "../../utils/index.js"; import { CheckExchangeErrors, Props, State } from "./index.js"; import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; function urlFromInput(str: string): URL { let result: URL; try { result = new URL(str) } catch (original) { try { result = new URL(`https://${str}`) } catch (e) { throw original } } if (!result.pathname.endsWith("/")) { result.pathname = result.pathname + "/"; } result.search = ""; result.hash = ""; return result; } export function useComponentState({ onBack, currency, noDebounce }: Props): RecursiveState { const [verified, setVerified] = useState(); const api = useBackendContext(); const hook = useAsyncAsHook(() => api.wallet.call(WalletApiOperation.ListExchanges, {}), ); const walletExchanges = !hook ? [] : hook.hasError ? [] : hook.response.exchanges const used = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Used); const preset = walletExchanges.filter(e => e.exchangeEntryStatus === ExchangeEntryStatus.Preset); if (!verified) { return (): State => { const checkExchangeBaseUrl_memo = useCallback(async function checkExchangeBaseUrl(str: string) { const baseUrl = urlFromInput(str) if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") { return opKnownFailureWithBody("invalid-protocol", undefined) } const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href); if (found !== -1) { return opKnownFailureWithBody("already-active", undefined); } /** * FIXME: For some reason typescript doesn't like the next BrowserFetchHttpLib * * │ src/wallet/AddExchange/state.ts(68,63): error TS2345: Argument of type 'BrowserFetchHttpLib' is not assignable to parameter of ty * │ Types of property 'fetch' are incompatible. * │ Type '(requestUrl: string, options?: HttpRequestOptions | undefined) => Promise' is not assignable to type '(ur * │ Types of parameters 'options' and 'opt' are incompatible. * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", { wi * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/http-common", { * │ Types of property 'cancellationToken' are incompatible. * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellation * │ Type 'import("$PATH/wallet.git/packages/taler-util/lib/Cancellati * │ Types have separate declarations of a private property '_isCancelled'. * */ const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserFetchHttpLib() as any); const config = await api.getConfig() if (!api.isCompatible(config.body.version)) { return opKnownFailureWithBody("invalid-version", config.body.version) } if (currency !== undefined && currency !== config.body.currency) { return opKnownFailureWithBody("invalid-currency", config.body.currency) } const keys = await api.getKeys() return keys }, [used]) const { result, value: url, loading, update, error: requestError } = useDebounce(checkExchangeBaseUrl_memo, noDebounce ?? false) const [inputError, setInputError] = useState() return { status: "verify", error: undefined, onCancel: onBack, expectedCurrency: currency, onAccept: async () => { if (!result || result.type !== "ok") return; setVerified(result.body.base_url) }, result, loading, knownExchanges: preset.map(e => new URL(e.exchangeBaseUrl)), url: { value: url ?? "", error: inputError ?? requestError, onInput: withSafe(update, (e) => { setInputError(e.message) }) }, }; } } async function onConfirm() { if (!verified) return; await api.wallet.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: canonicalizeBaseUrl(verified), forceUpdate: true, }); onBack(); } return { status: "confirm", error: undefined, onCancel: onBack, onConfirm, url: verified }; } function useDebounce( onTrigger: (v: string) => Promise, disabled: boolean, ): { loading: boolean; error?: Error; value: string | undefined; result: T | undefined; update: (s: string) => void; } { const [value, setValue] = useState(); const [dirty, setDirty] = useState(false); const [loading, setLoading] = useState(false); const [result, setResult] = useState(undefined); const [error, setError] = useState(undefined); const [handler, setHandler] = useState(undefined); if (!disabled) { useEffect(() => { if (!value) return; clearTimeout(handler); const h = setTimeout(async () => { setDirty(true); setLoading(true); try { const result = await onTrigger(value); setResult(result); setError(undefined); setLoading(false); } catch (er) { if (er instanceof Error) { setError(er); } else { // @ts-expect-error cause still not in typescript setError(new Error('unkown error on debounce', { cause: er })) } setLoading(false); setResult(undefined); } }, 500); setHandler(h); }, [value, setHandler, onTrigger]); } return { error: dirty ? error : undefined, loading: loading, result: result, value: value, update: disabled ? onTrigger : setValue, }; }