/*
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 { BrowserHttpLib } 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 {
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);
}
const api = new TalerExchangeHttpClient(baseUrl.href, new BrowserHttpLib() 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,
};
}