summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-02-28 19:03:43 -0300
committerSebastian <sebasjm@gmail.com>2023-02-28 19:03:43 -0300
commit9922192b0dba2e479b5af3e29c1d44b98e4d29d7 (patch)
tree260f2836892b93188bf17e30b2024ccea21262dd /packages
parent740849dd89e3746fdc34c3a112288dbfe4bd7220 (diff)
downloadwallet-core-9922192b0dba2e479b5af3e29c1d44b98e4d29d7.tar.gz
wallet-core-9922192b0dba2e479b5af3e29c1d44b98e4d29d7.tar.bz2
wallet-core-9922192b0dba2e479b5af3e29c1d44b98e4d29d7.zip
fix #7729
Diffstat (limited to 'packages')
-rw-r--r--packages/demobank-ui/src/components/Cashouts/test.ts12
-rw-r--r--packages/demobank-ui/src/hooks/backend.ts5
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts10
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx149
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx100
-rw-r--r--packages/demobank-ui/src/pages/BusinessAccount.tsx311
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx29
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx4
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx64
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx8
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx65
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx111
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx306
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx28
-rw-r--r--packages/demobank-ui/src/scss/demo.scss4
-rw-r--r--packages/demobank-ui/src/utils.ts64
16 files changed, 567 insertions, 703 deletions
diff --git a/packages/demobank-ui/src/components/Cashouts/test.ts b/packages/demobank-ui/src/components/Cashouts/test.ts
index 014819f44..6d61b0af4 100644
--- a/packages/demobank-ui/src/components/Cashouts/test.ts
+++ b/packages/demobank-ui/src/components/Cashouts/test.ts
@@ -32,7 +32,9 @@ describe("Transaction states", () => {
const props: Props = {
account: "123",
- onSelected: () => { null },
+ onSelected: () => {
+ null;
+ },
};
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
@@ -117,7 +119,9 @@ describe("Transaction states", () => {
const props: Props = {
account: "123",
- onSelected: () => { null },
+ onSelected: () => {
+ null;
+ },
};
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
@@ -151,7 +155,9 @@ describe("Transaction states", () => {
const props: Props = {
account: "123",
- onSelected: () => { null },
+ onSelected: () => {
+ null;
+ },
};
env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
diff --git a/packages/demobank-ui/src/hooks/backend.ts b/packages/demobank-ui/src/hooks/backend.ts
index 8d526c0e4..3f2981edf 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -279,7 +279,10 @@ export function useAuthenticatedBackend(): useBackendType {
sandboxCashoutFetcher,
};
}
-
+/**
+ *
+ * @deprecated
+ */
export function useBackendConfig(): HttpResponse<
SandboxBackend.Config,
SandboxBackend.SandboxError
diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts
index 6cf543a3c..c2563adb4 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -82,11 +82,11 @@ export function useAdminAccountAPI(): AdminAccountAPI {
contentType: "json",
});
if (account === state.username) {
- await mutateAll(/.*/)
+ await mutateAll(/.*/);
logIn({
username: account,
- password: data.new_password
- })
+ password: data.new_password,
+ });
}
return res;
};
@@ -284,7 +284,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
HttpResponseOk<SandboxBackend.Circuit.Config>,
RequestError<SandboxBackend.SandboxError>
>([`circuit-api/config`], fetcher, {
- refreshInterval: 0,
+ refreshInterval: 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
@@ -298,7 +298,7 @@ export function useRatiosAndFeeConfig(): HttpResponse<
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"
+ data.data.ratios_and_fees.fiat_currency = "FIAT";
}
}
if (data) return data;
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
index 0a1dc26ec..2a5701a95 100644
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -16,6 +16,7 @@
import {
Amounts,
+ HttpStatusCode,
parsePaytoUri,
TranslatedString,
} from "@gnu-taler/taler-util";
@@ -35,11 +36,13 @@ import {
useAdminAccountAPI,
} from "../hooks/circuit.js";
import {
+ buildRequestErrorMessage,
PartialButDefined,
+ RecursivePartial,
undefinedIfEmpty,
WithIntermediate,
} from "../utils.js";
-import { ErrorBanner } from "./BankFrame.js";
+import { ErrorBannerFloat } from "./BankFrame.js";
import { ShowCashoutDetails } from "./BusinessAccount.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@@ -373,7 +376,7 @@ export function UpdateAccountPassword({
</h1>
</div>
{error && (
- <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<form class="pure-form">
@@ -435,7 +438,17 @@ export function UpdateAccountPassword({
});
onUpdateSuccess();
} 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>
</div>
{error && (
- <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<AccountForm
template={undefined}
purpose="create"
- onChange={(a) => setSubmitAccount(a)}
+ onChange={(a) => {
+ console.log(a);
+ setSubmitAccount(a);
+ }}
/>
<p>
@@ -514,7 +530,28 @@ function CreateNewAccount({
await createAccount(account);
onCreateSuccess(account.password);
} 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>
</div>
{error && (
- <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<AccountForm
template={result.data}
@@ -622,7 +659,26 @@ export function ShowAccountDetails({
});
onUpdateSuccess();
} 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>
</div>
{!isBalanceEmpty && (
- <ErrorBanner
+ <ErrorBannerFloat
error={{
title: i18n.str`Can't delete the account`,
description: i18n.str`Balance is not empty`,
@@ -681,7 +737,7 @@ function RemoveAccount({
/>
)}
{error && (
- <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<p>
@@ -710,7 +766,28 @@ function RemoveAccount({
const r = await deleteAccount(account);
onUpdateSuccess();
} 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>
);
}
-
/**
* Create valid account object to update or create
* Take template as initial values for the form
@@ -740,7 +816,9 @@ function AccountForm({
}): VNode {
const initial = initializeFromTemplate(template);
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();
function updateForm(newForm: typeof initial): void {
@@ -748,7 +826,7 @@ function AccountForm({
? undefined
: parsePaytoUri(newForm.cashout_address);
- const validationResult = undefinedIfEmpty<typeof initial>({
+ const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
cashout_address: !newForm.cashout_address
? i18n.str`required`
: !parsed
@@ -758,20 +836,20 @@ function AccountForm({
: !IBAN_REGEX.test(parsed.iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
: undefined,
- contact_data: {
- email: !newForm.contact_data.email
+ contact_data: undefinedIfEmpty({
+ email: !newForm.contact_data?.email
? undefined
: !EMAIL_REGEX.test(newForm.contact_data.email)
? i18n.str`it should be an email`
: undefined,
- phone: !newForm.contact_data.phone
+ phone: !newForm.contact_data?.phone
? undefined
: !newForm.contact_data.phone.startsWith("+")
? i18n.str`should start with +`
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
? i18n.str`phone number can't have other than numbers`
: undefined,
- },
+ }),
iban: !newForm.iban
? i18n.str`required`
: !IBAN_REGEX.test(newForm.iban)
@@ -780,10 +858,9 @@ function AccountForm({
name: !newForm.name ? i18n.str`required` : undefined,
username: !newForm.username ? i18n.str`required` : undefined,
});
-
- setErrors(validationResult);
+ setErrors(errors);
setForm(newForm);
- onChange(validationResult === undefined ? undefined : (newForm as any));
+ onChange(errors === undefined ? (newForm as any) : undefined);
}
return (
@@ -846,7 +923,7 @@ function AccountForm({
}}
/>
<ShowInputErrorLabel
- message={errors?.contact_data.email}
+ message={errors?.contact_data?.email}
isDirty={form.contact_data.email !== undefined}
/>
</fieldset>
@@ -861,7 +938,7 @@ function AccountForm({
}}
/>
<ShowInputErrorLabel
- message={errors?.contact_data.phone}
+ message={errors?.contact_data?.phone}
isDirty={form.contact_data?.phone !== undefined}
/>
</fieldset>
@@ -883,29 +960,3 @@ function AccountForm({
</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),
- });
- }
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index cf52cb0f3..e75a5c1d0 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -126,14 +126,6 @@ export function BankFrame({
</nav>
</div>
<section id="main" class="content">
- {pageState.error && (
- <ErrorBanner
- error={pageState.error}
- onClear={() => {
- pageStateSetter((prev) => ({ ...prev, error: undefined }));
- }}
- />
- )}
<StatusBanner />
{backend.state.status === "loggedIn" ? (
<div class="top-right">
@@ -191,20 +183,48 @@ function maybeDemoContent(content: VNode): VNode {
return <Fragment />;
}
-export function ErrorBanner({
+export function ErrorBannerFloat({
error,
onClear,
}: {
error: ErrorMessage;
onClear?: () => void;
-}): VNode | null {
+}): VNode {
+ return (
+ <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 }}>
+ <div
+ class="informational informational-fail"
+ style={{
+ marginTop: 8,
+ paddingLeft: 16,
+ paddingRight: 16,
+ }}
+ >
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p>
<b>{error.title}</b>
</p>
- <div>
+ <div style={{ marginTop: "auto", marginBottom: "auto" }}>
{onClear && (
<input
type="button"
@@ -225,26 +245,46 @@ export function ErrorBanner({
function StatusBanner(): VNode | null {
const { pageState, pageStateSetter } = usePageContext();
- if (!pageState.info) return null;
- const rval = (
- <div class="informational informational-ok" style={{ marginTop: 8 }}>
- <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 }));
- }}
- />
+ return (
+ <div
+ style={{
+ position: "fixed",
+ top: 0,
+ zIndex: 200,
+ width: "90%",
+ }}
+ >
+ {!pageState.info ? undefined : (
+ <div
+ class="informational informational-ok"
+ 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>
+ )}
+ {!pageState.error ? undefined : (
+ <ErrorBanner
+ error={pageState.error}
+ onClear={() => {
+ pageStateSetter((prev) => ({ ...prev, error: undefined }));
+ }}
+ />
+ )}
</div>
);
- return rval;
}
diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx
index 6278fe08b..9bd799746 100644
--- a/packages/demobank-ui/src/pages/BusinessAccount.tsx
+++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx
@@ -20,13 +20,13 @@ import {
TranslatedString,
} from "@gnu-taler/taler-util";
import {
- ErrorType,
+ HttpResponse,
HttpResponsePaginated,
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
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 { useBackendContext } from "../context/backend.js";
import { ErrorMessage, usePageContext } from "../context/pageState.js";
@@ -36,9 +36,13 @@ import {
useCircuitAccountAPI,
useRatiosAndFeeConfig,
} from "../hooks/circuit.js";
-import { TanChannel, undefinedIfEmpty } from "../utils.js";
+import {
+ buildRequestErrorMessage,
+ TanChannel,
+ undefinedIfEmpty,
+} from "../utils.js";
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
-import { ErrorBanner } from "./BankFrame.js";
+import { ErrorBannerFloat } from "./BankFrame.js";
import { LoginForm } from "./LoginForm.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@@ -177,6 +181,46 @@ type ErrorFrom<T> = {
[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({
account,
onComplete,
@@ -207,15 +251,6 @@ function CreateCashout({
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_debit = !amount
? zero
@@ -256,7 +291,7 @@ function CreateCashout({
return (
<div>
{error && (
- <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<h1>New cashout</h1>
<form class="pure-form">
@@ -555,74 +590,31 @@ function CreateCashout({
onComplete(res.data.uuid);
} catch (error) {
if (error instanceof RequestError) {
- 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(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.BadRequest
+ ? i18n.str`The exchange rate was incorrectly applied`
+ : status === HttpStatusCode.Forbidden
+ ? i18n.str`A institutional user tried the operation`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`Need a contact data where to send the TAN`
+ : status === HttpStatusCode.PreconditionFailed
+ ? i18n.str`The account does not have sufficient funds`
+ : undefined,
+ onServerError: (status) =>
+ status === HttpStatusCode.ServiceUnavailable
+ ? i18n.str`The bank does not support the TAN channel for this operation`
+ : undefined,
+ }),
+ );
+ } else {
saveError({
- title: i18n.str`Cashout failed, please report`,
- description: error.message,
+ title: i18n.str`Operation failed, please report`,
+ 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 {
id: string;
onCancel: () => void;
@@ -662,7 +673,7 @@ export function ShowCashoutDetails({
<div>
<h1>Cashout details {id}</h1>
{error && (
- <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ <ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<form class="pure-form">
<fieldset>
@@ -744,68 +755,27 @@ export function ShowCashoutDetails({
onClick={async (e) => {
e.preventDefault();
try {
- const rest = await abortCashout(id);
+ await abortCashout(id);
onCancel();
} catch (error) {
if (error instanceof RequestError) {
- 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(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.NotFound
+ ? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
+ : status === HttpStatusCode.PreconditionFailed
+ ? i18n.str`Cashout was already confimed`
+ : undefined,
+ }),
+ );
+ } else {
saveError({
- title: i18n.str`Aborting failed, please report`,
- description: error.message,
+ title: i18n.str`Operation failed, please report`,
+ description:
+ error instanceof Error
+ ? error.message
+ : JSON.stringify(error),
});
}
}
@@ -827,48 +797,27 @@ export function ShowCashoutDetails({
});
} catch (error) {
if (error instanceof RequestError) {
- 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(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.NotFound
+ ? i18n.str`Cashout not found. It may be also mean that it was already aborted.`
+ : status === HttpStatusCode.PreconditionFailed
+ ? i18n.str`Cashout was already confimed`
+ : status === HttpStatusCode.Conflict
+ ? i18n.str`Confirmation failed. Maybe the user changed their cash-out address between the creation and the confirmation`
+ : status === HttpStatusCode.Forbidden
+ ? i18n.str`Invalid code`
+ : undefined,
+ }),
+ );
+ } else {
saveError({
- title: i18n.str`Confirmation failed, please report`,
- description: error.message,
+ title: i18n.str`Operation failed, please report`,
+ description:
+ error instanceof Error
+ ? error.message
+ : JSON.stringify(error),
});
}
}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
index a360bd64c..7ef4284bf 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -14,11 +14,10 @@
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 {
ErrorType,
HttpResponsePaginated,
- RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
@@ -79,7 +78,27 @@ export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
account={backend.state.username}
withdrawalId={withdrawalId}
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(
backend.state.username,
saveError,
@@ -147,7 +166,7 @@ function handleNotOkResult(
break;
}
case ErrorType.CLIENT: {
- const errorData = result.error;
+ const errorData = result.payload;
onErrorHandler({
title: i18n.str`Could not load due to a client error`,
description: errorData.error.description,
@@ -168,7 +187,7 @@ function handleNotOkResult(
onErrorHandler({
title: i18n.str`Unexpected error.`,
description: `Diagnostic from ${result.info?.url} is "${result.message}"`,
- debug: JSON.stringify(result.error),
+ debug: JSON.stringify(result.exception),
});
break;
}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index dd04ed6e2..610efafc0 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -14,12 +14,12 @@
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 { 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 { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
/**
* Let the user choose a payment option,
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 07b011a00..9698d5b98 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -17,22 +17,20 @@
import {
Amounts,
buildPayto,
+ HttpStatusCode,
Logger,
parsePaytoUri,
stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import {
- InternationalizationAPI,
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType } from "../context/pageState.js";
import { useAccessAPI } from "../hooks/access.js";
-import { BackendState } from "../hooks/backend.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("PaytoWireTransferForm");
@@ -184,11 +182,35 @@ export function PaytoWireTransferForm({
ibanPayto.params.message = encodeURIComponent(subject);
const paytoUri = stringifyPaytoUri(ibanPayto);
- await createTransaction({
- paytoUri,
- amount: `${currency}:${amount}`,
- });
- onSuccess();
+ try {
+ await createTransaction({
+ paytoUri,
+ amount: `${currency}:${amount}`,
+ });
+ 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
@@ -298,13 +320,21 @@ export function PaytoWireTransferForm({
rawPaytoInputSetter(undefined);
} catch (error) {
if (error instanceof RequestError) {
- const errorData: SandboxBackend.SandboxError =
- error.info.error;
-
+ 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`Transfer creation gave response error`,
- description: errorData.error.description,
- debug: JSON.stringify(errorData),
+ title: i18n.str`Operation failed, please report`,
+ description:
+ error instanceof Error
+ ? error.message
+ : JSON.stringify(error),
});
}
}
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index 708e28657..8f85fff91 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -14,17 +14,17 @@
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 { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
export function QrCodeSection({
talerWithdrawUri,
- onAbort,
+ onAborted,
}: {
talerWithdrawUri: string;
- onAbort: () => void;
+ onAborted: () => void;
}): VNode {
const { i18n } = useTranslationContext();
useEffect(() => {
@@ -64,7 +64,7 @@ export function QrCodeSection({
<br />
<a
class="pure-button btn-cancel"
- onClick={onAbort}
+ onClick={onAborted}
>{i18n.str`Abort`}</a>
</div>
</article>
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index c6bc3c327..f22475e10 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -15,7 +15,6 @@
*/
import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
import {
- ErrorType,
RequestError,
useTranslationContext,
} 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 { useTestingAPI } from "../hooks/access.js";
import { bankUiSettings } from "../settings.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("RegistrationPage");
@@ -177,52 +176,22 @@ function RegistrationForm({
onComplete();
} catch (error) {
if (error instanceof RequestError) {
- 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);
- }
- }
+ onError(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? i18n.str`That username is already taken`
+ : undefined,
+ }),
+ );
+ } else {
+ onError({
+ title: i18n.str`Operation failed, please report`,
+ description:
+ error instanceof Error
+ ? error.message
+ : JSON.stringify(error),
+ });
}
}
}}
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 02b389c6c..c1ad2f0cf 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -14,16 +14,16 @@
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 {
RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { h, VNode } from "preact";
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 { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WalletWithdrawForm");
@@ -127,16 +127,21 @@ export function WalletWithdrawForm({
onSuccess(result.data);
} catch (error) {
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({
- title: i18n.str`Could not create withdrawal operation`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- });
- }
- if (error instanceof Error) {
- onError({
- title: i18n.str`Something when wrong trying to start the withdrawal`,
- description: error.message,
+ title: i18n.str`Operation failed, please report`,
+ description:
+ error instanceof Error
+ ? error.message
+ : JSON.stringify(error),
});
}
}
@@ -147,85 +152,3 @@ export function WalletWithdrawForm({
</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,
-// }));
-// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 4e5c621e2..d7ed215be 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -14,32 +14,36 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Logger } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
+import {
+ RequestError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { usePageContext } from "../context/pageState.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
import { useAccessAPI } from "../hooks/access.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props {
- account: string;
withdrawalId: string;
+ onError: (e: PageStateType["error"]) => void;
+ onConfirmed: () => void;
+ onAborted: () => void;
}
/**
* Additional authentication required to complete the operation.
* Not providing a back button, only abort.
*/
export function WithdrawalConfirmationQuestion({
- account,
+ onError,
+ onConfirmed,
+ onAborted,
withdrawalId,
}: Props): VNode {
- const { pageState, pageStateSetter } = usePageContext();
- const backend = useBackendContext();
const { i18n } = useTranslationContext();
const captchaNumbers = useMemo(() => {
@@ -111,35 +115,29 @@ export function WithdrawalConfirmationQuestion({
e.preventDefault();
try {
await confirmWithdrawal(withdrawalId);
- pageStateSetter((prevState) => {
- const { talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
- info: i18n.str`Withdrawal confirmed!`,
- };
- });
+ onConfirmed();
} catch (error) {
- pageStateSetter((prevState) => ({
- ...prevState,
- error: {
- title: i18n.str`Could not confirm the withdrawal`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
+ if (error instanceof RequestError) {
+ onError(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? 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`}
@@ -151,29 +149,27 @@ export function WithdrawalConfirmationQuestion({
e.preventDefault();
try {
await abortWithdrawal(withdrawalId);
- pageStateSetter((prevState) => {
- const { talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
- info: i18n.str`Withdrawal confirmed!`,
- };
- });
+ onAborted();
} catch (error) {
- pageStateSetter((prevState) => ({
- ...prevState,
- error: {
- title: i18n.str`Could not confirm the withdrawal`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
+ if (error instanceof RequestError) {
+ onError(
+ buildRequestErrorMessage(i18n, error.cause, {
+ onClientError: (status) =>
+ status === HttpStatusCode.Conflict
+ ? 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`}
@@ -195,199 +191,3 @@ export function WithdrawalConfirmationQuestion({
</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!`,
-// };
-// });
-// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 5169fc00f..1a4157d06 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -21,7 +21,7 @@ import {
} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
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 { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
@@ -32,7 +32,9 @@ interface Props {
account: string;
withdrawalId: string;
talerWithdrawUri: string;
- onAbort: () => void;
+ onError: (e: PageStateType["error"]) => void;
+ onAborted: () => void;
+ onConfirmed: () => void;
onLoadNotOk: <T>(
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode;
@@ -46,10 +48,12 @@ export function WithdrawalQRCode({
account,
withdrawalId,
talerWithdrawUri,
- onAbort,
+ onConfirmed,
+ onAborted,
+ onError,
onLoadNotOk,
}: Props): VNode {
- logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`);
+ const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(account, withdrawalId);
if (!result.ok) {
@@ -61,18 +65,24 @@ export function WithdrawalQRCode({
if (data.aborted) {
// signal that this withdrawal is aborted
// will redirect to account info
- onAbort();
+ onAborted();
return <Loading />;
}
const parsedUri = parseWithdrawUri(talerWithdrawUri);
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) {
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).
return (
<WithdrawalConfirmationQuestion
- account={account}
withdrawalId={parsedUri.withdrawalOperationId}
+ onError={onError}
+ onConfirmed={onConfirmed}
+ onAborted={onAborted}
/>
);
}
diff --git a/packages/demobank-ui/src/scss/demo.scss b/packages/demobank-ui/src/scss/demo.scss
index 3b7acaa1f..cd676f8d9 100644
--- a/packages/demobank-ui/src/scss/demo.scss
+++ b/packages/demobank-ui/src/scss/demo.scss
@@ -66,14 +66,14 @@ body {
width: 100vw;
backdrop-filter: blur(10px);
opacity: 1;
- z-index: 10000;
+ z-index: 100;
}
nav {
left: 1vw;
position: relative;
background: #0042b2;
- z-index: 10000;
+ z-index: 100;
}
nav a,
diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts
index 49b9ac276..81dd450a4 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -14,7 +14,13 @@
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,
@@ -58,6 +64,13 @@ export type WithIntermediate<Type extends object> = {
? WithIntermediate<Type[prop]>
: 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 {
SMS = "sms",
@@ -99,3 +112,52 @@ export enum CashoutStatus {
export const PAGE_SIZE = 20;
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;
+}