From 9922192b0dba2e479b5af3e29c1d44b98e4d29d7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 28 Feb 2023 19:03:43 -0300 Subject: fix #7729 --- .../demobank-ui/src/components/Cashouts/test.ts | 12 +- packages/demobank-ui/src/hooks/backend.ts | 5 +- packages/demobank-ui/src/hooks/circuit.ts | 10 +- packages/demobank-ui/src/pages/AdminPage.tsx | 149 ++++++---- packages/demobank-ui/src/pages/BankFrame.tsx | 100 +++++-- packages/demobank-ui/src/pages/BusinessAccount.tsx | 311 +++++++++------------ packages/demobank-ui/src/pages/HomePage.tsx | 29 +- packages/demobank-ui/src/pages/PaymentOptions.tsx | 4 +- .../src/pages/PaytoWireTransferForm.tsx | 64 +++-- packages/demobank-ui/src/pages/QrCodeSection.tsx | 8 +- .../demobank-ui/src/pages/RegistrationPage.tsx | 65 ++--- .../demobank-ui/src/pages/WalletWithdrawForm.tsx | 111 ++------ .../src/pages/WithdrawalConfirmationQuestion.tsx | 306 ++++---------------- .../demobank-ui/src/pages/WithdrawalQRCode.tsx | 28 +- packages/demobank-ui/src/scss/demo.scss | 4 +- packages/demobank-ui/src/utils.ts | 64 ++++- 16 files changed, 567 insertions(+), 703 deletions(-) (limited to 'packages/demobank-ui') 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, RequestError >([`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({ {error && ( - saveError(undefined)} /> + saveError(undefined)} /> )}
@@ -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({ {error && ( - saveError(undefined)} /> + saveError(undefined)} /> )} setSubmitAccount(a)} + onChange={(a) => { + console.log(a); + setSubmitAccount(a); + }} />

@@ -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({ {error && ( - saveError(undefined)} /> + saveError(undefined)} /> )} + 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({ {!isBalanceEmpty && ( - )} {error && ( - saveError(undefined)} /> + saveError(undefined)} /> )}

@@ -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({ ); } - /** * 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(undefined); + const [errors, setErrors] = useState< + RecursivePartial | 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({ + const errors = undefinedIfEmpty>({ 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({ }} /> @@ -861,7 +938,7 @@ function AccountForm({ }} /> @@ -883,29 +960,3 @@ function AccountForm({ ); } - -function handleError( - error: unknown, - saveError: (e: ErrorMessage) => void, - i18n: ReturnType["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({

- {pageState.error && ( - { - pageStateSetter((prev) => ({ ...prev, error: undefined })); - }} - /> - )} {backend.state.status === "loggedIn" ? (
@@ -191,20 +183,48 @@ function maybeDemoContent(content: VNode): VNode { return ; } -export function ErrorBanner({ +export function ErrorBannerFloat({ error, onClear, }: { error: ErrorMessage; onClear?: () => void; -}): VNode | null { +}): VNode { + return ( +
+ +
+ ); +} + +function ErrorBanner({ + error, + onClear, +}: { + error: ErrorMessage; + onClear?: () => void; +}): VNode { return ( -
+

{error.title}

-
+
{onClear && ( -
-

- {pageState.info} -

-
- { - pageStateSetter((prev) => ({ ...prev, info: undefined })); - }} - /> + return ( +
+ {!pageState.info ? undefined : ( +
+
+

+ {pageState.info} +

+
+ { + pageStateSetter((prev) => ({ ...prev, info: undefined })); + }} + /> +
+
-
+ )} + {!pageState.error ? undefined : ( + { + pageStateSetter((prev) => ({ ...prev, error: undefined })); + }} + /> + )}
); - 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 = { [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
error rate
; - 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 (
{error && ( - saveError(undefined)} /> + saveError(undefined)} /> )}

New cashout

@@ -555,74 +590,31 @@ function CreateCashout({ onComplete(res.data.uuid); } catch (error) { if (error instanceof RequestError) { - const e = error as RequestError; - 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({

Cashout details {id}

{error && ( - saveError(undefined)} /> + saveError(undefined)} /> )}
@@ -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; - 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; - 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 */ -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 */ +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), + }); + } + } }} /> + 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 */ +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({
{i18n.str`Abort`}
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; - 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 */ -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({ ); } - -// /** -// * 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, -// i18n: InternationalizationAPI, -// ): Promise { -// 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 */ -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({ ); } - -/** - * 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, -// i18n: InternationalizationAPI, -// ): Promise { -// 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, -// i18n: InternationalizationAPI, -// ): Promise { -// 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: ( error: HttpResponsePaginated, ) => 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 ; } 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 ; } if (!data.selection_done) { return ( - + ); } @@ -80,8 +90,10 @@ export function WithdrawalQRCode({ // user to authorize the operation (here CAPTCHA). return ( ); } 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 */ -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 = { ? WithIntermediate : Type[prop] | undefined; }; +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object + ? RecursivePartial + : 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["i18n"], + cause: HttpError, + 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; +} -- cgit v1.2.3