diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts new file mode 100644 index 000000000..4a04f762a --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts @@ -0,0 +1,198 @@ +/* + 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 { ExchangeEntryStatus, TalerExchangeHttpClient, canonicalizeBaseUrl, opKnownFailureWithBody } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { BrowserFetchHttpLib } from "@gnu-taler/web-util/browser"; +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"; + +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<State> { + const [verified, setVerified] = useState<string>(); + + 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<CheckExchangeErrors>("invalid-protocol", undefined) + } + const found = used.findIndex((e) => e.exchangeBaseUrl === baseUrl.href); + if (found !== -1) { + return opKnownFailureWithBody<CheckExchangeErrors>("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<HttpResponse>' 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 (config.type === "fail") { + return opKnownFailureWithBody<CheckExchangeErrors>("not-found", undefined) + } + if (!api.isCompatible(config.body.version)) { + return opKnownFailureWithBody<CheckExchangeErrors>("invalid-version", config.body.version) + } + if (currency !== undefined && currency !== config.body.currency) { + return opKnownFailureWithBody<CheckExchangeErrors>("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<string>() + + 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<T>( + onTrigger: (v: string) => Promise<T>, + disabled: boolean, +): { + loading: boolean; + error?: Error; + value: string | undefined; + result: T | undefined; + update: (s: string) => void; +} { + const [value, setValue] = useState<string>(); + const [dirty, setDirty] = useState(false); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState<T | undefined>(undefined); + const [error, setError] = useState<Error | undefined>(undefined); + + const [handler, setHandler] = useState<number | undefined>(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 as unknown as number); + }, [value, setHandler, onTrigger]); + } + + return { + error: dirty ? error : undefined, + loading: loading, + result: result, + value: value, + update: disabled ? onTrigger : setValue, + }; +} + |