some fixes afte testing demobank with ms

This commit is contained in:
Sebastian 2023-02-25 19:43:45 -03:00
parent dd2599ff94
commit 1723f16b9c
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
12 changed files with 462 additions and 159 deletions

View File

@ -33,7 +33,7 @@ const useSWR = _useSWR as unknown as SWRHook;
export function useAdminAccountAPI(): AdminAccountAPI { export function useAdminAccountAPI(): AdminAccountAPI {
const { request } = useAuthenticatedBackend(); const { request } = useAuthenticatedBackend();
const mutateAll = useMatchMutate(); const mutateAll = useMatchMutate();
const { state } = useBackendContext(); const { state, logIn } = useBackendContext();
if (state.status === "loggedOut") { if (state.status === "loggedOut") {
throw Error("access-api can't be used when the user is not logged In"); throw Error("access-api can't be used when the user is not logged In");
} }
@ -81,6 +81,13 @@ export function useAdminAccountAPI(): AdminAccountAPI {
data, data,
contentType: "json", contentType: "json",
}); });
if (account === state.username) {
await mutateAll(/.*/)
logIn({
username: account,
password: data.new_password
})
}
return res; return res;
}; };
@ -288,6 +295,12 @@ export function useRatiosAndFeeConfig(): HttpResponse<
keepPreviousData: true, 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 (data) return data;
if (error) return error.info; if (error) return error.info;
return { loading: true }; return { loading: true };

View File

@ -28,7 +28,9 @@ import { PaymentOptions } from "./PaymentOptions.js";
interface Props { interface Props {
account: string; 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 * Query account information and show QR code if there is pending withdrawal

View File

@ -59,7 +59,9 @@ function randomPassword(): string {
} }
interface Props { 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 * Query account information and show QR code if there is pending withdrawal
@ -109,6 +111,11 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
if (showCashouts) { if (showCashouts) {
return ( return (
<div> <div>
<div>
<h1 class="nav welcome-text">
<i18n.Translate>Cashout for account {showCashouts}</i18n.Translate>
</h1>
</div>
<Cashouts <Cashouts
account={showCashouts} account={showCashouts}
onSelected={(id) => { onSelected={(id) => {
@ -116,15 +123,17 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
setShowCashouts(undefined); setShowCashouts(undefined);
}} }}
/> />
<input <p>
class="pure-button" <input
type="submit" class="pure-button"
value={i18n.str`Close`} type="submit"
onClick={async (e) => { value={i18n.str`Close`}
e.preventDefault(); onClick={async (e) => {
setShowCashouts(undefined); e.preventDefault();
}} setShowCashouts(undefined);
/> }}
/>
</p>
</div> </div>
); );
} }
@ -184,7 +193,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
onClose={() => setCreateAccount(false)} onClose={() => setCreateAccount(false)}
onCreateSuccess={(password) => { onCreateSuccess={(password) => {
showInfoMessage( 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); setCreateAccount(false);
}} }}
@ -326,7 +335,9 @@ export function UpdateAccountPassword({
onUpdateSuccess, onUpdateSuccess,
onLoadNotOk, onLoadNotOk,
}: { }: {
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
onClear: () => void; onClear: () => void;
onUpdateSuccess: () => void; onUpdateSuccess: () => void;
account: string; account: string;
@ -521,7 +532,9 @@ export function ShowAccountDetails({
onLoadNotOk, onLoadNotOk,
onChangePassword, onChangePassword,
}: { }: {
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
onClear?: () => void; onClear?: () => void;
onChangePassword: () => void; onChangePassword: () => void;
onUpdateSuccess: () => void; onUpdateSuccess: () => void;
@ -628,7 +641,9 @@ function RemoveAccount({
onUpdateSuccess, onUpdateSuccess,
onLoadNotOk, onLoadNotOk,
}: { }: {
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
onClear: () => void; onClear: () => void;
onUpdateSuccess: () => void; onUpdateSuccess: () => void;
account: string; account: string;
@ -806,7 +821,7 @@ function AccountForm({
/> />
</fieldset> </fieldset>
<fieldset> <fieldset>
<label>{i18n.str`IBAN`}</label> <label>{i18n.str`Internal IBAN`}</label>
<input <input
disabled={purpose !== "create"} disabled={purpose !== "create"}
value={form.iban ?? ""} value={form.iban ?? ""}

View File

@ -20,6 +20,7 @@ import {
TranslatedString, TranslatedString,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
ErrorType,
HttpResponsePaginated, HttpResponsePaginated,
RequestError, RequestError,
useTranslationContext, useTranslationContext,
@ -44,7 +45,9 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
interface Props { interface Props {
onClose: () => void; onClose: () => void;
onRegister: () => void; onRegister: () => void;
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
} }
export function BusinessAccount({ export function BusinessAccount({
onClose, onClose,
@ -79,6 +82,9 @@ export function BusinessAccount({
setNewcashout(false); setNewcashout(false);
}} }}
onComplete={(id) => { onComplete={(id) => {
showInfoMessage(
i18n.str`Cashout created. You need to confirm the operation to complete the transaction.`,
);
setNewcashout(false); setNewcashout(false);
setShowCashoutDetails(id); setShowCashoutDetails(id);
}} }}
@ -156,7 +162,9 @@ interface PropsCashout {
account: string; account: string;
onComplete: (id: string) => void; onComplete: (id: string) => void;
onCancel: () => void; onCancel: () => void;
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
} }
type FormType = { type FormType = {
@ -180,7 +188,7 @@ function CreateCashout({
const result = useAccountDetails(account); const result = useAccountDetails(account);
const [error, saveError] = useState<ErrorMessage | undefined>(); const [error, saveError] = useState<ErrorMessage | undefined>();
const [form, setForm] = useState<Partial<FormType>>({}); const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
const { createCashout } = useCircuitAccountAPI(); const { createCashout } = useCircuitAccountAPI();
if (!result.ok) return onLoadNotOk(result); if (!result.ok) return onLoadNotOk(result);
@ -277,10 +285,14 @@ function CreateCashout({
type="text" type="text"
readonly readonly
class="currency-indicator" class="currency-indicator"
size={balance.currency.length} size={
maxLength={balance.currency.length} !form.isDebit ? fiatCurrency.length : balance.currency.length
}
maxLength={
!form.isDebit ? fiatCurrency.length : balance.currency.length
}
tabIndex={-1} tabIndex={-1}
value={balance.currency} value={!form.isDebit ? fiatCurrency : balance.currency}
/> />
&nbsp; &nbsp;
<input <input
@ -389,16 +401,16 @@ function CreateCashout({
{Amounts.isZero(sellFee) ? undefined : ( {Amounts.isZero(sellFee) ? undefined : (
<Fragment> <Fragment>
<fieldset> <fieldset>
<label>{i18n.str`Transfer before fee`}</label> <label>{i18n.str`Amount after conversion`}</label>
<div style={{ width: "max-content" }}> <div style={{ width: "max-content" }}>
<input <input
type="text" type="text"
readonly readonly
class="currency-indicator" class="currency-indicator"
size={balance.currency.length} size={fiatCurrency.length}
maxLength={balance.currency.length} maxLength={fiatCurrency.length}
tabIndex={-1} tabIndex={-1}
value={balance.currency} value={fiatCurrency}
/> />
&nbsp; &nbsp;
<input <input
@ -417,10 +429,10 @@ function CreateCashout({
type="text" type="text"
readonly readonly
class="currency-indicator" class="currency-indicator"
size={balance.currency.length} size={fiatCurrency.length}
maxLength={balance.currency.length} maxLength={fiatCurrency.length}
tabIndex={-1} tabIndex={-1}
value={balance.currency} value={fiatCurrency}
/> />
&nbsp; &nbsp;
<input <input
@ -442,10 +454,10 @@ function CreateCashout({
type="text" type="text"
readonly readonly
class="currency-indicator" class="currency-indicator"
size={balance.currency.length} size={fiatCurrency.length}
maxLength={balance.currency.length} maxLength={fiatCurrency.length}
tabIndex={-1} tabIndex={-1}
value={balance.currency} value={fiatCurrency}
/> />
&nbsp; &nbsp;
<input <input
@ -543,34 +555,69 @@ function CreateCashout({
onComplete(res.data.uuid); onComplete(res.data.uuid);
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const errorData: SandboxBackend.SandboxError = const e = error as RequestError<SandboxBackend.SandboxError>;
error.info.error; switch (e.cause.type) {
if (error.info.status === HttpStatusCode.PreconditionFailed) { case ErrorType.TIMEOUT: {
saveError({ saveError({
title: i18n.str`The account does not have sufficient funds`, title: i18n.str`Request timeout, try again later.`,
description: errorData.error.description, });
debug: JSON.stringify(error.info), break;
}); }
} else if ( case ErrorType.CLIENT: {
error.info.status === HttpStatusCode.ServiceUnavailable const errorData = e.cause.error;
) {
saveError({ if (
title: i18n.str`The bank does not support the TAN channel for this operation`, e.cause.status === HttpStatusCode.PreconditionFailed
description: errorData.error.description, ) {
debug: JSON.stringify(error.info), saveError({
}); title: i18n.str`The account does not have sufficient funds`,
} else if (error.info.status === HttpStatusCode.Conflict) { description: errorData.error.description,
saveError({ debug: JSON.stringify(error.info),
title: i18n.str`No contact information for this channel`, });
description: errorData.error.description, } else if (e.cause.status === HttpStatusCode.Conflict) {
debug: JSON.stringify(error.info), saveError({
}); title: i18n.str`No contact information for this channel`,
} else { description: errorData.error.description,
saveError({ debug: JSON.stringify(error.info),
title: i18n.str`New cashout gave response error`, });
description: errorData.error.description, } else {
debug: JSON.stringify(error.info), 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) { } else if (error instanceof Error) {
saveError({ saveError({
@ -592,7 +639,9 @@ function CreateCashout({
interface ShowCashoutProps { interface ShowCashoutProps {
id: string; id: string;
onCancel: () => void; onCancel: () => void;
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
} }
export function ShowCashoutDetails({ export function ShowCashoutDetails({
id, id,
@ -699,22 +748,59 @@ export function ShowCashoutDetails({
onCancel(); onCancel();
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const errorData: SandboxBackend.SandboxError = const e =
error.info.error; error as RequestError<SandboxBackend.SandboxError>;
if ( switch (e.cause.type) {
error.info.status === HttpStatusCode.PreconditionFailed case ErrorType.TIMEOUT: {
) { saveError({
saveError({ title: i18n.str`Request timeout, try again later.`,
title: i18n.str`Cashout was already aborted`, });
description: errorData.error.description, break;
debug: JSON.stringify(error.info), }
}); case ErrorType.CLIENT: {
} else { const errorData = e.cause.error;
saveError({ if (
title: i18n.str`Aborting cashout gave response error`, e.cause.status === HttpStatusCode.PreconditionFailed
description: errorData.error.description, ) {
debug: JSON.stringify(error.info), 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) { } else if (error instanceof Error) {
saveError({ saveError({
@ -741,13 +827,44 @@ export function ShowCashoutDetails({
}); });
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const errorData: SandboxBackend.SandboxError = const e =
error.info.error; error as RequestError<SandboxBackend.SandboxError>;
saveError({ switch (e.cause.type) {
title: i18n.str`Confirmation of cashout gave response error`, case ErrorType.TIMEOUT: {
description: errorData.error.description, saveError({
debug: JSON.stringify(error.info), 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) { } else if (error instanceof Error) {
saveError({ saveError({
title: i18n.str`Confirmation failed, please report`, title: i18n.str`Confirmation failed, please report`,
@ -767,3 +884,7 @@ export function ShowCashoutDetails({
</div> </div>
); );
} }
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -14,9 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 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 { import {
ErrorType,
HttpResponsePaginated, HttpResponsePaginated,
RequestError,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
@ -119,9 +121,9 @@ function handleNotOkResult(
onErrorHandler: (state: PageStateType["error"]) => void, onErrorHandler: (state: PageStateType["error"]) => void,
i18n: ReturnType<typeof useTranslationContext>["i18n"], i18n: ReturnType<typeof useTranslationContext>["i18n"],
onRegister: () => void, onRegister: () => void,
): <T, E>(result: HttpResponsePaginated<T, E>) => VNode { ): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode {
return function handleNotOkResult2<T, E>( return function handleNotOkResult2<T>(
result: HttpResponsePaginated<T, E>, result: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
): VNode { ): VNode {
if (result.clientError && result.isUnauthorized) { if (result.clientError && result.isUnauthorized) {
onErrorHandler({ onErrorHandler({
@ -137,13 +139,49 @@ function handleNotOkResult(
} }
if (result.loading) return <Loading />; if (result.loading) return <Loading />;
if (!result.ok) { if (!result.ok) {
onErrorHandler({ switch (result.type) {
title: i18n.str`The backend reported a problem: HTTP status #${result.status}`, case ErrorType.TIMEOUT: {
description: `Diagnostic from ${result.info?.url} is "${result.message}"`, onErrorHandler({
debug: JSON.stringify(result.error), 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 <LoginForm onRegister={onRegister} />;
} }
return <div />; return <div />;
}; };
} }
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -48,7 +48,7 @@ export function PaytoWireTransferForm({
onSuccess: () => void; onSuccess: () => void;
currency: string; currency: string;
}): VNode { }): VNode {
const backend = useBackendContext(); // const backend = useBackendContext();
// const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button? // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
const [isRawPayto, setIsRawPayto] = useState(false); const [isRawPayto, setIsRawPayto] = useState(false);
@ -188,17 +188,7 @@ export function PaytoWireTransferForm({
paytoUri, paytoUri,
amount: `${currency}:${amount}`, amount: `${currency}:${amount}`,
}); });
// return await createTransactionCall( onSuccess();
// transactionData,
// backend.state,
// pageStateSetter,
// () => {
// setAmount(undefined);
// setIban(undefined);
// setSubject(undefined);
// },
// i18n,
// );
}} }}
/> />
<input <input

View File

@ -36,7 +36,9 @@ const logger = new Logger("PublicHistoriesPage");
// } // }
interface Props { interface Props {
onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode; onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
} }
/** /**

View File

@ -15,6 +15,7 @@
*/ */
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util"; import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
import { import {
ErrorType,
RequestError, RequestError,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
@ -176,26 +177,52 @@ function RegistrationForm({
onComplete(); onComplete();
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const errorData: SandboxBackend.SandboxError = const e =
error.info.error; error as RequestError<SandboxBackend.SandboxError>;
if (error.info.status === HttpStatusCode.Conflict) { switch (e.cause.type) {
onError({ case ErrorType.TIMEOUT: {
title: i18n.str`That username is already taken`, onError({
description: errorData.error.description, title: i18n.str`Request timeout, try again later.`,
debug: JSON.stringify(error.info), });
}); break;
} else { }
onError({ case ErrorType.CLIENT: {
title: i18n.str`New registration gave response error`, const errorData = e.cause.error;
description: errorData.error.description, if (e.cause.status === HttpStatusCode.Conflict) {
debug: JSON.stringify(error.info), 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> </Fragment>
); );
} }
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -15,6 +15,7 @@
*/ */
import { import {
ErrorType,
HttpResponsePaginated, HttpResponsePaginated,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
@ -34,9 +35,9 @@ function handleNotOkResult(
safe: string, safe: string,
saveError: (state: PageStateType["error"]) => void, saveError: (state: PageStateType["error"]) => void,
i18n: ReturnType<typeof useTranslationContext>["i18n"], i18n: ReturnType<typeof useTranslationContext>["i18n"],
): <T, E>(result: HttpResponsePaginated<T, E>) => VNode { ): <T>(result: HttpResponsePaginated<T, SandboxBackend.SandboxError>) => VNode {
return function handleNotOkResult2<T, E>( return function handleNotOkResult2<T>(
result: HttpResponsePaginated<T, E>, result: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
): VNode { ): VNode {
if (result.clientError && result.isUnauthorized) { if (result.clientError && result.isUnauthorized) {
route(safe); route(safe);
@ -50,12 +51,45 @@ function handleNotOkResult(
} }
if (result.loading) return <Loading />; if (result.loading) return <Loading />;
if (!result.ok) { if (!result.ok) {
saveError({ switch (result.type) {
title: i18n.str`The backend reported a problem: HTTP status #${result.status}`, case ErrorType.TIMEOUT: {
description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`, saveError({
debug: JSON.stringify(result.error), title: i18n.str`Request timeout, try again later.`,
}); });
route(safe); 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 />; return <div />;
}; };
@ -137,3 +171,7 @@ function Redirect({ to }: { to: string }): VNode {
}, []); }, []);
return <div>being redirected to {to}</div>; return <div>being redirected to {to}</div>;
} }
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -33,7 +33,9 @@ interface Props {
withdrawalId: string; withdrawalId: string;
talerWithdrawUri: string; talerWithdrawUri: string;
onAbort: () => void; 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 * Offer the QR code (and a clickable taler://-link) to

View File

@ -22,6 +22,7 @@
import { import {
useTranslationContext, useTranslationContext,
HttpError, HttpError,
ErrorType,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
import { format } from "date-fns"; import { format } from "date-fns";
import { Fragment, FunctionComponent, h, VNode } from "preact"; import { Fragment, FunctionComponent, h, VNode } from "preact";
@ -163,16 +164,25 @@ export function InstanceRoutes({
return function ServerErrorRedirectToImpl( return function ServerErrorRedirectToImpl(
error: HttpError<MerchantBackend.ErrorDetail>, error: HttpError<MerchantBackend.ErrorDetail>,
) { ) {
setGlobalNotification({ if (error.type === ErrorType.TIMEOUT) {
message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, setGlobalNotification({
description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, message: i18n.str`The request to the backend take too long and was cancelled`,
details: description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`,
error.clientError || error.serverError type: "ERROR",
? error.error?.detail to,
: undefined, });
type: "ERROR", } else {
to, 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} />; return <Redirect to={to} />;
}; };
} }
@ -572,19 +582,25 @@ function AdminInstanceUpdatePage({
{...rest} {...rest}
instanceId={id} instanceId={id}
onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => { onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => {
return ( const notif =
<Fragment> error.type === ErrorType.TIMEOUT
<NotificationCard ? {
notification={{ 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}`, message: i18n.str`The backend reported a problem: HTTP status #${error.status}`,
description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`, description: i18n.str`Diagnostic from ${error.info?.url} is "${error.message}"`,
details: details:
error.clientError || error.serverError error.clientError || error.serverError
? error.error?.detail ? error.error?.detail
: undefined, : undefined,
type: "ERROR", type: "ERROR" as const,
}} };
/> return (
<Fragment>
<NotificationCard notification={notif} />
<LoginPage onConfirm={updateLoginStatus} /> <LoginPage onConfirm={updateLoginStatus} />
</Fragment> </Fragment>
); );

View File

@ -17,6 +17,10 @@
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { base64encode } from "./base64.js"; import { base64encode } from "./base64.js";
export enum ErrorType {
CLIENT, SERVER, TIMEOUT, UNEXPECTED
}
/** /**
* *
* @param baseUrl URL where the service is located * @param baseUrl URL where the service is located
@ -39,7 +43,7 @@ export async function defaultRequestHandler<T>(
const requestMethod = options?.method ?? "GET"; const requestMethod = options?.method ?? "GET";
const requestBody = options?.data; const requestBody = options?.data;
const requestTimeout = options?.timeout ?? 2 * 1000; const requestTimeout = options?.timeout ?? 5 * 1000;
const requestParams = options.params ?? {}; const requestParams = options.params ?? {};
const _url = new URL(`${baseUrl}${endpoint}`); const _url = new URL(`${baseUrl}${endpoint}`);
@ -85,10 +89,13 @@ export async function defaultRequestHandler<T>(
hasToken: !!options.token, hasToken: !!options.token,
status: 0, status: 0,
}; };
const error: HttpResponseUnexpectedError = { const error: HttpRequestTimeoutError = {
clientError: true,
isNotfound: false,
isUnauthorized: false,
error: undefined,
info, info,
status: 0, type: ErrorType.TIMEOUT,
error: ex,
message: "Request timeout", message: "Request timeout",
}; };
throw new RequestError(error); throw new RequestError(error);
@ -166,32 +173,50 @@ export interface WithPagination {
} }
export type HttpError<ErrorDetail> = export type HttpError<ErrorDetail> =
| HttpRequestTimeoutError
| HttpResponseClientError<ErrorDetail> | HttpResponseClientError<ErrorDetail>
| HttpResponseServerError<ErrorDetail> | HttpResponseServerError<ErrorDetail>
| HttpResponseUnexpectedError; | HttpResponseUnexpectedError;
export interface HttpResponseServerError<ErrorDetail> { export interface HttpResponseServerError<ErrorDetail> {
ok?: false; ok?: false;
loading?: false; loading?: false;
clientError?: false; clientError?: false;
serverError: true; serverError: true;
type: ErrorType.SERVER,
error?: ErrorDetail; error: ErrorDetail;
status: HttpStatusCode; status: HttpStatusCode;
message: string; message: string;
info?: RequestInfo; 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> { interface HttpResponseClientError<ErrorDetail> {
ok?: false; ok?: false;
loading?: false; loading?: false;
clientError: true; clientError: true;
serverError?: false; serverError?: false;
type: ErrorType.CLIENT,
info?: RequestInfo; info?: RequestInfo;
isUnauthorized: boolean; isUnauthorized: boolean;
isNotfound: boolean; isNotfound: boolean;
status: HttpStatusCode; status: HttpStatusCode;
error?: ErrorDetail; error: ErrorDetail;
message: string; message: string;
} }
@ -200,6 +225,7 @@ interface HttpResponseUnexpectedError {
loading?: false; loading?: false;
clientError?: false; clientError?: false;
serverError?: false; serverError?: false;
type: ErrorType.UNEXPECTED,
info?: RequestInfo; info?: RequestInfo;
status?: HttpStatusCode; status?: HttpStatusCode;
@ -208,10 +234,15 @@ interface HttpResponseUnexpectedError {
} }
export class RequestError<ErrorDetail> extends Error { export class RequestError<ErrorDetail> extends Error {
/**
* @deprecated use cause
*/
info: HttpError<ErrorDetail>; info: HttpError<ErrorDetail>;
cause: HttpError<ErrorDetail>;
constructor(d: HttpError<ErrorDetail>) { constructor(d: HttpError<ErrorDetail>) {
super(d.message) super(d.message)
this.info = d this.info = d
this.cause = d
} }
} }
@ -277,6 +308,7 @@ async function buildRequestFailed<ErrorDetail>(
clientError: true, clientError: true,
isNotfound: status === 404, isNotfound: status === 404,
isUnauthorized: status === 401, isUnauthorized: status === 401,
type: ErrorType.CLIENT,
status, status,
info, info,
message: data?.hint, message: data?.hint,
@ -287,6 +319,7 @@ async function buildRequestFailed<ErrorDetail>(
if (status && status >= 500 && status < 600) { if (status && status >= 500 && status < 600) {
const error: HttpResponseServerError<ErrorDetail> = { const error: HttpResponseServerError<ErrorDetail> = {
serverError: true, serverError: true,
type: ErrorType.SERVER,
status, status,
info, info,
message: `${data?.hint} (code ${data?.code})`, message: `${data?.hint} (code ${data?.code})`,
@ -296,6 +329,7 @@ async function buildRequestFailed<ErrorDetail>(
} }
return { return {
info, info,
type: ErrorType.UNEXPECTED,
status, status,
error: {}, error: {},
message: "NOT DEFINED", message: "NOT DEFINED",
@ -304,6 +338,7 @@ async function buildRequestFailed<ErrorDetail>(
const error: HttpResponseUnexpectedError = { const error: HttpResponseUnexpectedError = {
info, info,
status, status,
type: ErrorType.UNEXPECTED,
error: ex, error: ex,
message: "NOT DEFINED", message: "NOT DEFINED",
}; };