diff options
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<T>( .then(getJsonIfOk); } +function timeout<T>(ms: number, promise: Promise<T>): Promise<T> { + 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<T>( url: string, ): Promise<T> { - 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 ( <ExchangeSetUrlPage onCancel={onBack} - knownExchanges={knownExchanges} - onVerify={(url) => 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<TalerConfigResponse>(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<TalerConfigResponse | undefined>; onConfirm: (url: string) => Promise<string | undefined>; withError?: string; } +function useEndpointStatus<T>( + endpoint: string, + onVerify: (e: string) => Promise<T>, +): { + loading: boolean; + error?: string; + endpoint: string; + result: T | undefined; + updateEndpoint: (s: string) => void; +} { + const [value, setValue] = useState<string>(endpoint); + 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<number | undefined>(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<string>(initialValue || ""); - const [dirty, setDirty] = useState(false); - const [result, setResult] = useState<TalerConfigResponse | undefined>( - undefined, - ); - const [error, setError] = useState<string | undefined>(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 ( <Fragment> @@ -72,21 +95,32 @@ export function ExchangeSetUrlPage({ ) : ( <h2>Add exchange for {expectedCurrency}</h2> )} + {result && expectedCurrency && expectedCurrency !== result.currency && ( + <WarningBox> + This exchange doesn't match the expected currency{" "} + <b>{expectedCurrency}</b> + </WarningBox> + )} <ErrorMessage title={error && "Unable to add this exchange"} description={error} /> + <ErrorMessage + title={confirmationError && "Unable to add this exchange"} + description={confirmationError} + /> <p> - <Input invalid={dirty && !!error}> + <Input invalid={!!error}> <label>URL</label> <input type="text" placeholder="https://" - value={value} - onInput={(e) => setValue(e.currentTarget.value)} + value={endpoint} + onInput={(e) => updateEndpoint(e.currentTarget.value)} /> </Input> - {result && ( + {loading && <div>loading... </div>} + {result && !loading && ( <Fragment> <Input> <label>Version</label> @@ -100,12 +134,6 @@ export function ExchangeSetUrlPage({ )} </p> </section> - {result && expectedCurrency && expectedCurrency !== result.currency && ( - <WarningBox> - This exchange doesn't match the expected currency{" "} - <b>{expectedCurrency}</b> - </WarningBox> - )} <footer> <Button onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> @@ -118,8 +146,10 @@ export function ExchangeSetUrlPage({ expectedCurrency !== result.currency) } onClick={() => { - const url = canonicalizeBaseUrl(value); - return onConfirm(url).then((r) => (r ? setError(r) : undefined)); + const url = canonicalizeBaseUrl(endpoint); + return onConfirm(url).then((r) => + r ? setConfirmationError(r) : undefined, + ); }} > <i18n.Translate>Next</i18n.Translate> |