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/demobank-ui/src/pages | |
parent | 9e925a2f56677600973c4659f82776a6a56339bb (diff) | |
download | wallet-core-7582855e2723e11de25f10b6d5ba8a0757616765.tar.xz |
some ui fixes
Diffstat (limited to 'packages/demobank-ui/src/pages')
14 files changed, 249 insertions, 494 deletions
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`} |