wallet-core/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts

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 ,
};
}