diff options
Diffstat (limited to 'packages/demobank-ui/src')
20 files changed, 343 insertions, 222 deletions
diff --git a/packages/demobank-ui/src/components/Attention.tsx b/packages/demobank-ui/src/components/Attention.tsx index 3313e5796..57d0a4199 100644 --- a/packages/demobank-ui/src/components/Attention.tsx +++ b/packages/demobank-ui/src/components/Attention.tsx @@ -9,7 +9,7 @@ interface Props { children?: ComponentChildren , } export function Attention({ type = "info", title, children, onClose }: Props): VNode { - return <div class={`group attention-${type} mt-2`}> + return <div class={`group attention-${type} mt-2 shadow-lg`}> <div class="rounded-md group-[.attention-info]:bg-blue-50 group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 group-[.attention-success]:bg-green-50 p-4 shadow"> <div class="flex"> <div > diff --git a/packages/demobank-ui/src/components/ShowLocalNotification.tsx b/packages/demobank-ui/src/components/ShowLocalNotification.tsx new file mode 100644 index 000000000..bb62a48f0 --- /dev/null +++ b/packages/demobank-ui/src/components/ShowLocalNotification.tsx @@ -0,0 +1,43 @@ +import { Notification } from "@gnu-taler/web-util/browser"; +import { h, Fragment, VNode } from "preact"; +import { Attention } from "./Attention.js"; +import { useSettings } from "../hooks/settings.js"; + +export function ShowLocalNotification({ notification }: { notification?: Notification }): VNode { + if (!notification) return <Fragment /> + switch (notification.message.type) { + case "error": + return <div class="relative"> + <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> + <Attention type="danger" title={notification.message.title} onClose={() => { + notification.remove() + }}> + {notification.message.description && + <div class="mt-2 text-sm text-red-700"> + {notification.message.description} + </div> + } + <MaybeShowDebugInfo info={notification.message.debug} /> + </Attention> + </div> + </div> + case "info": + return <div class="relative"> + <div class="fixed top-0 left-0 right-0 z-20 w-full p-4"> + <Attention type="success" title={notification.message.title} onClose={() => { + notification.remove(); + }} /></div></div> + } +} + + +function MaybeShowDebugInfo({ info }: { info: any }): VNode { + const [settings] = useSettings() + if (settings.showDebugInfo) { + return <pre class="whitespace-break-spaces "> + {info} + </pre> + } + return <Fragment /> +} + diff --git a/packages/demobank-ui/src/demobank-ui-settings.js b/packages/demobank-ui/src/demobank-ui-settings.js index 99c6f3873..827f207f8 100644 --- a/packages/demobank-ui/src/demobank-ui-settings.js +++ b/packages/demobank-ui/src/demobank-ui-settings.js @@ -4,7 +4,7 @@ * Global settings for the demobank UI. */ globalThis.talerDemobankSettings = { - backendBaseURL: "http://bank.taler.test/", + backendBaseURL: "http://bank.taler.test:1180/", allowRegistrations: true, showDemoNav: true, simplePasswordForRandomAccounts: true, diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx index b18f29d86..f21e98343 100644 --- a/packages/demobank-ui/src/pages/LoginForm.tsx +++ b/packages/demobank-ui/src/pages/LoginForm.tsx @@ -15,7 +15,7 @@ */ import { TranslatedString } from "@gnu-taler/taler-util"; -import { notify, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Notification, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; @@ -25,6 +25,8 @@ import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; +import { Attention } from "../components/Attention.js"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; /** @@ -37,25 +39,19 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb const [password, setPassword] = useState<string | undefined>(); const { i18n } = useTranslationContext(); const { api } = useBankCoreApiContext(); - + const [notification, notify, handleError] = useLocalNotification() /** * Register form may be shown in the initialization step. - * If this is an error when usgin the app the registration - * callback is not set + * If no register handler then this is invoke + * to show a session expired or unauthorized */ - const isSessionExpired = !onRegister + const isLogginAgain = !onRegister - // useEffect(() => { - // if (backend.state.status === "loggedIn") { - // backend.expired() - // } - // },[]) const ref = useRef<HTMLInputElement>(null); useEffect(function focusInput() { - //FIXME: show invalidate session and allow relogin - if (isSessionExpired) { - localStorage.removeItem("backend-state"); + if (isLogginAgain && backend.state.status !== "expired") { + backend.expired() window.location.reload() } ref.current?.focus(); @@ -78,7 +74,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb async function doLogin() { if (!username || !password) return; setBusy({}) - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.getAuthenticationAPI(username).createAccessToken(password, { // scope: "readwrite" as "write", //FIX: different than merchant scope: "readwrite", @@ -114,7 +110,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb return ( <div class="flex min-h-full flex-col justify-center"> - + <ShowLocalNotification notification={notification} /> <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm"> <form class="space-y-6" noValidate onSubmit={(e) => { @@ -135,7 +131,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb id="username" class="block w-full disabled:bg-gray-200 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={username ?? ""} - disabled={isSessionExpired} + disabled={isLogginAgain} enterkeyhint="next" placeholder="identification" autocomplete="username" @@ -177,7 +173,7 @@ export function LoginForm({ reason, onRegister }: { reason?: "not-found" | "forb </div> </div> - {isSessionExpired ? <div class="flex justify-between"> + {isLogginAgain ? <div class="flex justify-between"> <button type="submit" class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600" onClick={(e) => { diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts index bc3555c48..b17b0d787 100644 --- a/packages/demobank-ui/src/pages/OperationState/index.ts +++ b/packages/demobank-ui/src/pages/OperationState/index.ts @@ -19,7 +19,7 @@ import { utils } from "@gnu-taler/web-util/browser"; import { ErrorLoading } from "../../components/ErrorLoading.js"; import { Loading } from "../../components/Loading.js"; import { useComponentState } from "./state.js"; -import { AbortedView, ConfirmedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js"; +import { AbortedView, ConfirmedView, FailedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js"; export interface Props { currency: string; @@ -29,6 +29,7 @@ export interface Props { export type State = State.Loading | State.LoadingError | State.Ready | + State.Failed | State.Aborted | State.Confirmed | State.InvalidPayto | @@ -42,6 +43,11 @@ export namespace State { error: undefined; } + export interface Failed { + status: "failed"; + error: TalerCoreBankErrorsByMethod<"createWithdrawal">; + } + export interface LoadingError { status: "loading-error"; error: TalerError; @@ -54,8 +60,7 @@ export namespace State { status: "ready"; error: undefined; uri: WithdrawUriResult, - onClose: () => void; - onAbort: () => void; + onClose: () => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined>; } export interface InvalidPayto { @@ -78,8 +83,8 @@ export namespace State { } export interface NeedConfirmation { status: "need-confirmation", - onAbort: () => void; - onConfirm: () => void; + onAbort: () => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined>; + onConfirm: () => Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined>; error: undefined; busy: boolean, } @@ -106,6 +111,7 @@ export interface Transaction { const viewMapping: utils.StateViewMap<State> = { loading: Loading, + "failed": FailedView, "invalid-payto": InvalidPaytoView, "invalid-withdrawal": InvalidWithdrawalView, "invalid-reserve": InvalidReserveView, diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts index a4890d726..2d33ff78b 100644 --- a/packages/demobank-ui/src/pages/OperationState/state.ts +++ b/packages/demobank-ui/src/pages/OperationState/state.ts @@ -14,65 +14,40 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, HttpStatusCode, TalerError, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { RequestError, notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; +import { Amounts, FailCasesByMethod, TalerCoreBankErrorsByMethod, TalerError, TalerErrorDetail, TranslatedString, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util"; +import { notify, notifyError, notifyInfo, useTranslationContext, utils } from "@gnu-taler/web-util/browser"; import { useEffect, useState } from "preact/hooks"; +import { mutate } from "swr"; 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, withRuntimeErrorHandling } from "../../utils.js"; -import { Props, State } from "./index.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { mutate } from "swr"; +import { Props, State } from "./index.js"; export function useComponentState({ currency, onClose }: Props): utils.RecursiveState<State> { - const { i18n } = useTranslationContext(); const [settings, updateSettings] = useSettings() const { state: credentials } = useBackendState() const creds = credentials.status !== "loggedIn" ? undefined : credentials const { api } = useBankCoreApiContext() - // const { createWithdrawal } = useAccessAPI(); - // const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI(); - const [busy, setBusy] = useState<Record<string, undefined>>() + const [busy, setBusy] = useState<Record<string, undefined>>() + const [failure, setFailure] = useState<TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined>() const amount = settings.maxWithdrawalAmount async function doSilentStart() { //FIXME: if amount is not enough use balance const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`) if (!creds) return; - await withRuntimeErrorHandling(i18n, async () => { - const resp = await api.createWithdrawal(creds, { - amount: Amounts.stringify(parsedAmount), - }); - if (resp.type === "fail") { - switch (resp.case) { - case "insufficient-funds": return notify({ - type: "error", - title: i18n.str`The operation was rejected due to insufficient funds.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "unauthorized": return notify({ - type: "error", - title: i18n.str`Unauthorized to make the opeartion, maybe the session has expired or the password changed.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - default: assertUnreachable(resp) - } - } + const resp = await api.createWithdrawal(creds, { + amount: Amounts.stringify(parsedAmount), + }); + if (resp.type === "fail") { + setFailure(resp) + return; + } + updateSettings("currentWithdrawalOperationId", resp.body.withdrawal_id) - const uri = parseWithdrawUri(resp.body.taler_withdraw_uri); - if (!uri) { - return notifyError( - i18n.str`Server responded with an invalid withdraw URI`, - i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`); - } else { - updateSettings("currentWithdrawalOperationId", uri.withdrawalOperationId) - } - }) } const withdrawalOperationId = settings.currentWithdrawalOperationId @@ -82,6 +57,13 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive } }, [settings.fastWithdrawal, amount]) + if (failure) { + return { + status: "failed", + error: failure + } + } + if (!withdrawalOperationId) { return { status: "loading", @@ -92,77 +74,24 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive const wid = withdrawalOperationId async function doAbort() { - await withRuntimeErrorHandling(i18n, async () => { - const resp = await api.abortWithdrawalById(wid); - if (resp.type === "ok") { - updateSettings("currentWithdrawalOperationId", undefined) - onClose(); - } else { - switch (resp.case) { - case "previously-confirmed": return notify({ - type: "error", - title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "invalid-id": return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "not-found": return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - default: assertUnreachable(resp) - } - } - }) + const resp = await api.abortWithdrawalById(wid); + if (resp.type === "ok") { + updateSettings("currentWithdrawalOperationId", undefined) + onClose(); + } else { + return resp; + } } - async function doConfirm() { + async function doConfirm(): Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined> { 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!`) - } - } else { - switch (resp.case) { - case "previously-aborted": return notify({ - type: "error", - title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "no-exchange-or-reserve-selected": return notify({ - type: "error", - title: i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }) - case "invalid-id": return notify({ - type: "error", - title: i18n.str`The operation id is invalid.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - case "not-found": return notify({ - type: "error", - title: i18n.str`The operation was not found.`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); - default: assertUnreachable(resp) - } - } - }) + const resp = await api.confirmWithdrawalById(wid); setBusy(undefined) + if (resp.type === "ok") { + mutate(() => true)//clean withdrawal state + } else { + return resp; + } } const uri = stringifyWithdrawUri({ @@ -261,7 +190,6 @@ export function useComponentState({ currency, onClose }: Props): utils.Recursive error: undefined, uri: parsedUri, onClose: doAbort, - onAbort: doAbort, } } diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx index 2cb7385db..b7d7e5520 100644 --- a/packages/demobank-ui/src/pages/OperationState/views.tsx +++ b/packages/demobank-ui/src/pages/OperationState/views.tsx @@ -14,8 +14,8 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { stringifyWithdrawUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util"; +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useMemo, useState } from "preact/hooks"; import { QR } from "../../components/QR.js"; @@ -23,6 +23,10 @@ import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { useSettings } from "../../hooks/settings.js"; import { undefinedIfEmpty } from "../../utils.js"; import { State } from "./index.js"; +import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; +import { ErrorLoading } from "../../components/ErrorLoading.js"; +import { Attention } from "../../components/Attention.js"; +import { assertUnreachable } from "../WithdrawalOperationPage.js"; export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) { return ( @@ -40,8 +44,10 @@ export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) { ); } -export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State.NeedConfirmation) { +export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, busy }: State.NeedConfirmation) { const { i18n } = useTranslationContext() + const [settings] = useSettings() + const [notification, notify, errorHandler] = useLocalNotification() const captchaNumbers = useMemo(() => { return { @@ -61,8 +67,76 @@ export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State. : undefined, }) ?? (busy ? {} as Record<string, undefined> : undefined); + async function onCancel() { + errorHandler(async () => { + const resp = await doAbort() + if (!resp) return; + switch (resp.case) { + case "previously-confirmed": return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }) + case "invalid-id": return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + case "not-found": return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: resp.detail.hint as TranslatedString, + debug: resp.detail, + }); + default: assertUnreachable(resp) + } + }) + } + + async function onConfirm() { + errorHandler(async () => { + const hasError = await doConfirm() + if (!hasError) { + if (!settings.showWithdrawalSuccess) { + notifyInfo(i18n.str`Wire transfer completed!`) + } + return + } + switch (hasError.case) { + case "previously-aborted": return notify({ + type: "error", + title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }) + case "no-exchange-or-reserve-selected": return notify({ + type: "error", + title: i18n.str`The withdraw operation cannot be confirmed because no exchange and reserve public key selection happened before`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }) + case "invalid-id": return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + case "not-found": return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + default: assertUnreachable(hasError) + } + }) + } + return ( <div class="bg-white shadow sm:rounded-lg"> + <ShowLocalNotification notification={notification} /> <div class="px-4 py-5 sm:p-6"> <h3 class="text-base font-semibold text-gray-900"> <i18n.Translate>Confirm the withdrawal operation</i18n.Translate> @@ -161,7 +235,10 @@ export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State. </div> <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> <button type="button" class="text-sm font-semibold leading-6 text-gray-900" - onClick={onAbort} + onClick={(e) => { + e.preventDefault() + onCancel() + }} > <i18n.Translate>Cancel</i18n.Translate></button> <button type="submit" @@ -246,6 +323,25 @@ export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: State. ); } +export function FailedView({ error }: State.Failed) { + const { i18n } = useTranslationContext(); + switch (error.case) { + case "unauthorized": return <Attention type="danger" + title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}> + <div class="mt-2 text-sm text-red-700"> + {error.detail.hint} + </div> + </Attention> + case "insufficient-funds": return <Attention type="danger" + title={i18n.str`The operation was rejected due to insufficient funds.`}> + <div class="mt-2 text-sm text-red-700"> + {error.detail.hint} + </div> + </Attention> + default: assertUnreachable(error) + } +} + export function AbortedView({ error, onClose }: State.Aborted) { return ( <div>aborted</div> @@ -308,8 +404,9 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) { ); } -export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> { +export function ReadyView({ uri, onClose: doClose }: State.Ready): VNode<{}> { const { i18n } = useTranslationContext(); + const [notification, notify, errorHandler] = useLocalNotification() useEffect(() => { //Taler Wallet WebExtension is listening to headers response and tab updates. @@ -320,7 +417,38 @@ export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> { document.title = `${document.title} ${uri.withdrawalOperationId}`; }, []); const talerWithdrawUri = stringifyWithdrawUri(uri); + + async function onClose() { + errorHandler(async () => { + const hasError = await doClose() + if (!hasError) return; + switch (hasError.case) { + case "previously-confirmed": return notify({ + type: "error", + title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }) + case "invalid-id": return notify({ + type: "error", + title: i18n.str`The operation id is invalid.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + case "not-found": return notify({ + type: "error", + title: i18n.str`The operation was not found.`, + description: hasError.detail.hint as TranslatedString, + debug: hasError.detail, + }); + default: assertUnreachable(hasError) + } + }) + } + return <Fragment> + <ShowLocalNotification notification={notification} /> + <div class="flex justify-end mt-4"> <button type="button" class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500" diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 63cb3e865..6649d224e 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -18,33 +18,30 @@ import { AmountJson, AmountString, Amounts, - HttpStatusCode, Logger, - TalerError, + PaytoString, TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { - RequestError, - notify, - notifyError, - useTranslationContext, + useLocalNotification, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, Ref, VNode, h } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; +import { mutate } from "swr"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; +import { useBankCoreApiContext } from "../context/config.js"; +import { useBackendState } from "../hooks/backend.js"; import { - buildRequestErrorMessage, undefinedIfEmpty, validateIBAN, - withRuntimeErrorHandling, + withRuntimeErrorHandling } from "../utils.js"; -import { useBankCoreApiContext } from "../context/config.js"; -import { useBackendState } from "../hooks/backend.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { mutate } from "swr"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; const logger = new Logger("PaytoWireTransferForm"); @@ -82,6 +79,7 @@ export function PaytoWireTransferForm({ const trimmedAmountStr = amount?.trim(); const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`); const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; + const [notification, notify, handleError] = useLocalNotification() const errorsWire = undefinedIfEmpty({ iban: !iban @@ -122,7 +120,7 @@ export function PaytoWireTransferForm({ }); async function doSend() { - let payto_uri: string | undefined; + let payto_uri: PaytoString | undefined; let sendingAmount: AmountString | undefined; if (credentials.status !== "loggedIn") return; if (rawPaytoInput) { @@ -141,7 +139,7 @@ export function PaytoWireTransferForm({ } const puri = payto_uri; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const res = await api.createTransaction(credentials, { payto_uri: puri, amount: sendingAmount, @@ -367,6 +365,7 @@ export function PaytoWireTransferForm({ <i18n.Translate>Send</i18n.Translate> </button> </div> + <ShowLocalNotification notification={notification} /> </form> </div > ) diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx index 9ae1cf268..ca2a89f48 100644 --- a/packages/demobank-ui/src/pages/QrCodeSection.tsx +++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx @@ -15,24 +15,21 @@ */ import { - HttpStatusCode, stringifyWithdrawUri, - TalerError, TranslatedString, - WithdrawUriResult, + WithdrawUriResult } from "@gnu-taler/taler-util"; import { - notify, - notifyError, - RequestError, - useTranslationContext, + useLocalNotification, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../components/QR.js"; -import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../utils.js"; import { useBankCoreApiContext } from "../context/config.js"; +import { withRuntimeErrorHandling } from "../utils.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; export function QrCodeSection({ withdrawUri, @@ -51,18 +48,19 @@ export function QrCodeSection({ document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`; }, []); const talerWithdrawUri = stringifyWithdrawUri(withdrawUri); + const [notification, notify, handleError] = useLocalNotification() const { api } = useBankCoreApiContext() async function doAbort() { - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.abortWithdrawalById(withdrawUri.withdrawalOperationId); if (resp.type === "ok") { onAborted(); } else { switch (resp.case) { case "previously-confirmed": return notify({ - type: "info", + type: "error", title: i18n.str`The reserve operation has been confirmed previously and can't be aborted` }) case "invalid-id": return notify({ @@ -87,6 +85,7 @@ export function QrCodeSection({ return ( <Fragment> + <ShowLocalNotification notification={notification} /> <div class="bg-white shadow-xl sm:rounded-lg"> <div class="px-4 py-5 sm:p-6"> <h3 class="text-base font-semibold leading-6 text-gray-900"> diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx index 3520405c5..fdf2c0e9d 100644 --- a/packages/demobank-ui/src/pages/RegistrationPage.tsx +++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx @@ -15,7 +15,7 @@ */ import { AccessToken, Logger, TranslatedString } from "@gnu-taler/taler-util"; import { - notify, + useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -26,6 +26,7 @@ import { useBackendState } from "../hooks/backend.js"; import { bankUiSettings } from "../settings.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { getRandomPassword, getRandomUsername } from "./rnd.js"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; const logger = new Logger("RegistrationPage"); @@ -60,6 +61,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on const [phone, setPhone] = useState<string | undefined>(); const [email, setEmail] = useState<string | undefined>(); const [repeatPassword, setRepeatPassword] = useState<string | undefined>(); + const [notification, notify, handleError] = useLocalNotification() const { api } = useBankCoreApiContext() // const { register } = useTestingAPI(); @@ -93,7 +95,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on }); async function doRegistrationAndLogin(name: string | undefined, username: string, password: string) { - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const creationResponse = await api.createAccount("" as AccessToken, { name: name ?? "", username, password }); if (creationResponse.type === "fail") { switch (creationResponse.case) { @@ -171,7 +173,7 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on return ( <Fragment> - <h1 class="nav"></h1> + <ShowLocalNotification notification={notification} /> <div class="flex min-h-full flex-col justify-center"> <div class="sm:mx-auto sm:w-full sm:max-w-sm"> diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx index b109441a6..eb8ea8f20 100644 --- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx @@ -1,5 +1,5 @@ import { TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util"; -import { notify, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoading } from "../components/ErrorLoading.js"; @@ -8,10 +8,11 @@ import { useBankCoreApiContext } from "../context/config.js"; import { useAccountDetails } from "../hooks/access.js"; import { useBackendState } from "../hooks/backend.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { LoginForm } from "./LoginForm.js"; -import { AccountForm } from "./admin/AccountForm.js"; import { ProfileNavigation } from "./ProfileNavigation.js"; +import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { AccountForm } from "./admin/AccountForm.js"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; export function ShowAccountDetails({ account, @@ -31,6 +32,7 @@ export function ShowAccountDetails({ const [update, setUpdate] = useState(false); const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountData | undefined>(); + const [notification, notify, handleError] = useLocalNotification() const result = useAccountDetails(account); if (!result) { @@ -50,7 +52,7 @@ export function ShowAccountDetails({ async function doUpdate() { if (!update || !submitAccount || !creds) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.updateAccount(creds, { cashout_address: submitAccount.cashout_payto_uri, challenge_contact_data: undefinedIfEmpty({ @@ -93,6 +95,7 @@ export function ShowAccountDetails({ return ( <Fragment> + <ShowLocalNotification notification={notification} /> {accountIsTheCurrentUser ? <ProfileNavigation current="details" /> : diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx index b14c6d90b..d30216f3f 100644 --- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx @@ -1,13 +1,14 @@ -import { notify, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; import { ProfileNavigation } from "./ProfileNavigation.js"; +import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; export function UpdateAccountPassword({ account: accountName, @@ -41,11 +42,12 @@ export function UpdateAccountPassword({ ? i18n.str`password doesn't match` : undefined, }); + const [notification, notify, handleError] = useLocalNotification() async function doChangePassword() { if (!!errors || !password || !token) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.updatePassword({ username: accountName, token }, { old_password: current, new_password: password, @@ -77,6 +79,7 @@ export function UpdateAccountPassword({ return ( <Fragment> + <ShowLocalNotification notification={notification} /> {accountIsTheCurrentUser ? <ProfileNavigation current="credentials" /> : <h1 class="text-base font-semibold leading-6 text-gray-900"> diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx index 0637a8af4..abdebf9bf 100644 --- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx +++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx @@ -17,17 +17,14 @@ import { AmountJson, Amounts, - HttpStatusCode, Logger, - TalerError, TranslatedString, parseWithdrawUri } from "@gnu-taler/taler-util"; import { - RequestError, - notify, notifyError, - useTranslationContext, + useLocalNotification, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { forwardRef } from "preact/compat"; @@ -36,10 +33,11 @@ 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, withRuntimeErrorHandling } from "../utils.js"; -import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { OperationState } from "./OperationState/index.js"; import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js"; +import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; const logger = new Logger("WalletWithdrawForm"); const RefAmount = forwardRef(InputAmount); @@ -59,6 +57,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { const { api } = useBankCoreApiContext() const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`); + const [notification, notify, handleError] = useLocalNotification() if (!!settings.currentWithdrawalOperationId) { return <Attention type="warning" title={i18n.str`There is an operation already`}> @@ -88,7 +87,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { async function doStart() { if (!parsedAmount || !creds) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.createWithdrawal(creds, { amount: Amounts.stringify(parsedAmount), }); @@ -136,6 +135,8 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: { e.preventDefault() }} > + <ShowLocalNotification notification={notification} /> + <div class="px-4 py-6 "> <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"> diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx index 5e0fa322f..89538e305 100644 --- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx +++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -16,32 +16,28 @@ import { AmountJson, - Amounts, - HttpStatusCode, Logger, PaytoUri, PaytoUriIBAN, PaytoUriTalerBank, - TalerError, TranslatedString, WithdrawUriResult } from "@gnu-taler/taler-util"; import { - RequestError, - notify, - notifyError, notifyInfo, - useTranslationContext, + useLocalNotification, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useMemo, useState } from "preact/hooks"; +import { mutate } from "swr"; import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js"; -import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; +import { useBankCoreApiContext } from "../context/config.js"; import { useSettings } from "../hooks/settings.js"; +import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; -import { useBankCoreApiContext } from "../context/config.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; -import { mutate } from "swr"; +import { ShowLocalNotification } from "../components/ShowLocalNotification.js"; const logger = new Logger("WithdrawalConfirmationQuestion"); @@ -72,6 +68,7 @@ export function WithdrawalConfirmationQuestion({ b: Math.floor(Math.random() * 10), }; }, []); + const [notification, notify, handleError] = useLocalNotification() const { api } = useBankCoreApiContext() const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>(); @@ -89,7 +86,7 @@ export function WithdrawalConfirmationQuestion({ async function doTransfer() { setBusy({}) - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.confirmWithdrawalById(withdrawUri.withdrawalOperationId); if (resp.type === "ok") { mutate(() => true)// clean any info that we have @@ -131,7 +128,7 @@ export function WithdrawalConfirmationQuestion({ async function doCancel() { setBusy({}) - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.abortWithdrawalById(withdrawUri.withdrawalOperationId); if (resp.type === "ok") { onAborted(); @@ -164,6 +161,8 @@ export function WithdrawalConfirmationQuestion({ return ( <Fragment> + <ShowLocalNotification notification={notification} /> + <div class="bg-white shadow sm:rounded-lg"> <div class="px-4 py-5 sm:p-6"> <h3 class="text-base font-semibold text-gray-900"> diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx b/packages/demobank-ui/src/pages/admin/Account.tsx index 1818de655..7109b082f 100644 --- a/packages/demobank-ui/src/pages/admin/Account.tsx +++ b/packages/demobank-ui/src/pages/admin/Account.tsx @@ -23,9 +23,9 @@ export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { o } if (result.type === "fail") { switch (result.case) { - case "unauthorized": return <LoginForm reason="forbidden" onRegister={onRegister} /> - case "not-found": return <LoginForm reason="not-found" onRegister={onRegister} /> - case "no-rights": return <LoginForm reason="not-found" onRegister={onRegister} /> + case "unauthorized": return <LoginForm reason="forbidden" /> + case "not-found": return <LoginForm reason="not-found" /> + case "no-rights": return <LoginForm reason="not-found" /> default: assertUnreachable(result) } } diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index 410683dcb..fa3a28057 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -3,7 +3,7 @@ import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; import { useEffect, useRef, useState } from "preact/hooks"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { TalerCorebankApi, buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; +import { PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri } from "@gnu-taler/taler-util"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; import { CopyButton } from "../../components/CopyButton.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; @@ -52,7 +52,7 @@ export function AccountForm({ : buildPayto("iban", newForm.cashout_payto_uri, undefined);; const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({ - cashout_payto_uri: !newForm.cashout_payto_uri + cashout_payto_uri: (!newForm.cashout_payto_uri ? i18n.str`required` : !parsed ? i18n.str`does not follow the pattern` @@ -60,7 +60,7 @@ export function AccountForm({ ? i18n.str`only "IBAN" target are supported` : !IBAN_REGEX.test(parsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(parsed.iban, i18n), + : validateIBAN(parsed.iban, i18n)) as PaytoString, contact_data: undefinedIfEmpty({ email: !newForm.contact_data?.email ? i18n.str`required` @@ -165,7 +165,7 @@ export function AccountForm({ </div> - {purpose !== "create" && (<RenderPaytoDisabledField paytoURI={form.payto_uri} />)} + {purpose !== "create" && (<RenderPaytoDisabledField paytoURI={form.payto_uri as PaytoString} />)} <div class="sm:col-span-5"> <label @@ -252,7 +252,7 @@ export function AccountForm({ disabled={purpose === "show"} value={form.cashout_payto_uri ?? ""} onChange={(e) => { - form.cashout_payto_uri = e.currentTarget.value; + form.cashout_payto_uri = e.currentTarget.value as PaytoString; updateForm(structuredClone(form)); }} autocomplete="off" @@ -303,7 +303,7 @@ function initializeFromTemplate( } -function RenderPaytoDisabledField({ paytoURI }: { paytoURI: string | undefined }): VNode { +function RenderPaytoDisabledField({ paytoURI }: { paytoURI: PaytoString | undefined }): VNode { const { i18n } = useTranslationContext() const payto = parsePaytoUri(paytoURI ?? ""); if (payto?.isKnown) { diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index ea40001c0..f2c1d5456 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -1,15 +1,16 @@ -import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from "@gnu-taler/taler-util"; -import { RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { TalerCorebankApi, TranslatedString } from "@gnu-taler/taler-util"; +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { buildRequestErrorMessage, withRuntimeErrorHandling } from "../../utils.js"; -import { getRandomPassword } from "../rnd.js"; -import { AccountForm, AccountFormData } from "./AccountForm.js"; -import { useBackendState } from "../../hooks/backend.js"; -import { useBankCoreApiContext } from "../../context/config.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; import { mutate } from "swr"; import { Attention } from "../../components/Attention.js"; +import { useBankCoreApiContext } from "../../context/config.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { withRuntimeErrorHandling } from "../../utils.js"; +import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { getRandomPassword } from "../rnd.js"; +import { AccountForm, AccountFormData } from "./AccountForm.js"; +import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; export function CreateNewAccount({ onCancel, @@ -25,10 +26,11 @@ export function CreateNewAccount({ const { api } = useBankCoreApiContext(); const [submitAccount, setSubmitAccount] = useState<AccountFormData | undefined>(); + const [notification, notify, handleError] = useLocalNotification() async function doCreate() { if (!submitAccount || !token) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const account: TalerCorebankApi.RegisterAccountRequest = { cashout_payto_uri: submitAccount.cashout_payto_uri, challenge_contact_data: submitAccount.contact_data, @@ -85,6 +87,8 @@ export function CreateNewAccount({ 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"> + <ShowLocalNotification notification={notification} /> + <div class="px-4 sm:px-0"> <h2 class="text-base font-semibold leading-7 text-gray-900"> <i18n.Translate>New business account</i18n.Translate> diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx index 89f634080..1a5255595 100644 --- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx @@ -1,18 +1,19 @@ -import { Amounts, HttpStatusCode, TalerError, TranslatedString } from "@gnu-taler/taler-util"; -import { HttpResponsePaginated, RequestError, notify, notifyError, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util"; +import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { Attention } from "../../components/Attention.js"; import { ErrorLoading } from "../../components/ErrorLoading.js"; import { Loading } from "../../components/Loading.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; +import { useBankCoreApiContext } from "../../context/config.js"; import { useAccountDetails } from "../../hooks/access.js"; -import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling } from "../../utils.js"; -import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { useBackendState } from "../../hooks/backend.js"; +import { undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; -import { useBankCoreApiContext } from "../../context/config.js"; -import { useBackendState } from "../../hooks/backend.js"; +import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; export function RemoveAccount({ account, @@ -32,6 +33,7 @@ export function RemoveAccount({ const { state } = useBackendState(); const token = state.status !== "loggedIn" ? undefined : state.token const { api } = useBankCoreApiContext() + const [notification, notify, handleError] = useLocalNotification() if (!result) { return <Loading /> @@ -61,7 +63,7 @@ export function RemoveAccount({ async function doRemove() { if (!token) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.deleteAccount({ username: account, token }); if (resp.type === "ok") { notifyInfo(i18n.str`Account removed`); @@ -111,6 +113,8 @@ export function RemoveAccount({ return ( <div> + <ShowLocalNotification notification={notification} /> + <Attention type="warning" title={i18n.str`You are going to remove the account`}> <i18n.Translate>This step can't be undone.</i18n.Translate> </Attention> diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index a71915622..8d90e9205 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -19,7 +19,7 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { - notify, + useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -43,6 +43,7 @@ import { import { LoginForm } from "../LoginForm.js"; import { InputAmount } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; +import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; interface Props { account: string; @@ -76,6 +77,7 @@ export function CreateCashout({ const creds = state.status !== "loggedIn" ? undefined : state const { api, config } = useBankCoreApiContext() const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); + const [notification, notify, handleError] = useLocalNotification() if (!config.have_cashout) { return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}> @@ -144,7 +146,7 @@ export function CreateCashout({ useEffect(() => { async function doAsync() { - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await (form.isDebit ? calculateFromDebit(amount, sellFee, safeSellRate) : calculateFromCredit(amount, sellFee, safeSellRate)); @@ -176,6 +178,7 @@ export function CreateCashout({ return ( <div> + <ShowLocalNotification notification={notification} /> <h1>New cashout</h1> <form class="pure-form"> <fieldset> @@ -360,7 +363,7 @@ export function CreateCashout({ e.preventDefault(); if (errors || !creds) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.createCashout(creds, { amount_credit: Amounts.stringify(calc.credit), amount_debit: Amounts.stringify(calc.debit), diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index b8e566348..7e7ed21cb 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -18,13 +18,14 @@ import { TranslatedString } from "@gnu-taler/taler-util"; import { - notify, + useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; +import { Attention } from "../../components/Attention.js"; import { ErrorLoading } from "../../components/ErrorLoading.js"; import { Loading } from "../../components/Loading.js"; import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js"; @@ -38,7 +39,7 @@ import { withRuntimeErrorHandling } from "../../utils.js"; import { assertUnreachable } from "../WithdrawalOperationPage.js"; -import { Attention } from "../../components/Attention.js"; +import { ShowLocalNotification } from "../../components/ShowLocalNotification.js"; interface Props { id: string; @@ -54,6 +55,7 @@ export function ShowCashoutDetails({ const { api } = useBankCoreApiContext() const result = useCashoutDetails(id); const [code, setCode] = useState<string | undefined>(undefined); + const [notification, notify, handleError] = useLocalNotification() if (!result) { return <Loading /> @@ -76,6 +78,7 @@ export function ShowCashoutDetails({ const isPending = String(result.body.status).toUpperCase() === "PENDING"; return ( <div> + <ShowLocalNotification notification={notification} /> <h1>Cashout details {id}</h1> <form class="pure-form"> <fieldset> @@ -161,7 +164,7 @@ export function ShowCashoutDetails({ onClick={async (e) => { e.preventDefault(); if (!creds) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.abortCashoutById(creds, id); if (resp.type === "ok") { onCancel(); @@ -203,7 +206,7 @@ export function ShowCashoutDetails({ onClick={async (e) => { e.preventDefault(); if (!creds || !code) return; - await withRuntimeErrorHandling(i18n, async () => { + await handleError(async () => { const resp = await api.confirmCashoutById(creds, id, { tan: code, }); |