This commit is contained in:
Sebastian 2023-02-28 19:03:43 -03:00
parent 740849dd89
commit 9922192b0d
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
16 changed files with 567 additions and 703 deletions

View File

@ -32,7 +32,9 @@ describe("Transaction states", () => {
const props: Props = { const props: Props = {
account: "123", account: "123",
onSelected: () => { null }, onSelected: () => {
null;
},
}; };
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, { env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
@ -117,7 +119,9 @@ describe("Transaction states", () => {
const props: Props = { const props: Props = {
account: "123", account: "123",
onSelected: () => { null }, onSelected: () => {
null;
},
}; };
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {}); env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
@ -151,7 +155,9 @@ describe("Transaction states", () => {
const props: Props = { const props: Props = {
account: "123", account: "123",
onSelected: () => { null }, onSelected: () => {
null;
},
}; };
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {}); env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});

View File

@ -279,7 +279,10 @@ export function useAuthenticatedBackend(): useBackendType {
sandboxCashoutFetcher, sandboxCashoutFetcher,
}; };
} }
/**
*
* @deprecated
*/
export function useBackendConfig(): HttpResponse< export function useBackendConfig(): HttpResponse<
SandboxBackend.Config, SandboxBackend.Config,
SandboxBackend.SandboxError SandboxBackend.SandboxError

View File

@ -82,11 +82,11 @@ export function useAdminAccountAPI(): AdminAccountAPI {
contentType: "json", contentType: "json",
}); });
if (account === state.username) { if (account === state.username) {
await mutateAll(/.*/) await mutateAll(/.*/);
logIn({ logIn({
username: account, username: account,
password: data.new_password password: data.new_password,
}) });
} }
return res; return res;
}; };
@ -284,7 +284,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
HttpResponseOk<SandboxBackend.Circuit.Config>, HttpResponseOk<SandboxBackend.Circuit.Config>,
RequestError<SandboxBackend.SandboxError> RequestError<SandboxBackend.SandboxError>
>([`circuit-api/config`], fetcher, { >([`circuit-api/config`], fetcher, {
refreshInterval: 0, refreshInterval: 1000,
refreshWhenHidden: false, refreshWhenHidden: false,
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateOnReconnect: false, revalidateOnReconnect: false,
@ -298,7 +298,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
if (data) { if (data) {
// data.data.ratios_and_fees.sell_out_fee = 2 // data.data.ratios_and_fees.sell_out_fee = 2
if (!data.data.ratios_and_fees.fiat_currency) { if (!data.data.ratios_and_fees.fiat_currency) {
data.data.ratios_and_fees.fiat_currency = "FIAT" data.data.ratios_and_fees.fiat_currency = "FIAT";
} }
} }
if (data) return data; if (data) return data;

View File

@ -16,6 +16,7 @@
import { import {
Amounts, Amounts,
HttpStatusCode,
parsePaytoUri, parsePaytoUri,
TranslatedString, TranslatedString,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -35,11 +36,13 @@ import {
useAdminAccountAPI, useAdminAccountAPI,
} from "../hooks/circuit.js"; } from "../hooks/circuit.js";
import { import {
buildRequestErrorMessage,
PartialButDefined, PartialButDefined,
RecursivePartial,
undefinedIfEmpty, undefinedIfEmpty,
WithIntermediate, WithIntermediate,
} from "../utils.js"; } from "../utils.js";
import { ErrorBanner } from "./BankFrame.js"; import { ErrorBannerFloat } from "./BankFrame.js";
import { ShowCashoutDetails } from "./BusinessAccount.js"; import { ShowCashoutDetails } from "./BusinessAccount.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@ -373,7 +376,7 @@ export function UpdateAccountPassword({
</h1> </h1>
</div> </div>
{error && ( {error && (
<ErrorBanner error={error} onClear={() => saveError(undefined)} /> <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)} )}
<form class="pure-form"> <form class="pure-form">
@ -435,7 +438,17 @@ export function UpdateAccountPassword({
}); });
onUpdateSuccess(); onUpdateSuccess();
} catch (error) { } catch (error) {
handleError(error, saveError, i18n); if (error instanceof RequestError) {
saveError(buildRequestErrorMessage(i18n, error.cause));
} else {
saveError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
} }
}} }}
/> />
@ -467,13 +480,16 @@ function CreateNewAccount({
</h1> </h1>
</div> </div>
{error && ( {error && (
<ErrorBanner error={error} onClear={() => saveError(undefined)} /> <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)} )}
<AccountForm <AccountForm
template={undefined} template={undefined}
purpose="create" purpose="create"
onChange={(a) => setSubmitAccount(a)} onChange={(a) => {
console.log(a);
setSubmitAccount(a);
}}
/> />
<p> <p>
@ -514,7 +530,28 @@ function CreateNewAccount({
await createAccount(account); await createAccount(account);
onCreateSuccess(account.password); onCreateSuccess(account.password);
} catch (error) { } catch (error) {
handleError(error, saveError, i18n); if (error instanceof RequestError) {
saveError(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.Forbidden
? i18n.str`The rights to perform the operation are not sufficient`
: status === HttpStatusCode.BadRequest
? i18n.str`Input data was invalid`
: status === HttpStatusCode.Conflict
? i18n.str`At least one registration detail was not available`
: undefined,
}),
);
} else {
saveError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
} }
}} }}
/> />
@ -564,7 +601,7 @@ export function ShowAccountDetails({
</h1> </h1>
</div> </div>
{error && ( {error && (
<ErrorBanner error={error} onClear={() => saveError(undefined)} /> <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)} )}
<AccountForm <AccountForm
template={result.data} template={result.data}
@ -622,7 +659,26 @@ export function ShowAccountDetails({
}); });
onUpdateSuccess(); onUpdateSuccess();
} catch (error) { } catch (error) {
handleError(error, saveError, i18n); if (error instanceof RequestError) {
saveError(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.Forbidden
? i18n.str`The rights to change the account are not sufficient`
: status === HttpStatusCode.NotFound
? i18n.str`The username was not found`
: undefined,
}),
);
} else {
saveError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
} }
} }
}} }}
@ -673,7 +729,7 @@ function RemoveAccount({
</h1> </h1>
</div> </div>
{!isBalanceEmpty && ( {!isBalanceEmpty && (
<ErrorBanner <ErrorBannerFloat
error={{ error={{
title: i18n.str`Can't delete the account`, title: i18n.str`Can't delete the account`,
description: i18n.str`Balance is not empty`, description: i18n.str`Balance is not empty`,
@ -681,7 +737,7 @@ function RemoveAccount({
/> />
)} )}
{error && ( {error && (
<ErrorBanner error={error} onClear={() => saveError(undefined)} /> <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)} )}
<p> <p>
@ -710,7 +766,28 @@ function RemoveAccount({
const r = await deleteAccount(account); const r = await deleteAccount(account);
onUpdateSuccess(); onUpdateSuccess();
} catch (error) { } catch (error) {
handleError(error, saveError, i18n); if (error instanceof RequestError) {
saveError(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.Forbidden
? i18n.str`The administrator specified a institutional username`
: status === HttpStatusCode.NotFound
? i18n.str`The username was not found`
: status === HttpStatusCode.PreconditionFailed
? i18n.str`Balance was not zero`
: undefined,
}),
);
} else {
saveError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
} }
}} }}
/> />
@ -720,7 +797,6 @@ function RemoveAccount({
</div> </div>
); );
} }
/** /**
* Create valid account object to update or create * Create valid account object to update or create
* Take template as initial values for the form * Take template as initial values for the form
@ -740,7 +816,9 @@ function AccountForm({
}): VNode { }): VNode {
const initial = initializeFromTemplate(template); const initial = initializeFromTemplate(template);
const [form, setForm] = useState(initial); const [form, setForm] = useState(initial);
const [errors, setErrors] = useState<typeof initial | undefined>(undefined); const [errors, setErrors] = useState<
RecursivePartial<typeof initial> | undefined
>(undefined);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
function updateForm(newForm: typeof initial): void { function updateForm(newForm: typeof initial): void {
@ -748,7 +826,7 @@ function AccountForm({
? undefined ? undefined
: parsePaytoUri(newForm.cashout_address); : parsePaytoUri(newForm.cashout_address);
const validationResult = undefinedIfEmpty<typeof initial>({ const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
cashout_address: !newForm.cashout_address cashout_address: !newForm.cashout_address
? i18n.str`required` ? i18n.str`required`
: !parsed : !parsed
@ -758,20 +836,20 @@ function AccountForm({
: !IBAN_REGEX.test(parsed.iban) : !IBAN_REGEX.test(parsed.iban)
? i18n.str`IBAN should have just uppercased letters and numbers` ? i18n.str`IBAN should have just uppercased letters and numbers`
: undefined, : undefined,
contact_data: { contact_data: undefinedIfEmpty({
email: !newForm.contact_data.email email: !newForm.contact_data?.email
? undefined ? undefined
: !EMAIL_REGEX.test(newForm.contact_data.email) : !EMAIL_REGEX.test(newForm.contact_data.email)
? i18n.str`it should be an email` ? i18n.str`it should be an email`
: undefined, : undefined,
phone: !newForm.contact_data.phone phone: !newForm.contact_data?.phone
? undefined ? undefined
: !newForm.contact_data.phone.startsWith("+") : !newForm.contact_data.phone.startsWith("+")
? i18n.str`should start with +` ? i18n.str`should start with +`
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
? i18n.str`phone number can't have other than numbers` ? i18n.str`phone number can't have other than numbers`
: undefined, : undefined,
}, }),
iban: !newForm.iban iban: !newForm.iban
? i18n.str`required` ? i18n.str`required`
: !IBAN_REGEX.test(newForm.iban) : !IBAN_REGEX.test(newForm.iban)
@ -780,10 +858,9 @@ function AccountForm({
name: !newForm.name ? i18n.str`required` : undefined, name: !newForm.name ? i18n.str`required` : undefined,
username: !newForm.username ? i18n.str`required` : undefined, username: !newForm.username ? i18n.str`required` : undefined,
}); });
setErrors(errors);
setErrors(validationResult);
setForm(newForm); setForm(newForm);
onChange(validationResult === undefined ? undefined : (newForm as any)); onChange(errors === undefined ? (newForm as any) : undefined);
} }
return ( return (
@ -846,7 +923,7 @@ function AccountForm({
}} }}
/> />
<ShowInputErrorLabel <ShowInputErrorLabel
message={errors?.contact_data.email} message={errors?.contact_data?.email}
isDirty={form.contact_data.email !== undefined} isDirty={form.contact_data.email !== undefined}
/> />
</fieldset> </fieldset>
@ -861,7 +938,7 @@ function AccountForm({
}} }}
/> />
<ShowInputErrorLabel <ShowInputErrorLabel
message={errors?.contact_data.phone} message={errors?.contact_data?.phone}
isDirty={form.contact_data?.phone !== undefined} isDirty={form.contact_data?.phone !== undefined}
/> />
</fieldset> </fieldset>
@ -883,29 +960,3 @@ function AccountForm({
</form> </form>
); );
} }
function handleError(
error: unknown,
saveError: (e: ErrorMessage) => void,
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): void {
if (error instanceof RequestError) {
const payload = error.info.error as SandboxBackend.SandboxError;
saveError({
title: error.info.serverError
? i18n.str`Server had an error`
: i18n.str`Server didn't accept the request`,
description: payload.error.description,
});
} else if (error instanceof Error) {
saveError({
title: i18n.str`Could not update account`,
description: error.message,
});
} else {
saveError({
title: i18n.str`Error, please report`,
debug: JSON.stringify(error),
});
}
}

View File

@ -126,14 +126,6 @@ export function BankFrame({
</nav> </nav>
</div> </div>
<section id="main" class="content"> <section id="main" class="content">
{pageState.error && (
<ErrorBanner
error={pageState.error}
onClear={() => {
pageStateSetter((prev) => ({ ...prev, error: undefined }));
}}
/>
)}
<StatusBanner /> <StatusBanner />
{backend.state.status === "loggedIn" ? ( {backend.state.status === "loggedIn" ? (
<div class="top-right"> <div class="top-right">
@ -191,20 +183,48 @@ function maybeDemoContent(content: VNode): VNode {
return <Fragment />; return <Fragment />;
} }
export function ErrorBanner({ export function ErrorBannerFloat({
error, error,
onClear, onClear,
}: { }: {
error: ErrorMessage; error: ErrorMessage;
onClear?: () => void; onClear?: () => void;
}): VNode | null { }): VNode {
return ( return (
<div class="informational informational-fail" style={{ marginTop: 8 }}> <div
style={{
position: "fixed",
top: 0,
zIndex: 200,
width: "90%",
}}
>
<ErrorBanner error={error} onClear={onClear} />
</div>
);
}
function ErrorBanner({
error,
onClear,
}: {
error: ErrorMessage;
onClear?: () => void;
}): VNode {
return (
<div
class="informational informational-fail"
style={{
marginTop: 8,
paddingLeft: 16,
paddingRight: 16,
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}> <div style={{ display: "flex", justifyContent: "space-between" }}>
<p> <p>
<b>{error.title}</b> <b>{error.title}</b>
</p> </p>
<div> <div style={{ marginTop: "auto", marginBottom: "auto" }}>
{onClear && ( {onClear && (
<input <input
type="button" type="button"
@ -225,26 +245,46 @@ export function ErrorBanner({
function StatusBanner(): VNode | null { function StatusBanner(): VNode | null {
const { pageState, pageStateSetter } = usePageContext(); const { pageState, pageStateSetter } = usePageContext();
if (!pageState.info) return null;
const rval = ( return (
<div class="informational informational-ok" style={{ marginTop: 8 }}> <div
<div style={{ display: "flex", justifyContent: "space-between" }}> style={{
<p> position: "fixed",
<b>{pageState.info}</b> top: 0,
</p> zIndex: 200,
<div> width: "90%",
<input }}
type="button" >
class="pure-button" {!pageState.info ? undefined : (
value="Clear" <div
onClick={async () => { class="informational informational-ok"
pageStateSetter((prev) => ({ ...prev, info: undefined })); style={{ marginTop: 8, paddingLeft: 16, paddingRight: 16 }}
}} >
/> <div style={{ display: "flex", justifyContent: "space-between" }}>
<p>
<b>{pageState.info}</b>
</p>
<div>
<input
type="button"
class="pure-button"
value="Clear"
onClick={async () => {
pageStateSetter((prev) => ({ ...prev, info: undefined }));
}}
/>
</div>
</div>
</div> </div>
</div> )}
{!pageState.error ? undefined : (
<ErrorBanner
error={pageState.error}
onClear={() => {
pageStateSetter((prev) => ({ ...prev, error: undefined }));
}}
/>
)}
</div> </div>
); );
return rval;
} }

View File

@ -20,13 +20,13 @@ import {
TranslatedString, TranslatedString,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
ErrorType, HttpResponse,
HttpResponsePaginated, HttpResponsePaginated,
RequestError, 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";
import { useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Cashouts } from "../components/Cashouts/index.js"; import { Cashouts } from "../components/Cashouts/index.js";
import { useBackendContext } from "../context/backend.js"; import { useBackendContext } from "../context/backend.js";
import { ErrorMessage, usePageContext } from "../context/pageState.js"; import { ErrorMessage, usePageContext } from "../context/pageState.js";
@ -36,9 +36,13 @@ import {
useCircuitAccountAPI, useCircuitAccountAPI,
useRatiosAndFeeConfig, useRatiosAndFeeConfig,
} from "../hooks/circuit.js"; } from "../hooks/circuit.js";
import { TanChannel, undefinedIfEmpty } from "../utils.js"; import {
buildRequestErrorMessage,
TanChannel,
undefinedIfEmpty,
} from "../utils.js";
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js"; import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
import { ErrorBanner } from "./BankFrame.js"; import { ErrorBannerFloat } from "./BankFrame.js";
import { LoginForm } from "./LoginForm.js"; import { LoginForm } from "./LoginForm.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@ -177,6 +181,46 @@ type ErrorFrom<T> = {
[P in keyof T]+?: string; [P in keyof T]+?: string;
}; };
// check #7719
function useRatiosAndFeeConfigWithChangeDetection(): HttpResponse<
SandboxBackend.Circuit.Config & { hasChanged?: boolean },
SandboxBackend.SandboxError
> {
const result = useRatiosAndFeeConfig();
const [oldResult, setOldResult] = useState<
SandboxBackend.Circuit.Config | undefined
>(undefined);
const dataFromBackend = result.ok ? result.data : undefined;
useEffect(() => {
// save only the first result of /config to the backend
if (!dataFromBackend || oldResult !== undefined) return;
setOldResult(dataFromBackend);
}, [dataFromBackend]);
if (!result.ok) return result;
const data = !oldResult ? result.data : oldResult;
const hasChanged =
oldResult &&
(result.data.name !== oldResult.name ||
result.data.version !== oldResult.version ||
result.data.ratios_and_fees.buy_at_ratio !==
oldResult.ratios_and_fees.buy_at_ratio ||
result.data.ratios_and_fees.buy_in_fee !==
oldResult.ratios_and_fees.buy_in_fee ||
result.data.ratios_and_fees.sell_at_ratio !==
oldResult.ratios_and_fees.sell_at_ratio ||
result.data.ratios_and_fees.sell_out_fee !==
oldResult.ratios_and_fees.sell_out_fee ||
result.data.ratios_and_fees.fiat_currency !==
oldResult.ratios_and_fees.fiat_currency);
return {
...result,
data: { ...data, hasChanged },
};
}
function CreateCashout({ function CreateCashout({
account, account,
onComplete, onComplete,
@ -207,15 +251,6 @@ function CreateCashout({
if (!sellRate || sellRate < 0) return <div>error rate</div>; if (!sellRate || sellRate < 0) return <div>error rate</div>;
function truncate(a: AmountJson): AmountJson {
const str = Amounts.stringify(a);
const idx = str.indexOf(".");
if (idx === -1) return a;
const truncated = str.substring(0, idx + 3);
console.log(str, truncated);
return Amounts.parseOrThrow(truncated);
}
const amount = Amounts.parse(`${balance.currency}:${form.amount}`); const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
const amount_debit = !amount const amount_debit = !amount
? zero ? zero
@ -256,7 +291,7 @@ function CreateCashout({
return ( return (
<div> <div>
{error && ( {error && (
<ErrorBanner error={error} onClear={() => saveError(undefined)} /> <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)} )}
<h1>New cashout</h1> <h1>New cashout</h1>
<form class="pure-form"> <form class="pure-form">
@ -555,74 +590,31 @@ function CreateCashout({
onComplete(res.data.uuid); onComplete(res.data.uuid);
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const e = error as RequestError<SandboxBackend.SandboxError>; saveError(
switch (e.cause.type) { buildRequestErrorMessage(i18n, error.cause, {
case ErrorType.TIMEOUT: { onClientError: (status) =>
saveError({ status === HttpStatusCode.BadRequest
title: i18n.str`Request timeout, try again later.`, ? i18n.str`The exchange rate was incorrectly applied`
}); : status === HttpStatusCode.Forbidden
break; ? i18n.str`A institutional user tried the operation`
} : status === HttpStatusCode.Conflict
case ErrorType.CLIENT: { ? i18n.str`Need a contact data where to send the TAN`
const errorData = e.cause.error; : status === HttpStatusCode.PreconditionFailed
? i18n.str`The account does not have sufficient funds`
if ( : undefined,
e.cause.status === HttpStatusCode.PreconditionFailed onServerError: (status) =>
) { status === HttpStatusCode.ServiceUnavailable
saveError({ ? i18n.str`The bank does not support the TAN channel for this operation`
title: i18n.str`The account does not have sufficient funds`, : undefined,
description: errorData.error.description, }),
debug: JSON.stringify(error.info), );
}); } else {
} 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({ saveError({
title: i18n.str`Cashout failed, please report`, title: i18n.str`Operation failed, please report`,
description: error.message, description:
error instanceof Error
? error.message
: JSON.stringify(error),
}); });
} }
} }
@ -636,6 +628,25 @@ function CreateCashout({
); );
} }
const MAX_AMOUNT_DIGIT = 2;
/**
* Truncate the amount of digits to display
* in the form based on the fee calculations
*
* Backend must have the same truncation
* @param a
* @returns
*/
function truncate(a: AmountJson): AmountJson {
const str = Amounts.stringify(a);
const idx = str.indexOf(".");
if (idx === -1) {
return a;
}
const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
return Amounts.parseOrThrow(truncated);
}
interface ShowCashoutProps { interface ShowCashoutProps {
id: string; id: string;
onCancel: () => void; onCancel: () => void;
@ -662,7 +673,7 @@ export function ShowCashoutDetails({
<div> <div>
<h1>Cashout details {id}</h1> <h1>Cashout details {id}</h1>
{error && ( {error && (
<ErrorBanner error={error} onClear={() => saveError(undefined)} /> <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)} )}
<form class="pure-form"> <form class="pure-form">
<fieldset> <fieldset>
@ -744,68 +755,27 @@ export function ShowCashoutDetails({
onClick={async (e) => { onClick={async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const rest = await abortCashout(id); await abortCashout(id);
onCancel(); onCancel();
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const e = saveError(
error as RequestError<SandboxBackend.SandboxError>; buildRequestErrorMessage(i18n, error.cause, {
switch (e.cause.type) { onClientError: (status) =>
case ErrorType.TIMEOUT: { status === HttpStatusCode.NotFound
saveError({ ? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
title: i18n.str`Request timeout, try again later.`, : status === HttpStatusCode.PreconditionFailed
}); ? i18n.str`Cashout was already confimed`
break; : undefined,
} }),
case ErrorType.CLIENT: { );
const errorData = e.cause.error; } else {
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({ saveError({
title: i18n.str`Aborting failed, please report`, title: i18n.str`Operation failed, please report`,
description: error.message, description:
error instanceof Error
? error.message
: JSON.stringify(error),
}); });
} }
} }
@ -827,48 +797,27 @@ export function ShowCashoutDetails({
}); });
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const e = saveError(
error as RequestError<SandboxBackend.SandboxError>; buildRequestErrorMessage(i18n, error.cause, {
switch (e.cause.type) { onClientError: (status) =>
case ErrorType.TIMEOUT: { status === HttpStatusCode.NotFound
saveError({ ? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
title: i18n.str`Request timeout, try again later.`, : status === HttpStatusCode.PreconditionFailed
}); ? i18n.str`Cashout was already confimed`
break; : status === HttpStatusCode.Conflict
} ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
case ErrorType.CLIENT: { : status === HttpStatusCode.Forbidden
const errorData = e.cause.error; ? i18n.str`Invalid code`
saveError({ : undefined,
title: i18n.str`Confirmation of cashout gave response error`, }),
description: errorData.error.description, );
debug: JSON.stringify(error.cause), } else {
});
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({ saveError({
title: i18n.str`Confirmation failed, please report`, title: i18n.str`Operation failed, please report`,
description: error.message, description:
error instanceof Error
? error.message
: JSON.stringify(error),
}); });
} }
} }

View File

@ -14,11 +14,10 @@
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 { HttpStatusCode, Logger } from "@gnu-taler/taler-util"; import { Logger } from "@gnu-taler/taler-util";
import { import {
ErrorType, 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";
@ -79,7 +78,27 @@ export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
account={backend.state.username} account={backend.state.username}
withdrawalId={withdrawalId} withdrawalId={withdrawalId}
talerWithdrawUri={talerWithdrawUri} talerWithdrawUri={talerWithdrawUri}
onAbort={clearCurrentWithdrawal} onConfirmed={() => {
pageStateSetter((prevState) => {
const { talerWithdrawUri, ...rest } = prevState;
// remove talerWithdrawUri and add info
return {
...rest,
info: i18n.str`Withdrawal confirmed!`,
};
});
}}
onError={(error) => {
pageStateSetter((prevState) => {
const { talerWithdrawUri, ...rest } = prevState;
// remove talerWithdrawUri and add error
return {
...rest,
error,
};
});
}}
onAborted={clearCurrentWithdrawal}
onLoadNotOk={handleNotOkResult( onLoadNotOk={handleNotOkResult(
backend.state.username, backend.state.username,
saveError, saveError,
@ -147,7 +166,7 @@ function handleNotOkResult(
break; break;
} }
case ErrorType.CLIENT: { case ErrorType.CLIENT: {
const errorData = result.error; const errorData = result.payload;
onErrorHandler({ onErrorHandler({
title: i18n.str`Could not load due to a client error`, title: i18n.str`Could not load due to a client error`,
description: errorData.error.description, description: errorData.error.description,
@ -168,7 +187,7 @@ function handleNotOkResult(
onErrorHandler({ onErrorHandler({
title: i18n.str`Unexpected error.`, title: i18n.str`Unexpected error.`,
description: `Diagnostic from ${result.info?.url} is "${result.message}"`, description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
debug: JSON.stringify(result.error), debug: JSON.stringify(result.exception),
}); });
break; break;
} }

View File

@ -14,12 +14,12 @@
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 { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import { PageStateType, usePageContext } from "../context/pageState.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js"; import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
import { PageStateType, usePageContext } from "../context/pageState.js";
/** /**
* Let the user choose a payment option, * Let the user choose a payment option,

View File

@ -17,22 +17,20 @@
import { import {
Amounts, Amounts,
buildPayto, buildPayto,
HttpStatusCode,
Logger, Logger,
parsePaytoUri, parsePaytoUri,
stringifyPaytoUri, stringifyPaytoUri,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
InternationalizationAPI,
RequestError, RequestError,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { StateUpdater, useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js"; import { PageStateType } from "../context/pageState.js";
import { PageStateType, usePageContext } from "../context/pageState.js";
import { useAccessAPI } from "../hooks/access.js"; import { useAccessAPI } from "../hooks/access.js";
import { BackendState } from "../hooks/backend.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("PaytoWireTransferForm"); const logger = new Logger("PaytoWireTransferForm");
@ -184,11 +182,35 @@ export function PaytoWireTransferForm({
ibanPayto.params.message = encodeURIComponent(subject); ibanPayto.params.message = encodeURIComponent(subject);
const paytoUri = stringifyPaytoUri(ibanPayto); const paytoUri = stringifyPaytoUri(ibanPayto);
await createTransaction({ try {
paytoUri, await createTransaction({
amount: `${currency}:${amount}`, paytoUri,
}); amount: `${currency}:${amount}`,
onSuccess(); });
onSuccess();
setAmount(undefined);
setIban(undefined);
setSubject(undefined);
} catch (error) {
if (error instanceof RequestError) {
onError(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.BadRequest
? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
: undefined,
}),
);
} else {
onError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
}
}} }}
/> />
<input <input
@ -298,13 +320,21 @@ export function PaytoWireTransferForm({
rawPaytoInputSetter(undefined); rawPaytoInputSetter(undefined);
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const errorData: SandboxBackend.SandboxError = onError(
error.info.error; buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.BadRequest
? i18n.str`The request was invalid or the payto://-URI used unacceptable features.`
: undefined,
}),
);
} else {
onError({ onError({
title: i18n.str`Transfer creation gave response error`, title: i18n.str`Operation failed, please report`,
description: errorData.error.description, description:
debug: JSON.stringify(errorData), error instanceof Error
? error.message
: JSON.stringify(error),
}); });
} }
} }

View File

@ -14,17 +14,17 @@
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 { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useEffect } from "preact/hooks"; import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js"; import { QR } from "../components/QR.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
export function QrCodeSection({ export function QrCodeSection({
talerWithdrawUri, talerWithdrawUri,
onAbort, onAborted,
}: { }: {
talerWithdrawUri: string; talerWithdrawUri: string;
onAbort: () => void; onAborted: () => void;
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
useEffect(() => { useEffect(() => {
@ -64,7 +64,7 @@ export function QrCodeSection({
<br /> <br />
<a <a
class="pure-button btn-cancel" class="pure-button btn-cancel"
onClick={onAbort} onClick={onAborted}
>{i18n.str`Abort`}</a> >{i18n.str`Abort`}</a>
</div> </div>
</article> </article>

View File

@ -15,7 +15,6 @@
*/ */
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";
@ -25,7 +24,7 @@ import { useBackendContext } from "../context/backend.js";
import { PageStateType } from "../context/pageState.js"; import { PageStateType } from "../context/pageState.js";
import { useTestingAPI } from "../hooks/access.js"; import { useTestingAPI } from "../hooks/access.js";
import { bankUiSettings } from "../settings.js"; import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("RegistrationPage"); const logger = new Logger("RegistrationPage");
@ -177,52 +176,22 @@ function RegistrationForm({
onComplete(); onComplete();
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
const e = onError(
error as RequestError<SandboxBackend.SandboxError>; buildRequestErrorMessage(i18n, error.cause, {
switch (e.cause.type) { onClientError: (status) =>
case ErrorType.TIMEOUT: { status === HttpStatusCode.Conflict
onError({ ? i18n.str`That username is already taken`
title: i18n.str`Request timeout, try again later.`, : undefined,
}); }),
break; );
} } else {
case ErrorType.CLIENT: { onError({
const errorData = e.cause.error; title: i18n.str`Operation failed, please report`,
if (e.cause.status === HttpStatusCode.Conflict) { description:
onError({ error instanceof Error
title: i18n.str`That username is already taken`, ? error.message
description: errorData.error.description, : JSON.stringify(error),
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);
}
}
} }
} }
}} }}

View File

@ -14,16 +14,16 @@
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 { Amounts, Logger } from "@gnu-taler/taler-util"; import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
import { import {
RequestError, RequestError,
useTranslationContext, useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser"; } from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import { PageStateType, usePageContext } from "../context/pageState.js"; import { PageStateType } from "../context/pageState.js";
import { useAccessAPI } from "../hooks/access.js"; import { useAccessAPI } from "../hooks/access.js";
import { undefinedIfEmpty } from "../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WalletWithdrawForm"); const logger = new Logger("WalletWithdrawForm");
@ -127,16 +127,21 @@ export function WalletWithdrawForm({
onSuccess(result.data); onSuccess(result.data);
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
onError(
buildRequestErrorMessage(i18n, error.cause, {
onClientError: (status) =>
status === HttpStatusCode.Forbidden
? i18n.str`The operation was rejected due to insufficient funds`
: undefined,
}),
);
} else {
onError({ onError({
title: i18n.str`Could not create withdrawal operation`, title: i18n.str`Operation failed, please report`,
description: (error as any).error.description, description:
debug: JSON.stringify(error), error instanceof Error
}); ? error.message
} : JSON.stringify(error),
if (error instanceof Error) {
onError({
title: i18n.str`Something when wrong trying to start the withdrawal`,
description: error.message,
}); });
} }
} }
@ -147,85 +152,3 @@ export function WalletWithdrawForm({
</form> </form>
); );
} }
// /**
// * This function creates a withdrawal operation via the Access API.
// *
// * After having successfully created the withdrawal operation, the
// * user should receive a QR code of the "taler://withdraw/" type and
// * supposed to scan it with their phone.
// *
// * TODO: (1) after the scan, the page should refresh itself and inform
// * the user about the operation's outcome. (2) use POST helper. */
// async function createWithdrawalCall(
// amount: string,
// backendState: BackendState,
// pageStateSetter: StateUpdater<PageStateType>,
// i18n: InternationalizationAPI,
// ): Promise<void> {
// if (backendState?.status === "loggedOut") {
// logger.error("Page has a problem: no credentials found in the state.");
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`No credentials given.`,
// },
// }));
// return;
// }
// let res: Response;
// try {
// const { username, password } = backendState;
// const headers = prepareHeaders(username, password);
// // Let bank generate withdraw URI:
// const url = new URL(
// `access-api/accounts/${backendState.username}/withdrawals`,
// backendState.url,
// );
// res = await fetch(url.href, {
// method: "POST",
// headers,
// body: JSON.stringify({ amount }),
// });
// } catch (error) {
// logger.trace("Could not POST withdrawal request to the bank", error);
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`Could not create withdrawal operation`,
// description: (error as any).error.description,
// debug: JSON.stringify(error),
// },
// }));
// return;
// }
// if (!res.ok) {
// const response = await res.json();
// logger.error(
// `Withdrawal creation gave response error: ${response} (${res.status})`,
// );
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`Withdrawal creation gave response error`,
// description: response.error.description,
// debug: JSON.stringify(response),
// },
// }));
// return;
// }
// logger.trace("Withdrawal operation created!");
// const resp = await res.json();
// pageStateSetter((prevState: PageStateType) => ({
// ...prevState,
// withdrawalInProgress: true,
// talerWithdrawUri: resp.taler_withdraw_uri,
// withdrawalId: resp.withdrawal_id,
// }));
// }

View File

@ -14,32 +14,36 @@
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 { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser"; import {
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks"; import { useMemo, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js"; import { PageStateType, usePageContext } from "../context/pageState.js";
import { usePageContext } from "../context/pageState.js";
import { useAccessAPI } from "../hooks/access.js"; import { useAccessAPI } from "../hooks/access.js";
import { undefinedIfEmpty } from "../utils.js"; import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WithdrawalConfirmationQuestion"); const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props { interface Props {
account: string;
withdrawalId: string; withdrawalId: string;
onError: (e: PageStateType["error"]) => void;
onConfirmed: () => void;
onAborted: () => void;
} }
/** /**
* Additional authentication required to complete the operation. * Additional authentication required to complete the operation.
* Not providing a back button, only abort. * Not providing a back button, only abort.
*/ */
export function WithdrawalConfirmationQuestion({ export function WithdrawalConfirmationQuestion({
account, onError,
onConfirmed,
onAborted,
withdrawalId, withdrawalId,
}: Props): VNode { }: Props): VNode {
const { pageState, pageStateSetter } = usePageContext();
const backend = useBackendContext();
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const captchaNumbers = useMemo(() => { const captchaNumbers = useMemo(() => {
@ -111,35 +115,29 @@ export function WithdrawalConfirmationQuestion({
e.preventDefault(); e.preventDefault();
try { try {
await confirmWithdrawal(withdrawalId); await confirmWithdrawal(withdrawalId);
pageStateSetter((prevState) => { onConfirmed();
const { talerWithdrawUri, ...rest } = prevState;
return {
...rest,
info: i18n.str`Withdrawal confirmed!`,
};
});
} catch (error) { } catch (error) {
pageStateSetter((prevState) => ({ if (error instanceof RequestError) {
...prevState, onError(
error: { buildRequestErrorMessage(i18n, error.cause, {
title: i18n.str`Could not confirm the withdrawal`, onClientError: (status) =>
description: (error as any).error.description, status === HttpStatusCode.Conflict
debug: JSON.stringify(error), ? i18n.str`The withdrawal has been aborted previously and can't be confirmed`
}, : status === HttpStatusCode.UnprocessableEntity
})); ? i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`
: undefined,
}),
);
} else {
onError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
} }
// if (
// captchaAnswer ==
// (captchaNumbers.a + captchaNumbers.b).toString()
// ) {
// await confirmWithdrawalCall(
// backend.state,
// pageState.withdrawalId,
// pageStateSetter,
// i18n,
// );
// return;
// }
}} }}
> >
{i18n.str`Confirm`} {i18n.str`Confirm`}
@ -151,29 +149,27 @@ export function WithdrawalConfirmationQuestion({
e.preventDefault(); e.preventDefault();
try { try {
await abortWithdrawal(withdrawalId); await abortWithdrawal(withdrawalId);
pageStateSetter((prevState) => { onAborted();
const { talerWithdrawUri, ...rest } = prevState;
return {
...rest,
info: i18n.str`Withdrawal confirmed!`,
};
});
} catch (error) { } catch (error) {
pageStateSetter((prevState) => ({ if (error instanceof RequestError) {
...prevState, onError(
error: { buildRequestErrorMessage(i18n, error.cause, {
title: i18n.str`Could not confirm the withdrawal`, onClientError: (status) =>
description: (error as any).error.description, status === HttpStatusCode.Conflict
debug: JSON.stringify(error), ? i18n.str`The reserve operation has been confirmed previously and can't be aborted`
}, : undefined,
})); }),
);
} else {
onError({
title: i18n.str`Operation failed, please report`,
description:
error instanceof Error
? error.message
: JSON.stringify(error),
});
}
} }
// await abortWithdrawalCall(
// backend.state,
// pageState.withdrawalId,
// pageStateSetter,
// i18n,
// );
}} }}
> >
{i18n.str`Cancel`} {i18n.str`Cancel`}
@ -195,199 +191,3 @@ export function WithdrawalConfirmationQuestion({
</Fragment> </Fragment>
); );
} }
/**
* This function confirms a withdrawal operation AFTER
* the wallet has given the exchange's payment details
* to the bank (via the Integration API). Such details
* can be given by scanning a QR code or by passing the
* raw taler://withdraw-URI to the CLI wallet.
*
* This function will set the confirmation status in the
* 'page state' and let the related components refresh.
*/
// async function confirmWithdrawalCall(
// backendState: BackendState,
// withdrawalId: string | undefined,
// pageStateSetter: StateUpdater<PageStateType>,
// i18n: InternationalizationAPI,
// ): Promise<void> {
// if (backendState.status === "loggedOut") {
// logger.error("No credentials found.");
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`No credentials found.`,
// },
// }));
// return;
// }
// if (typeof withdrawalId === "undefined") {
// logger.error("No withdrawal ID found.");
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`No withdrawal ID found.`,
// },
// }));
// return;
// }
// let res: Response;
// try {
// const { username, password } = backendState;
// const headers = prepareHeaders(username, password);
// /**
// * NOTE: tests show that when a same object is being
// * POSTed, caching might prevent same requests from being
// * made. Hence, trying to POST twice the same amount might
// * get silently ignored.
// *
// * headers.append("cache-control", "no-store");
// * headers.append("cache-control", "no-cache");
// * headers.append("pragma", "no-cache");
// * */
// // Backend URL must have been stored _with_ a final slash.
// const url = new URL(
// `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
// backendState.url,
// );
// res = await fetch(url.href, {
// method: "POST",
// headers,
// });
// } catch (error) {
// logger.error("Could not POST withdrawal confirmation to the bank", error);
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`Could not confirm the withdrawal`,
// description: (error as any).error.description,
// debug: JSON.stringify(error),
// },
// }));
// return;
// }
// if (!res || !res.ok) {
// const response = await res.json();
// // assume not ok if res is null
// logger.error(
// `Withdrawal confirmation gave response error (${res.status})`,
// res.statusText,
// );
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`Withdrawal confirmation gave response error`,
// debug: JSON.stringify(response),
// },
// }));
// return;
// }
// logger.trace("Withdrawal operation confirmed!");
// pageStateSetter((prevState) => {
// const { talerWithdrawUri, ...rest } = prevState;
// return {
// ...rest,
// info: i18n.str`Withdrawal confirmed!`,
// };
// });
// }
// /**
// * Abort a withdrawal operation via the Access API's /abort.
// */
// async function abortWithdrawalCall(
// backendState: BackendState,
// withdrawalId: string | undefined,
// pageStateSetter: StateUpdater<PageStateType>,
// i18n: InternationalizationAPI,
// ): Promise<void> {
// if (backendState.status === "loggedOut") {
// logger.error("No credentials found.");
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`No credentials found.`,
// },
// }));
// return;
// }
// if (typeof withdrawalId === "undefined") {
// logger.error("No withdrawal ID found.");
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`No withdrawal ID found.`,
// },
// }));
// return;
// }
// let res: Response;
// try {
// const { username, password } = backendState;
// const headers = prepareHeaders(username, password);
// /**
// * NOTE: tests show that when a same object is being
// * POSTed, caching might prevent same requests from being
// * made. Hence, trying to POST twice the same amount might
// * get silently ignored. Needs more observation!
// *
// * headers.append("cache-control", "no-store");
// * headers.append("cache-control", "no-cache");
// * headers.append("pragma", "no-cache");
// * */
// // Backend URL must have been stored _with_ a final slash.
// const url = new URL(
// `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
// backendState.url,
// );
// res = await fetch(url.href, { method: "POST", headers });
// } catch (error) {
// logger.error("Could not abort the withdrawal", error);
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`Could not abort the withdrawal.`,
// description: (error as any).error.description,
// debug: JSON.stringify(error),
// },
// }));
// return;
// }
// if (!res.ok) {
// const response = await res.json();
// logger.error(
// `Withdrawal abort gave response error (${res.status})`,
// res.statusText,
// );
// pageStateSetter((prevState) => ({
// ...prevState,
// error: {
// title: i18n.str`Withdrawal abortion failed.`,
// description: response.error.description,
// debug: JSON.stringify(response),
// },
// }));
// return;
// }
// logger.trace("Withdrawal operation aborted!");
// pageStateSetter((prevState) => {
// const { ...rest } = prevState;
// return {
// ...rest,
// info: i18n.str`Withdrawal aborted!`,
// };
// });
// }

View File

@ -21,7 +21,7 @@ import {
} 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";
import { Loading } from "../components/Loading.js"; import { Loading } from "../components/Loading.js";
import { usePageContext } from "../context/pageState.js"; import { PageStateType } from "../context/pageState.js";
import { useWithdrawalDetails } from "../hooks/access.js"; import { useWithdrawalDetails } from "../hooks/access.js";
import { QrCodeSection } from "./QrCodeSection.js"; import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
@ -32,7 +32,9 @@ interface Props {
account: string; account: string;
withdrawalId: string; withdrawalId: string;
talerWithdrawUri: string; talerWithdrawUri: string;
onAbort: () => void; onError: (e: PageStateType["error"]) => void;
onAborted: () => void;
onConfirmed: () => void;
onLoadNotOk: <T>( onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode; ) => VNode;
@ -46,10 +48,12 @@ export function WithdrawalQRCode({
account, account,
withdrawalId, withdrawalId,
talerWithdrawUri, talerWithdrawUri,
onAbort, onConfirmed,
onAborted,
onError,
onLoadNotOk, onLoadNotOk,
}: Props): VNode { }: Props): VNode {
logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`); const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(account, withdrawalId); const result = useWithdrawalDetails(account, withdrawalId);
if (!result.ok) { if (!result.ok) {
@ -61,18 +65,24 @@ export function WithdrawalQRCode({
if (data.aborted) { if (data.aborted) {
// signal that this withdrawal is aborted // signal that this withdrawal is aborted
// will redirect to account info // will redirect to account info
onAbort(); onAborted();
return <Loading />; return <Loading />;
} }
const parsedUri = parseWithdrawUri(talerWithdrawUri); const parsedUri = parseWithdrawUri(talerWithdrawUri);
if (!parsedUri) { if (!parsedUri) {
throw Error("can't parse withdrawal URI"); onError({
title: i18n.str`The Withdrawal URI is not valid: "${talerWithdrawUri}"`,
});
return <Loading />;
} }
if (!data.selection_done) { if (!data.selection_done) {
return ( return (
<QrCodeSection talerWithdrawUri={talerWithdrawUri} onAbort={onAbort} /> <QrCodeSection
talerWithdrawUri={talerWithdrawUri}
onAborted={onAborted}
/>
); );
} }
@ -80,8 +90,10 @@ export function WithdrawalQRCode({
// user to authorize the operation (here CAPTCHA). // user to authorize the operation (here CAPTCHA).
return ( return (
<WithdrawalConfirmationQuestion <WithdrawalConfirmationQuestion
account={account}
withdrawalId={parsedUri.withdrawalOperationId} withdrawalId={parsedUri.withdrawalOperationId}
onError={onError}
onConfirmed={onConfirmed}
onAborted={onAborted}
/> />
); );
} }

View File

@ -66,14 +66,14 @@ body {
width: 100vw; width: 100vw;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
opacity: 1; opacity: 1;
z-index: 10000; z-index: 100;
} }
nav { nav {
left: 1vw; left: 1vw;
position: relative; position: relative;
background: #0042b2; background: #0042b2;
z-index: 10000; z-index: 100;
} }
nav a, nav a,

View File

@ -14,7 +14,13 @@
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 { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
import {
ErrorType,
HttpError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { ErrorMessage } from "./context/pageState.js";
/** /**
* Validate (the number part of) an amount. If needed, * Validate (the number part of) an amount. If needed,
@ -58,6 +64,13 @@ export type WithIntermediate<Type extends object> = {
? WithIntermediate<Type[prop]> ? WithIntermediate<Type[prop]>
: Type[prop] | undefined; : Type[prop] | undefined;
}; };
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? RecursivePartial<U>[]
: T[P] extends object
? RecursivePartial<T[P]>
: T[P];
};
export enum TanChannel { export enum TanChannel {
SMS = "sms", SMS = "sms",
@ -99,3 +112,52 @@ export enum CashoutStatus {
export const PAGE_SIZE = 20; export const PAGE_SIZE = 20;
export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
export function buildRequestErrorMessage(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
cause: HttpError<SandboxBackend.SandboxError>,
specialCases: {
onClientError?: (status: HttpStatusCode) => TranslatedString | undefined;
onServerError?: (status: HttpStatusCode) => TranslatedString | undefined;
} = {},
): ErrorMessage {
let result: ErrorMessage;
switch (cause.type) {
case ErrorType.TIMEOUT: {
result = {
title: i18n.str`Request timeout`,
};
break;
}
case ErrorType.CLIENT: {
const title =
specialCases.onClientError && specialCases.onClientError(cause.status);
result = {
title: title ? title : i18n.str`The server didn't accept the request`,
description: cause.payload.error.description,
debug: JSON.stringify(cause),
};
break;
}
case ErrorType.SERVER: {
const title =
specialCases.onServerError && specialCases.onServerError(cause.status);
result = {
title: title
? title
: i18n.str`The server had problems processing the request`,
description: cause.payload.error.description,
debug: JSON.stringify(cause),
};
break;
}
case ErrorType.UNEXPECTED: {
result = {
title: i18n.str`Unexpected error`,
debug: JSON.stringify(cause),
};
break;
}
}
return result;
}