150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
/*
|
|
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 { useState, useEffect, useCallback } from "preact/hooks";
|
|
import { Props, State } from "./index.js";
|
|
import { ExchangeEntryStatus, TalerConfigResponse, TranslatedString, canonicalizeBaseUrl } from "@gnu-taler/taler-util";
|
|
import { useBackendContext } from "../../context/backend.js";
|
|
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
|
|
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
|
import { RecursiveState } from "../../utils/index.js";
|
|
import { HttpResponse, useApiContext } from "@gnu-taler/web-util/browser";
|
|
import { alertFromError } from "../../context/alert.js";
|
|
import { withSafe } from "../../mui/handlers.js";
|
|
|
|
export function useComponentState({ onBack, currency, noDebounce }: Props): RecursiveState<State> {
|
|
const [verified, setVerified] = useState<
|
|
{ url: string; config: TalerConfigResponse } | undefined
|
|
>(undefined);
|
|
|
|
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 { request } = useApiContext();
|
|
const ccc = useCallback(async (str: string) => {
|
|
const c = canonicalizeBaseUrl(str)
|
|
const found = used.findIndex((e) => e.exchangeBaseUrl === c);
|
|
if (found !== -1) {
|
|
throw Error("This exchange is already active")
|
|
}
|
|
const result = await request<TalerConfigResponse>(c, "/keys")
|
|
return result
|
|
}, [used])
|
|
const { result, value: url, update, error: requestError } = useDebounce<HttpResponse<TalerConfigResponse, unknown>>(ccc, noDebounce ?? false)
|
|
const [inputError, setInputError] = useState<string>()
|
|
|
|
return {
|
|
status: "verify",
|
|
error: undefined,
|
|
onCancel: onBack,
|
|
expectedCurrency: currency,
|
|
onAccept: async () => {
|
|
if (!url || !result || !result.ok) return;
|
|
setVerified({ url, config: result.data })
|
|
},
|
|
result,
|
|
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.url),
|
|
forceUpdate: true,
|
|
});
|
|
onBack();
|
|
}
|
|
|
|
return {
|
|
status: "confirm",
|
|
error: undefined,
|
|
onCancel: onBack,
|
|
onConfirm,
|
|
url: verified.url
|
|
};
|
|
}
|
|
|
|
|
|
|
|
function useDebounce<T>(
|
|
onTrigger: (v: string) => Promise<T>,
|
|
disabled: boolean,
|
|
): {
|
|
loading: boolean;
|
|
error?: string;
|
|
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<string | undefined>(undefined);
|
|
|
|
const [handler, setHandler] = useState<any | 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 (e) {
|
|
const errorMessage =
|
|
e instanceof Error ? e.message : `unknown error: ${e}`;
|
|
setError(errorMessage);
|
|
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 ,
|
|
};
|
|
}
|
|
|