diff options
author | Sebastian <sebasjm@gmail.com> | 2023-10-30 15:27:25 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-10-30 15:27:25 -0300 |
commit | 768838285c25cbb1b171f645e8efb37a3c14273a (patch) | |
tree | 3404a7ea452a357baf4ebfc6c3b400f601849744 /packages/demobank-ui/src/pages/OperationState | |
parent | b7ba3119c1ff0d9ae3432cf0de1ef8cf92fc193c (diff) | |
download | wallet-core-768838285c25cbb1b171f645e8efb37a3c14273a.tar.xz |
local error impl: errors shown fixed position that are wiped when moved from the view
Diffstat (limited to 'packages/demobank-ui/src/pages/OperationState')
3 files changed, 179 insertions, 117 deletions
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" |