aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages/OperationState
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-10-30 15:27:25 -0300
committerSebastian <sebasjm@gmail.com>2023-10-30 15:27:25 -0300
commit768838285c25cbb1b171f645e8efb37a3c14273a (patch)
tree3404a7ea452a357baf4ebfc6c3b400f601849744 /packages/demobank-ui/src/pages/OperationState
parentb7ba3119c1ff0d9ae3432cf0de1ef8cf92fc193c (diff)
downloadwallet-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')
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts16
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts142
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx138
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"