summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/AddExchange/state.ts198
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,
+ };
+}
+