diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-19 15:10:18 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-19 15:10:18 -0300 |
commit | 7582855e2723e11de25f10b6d5ba8a0757616765 (patch) | |
tree | 320f30c7202c88ba63542b93de1cbda533a73356 /packages | |
parent | 9e925a2f56677600973c4659f82776a6a56339bb (diff) | |
download | wallet-core-7582855e2723e11de25f10b6d5ba8a0757616765.tar.gz wallet-core-7582855e2723e11de25f10b6d5ba8a0757616765.tar.bz2 wallet-core-7582855e2723e11de25f10b6d5ba8a0757616765.zip |
some ui fixes
Diffstat (limited to 'packages')
19 files changed, 306 insertions, 519 deletions
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx b/packages/demobank-ui/src/components/Transactions/views.tsx index 404e25619..5cdb47a0c 100644 --- a/packages/demobank-ui/src/components/Transactions/views.tsx +++ b/packages/demobank-ui/src/components/Transactions/views.tsx @@ -17,7 +17,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; -import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; +import { doAutoFocus, RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { State } from "./index.js"; export function LoadingUriView({ error }: State.LoadingUriError): VNode { @@ -57,7 +57,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Date`}</th> <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Amount`}</th> <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Counterpart`}</th> - <th scope="col" class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th> + <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th> </tr> </thead> <tbody> @@ -70,10 +70,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode </tr> {txs.map(item => { const time = item.when.t_ms === "never" ? "" : format(item.when.t_ms, "HH:mm:ss") - const amount = <Fragment> - { } - </Fragment> - return (<tr key={idx}> + return (<tr key={idx} class="border-b border-gray-200 last:border-none"> <td class="relative py-2 pl-2 pr-2 text-sm "> <div class="font-medium text-gray-900">{time}</div> <dl class="font-normal sm:hidden"> @@ -91,6 +88,11 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode <dd class="mt-1 truncate text-gray-500 sm:hidden"> {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart} </dd> + <dd class="mt-1 text-gray-500 sm:hidden" > + <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100"> + {item.subject} + </pre> + </dd> </dl> </td> <td data-negative={item.negative ? "true" : "false"} @@ -101,7 +103,7 @@ export function ReadyView({ transactions, onNext, onPrev }: State.Ready): VNode )} </td> <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">{item.counterpart}</td> - <td class="px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td> + <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">{item.subject}</td> </tr>) })} </Fragment> diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts index 2533d32fe..7023b8803 100644 --- a/packages/demobank-ui/src/hooks/access.ts +++ b/packages/demobank-ui/src/hooks/access.ts @@ -38,7 +38,8 @@ export function useAccountDetails(account: string) { return await api.getAccount({ username, token }) } const token = credentials.status !== "loggedIn" ? undefined : credentials.token - const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccount">, TalerHttpError>([account, token], fetcher, { + const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccount">, TalerHttpError>( + [account, token, "getAccount"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -65,7 +66,7 @@ export function useWithdrawalDetails(wid: string) { } const { data, error } = useSWR<TalerCoreBankResultByMethod<"getWithdrawalById">, TalerHttpError>( - [wid], fetcher, { + [wid, "getWithdrawalById"], fetcher, { refreshInterval: 1000, refreshWhenHidden: false, revalidateOnFocus: false, @@ -92,7 +93,7 @@ export function useTransactionDetails(account: string, tid: number) { } const { data, error } = useSWR<TalerCoreBankResultByMethod<"getTransactionById">, TalerHttpError>( - [account, token, tid], fetcher, { + [account, token, tid, "getTransactionById"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -121,7 +122,18 @@ export function usePublicAccounts(initial?: number) { }) } - const { data, error } = useSWR<TalerCoreBankResultByMethod<"getPublicAccounts">, TalerHttpError>([offset], fetcher); + const { data, error } = useSWR<TalerCoreBankResultByMethod<"getPublicAccounts">, TalerHttpError>( + [offset, "getPublicAccounts"], fetcher, { + refreshInterval: 0, + refreshWhenHidden: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + errorRetryCount: 0, + errorRetryInterval: 1, + shouldRetryOnError: false, + keepPreviousData: true, + }); const isLastPage = data && data.body.public_accounts.length < PAGE_SIZE; @@ -174,7 +186,7 @@ export function useTransactions(account: string, initial?: number) { } const { data, error } = useSWR<TalerCoreBankResultByMethod<"getTransactions">, TalerHttpError>( - [account, token, offset], fetcher, { + [account, token, offset, "getTransactions"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, refreshWhenOffline: false, diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 208663f8b..0f7af5fe5 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -106,9 +106,8 @@ export function useRatiosAndFeeConfig() { return api.getConversionRates() } - const { data, error } = useSWR< - TalerCoreBankResultByMethod<"getConversionRates">, TalerHttpError - >([], fetcher, { + const { data, error } = useSWR<TalerCoreBankResultByMethod<"getConversionRates">, TalerHttpError>( + [, "getConversionRates"], fetcher, { refreshInterval: 60 * 1000, refreshWhenHidden: false, revalidateOnFocus: false, @@ -138,7 +137,7 @@ export function useBusinessAccounts() { const [offset, setOffset] = useState<string | undefined>(); - function fetcher(token: AccessToken, offset?: string) { + function fetcher([token, offset]: [AccessToken, string]) { return api.getAccounts(token, { limit: MAX_RESULT_SIZE, offset, @@ -147,7 +146,7 @@ export function useBusinessAccounts() { } const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccounts">, TalerHttpError>( - [token, offset], fetcher, { + [token, offset, "getAccounts"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -192,8 +191,8 @@ function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId { } export function useCashouts(account: string) { const { state: credentials } = useBackendState(); - const token = credentials.status !== "loggedIn" ? undefined : credentials.token const { api } = useBankCoreApiContext(); + const token = credentials.status !== "loggedIn" ? undefined : credentials.token async function fetcher([username, token]: [string, AccessToken]) { const list = await api.getAccountCashouts({ username, token }) @@ -212,7 +211,7 @@ export function useCashouts(account: string) { } const { data, error } = useSWR<OperationOk<{ cashouts: CashoutWithId[] }>, TalerHttpError>( - [account, token], fetcher, { + [account, token, "getAccountCashouts"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, @@ -239,7 +238,7 @@ export function useCashoutDetails(cashoutId: string) { } const { data, error } = useSWR<TalerCoreBankResultByMethod<"getCashoutById">, TalerHttpError>( - [creds?.username, creds?.token, cashoutId], fetcher, { + [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, { refreshInterval: 0, refreshWhenHidden: false, revalidateOnFocus: false, diff --git a/packages/demobank-ui/src/i18n/strings.ts b/packages/demobank-ui/src/i18n/strings.ts index 1b19f7df7..86d1fff5b 100644 --- a/packages/demobank-ui/src/i18n/strings.ts +++ b/packages/demobank-ui/src/i18n/strings.ts @@ -167,7 +167,7 @@ strings["en"] = { "Transfer creation gave response error": [""], "Wire transfer created!": [""], "Amount to withdraw:": ["Amount to withdraw"], - Withdraw: ["Confirm withdrawal"], + Withdraw: ["Withdraw"], "No credentials given.": [""], "Could not create withdrawal operation": [""], "Withdrawal creation gave response error": [""], diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index a8167cca5..981b0f880 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -21,7 +21,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useBackendContext } from "../context/backend.js"; import { bankUiSettings } from "../settings.js"; -import { undefinedIfEmpty } from "../utils.js"; +import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; import { useBankCoreApiContext } from "../context/config.js"; import { assertUnreachable } from "./HomePage.js"; @@ -78,43 +78,36 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb async function doLogin() { if (!username || !password) return; setBusy({}) - const data: TalerAuthentication.TokenRequest = { - // scope: "readwrite" as "write", //FIX: different than merchant - scope: "readwrite", - duration: { - d_us: "forever" //FIX: should return shortest - // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 - }, - refreshable: true, - } - const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { - // scope: "readwrite" as "write", //FIX: different than merchant - scope: "readwrite", - duration: { - d_us: "forever" //FIX: should return shortest - // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 - }, - refreshable: true, - }) - if (resp.type === "ok") { - backend.logIn({ username, token: resp.body.access_token }); - } else { - switch (resp.case) { - case "wrong-credentials": return notify({ - type: "error", - title: i18n.str`Wrong credentials for "${username}"`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "not-found": return notify({ - type: "error", - title: i18n.str`Account not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) + await withRuntimeErrorHandling(i18n, async () => { + const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { + // scope: "readwrite" as "write", //FIX: different than merchant + scope: "readwrite", + duration: { + d_us: "forever" //FIX: should return shortest + // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 + }, + refreshable: true, + }) + if (resp.type === "ok") { + backend.logIn({ username, token: resp.body.access_token }); + } else { + switch (resp.case) { + case "wrong-credentials": return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${username}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } } - } + }) setPassword(undefined); setBusy(undefined) } @@ -198,7 +191,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb <button type="submit" class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" disabled={!!errors} - onClick={(e) => { + onClick={async (e) => { e.preventDefault() doLogin() }} diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index 148571ec9..c9c1fa238 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -21,7 +21,7 @@ import { useBankCoreApiContext } from "../../context/config.js"; import { useWithdrawalDetails } from "../../hooks/access.js"; import { useBackendState } from "../../hooks/backend.js"; import { useSettings } from "../../hooks/settings.js"; -import { buildRequestErrorMessage } from "../../utils.js"; +import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../../utils.js"; import { Props, State } from "./index.js"; import { assertUnreachable } from "../HomePage.js"; import { mutate } from "swr"; @@ -41,9 +41,8 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive async function doSilentStart() { //FIXME: if amount is not enough use balance const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) - - try { - if (!creds) return; + if (!creds) return; + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.createWithdrawal(creds, { amount: Amounts.stringify(parsedAmount), }); @@ -67,18 +66,7 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } else { updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) } const withdrawalOperationId = settings.currentWithdrawalOperationId @@ -98,10 +86,9 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive const wid = withdrawalOperationId async function doAbort() { - try { - setBusy({}) + setBusy({}) + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.abortWithdrawalById(wid); - setBusy(undefined) if (resp.type === "ok") { onClose(); } else { @@ -115,30 +102,19 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive default: assertUnreachable(resp.case) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) + setBusy(undefined) } async function doConfirm() { - try { - setBusy({}) + setBusy({}) + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.confirmWithdrawalById(wid); if (resp.type === "ok") { mutate(() => true)//clean withdrawal state if (!settings.showWithdrawalSuccess) { notifyInfo(i18n.str`Wire transfer completed!`) } - setBusy(undefined) } else { switch (resp.case) { case "previously-aborted": return notify({ @@ -156,18 +132,8 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive default: assertUnreachable(resp) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) + setBusy(undefined) } const uri = stringifyWithdrawUri({ diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index f60ba3270..c36d58691 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -18,7 +18,7 @@ import { AmountJson } from "@gnu-taler/taler-util"; import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js"; +import { PaytoWireTransferForm, doAutoFocus } from "./PaytoWireTransferForm.js"; import { WalletWithdrawForm } from "./WalletWithdrawForm.js"; import { useSettings } from "../hooks/settings.js"; @@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ const { i18n } = useTranslationContext(); const [settings] = useSettings(); - const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>(); + const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>("wire-transfer"); return ( <div class="mt-2"> @@ -46,28 +46,28 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => { setTab("charge-wallet") }} /> - <span class="flex flex-1"> + <div class="flex flex-col"> + <span class="flex"> <div class="text-4xl mr-4 my-auto">💵</div> - <span class="flex flex-col"> - <span id="project-type-0-label" class="block text-sm font-medium text-gray-900"> - <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate> - </span> - <span id="project-type-0-description-0" class="mt-1 flex items-center text-sm text-gray-500"> - <i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate> + <span class="grow self-center text-lg text-gray-900 align-middle text-center"> + <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate> </span> + <svg class="self-center flex-none h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </span> + <div class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate> + </div> {!!settings.currentWithdrawalOperationId && - <span class="inline-flex items-center gap-x-1.5 w-fit rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700"> + <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre"> <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true"> <circle cx="3" cy="3" r="3" /> </svg> <i18n.Translate>operation ready</i18n.Translate> </span> } - </span> - </span> - <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> - </svg> + </div> </label> @@ -75,20 +75,20 @@ export function PaymentOptions({ limit, goToConfirmOperation }: { limit: AmountJ <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => { setTab("wire-transfer") }} /> - <span class="flex flex-1"> - <div class="text-4xl mr-4 my-auto">↔</div> - <span class="flex flex-col"> - <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <div class="flex flex-col"> + <span class="flex"> + <div class="text-4xl mr-4 my-auto">↔</div> + <span class="grow self-center text-lg font-medium text-gray-900 align-middle text-center"> <i18n.Translate>another bank account</i18n.Translate> </span> - <span id="project-type-1-description-0" class="mt-1 flex items-center text-sm text-gray-500"> - <i18n.Translate>Make a wire transfer to an account which you know the bank account number</i18n.Translate> - </span> + <svg class="self-center flex-none h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> </span> - </span> - <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> - </svg> + <div class="mt-1 flex items-center text-sm text-gray-500"> + <i18n.Translate>Make a wire transfer to an account which you know the bank account number</i18n.Translate> + </div> + </div> </label> </div> {tab === "charge-wallet" && ( diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 7861bb0b3..e713324c5 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -39,6 +39,7 @@ import { buildRequestErrorMessage, undefinedIfEmpty, validateIBAN, + withRuntimeErrorHandling, } from "../utils.js"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; @@ -60,8 +61,7 @@ export function PaytoWireTransferForm({ onCancel: (() => void) | undefined; limit: AmountJson; }): VNode { - const [isRawPayto, setIsRawPayto] = useState(false); - // FIXME: remove this + const [isRawPayto, setIsRawPayto] = useState(true); const { state: credentials } = useBackendState() const { api } = useBankCoreApiContext(); const [iban, setIban] = useState<string | undefined>(); @@ -73,10 +73,6 @@ export function PaytoWireTransferForm({ ); const { i18n } = useTranslationContext(); const ibanRegex = "^[A-Z][A-Z][0-9]+$"; - const ref = useRef<HTMLInputElement>(null); - useEffect(() => { - if (focus) ref.current?.focus(); - }, [focus, isRawPayto]); const trimmedAmountStr = amount?.trim(); const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`); @@ -100,8 +96,6 @@ export function PaytoWireTransferForm({ : undefined, }); - // const { createTransaction } = useAccessAPI(); - const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput); const errorsPayto = undefinedIfEmpty({ @@ -125,12 +119,13 @@ export function PaytoWireTransferForm({ async function doSend() { let payto_uri: string | undefined; let sendingAmount: AmountString | undefined; + if (credentials.status !== "loggedIn") return; if (rawPaytoInput) { const p = parsePaytoUri(rawPaytoInput) if (!p) return; sendingAmount = p.params.amount delete p.params.amount - //it should have message + //if this payto is valid then it already have message payto_uri = stringifyPaytoUri(p) } else { if (!iban || !subject) return; @@ -139,11 +134,11 @@ export function PaytoWireTransferForm({ payto_uri = stringifyPaytoUri(ibanPayto); sendingAmount = `${limit.currency}:${trimmedAmountStr}` } + const puri = payto_uri; - try { - if (credentials.status !== "loggedIn") return; + await withRuntimeErrorHandling(i18n, async () => { const res = await api.createTransaction(credentials, { - payto_uri, + payto_uri: puri, amount: sendingAmount, }); mutate(() => true) @@ -168,32 +163,20 @@ export function PaytoWireTransferForm({ setAmount(undefined); setIban(undefined); setSubject(undefined); - rawPaytoInputSetter(undefined) - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } - + rawPaytoInputSetter(undefined) + }) } return (<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> {/** * FIXME: Scan a qr code */} - <div class="px-4 sm:px-0"> + <div class=""> <h2 class="text-base font-semibold leading-7 text-gray-900"> {title} </h2> <div> - <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 sm:gap-x-4"> + <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4"> <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}> <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => { if (parsed && parsed.isKnown && parsed.targetType === "iban") { @@ -207,8 +190,6 @@ export function PaytoWireTransferForm({ setSubject(subject) } } - //payto://iban/DE9714548806481?amount=LOCAL%3A2&message=011Y8V8KDCPFDEKPDZTHS7KZ14GHX7BVWKRDDPZ1N75TJ90166T0 - //payto://iban/DE9714548806481?receiver-name=Exchanger&amount=LOCAL%3A2&message=011Y8V8KDCPFDEKPDZTHS7KZ14GHX7BVWKRDDPZ1N75TJ90166T0 setIsRawPayto(false) }} /> <span class="flex flex-1"> @@ -236,7 +217,7 @@ export function PaytoWireTransferForm({ }} /> <span class="flex flex-1"> <span class="flex flex-col"> - <span class="block text-sm font-medium text-gray-900"> + <span class="block text-sm font-medium text-gray-900"> <i18n.Translate>Import payto:// URI</i18n.Translate> </span> </span> @@ -247,17 +228,16 @@ export function PaytoWireTransferForm({ </div> <form - class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 w-fit mx-auto" + class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md sm:rounded-xl md:col-span-2 w-fit mx-auto" autoCapitalize="none" autoCorrect="off" onSubmit={e => { e.preventDefault() }} > - <div class="px-4 py-6 sm:p-8"> + <div class="p-4 sm:p-8"> {!isRawPayto ? <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6"> - <div class="sm:col-span-5"> <label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Recipient`}</label> <div class="mt-2"> @@ -338,8 +318,8 @@ export function PaytoWireTransferForm({ name="address" id="address" type="textarea" - rows={3} - class="block overflow-hidden w-64 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" + rows={5} + class="block overflow-hidden w-44 sm:w-96 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" value={rawPaytoInput ?? ""} required placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`} @@ -393,7 +373,7 @@ export function doAutoFocus(element: HTMLElement | null) { element.scrollIntoView({ behavior: "smooth", block: "center", - inline: "center" + inline: "center", }) }, 100) } diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index a37de383d..64f9ec5ab 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -30,7 +30,7 @@ import { import { Fragment, h, VNode } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../components/QR.js"; -import { buildRequestErrorMessage } from "../utils.js"; +import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../utils.js"; import { useBankCoreApiContext } from "../context/config.js"; import { assertUnreachable } from "./HomePage.js"; @@ -55,36 +55,22 @@ export function QrCodeSection({ const { api } = useBankCoreApiContext() async function doAbort() { - try { + await withRuntimeErrorHandling(i18n, async () => { const result = await api.abortWithdrawalById(withdrawUri.withdrawalOperationId); if (result.type === "ok") { onAborted(); } else { switch (result.case) { - case "previously-confirmed": { - notify({ - type: "info", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted` - }) - break; - } + case "previously-confirmed": return notify({ + type: "info", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted` + }) default: { assertUnreachable(result.case) } } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) } return ( diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index fda2d904d..ce38a9fb8 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -24,7 +24,7 @@ import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useBackendContext } from "../context/backend.js"; import { bankUiSettings } from "../settings.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { getRandomPassword, getRandomUsername } from "./rnd.js"; import { useBankCoreApiContext } from "../context/config.js"; @@ -95,118 +95,80 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on }); async function doRegistrationAndLogin(name: string | undefined, username: string, password: string) { - const creationResponse = await api.createAccount("" as AccessToken, { name: name ?? "", username, password }); - if (creationResponse.type === "fail") { - switch (creationResponse.case) { - case "invalid-input": return notify({ - type: "error", - title: i18n.str`Some of the input fields are invalid.`, - description: creationResponse.detail.hint as TranslatedString, - debug: creationResponse.detail, - }) - case "unable-to-create": return notify({ - type: "error", - title: i18n.str`Unable to create that account.`, - description: creationResponse.detail.hint as TranslatedString, - debug: creationResponse.detail, - }) - case "unauthorized": return notify({ - type: "error", - title: i18n.str`No enough permission to create that account.`, - description: creationResponse.detail.hint as TranslatedString, - debug: creationResponse.detail, - }) - case "already-exist": return notify({ - type: "error", - title: i18n.str`That username is already taken`, - description: creationResponse.detail.hint as TranslatedString, - debug: creationResponse.detail, - }) - default: assertUnreachable(creationResponse) + await withRuntimeErrorHandling(i18n, async () => { + const creationResponse = await api.createAccount("" as AccessToken, { name: name ?? "", username, password }); + if (creationResponse.type === "fail") { + switch (creationResponse.case) { + case "invalid-input": return notify({ + type: "error", + title: i18n.str`Some of the input fields are invalid.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + case "unable-to-create": return notify({ + type: "error", + title: i18n.str`Unable to create that account.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + case "unauthorized": return notify({ + type: "error", + title: i18n.str`No enough permission to create that account.`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + case "already-exist": return notify({ + type: "error", + title: i18n.str`That username is already taken`, + description: creationResponse.detail.hint as TranslatedString, + debug: creationResponse.detail, + }) + default: assertUnreachable(creationResponse) + } } - } - const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { - // scope: "readwrite" as "write", //FIX: different than merchant - scope: "readwrite", - duration: { - d_us: "forever" //FIX: should return shortest - // d_us: 60 * 60 * 24 * 7 * 1000 * 1000 - }, - refreshable: true, - }) + const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { + scope: "readwrite", + duration: { d_us: "forever" }, + refreshable: true, + }) - if (resp.type === "ok") { - backend.logIn({ username, token: resp.body.access_token }); - } else { - switch (resp.case) { - case "wrong-credentials": return notify({ - type: "error", - title: i18n.str`Wrong credentials for "${username}"`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "not-found": return notify({ - type: "error", - title: i18n.str`Account not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) + if (resp.type === "ok") { + backend.logIn({ username, token: resp.body.access_token }); + } else { + switch (resp.case) { + case "wrong-credentials": return notify({ + type: "error", + title: i18n.str`Wrong credentials for "${username}"`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`Account not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) + } } - } + }) } async function doRegistrationStep() { if (!username || !password) return; - try { - await doRegistrationAndLogin(name, username, password) - setUsername(undefined); - onComplete(); - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + await doRegistrationAndLogin(name, username, password) + setUsername(undefined); setPassword(undefined); setRepeatPassword(undefined); + onComplete(); } - async function delay(ms: number): Promise<void> { - return new Promise((resolve) => { - setTimeout(() => { - resolve(undefined); - }, ms) - }) - } async function doRandomRegistration(tries: number = 3) { const user = getRandomUsername(); const pass = getRandomPassword(); - try { - setUsername(undefined); - setPassword(undefined); - setRepeatPassword(undefined); - const username = `_${user.first}-${user.second}_` - await doRegistrationAndLogin(name, username, pass) - onComplete(); - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + const username = `_${user.first}-${user.second}_` + await doRegistrationAndLogin(name, username, pass) + onComplete(); } return ( @@ -403,8 +365,9 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on <button type="submit" class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" disabled={!!errors} - onClick={(e) => { + onClick={async (e) => { e.preventDefault() + doRegistrationStep() }} > diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx index 3534f9733..c65b90503 100644 --- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -7,7 +7,7 @@ import { Loading } from "../components/Loading.js"; import { useBankCoreApiContext } from "../context/config.js"; import { useAccountDetails } from "../hooks/access.js"; import { useBackendState } from "../hooks/backend.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./HomePage.js"; import { LoginForm } from "./LoginForm.js"; import { AccountForm } from "./admin/AccountForm.js"; @@ -49,50 +49,41 @@ export function ShowAccountDetails({ async function doUpdate() { if (!update) { setUpdate(true); - } else { - if (!submitAccount || !creds) return; - try { - const resp = await api.updateAccount(creds, { - cashout_address: submitAccount.cashout_payto_uri, - challenge_contact_data: undefinedIfEmpty({ - email: submitAccount.contact_data?.email, - phone: submitAccount.contact_data?.phone, - }), - is_exchange: false, - name: submitAccount.name, - }); - if (resp.type === "ok") { - onUpdateSuccess(); - } else { - switch (resp.case) { - case "unauthorized": return notify({ - type: "error", - title: i18n.str`The rights to change the account are not sufficient`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "not-found": return notify({ - type: "error", - title: i18n.str`The username was not found`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - default: assertUnreachable(resp) - } - } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) + return; + } + if (!submitAccount || !creds) return; + await withRuntimeErrorHandling(i18n, async () => { + const resp = await api.updateAccount(creds, { + cashout_address: submitAccount.cashout_payto_uri, + challenge_contact_data: undefinedIfEmpty({ + email: submitAccount.contact_data?.email, + phone: submitAccount.contact_data?.phone, + }), + is_exchange: false, + name: submitAccount.name, + }); + + if (resp.type === "ok") { + onUpdateSuccess(); + } else { + switch (resp.case) { + case "unauthorized": return notify({ + type: "error", + title: i18n.str`The rights to change the account are not sufficient`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`The username was not found`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + default: assertUnreachable(resp) } } - } + }) + } return ( diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx index ac6e9fa9b..d82dac4b1 100644 --- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -3,7 +3,7 @@ import { HttpResponsePaginated, RequestError, notify, notifyError, useTranslatio import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; import { useBankCoreApiContext } from "../context/config.js"; import { assertUnreachable } from "./HomePage.js"; @@ -39,7 +39,7 @@ export function UpdateAccountPassword({ async function doChangePassword() { if (!!errors || !password || !creds) return; - try { + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.updatePassword(creds, { new_password: password, }); @@ -47,32 +47,18 @@ export function UpdateAccountPassword({ onUpdateSuccess(); } else { switch (resp.case) { - case "unauthorized": { - notify({ - type: "error", - title: i18n.str`Not authorized to change the password, maybe the session is invalid.` - }) - break; - } - case "not-found": { - notify({ - type: "error", - title: i18n.str`Account not found` - }) - break; - } + case "unauthorized": return notify({ + type: "error", + title: i18n.str`Not authorized to change the password, maybe the session is invalid.` + }) + case "not-found": return notify({ + type: "error", + title: i18n.str`Account not found` + }) default: assertUnreachable(resp) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError(i18n.str`Operation failed, please report`, (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString) - } - } + }) } return ( diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 2d80bad1f..28d5d7749 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -36,7 +36,7 @@ import { Attention } from "../components/Attention.js"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { useSettings } from "../hooks/settings.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./HomePage.js"; import { OperationState } from "./OperationState/index.js"; import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js"; @@ -62,6 +62,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { if (!!settings.currentWithdrawalOperationId) { return <Attention type="warning" title={i18n.str`There is an operation already`}> + <span ref={focus ? doAutoFocus : undefined}/> <i18n.Translate> To complete or cancel the operation click <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a> </i18n.Translate> @@ -87,7 +88,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { async function doStart() { if (!parsedAmount || !creds) return; - try { + await withRuntimeErrorHandling(i18n, async () => { const result = await api.createWithdrawal(creds, { amount: Amounts.stringify(parsedAmount), }); @@ -110,18 +111,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { default: assertUnreachable(result.case) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) } return <form diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index 602ec9bd8..87637f7ef 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -36,7 +36,7 @@ import { import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js"; +import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { useSettings } from "../hooks/settings.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { useBankCoreApiContext } from "../context/config.js"; @@ -88,8 +88,8 @@ export function WithdrawalConfirmationQuestion({ }) ?? busy; async function doTransfer() { - try { - setBusy({}) + setBusy({}) + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.confirmWithdrawalById(withdrawUri.withdrawalOperationId); if (resp.type === "ok") { mutate(() => true)// clean any info that we have @@ -113,53 +113,28 @@ export function WithdrawalConfirmationQuestion({ default: assertUnreachable(resp) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) setBusy(undefined) } async function doCancel() { - try { - setBusy({}) + setBusy({}) + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.abortWithdrawalById(withdrawUri.withdrawalOperationId); if (resp.type === "ok") { onAborted(); } else { switch (resp.case) { - case "previously-confirmed": { - notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted` - }); - break; - } + case "previously-confirmed": return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted` + }); default: { assertUnreachable(resp.case) } } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) setBusy(undefined) } diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx index 15910201e..7266e4de4 100644 --- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx @@ -29,6 +29,7 @@ import { useWithdrawalDetails } from "../hooks/access.js"; import { assertUnreachable } from "./HomePage.js"; import { QrCodeSection } from "./QrCodeSection.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; +import { Attention } from "../components/Attention.js"; const logger = new Logger("WithdrawalQRCode"); @@ -139,18 +140,21 @@ export function WithdrawalQRCode({ } if (!data.selected_reserve_pub) { - return <div> - the exchange is selcted but no reserve pub - </div> + return <Attention type="danger" + title={i18n.str`The operation is incomplete or some step in the withdrawal failed`} > + <i18n.Translate>The exchange is selected but no reserve public key found.</i18n.Translate> + </Attention> } const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account) if (!account) { - return <div> - the exchange is selcted but no account - </div> + return <Attention type="danger" + title={i18n.str`The operation is incomplete or some step in the withdrawal failed`} > + <i18n.Translate>The exchange is selected but the exchange payto URI is missing or invalid.</i18n.Translate> + </Attention> } + return ( <WithdrawalConfirmationQuestion withdrawUri={withdrawUri} diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index f6176e772..e10c3ad41 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -2,7 +2,7 @@ import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from " import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { buildRequestErrorMessage } from "../../utils.js"; +import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../../utils.js"; import { getRandomPassword } from "../rnd.js"; import { AccountForm } from "./AccountForm.js"; import { useBackendState } from "../../hooks/backend.js"; @@ -29,7 +29,7 @@ export function CreateNewAccount({ async function doCreate() { if (!submitAccount || !token) return; - try { + await withRuntimeErrorHandling(i18n, async () => { const account: TalerCorebankApi.RegisterAccountRequest = { cashout_payto_uri: submitAccount.cashout_payto_uri, challenge_contact_data: submitAccount.contact_data, @@ -72,18 +72,7 @@ export function CreateNewAccount({ default: assertUnreachable(resp) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) } return ( diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index ce8a53ca1..9a212ebd0 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -7,7 +7,7 @@ import { ErrorLoading } from "../../components/ErrorLoading.js"; import { Loading } from "../../components/Loading.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { useAccountDetails } from "../../hooks/access.js"; -import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js"; +import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../../utils.js"; import { assertUnreachable } from "../HomePage.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; @@ -60,7 +60,7 @@ export function RemoveAccount({ async function doRemove() { if (!token) return; - try { + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.deleteAccount({ username: account, token }); if (resp.type === "ok") { onUpdateSuccess(); @@ -95,16 +95,7 @@ export function RemoveAccount({ } } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError(i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString); - } - } + }) } const errors = undefinedIfEmpty({ diff --git a/packages/demobank-ui/src/pages/business/Home.tsx b/packages/demobank-ui/src/pages/business/Home.tsx index 03d7895e3..d7beda01d 100644 --- a/packages/demobank-ui/src/pages/business/Home.tsx +++ b/packages/demobank-ui/src/pages/business/Home.tsx @@ -45,6 +45,7 @@ import { TanChannel, buildRequestErrorMessage, undefinedIfEmpty, + withRuntimeErrorHandling, } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { InputAmount } from "../PaytoWireTransferForm.js"; @@ -241,41 +242,15 @@ function CreateCashout({ ); useEffect(() => { - if (form.isDebit) { - calculateFromDebit(amount, sellFee, sellRate) - .then((r) => { - setCalc(r); - }) - .catch((error) => { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - }); - } else { - calculateFromCredit(amount, sellFee, sellRate) - .then((r) => { - setCalc(r); - }) - .catch((error) => { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - }); + async function doAsync() { + await withRuntimeErrorHandling(i18n, async () => { + const resp = await (form.isDebit ? + calculateFromDebit(amount, sellFee, sellRate) : + calculateFromCredit(amount, sellFee, sellRate)); + setCalc(resp) + }) } + doAsync() }, [form.amount, form.isDebit]); const balanceAfter = Amounts.sub(account.balance, calc.debit).amount; @@ -484,7 +459,7 @@ function CreateCashout({ e.preventDefault(); if (errors || !creds) return; - try { + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.createCashout(creds, { amount_credit: Amounts.stringify(calc.credit), amount_debit: Amounts.stringify(calc.debit), @@ -529,18 +504,7 @@ function CreateCashout({ default: assertUnreachable(resp) } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) }} > {i18n.str`Create`} @@ -669,7 +633,7 @@ export function ShowCashoutDetails({ onClick={async (e) => { e.preventDefault(); if (!creds) return; - try { + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.abortCashoutById(creds, id); if (resp.type === "ok") { onCancel(); @@ -692,18 +656,7 @@ export function ShowCashoutDetails({ } } } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + }) }} > {i18n.str`Abort`} @@ -715,9 +668,8 @@ export function ShowCashoutDetails({ class="pure-button pure-button-primary " onClick={async (e) => { e.preventDefault(); - if (!creds) return; - try { - if (!code) return; + if (!creds || !code) return; + await withRuntimeErrorHandling(i18n, async () => { const resp = await api.confirmCashoutById(creds, id, { tan: code, }); @@ -745,19 +697,8 @@ export function ShowCashoutDetails({ }) default: assertUnreachable(resp) } - } - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)) - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : JSON.stringify(error)) as TranslatedString - ) - } - } + } + }) }} > {i18n.str`Confirm`} diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 310e80cd6..437618150 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -19,6 +19,8 @@ import { ErrorNotification, ErrorType, HttpError, + notify, + notifyError, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -92,10 +94,27 @@ 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: TalerError<{}>, -): ErrorNotification { +type Translator = ReturnType<typeof useTranslationContext>["i18n"] + +export async function withRuntimeErrorHandling<T>(i18n: Translator, cb: () => Promise<T>): Promise<void> { + try { + await cb() + } catch (error: unknown) { + if (error instanceof TalerError) { + notify(buildRequestErrorMessage(i18n, error)) + } else { + notifyError( + i18n.str`Operation failed, please report`, + (error instanceof Error + ? error.message + : JSON.stringify(error)) as TranslatedString + ) + } + } +} + + +export function buildRequestErrorMessage( i18n: Translator, cause: TalerError): ErrorNotification { let result: ErrorNotification; switch (cause.errorDetail.code) { case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { |