diff options
author | Sebastian <sebasjm@gmail.com> | 2023-02-25 19:43:45 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-02-25 19:43:59 -0300 |
commit | 1723f16b9c4b008e9e44578c2587c7a1bd6560b4 (patch) | |
tree | 4c10315d97edcdf215e3cd329ffb67231adf135e | |
parent | dd2599ff94f4ecc878abc9f6c87603fd180ac3de (diff) |
some fixes afte testing demobank with ms
-rw-r--r-- | packages/demobank-ui/src/hooks/circuit.ts | 15 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AccountPage.tsx | 4 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/AdminPage.tsx | 45 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/BusinessAccount.tsx | 257 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/HomePage.tsx | 56 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx | 14 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/PublicHistoriesPage.tsx | 4 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/RegistrationPage.tsx | 69 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/Routing.tsx | 56 | ||||
-rw-r--r-- | packages/demobank-ui/src/pages/WithdrawalQRCode.tsx | 4 | ||||
-rw-r--r-- | packages/merchant-backoffice-ui/src/InstanceRoutes.tsx | 50 | ||||
-rw-r--r-- | packages/web-util/src/utils/request.ts | 47 |
12 files changed, 462 insertions, 159 deletions
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 3abe8cd54..6cf543a3c 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -33,7 +33,7 @@ const useSWR = _useSWR as unknown as SWRHook; export function useAdminAccountAPI(): AdminAccountAPI { const { request } = useAuthenticatedBackend(); const mutateAll = useMatchMutate(); - const { state } = useBackendContext(); + const { state, logIn } = useBackendContext(); if (state.status === "loggedOut") { throw Error("access-api can't be used when the user is not logged In"); } @@ -81,6 +81,13 @@ export function useAdminAccountAPI(): AdminAccountAPI { data, contentType: "json", }); + if (account === state.username) { + await mutateAll(/.*/) + logIn({ + username: account, + password: data.new_password + }) + } return res; }; @@ -288,6 +295,12 @@ export function useRatiosAndFeeConfig(): HttpResponse< keepPreviousData: true, }); + if (data) { + // data.data.ratios_and_fees.sell_out_fee = 2 + if (!data.data.ratios_and_fees.fiat_currency) { + data.data.ratios_and_fees.fiat_currency = "FIAT" + } + } if (data) return data; if (error) return error.info; return { loading: true }; diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx index ae0c2b1f8..bd9a5acd7 100644 --- a/packages/demobank-ui/src/pages/AccountPage.tsx +++ b/packages/demobank-ui/src/pages/AccountPage.tsx @@ -28,7 +28,9 @@ import { PaymentOptions } from "./PaymentOptions.js"; interface Props { account: string; - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } /** * Query account information and show QR code if there is pending withdrawal diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx index b4ce58588..0a1dc26ec 100644 --- a/packages/demobank-ui/src/pages/AdminPage.tsx +++ b/packages/demobank-ui/src/pages/AdminPage.tsx @@ -59,7 +59,9 @@ function randomPassword(): string { } interface Props { - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } /** * Query account information and show QR code if there is pending withdrawal @@ -109,6 +111,11 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { if (showCashouts) { return ( <div> + <div> + <h1 class="nav welcome-text"> + <i18n.Translate>Cashout for account {showCashouts}</i18n.Translate> + </h1> + </div> <Cashouts account={showCashouts} onSelected={(id) => { @@ -116,15 +123,17 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { setShowCashouts(undefined); }} /> - <input - class="pure-button" - type="submit" - value={i18n.str`Close`} - onClick={async (e) => { - e.preventDefault(); - setShowCashouts(undefined); - }} - /> + <p> + <input + class="pure-button" + type="submit" + value={i18n.str`Close`} + onClick={async (e) => { + e.preventDefault(); + setShowCashouts(undefined); + }} + /> + </p> </div> ); } @@ -184,7 +193,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode { onClose={() => setCreateAccount(false)} onCreateSuccess={(password) => { showInfoMessage( - i18n.str`Account created with password "${password}"`, + i18n.str`Account created with password "${password}". The user must change the password on the next login.`, ); setCreateAccount(false); }} @@ -326,7 +335,9 @@ export function UpdateAccountPassword({ onUpdateSuccess, onLoadNotOk, }: { - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; onClear: () => void; onUpdateSuccess: () => void; account: string; @@ -521,7 +532,9 @@ export function ShowAccountDetails({ onLoadNotOk, onChangePassword, }: { - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; onClear?: () => void; onChangePassword: () => void; onUpdateSuccess: () => void; @@ -628,7 +641,9 @@ function RemoveAccount({ onUpdateSuccess, onLoadNotOk, }: { - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; onClear: () => void; onUpdateSuccess: () => void; account: string; @@ -806,7 +821,7 @@ function AccountForm({ /> </fieldset> <fieldset> - <label>{i18n.str`IBAN`}</label> + <label>{i18n.str`Internal IBAN`}</label> <input disabled={purpose !== "create"} value={form.iban ?? ""} diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx index 23a03cc9b..6278fe08b 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -20,6 +20,7 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { + ErrorType, HttpResponsePaginated, RequestError, useTranslationContext, @@ -44,7 +45,9 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; interface Props { onClose: () => void; onRegister: () => void; - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } export function BusinessAccount({ onClose, @@ -79,6 +82,9 @@ export function BusinessAccount({ setNewcashout(false); }} onComplete={(id) => { + showInfoMessage( + i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`, + ); setNewcashout(false); setShowCashoutDetails(id); }} @@ -156,7 +162,9 @@ interface PropsCashout { account: string; onComplete: (id: string) => void; onCancel: () => void; - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } type FormType = { @@ -180,7 +188,7 @@ function CreateCashout({ const result = useAccountDetails(account); const [error, saveError] = useState<ErrorMessage | undefined>(); - const [form, setForm] = useState<Partial<FormType>>({}); + const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); const { createCashout } = useCircuitAccountAPI(); if (!result.ok) return onLoadNotOk(result); @@ -277,10 +285,14 @@ function CreateCashout({ type="text" readonly class="currency-indicator" - size={balance.currency.length} - maxLength={balance.currency.length} + size={ + !form.isDebit ? fiatCurrency.length : balance.currency.length + } + maxLength={ + !form.isDebit ? fiatCurrency.length : balance.currency.length + } tabIndex={-1} - value={balance.currency} + value={!form.isDebit ? fiatCurrency : balance.currency} /> <input @@ -389,16 +401,16 @@ function CreateCashout({ {Amounts.isZero(sellFee) ? undefined : ( <Fragment> <fieldset> - <label>{i18n.str`Transfer before fee`}</label> + <label>{i18n.str`Amount after conversion`}</label> <div style={{ width: "max-content" }}> <input type="text" readonly class="currency-indicator" - size={balance.currency.length} - maxLength={balance.currency.length} + size={fiatCurrency.length} + maxLength={fiatCurrency.length} tabIndex={-1} - value={balance.currency} + value={fiatCurrency} /> <input @@ -417,10 +429,10 @@ function CreateCashout({ type="text" readonly class="currency-indicator" - size={balance.currency.length} - maxLength={balance.currency.length} + size={fiatCurrency.length} + maxLength={fiatCurrency.length} tabIndex={-1} - value={balance.currency} + value={fiatCurrency} /> <input @@ -442,10 +454,10 @@ function CreateCashout({ type="text" readonly class="currency-indicator" - size={balance.currency.length} - maxLength={balance.currency.length} + size={fiatCurrency.length} + maxLength={fiatCurrency.length} tabIndex={-1} - value={balance.currency} + value={fiatCurrency} /> <input @@ -543,34 +555,69 @@ function CreateCashout({ onComplete(res.data.uuid); } catch (error) { if (error instanceof RequestError) { - const errorData: SandboxBackend.SandboxError = - error.info.error; - if (error.info.status === HttpStatusCode.PreconditionFailed) { - saveError({ - title: i18n.str`The account does not have sufficient funds`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); - } else if ( - error.info.status === HttpStatusCode.ServiceUnavailable - ) { - saveError({ - title: i18n.str`The bank does not support the TAN channel for this operation`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); - } else if (error.info.status === HttpStatusCode.Conflict) { - saveError({ - title: i18n.str`No contact information for this channel`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); - } else { - saveError({ - title: i18n.str`New cashout gave response error`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); + const e = error as RequestError<SandboxBackend.SandboxError>; + switch (e.cause.type) { + case ErrorType.TIMEOUT: { + saveError({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.CLIENT: { + const errorData = e.cause.error; + + if ( + e.cause.status === HttpStatusCode.PreconditionFailed + ) { + saveError({ + title: i18n.str`The account does not have sufficient funds`, + description: errorData.error.description, + debug: JSON.stringify(error.info), + }); + } else if (e.cause.status === HttpStatusCode.Conflict) { + saveError({ + title: i18n.str`No contact information for this channel`, + description: errorData.error.description, + debug: JSON.stringify(error.info), + }); + } else { + saveError({ + title: i18n.str`New cashout gave response error`, + description: errorData.error.description, + debug: JSON.stringify(error.info), + }); + } + break; + } + case ErrorType.SERVER: { + const errorData = e.cause.error; + if ( + e.cause.status === HttpStatusCode.ServiceUnavailable + ) { + saveError({ + title: i18n.str`The bank does not support the TAN channel for this operation`, + description: errorData.error.description, + debug: JSON.stringify(error.info), + }); + } else { + saveError({ + title: i18n.str`Creating cashout returned with a server error`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + } + break; + } + case ErrorType.UNEXPECTED: { + saveError({ + title: i18n.str`Unexpected error trying to create cashout.`, + debug: JSON.stringify(error.cause), + }); + break; + } + default: { + assertUnreachable(e.cause); + } } } else if (error instanceof Error) { saveError({ @@ -592,7 +639,9 @@ function CreateCashout({ interface ShowCashoutProps { id: string; onCancel: () => void; - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } export function ShowCashoutDetails({ id, @@ -699,22 +748,59 @@ export function ShowCashoutDetails({ onCancel(); } catch (error) { if (error instanceof RequestError) { - const errorData: SandboxBackend.SandboxError = - error.info.error; - if ( - error.info.status === HttpStatusCode.PreconditionFailed - ) { - saveError({ - title: i18n.str`Cashout was already aborted`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); - } else { - saveError({ - title: i18n.str`Aborting cashout gave response error`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); + const e = + error as RequestError<SandboxBackend.SandboxError>; + switch (e.cause.type) { + case ErrorType.TIMEOUT: { + saveError({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.CLIENT: { + const errorData = e.cause.error; + if ( + e.cause.status === HttpStatusCode.PreconditionFailed + ) { + saveError({ + title: i18n.str`Cashout was already aborted`, + description: errorData.error.description, + debug: JSON.stringify(error.info), + }); + } else { + saveError({ + title: i18n.str`Aborting cashout gave response error`, + description: errorData.error.description, + debug: JSON.stringify(error.info), + }); + } + + saveError({ + title: i18n.str`Aborting cashout gave response error`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + break; + } + case ErrorType.SERVER: { + const errorData = e.cause.error; + saveError({ + title: i18n.str`Aborting cashout returned with a server error`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + break; + } + case ErrorType.UNEXPECTED: { + saveError({ + title: i18n.str`Unexpected error trying to abort cashout.`, + debug: JSON.stringify(error.cause), + }); + break; + } + default: { + assertUnreachable(e.cause); + } } } else if (error instanceof Error) { saveError({ @@ -741,13 +827,44 @@ export function ShowCashoutDetails({ }); } catch (error) { if (error instanceof RequestError) { - const errorData: SandboxBackend.SandboxError = - error.info.error; - saveError({ - title: i18n.str`Confirmation of cashout gave response error`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); + const e = + error as RequestError<SandboxBackend.SandboxError>; + switch (e.cause.type) { + case ErrorType.TIMEOUT: { + saveError({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.CLIENT: { + const errorData = e.cause.error; + saveError({ + title: i18n.str`Confirmation of cashout gave response error`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + break; + } + case ErrorType.SERVER: { + const errorData = e.cause.error; + saveError({ + title: i18n.str`Confirmation of cashout gave response error`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + break; + } + case ErrorType.UNEXPECTED: { + saveError({ + title: i18n.str`Unexpected error trying to cashout.`, + debug: JSON.stringify(error.cause), + }); + break; + } + default: { + assertUnreachable(e.cause); + } + } } else if (error instanceof Error) { saveError({ title: i18n.str`Confirmation failed, please report`, @@ -767,3 +884,7 @@ export function ShowCashoutDetails({ </div> ); } + +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx index 5af195f48..a360bd64c 100644 --- a/packages/demobank-ui/src/pages/HomePage.tsx +++ b/packages/demobank-ui/src/pages/HomePage.tsx @@ -14,9 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Logger } from "@gnu-taler/taler-util"; +import { HttpStatusCode, Logger } from "@gnu-taler/taler-util"; import { + ErrorType, HttpResponsePaginated, + RequestError, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; import { Fragment, h, VNode } from "preact"; @@ -119,9 +121,9 @@ function handleNotOkResult( onErrorHandler: (state: PageStateType["error"]) => void, i18n: ReturnType<typeof useTranslationContext>["i18n"], onRegister: () => void, -): <T, E>(result: HttpResponsePaginated<T, E>) => VNode { - return function handleNotOkResult2<T, E>( - result: HttpResponsePaginated<T, E>, +): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode { + return function handleNotOkResult2<T>( + result: HttpResponsePaginated<T, SandboxBackend.SandboxError>, ): VNode { if (result.clientError && result.isUnauthorized) { onErrorHandler({ @@ -137,13 +139,49 @@ function handleNotOkResult( } if (result.loading) return <Loading />; if (!result.ok) { - onErrorHandler({ - title: i18n.str`The backend reported a problem: HTTP status #${result.status}`, - description: `Diagnostic from ${result.info?.url} is "${result.message}"`, - debug: JSON.stringify(result.error), - }); + switch (result.type) { + case ErrorType.TIMEOUT: { + onErrorHandler({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.CLIENT: { + const errorData = result.error; + onErrorHandler({ + title: i18n.str`Could not load due to a client error`, + description: errorData.error.description, + debug: JSON.stringify(result), + }); + break; + } + case ErrorType.SERVER: { + const errorData = result.error; + onErrorHandler({ + title: i18n.str`Server returned with error`, + description: errorData.error.description, + debug: JSON.stringify(result), + }); + break; + } + case ErrorType.UNEXPECTED: { + onErrorHandler({ + title: i18n.str`Unexpected error.`, + description: `Diagnostic from ${result.info?.url} is "${result.message}"`, + debug: JSON.stringify(result.error), + }); + break; + } + default: { + assertUnreachable(result); + } + } + return <LoginForm onRegister={onRegister} />; } return <div />; }; } +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index d859b1cc7..07b011a00 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -48,7 +48,7 @@ export function PaytoWireTransferForm({ onSuccess: () => void; currency: string; }): VNode { - const backend = useBackendContext(); + // const backend = useBackendContext(); // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button? const [isRawPayto, setIsRawPayto] = useState(false); @@ -188,17 +188,7 @@ export function PaytoWireTransferForm({ paytoUri, amount: `${currency}:${amount}`, }); - // return await createTransactionCall( - // transactionData, - // backend.state, - // pageStateSetter, - // () => { - // setAmount(undefined); - // setIban(undefined); - // setSubject(undefined); - // }, - // i18n, - // ); + onSuccess(); }} /> <input diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx index 54a77b42a..2b5f7e26c 100644 --- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx +++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx @@ -36,7 +36,9 @@ const logger = new Logger("PublicHistoriesPage"); // } interface Props { - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } /** diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index 247ef8d80..c6bc3c327 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -15,6 +15,7 @@ */ import { HttpStatusCode, Logger } from "@gnu-taler/taler-util"; import { + ErrorType, RequestError, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; @@ -176,26 +177,52 @@ function RegistrationForm({ onComplete(); } catch (error) { if (error instanceof RequestError) { - const errorData: SandboxBackend.SandboxError = - error.info.error; - if (error.info.status === HttpStatusCode.Conflict) { - onError({ - title: i18n.str`That username is already taken`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); - } else { - onError({ - title: i18n.str`New registration gave response error`, - description: errorData.error.description, - debug: JSON.stringify(error.info), - }); + const e = + error as RequestError<SandboxBackend.SandboxError>; + switch (e.cause.type) { + case ErrorType.TIMEOUT: { + onError({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.CLIENT: { + const errorData = e.cause.error; + if (e.cause.status === HttpStatusCode.Conflict) { + onError({ + title: i18n.str`That username is already taken`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + } else { + onError({ + title: i18n.str`New registration gave response error`, + description: errorData.error.description, + debug: JSON.stringify(error.cause), + }); + } + break; + } + case ErrorType.SERVER: { + const errorData = e.cause.error; + onError({ + title: i18n.str`New registration gave response error`, + description: errorData?.error?.description, + debug: JSON.stringify(error.cause), + }); + break; + } + case ErrorType.UNEXPECTED: { + onError({ + title: i18n.str`Unexpected error doing the registration.`, + debug: JSON.stringify(error.cause), + }); + break; + } + default: { + assertUnreachable(e.cause); + } } - } else if (error instanceof Error) { - onError({ - title: i18n.str`Registration failed, please report`, - description: error.message, - }); } } }} @@ -222,3 +249,7 @@ function RegistrationForm({ </Fragment> ); } + +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx index 48f226574..8234d8988 100644 --- a/packages/demobank-ui/src/pages/Routing.tsx +++ b/packages/demobank-ui/src/pages/Routing.tsx @@ -15,6 +15,7 @@ */ import { + ErrorType, HttpResponsePaginated, useTranslationContext, } from "@gnu-taler/web-util/lib/index.browser"; @@ -34,9 +35,9 @@ function handleNotOkResult( safe: string, saveError: (state: PageStateType["error"]) => void, i18n: ReturnType<typeof useTranslationContext>["i18n"], -): <T, E>(result: HttpResponsePaginated<T, E>) => VNode { - return function handleNotOkResult2<T, E>( - result: HttpResponsePaginated<T, E>, +): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode { + return function handleNotOkResult2<T>( + result: HttpResponsePaginated<T, SandboxBackend.SandboxError>, ): VNode { if (result.clientError && result.isUnauthorized) { route(safe); @@ -50,12 +51,45 @@ function handleNotOkResult( } if (result.loading) return <Loading />; if (!result.ok) { - saveError({ - title: i18n.str`The backend reported a problem: HTTP status #${result.status}`, - description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`, - debug: JSON.stringify(result.error), - }); - route(safe); + switch (result.type) { + case ErrorType.TIMEOUT: { + saveError({ + title: i18n.str`Request timeout, try again later.`, + }); + break; + } + case ErrorType.CLIENT: { + const errorData = result.error; + saveError({ + title: i18n.str`Could not load due to a client error`, + description: errorData.error.description, + debug: JSON.stringify(result), + }); + break; + } + case ErrorType.SERVER: { + const errorData = result.error; + saveError({ + title: i18n.str`Server returned with error`, + description: errorData.error.description, + debug: JSON.stringify(result), + }); + break; + } + case ErrorType.UNEXPECTED: { + saveError({ + title: i18n.str`Unexpected error.`, + description: `Diagnostic from ${result.info?.url} is "${result.message}"`, + debug: JSON.stringify(result.error), + }); + break; + } + default: + { + assertUnreachable(result); + } + route(safe); + } } return <div />; }; @@ -137,3 +171,7 @@ function Redirect({ to }: { to: string }): VNode { }, []); return <div>being redirected to {to}</div>; } + +export function assertUnreachable(x: never): never { + throw new Error("Didn't expect to get here"); +} diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index fd91c0e1a..d4c95d830 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -33,7 +33,9 @@ interface Props { withdrawalId: string; talerWithdrawUri: string; onAbort: () => void; - onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; + onLoadNotOk: <T>( + error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, + ) => VNode; } /** * Offer the QR code (and a clickable taler://-link) to diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index 5929b031a..e31ff4513 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -22,6 +22,7 @@ import { useTranslationContext, HttpError, + ErrorType, } from "@gnu-taler/web-util/lib/index.browser"; import { format } from "date-fns"; import { Fragment, FunctionComponent, h, VNode } from "preact"; @@ -163,16 +164,25 @@ export function InstanceRoutes({ return function ServerErrorRedirectToImpl( error: HttpError<MerchantBackend.ErrorDetail>, ) { - setGlobalNotification({ - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, - details: - error.clientError || error.serverError - ? error.error?.detail - : undefined, - type: "ERROR", - to, - }); + if (error.type === ErrorType.TIMEOUT) { + setGlobalNotification({ + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, + type: "ERROR", + to, + }); + } else { + setGlobalNotification({ + message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, + details: + error.clientError || error.serverError + ? error.error?.detail + : undefined, + type: "ERROR", + to, + }); + } return <Redirect to={to} />; }; } @@ -572,19 +582,25 @@ function AdminInstanceUpdatePage({ {...rest} instanceId={id} onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => { - return ( - <Fragment> - <NotificationCard - notification={{ + const notif = + error.type === ErrorType.TIMEOUT + ? { + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, + type: "ERROR" as const, + } + : { message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, details: error.clientError || error.serverError ? error.error?.detail : undefined, - type: "ERROR", - }} - /> + type: "ERROR" as const, + }; + return ( + <Fragment> + <NotificationCard notification={notif} /> <LoginPage onConfirm={updateLoginStatus} /> </Fragment> ); diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts index 24342bb80..3d91024dc 100644 --- a/packages/web-util/src/utils/request.ts +++ b/packages/web-util/src/utils/request.ts @@ -17,6 +17,10 @@ import { HttpStatusCode } from "@gnu-taler/taler-util"; import { base64encode } from "./base64.js"; +export enum ErrorType { + CLIENT, SERVER, TIMEOUT, UNEXPECTED +} + /** * * @param baseUrl URL where the service is located @@ -39,7 +43,7 @@ export async function defaultRequestHandler<T>( const requestMethod = options?.method ?? "GET"; const requestBody = options?.data; - const requestTimeout = options?.timeout ?? 2 * 1000; + const requestTimeout = options?.timeout ?? 5 * 1000; const requestParams = options.params ?? {}; const _url = new URL(`${baseUrl}${endpoint}`); @@ -85,10 +89,13 @@ export async function defaultRequestHandler<T>( hasToken: !!options.token, status: 0, }; - const error: HttpResponseUnexpectedError = { + const error: HttpRequestTimeoutError = { + clientError: true, + isNotfound: false, + isUnauthorized: false, + error: undefined, info, - status: 0, - error: ex, + type: ErrorType.TIMEOUT, message: "Request timeout", }; throw new RequestError(error); @@ -166,32 +173,50 @@ export interface WithPagination { } export type HttpError<ErrorDetail> = + | HttpRequestTimeoutError | HttpResponseClientError<ErrorDetail> | HttpResponseServerError<ErrorDetail> | HttpResponseUnexpectedError; + export interface HttpResponseServerError<ErrorDetail> { ok?: false; loading?: false; clientError?: false; serverError: true; + type: ErrorType.SERVER, - error?: ErrorDetail; + error: ErrorDetail; status: HttpStatusCode; message: string; info?: RequestInfo; } +interface HttpRequestTimeoutError { + ok?: false; + loading?: false; + clientError: true; + serverError?: false; + type: ErrorType.TIMEOUT, + + info?: RequestInfo; + error: undefined, + + isUnauthorized: false; + isNotfound: false; + message: string; +} interface HttpResponseClientError<ErrorDetail> { ok?: false; loading?: false; clientError: true; serverError?: false; + type: ErrorType.CLIENT, info?: RequestInfo; isUnauthorized: boolean; isNotfound: boolean; status: HttpStatusCode; - error?: ErrorDetail; + error: ErrorDetail; message: string; } @@ -200,6 +225,7 @@ interface HttpResponseUnexpectedError { loading?: false; clientError?: false; serverError?: false; + type: ErrorType.UNEXPECTED, info?: RequestInfo; status?: HttpStatusCode; @@ -208,10 +234,15 @@ interface HttpResponseUnexpectedError { } export class RequestError<ErrorDetail> extends Error { + /** + * @deprecated use cause + */ info: HttpError<ErrorDetail>; + cause: HttpError<ErrorDetail>; constructor(d: HttpError<ErrorDetail>) { super(d.message) this.info = d + this.cause = d } } @@ -277,6 +308,7 @@ async function buildRequestFailed<ErrorDetail>( clientError: true, isNotfound: status === 404, isUnauthorized: status === 401, + type: ErrorType.CLIENT, status, info, message: data?.hint, @@ -287,6 +319,7 @@ async function buildRequestFailed<ErrorDetail>( if (status && status >= 500 && status < 600) { const error: HttpResponseServerError<ErrorDetail> = { serverError: true, + type: ErrorType.SERVER, status, info, message: `${data?.hint} (code ${data?.code})`, @@ -296,6 +329,7 @@ async function buildRequestFailed<ErrorDetail>( } return { info, + type: ErrorType.UNEXPECTED, status, error: {}, message: "NOT DEFINED", @@ -304,6 +338,7 @@ async function buildRequestFailed<ErrorDetail>( const error: HttpResponseUnexpectedError = { info, status, + type: ErrorType.UNEXPECTED, error: ex, message: "NOT DEFINED", }; |