From caa9a22d6970df331eebed032b9a9673d4217fc6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 6 Dec 2021 15:27:20 -0300 Subject: check timeout when doing a query to /keys to add an exchange --- .../taler-wallet-webextension/src/utils/index.ts | 25 ++++- .../src/wallet/ExchangeAddPage.tsx | 11 +- .../src/wallet/ExchangeAddSetUrl.stories.tsx | 40 ++++--- .../src/wallet/ExchangeSetUrl.tsx | 118 +++++++++++++-------- 4 files changed, 130 insertions(+), 64 deletions(-) diff --git a/packages/taler-wallet-webextension/src/utils/index.ts b/packages/taler-wallet-webextension/src/utils/index.ts index 8eb89d58f..88f9bc4b3 100644 --- a/packages/taler-wallet-webextension/src/utils/index.ts +++ b/packages/taler-wallet-webextension/src/utils/index.ts @@ -43,14 +43,37 @@ export async function queryToSlashConfig( .then(getJsonIfOk); } +function timeout(ms: number, promise: Promise): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Timeout: the query took longer than ${Math.floor(ms / 1000)} secs`)) + }, ms) + + promise + .then(value => { + clearTimeout(timer) + resolve(value) + }) + .catch(reason => { + clearTimeout(timer) + reject(reason) + }) + }) +} + export async function queryToSlashKeys( url: string, ): Promise { - return fetch(new URL("keys", url).href) + const endpoint = new URL("keys", url) + endpoint.searchParams.set("cacheBreaker", new Date().getTime() + ""); + + const query = fetch(endpoint.href) .catch(() => { throw new Error(`Network error`); }) .then(getJsonIfOk); + + return timeout(3000, query) } export function buildTermsOfServiceState(tos: GetExchangeTosResult): TermsState { diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx index 0c8336e69..6dbdf4c30 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx @@ -47,8 +47,15 @@ export function ExchangeAddPage({ onBack }: Props): VNode { return ( queryToSlashKeys(url)} + onVerify={async (url) => { + const found = + knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1; + + if (found) { + throw Error("This exchange is already known"); + } + return queryToSlashKeys(url); + }} onConfirm={(url) => queryToSlashKeys(url) .then((config) => { diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx index 9ea800fe4..6f0a58729 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx @@ -36,33 +36,39 @@ export default { export const ExpectedUSD = createExample(TestedComponent, { expectedCurrency: "USD", onVerify: queryToSlashKeys, - knownExchanges: [], }); export const ExpectedKUDOS = createExample(TestedComponent, { expectedCurrency: "KUDOS", onVerify: queryToSlashKeys, - knownExchanges: [], }); export const InitialState = createExample(TestedComponent, { onVerify: queryToSlashKeys, - knownExchanges: [], }); -export const WithDemoAsKnownExchange = createExample(TestedComponent, { - knownExchanges: [ - { - currency: "TESTKUDOS", - exchangeBaseUrl: "https://exchange.demo.taler.net/", - tos: { - currentVersion: "1", - acceptedVersion: "1", - content: "content of tos", - contentType: "text/plain", - }, - paytoUris: [], +const knownExchanges = [ + { + currency: "TESTKUDOS", + exchangeBaseUrl: "https://exchange.demo.taler.net/", + tos: { + currentVersion: "1", + acceptedVersion: "1", + content: "content of tos", + contentType: "text/plain", }, - ], - onVerify: queryToSlashKeys, + paytoUris: [], + }, +]; + +export const WithDemoAsKnownExchange = createExample(TestedComponent, { + onVerify: async (url) => { + const found = + knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1; + + if (found) { + throw Error("This exchange is already known"); + } + return queryToSlashKeys(url); + }, }); diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx index e87a8894f..d529d162b 100644 --- a/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx @@ -17,52 +17,75 @@ import { export interface Props { initialValue?: string; expectedCurrency?: string; - knownExchanges: ExchangeListItem[]; onCancel: () => void; onVerify: (s: string) => Promise; onConfirm: (url: string) => Promise; withError?: string; } +function useEndpointStatus( + endpoint: string, + onVerify: (e: string) => Promise, +): { + loading: boolean; + error?: string; + endpoint: string; + result: T | undefined; + updateEndpoint: (s: string) => void; +} { + const [value, setValue] = useState(endpoint); + 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); + + useEffect(() => { + if (!value) return; + window.clearTimeout(handler); + const h = window.setTimeout(async () => { + setDirty(true); + setLoading(true); + try { + const url = canonicalizeBaseUrl(value); + const result = await onVerify(url); + 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]); + + return { + error: dirty ? error : undefined, + loading: loading, + result: result, + endpoint: value, + updateEndpoint: setValue, + }; +} + export function ExchangeSetUrlPage({ initialValue, - knownExchanges, expectedCurrency, onCancel, onVerify, onConfirm, - withError, }: Props) { - const [value, setValue] = useState(initialValue || ""); - const [dirty, setDirty] = useState(false); - const [result, setResult] = useState( - undefined, - ); - const [error, setError] = useState(withError); - - useEffect(() => { - try { - const url = canonicalizeBaseUrl(value); + const { loading, result, endpoint, updateEndpoint, error } = + useEndpointStatus(initialValue ?? "", onVerify); - const found = - knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1; - - if (found) { - setError("This exchange is already known"); - return; - } - onVerify(url) - .then((r) => { - setResult(r); - }) - .catch(() => { - setResult(undefined); - }); - setDirty(true); - } catch { - setResult(undefined); - } - }, [value]); + const [confirmationError, setConfirmationError] = useState< + string | undefined + >(undefined); return ( @@ -72,21 +95,32 @@ export function ExchangeSetUrlPage({ ) : (

Add exchange for {expectedCurrency}

)} + {result && expectedCurrency && expectedCurrency !== result.currency && ( + + This exchange doesn't match the expected currency{" "} + {expectedCurrency} + + )} +

- + setValue(e.currentTarget.value)} + value={endpoint} + onInput={(e) => updateEndpoint(e.currentTarget.value)} /> - {result && ( + {loading &&

loading...
} + {result && !loading && ( @@ -100,12 +134,6 @@ export function ExchangeSetUrlPage({ )}

- {result && expectedCurrency && expectedCurrency !== result.currency && ( - - This exchange doesn't match the expected currency{" "} - {expectedCurrency} - - )}