summaryrefslogtreecommitdiff
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
parentb7ba3119c1ff0d9ae3432cf0de1ef8cf92fc193c (diff)
downloadwallet-core-768838285c25cbb1b171f645e8efb37a3c14273a.tar.gz
wallet-core-768838285c25cbb1b171f645e8efb37a3c14273a.tar.bz2
wallet-core-768838285c25cbb1b171f645e8efb37a3c14273a.zip
local error impl: errors shown fixed position that are wiped when moved from the view
-rw-r--r--packages/demobank-ui/src/components/Attention.tsx2
-rw-r--r--packages/demobank-ui/src/components/ShowLocalNotification.tsx43
-rw-r--r--packages/demobank-ui/src/demobank-ui-settings.js2
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx30
-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
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx27
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx19
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx8
-rw-r--r--packages/demobank-ui/src/pages/ShowAccountDetails.tsx11
-rw-r--r--packages/demobank-ui/src/pages/UpdateAccountPassword.tsx9
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx17
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx23
-rw-r--r--packages/demobank-ui/src/pages/admin/Account.tsx6
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx12
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx22
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx18
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx9
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx11
-rw-r--r--packages/taler-harness/src/index.ts11
-rw-r--r--packages/taler-util/src/amounts.ts21
-rw-r--r--packages/taler-util/src/http-client/types.ts87
-rw-r--r--packages/taler-util/src/index.node.ts1
-rw-r--r--packages/taler-util/src/payto.ts26
-rw-r--r--packages/taler-util/src/taleruri.ts22
-rw-r--r--packages/web-util/src/hooks/useNotifications.ts98
27 files changed, 556 insertions, 275 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,
});
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
index c83457be4..717aee57d 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -18,13 +18,11 @@
* Imports.
*/
import {
- AccessToken,
AmountString,
Amounts,
Configuration,
Duration,
HttpStatusCode,
- LibtoolVersion,
Logger,
MerchantApiClient,
MerchantInstanceConfig,
@@ -34,12 +32,11 @@ import {
TalerError,
addPaytoQueryParams,
decodeCrock,
- encodeCrock,
generateIban,
- getRandomBytes,
j2s,
rsaBlind,
setGlobalLogLevelFromString,
+ setPrintHttpRequestAsCurl,
} from "@gnu-taler/taler-util";
import { clk } from "@gnu-taler/taler-util/clk";
import {
@@ -54,6 +51,7 @@ import {
} from "@gnu-taler/taler-wallet-core";
import { deepStrictEqual } from "assert";
import fs from "fs";
+import { BankCoreSmokeTest } from "http-client/bank-core.js";
import os from "os";
import path from "path";
import { runBench1 } from "./bench1.js";
@@ -68,7 +66,6 @@ import {
} from "./harness/harness.js";
import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
import { lintExchangeDeployment } from "./lint.js";
-import { BankCoreSmokeTest } from "http-client/bank-core.js";
const logger = new Logger("taler-harness:index.ts");
@@ -665,11 +662,15 @@ deploymentCli
})
.requiredArgument("corebankApiBaseUrl", clk.STRING)
.maybeOption("adminPwd", ["--admin-password"], clk.STRING)
+ .flag("showCurl", ["--show-curl"])
.action(async (args) => {
const httpLib = createPlatformHttpLib();
const api = new TalerCoreBankHttpClient(args.testBankAPI.corebankApiBaseUrl, httpLib);
const tester = new BankCoreSmokeTest(api)
+ if (args.testBankAPI.showCurl) {
+ setPrintHttpRequestAsCurl(true)
+ }
try {
process.stdout.write("config: ");
const config = await tester.testConfig()
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
index 082a8168e..5c6444b00 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -26,6 +26,9 @@ import {
codecForString,
codecForNumber,
Codec,
+ Context,
+ DecodingError,
+ renderContext,
} from "./codec.js";
import { AmountString } from "./taler-types.js";
@@ -74,7 +77,23 @@ export const codecForAmountJson = (): Codec<AmountJson> =>
.property("fraction", codecForNumber())
.build("AmountJson");
-export const codecForAmountString = (): Codec<AmountString> => codecForString() as Codec<AmountString>;
+export function codecForAmountString(): Codec<AmountString> {
+ return {
+ decode(x: any, c?: Context): AmountString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (Amounts.parse(x) === undefined) {
+ throw new DecodingError(
+ `invalid amount at ${renderContext(c)} got "${x}"`,
+ );
+ }
+ return x as AmountString;
+ },
+ };
+}
/**
* Result of a possibly overflowing operation.
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index b9a5032d1..1bb8f99c1 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -1,6 +1,8 @@
import { codecForAmountString } from "../amounts.js";
import { Codec, buildCodecForObject, buildCodecForUnion, codecForBoolean, codecForConstString, codecForEither, codecForList, codecForMap, codecForNumber, codecForString, codecOptional } from "../codec.js";
+import { PaytoString, PaytoUri, codecForPaytoString } from "../payto.js";
import { AmountString } from "../taler-types.js";
+import { TalerActionString, WithdrawUriResult, codecForTalerActionString } from "../taleruri.js";
import { codecForTimestamp } from "../time.js";
import { TalerErrorDetail } from "../wallet-types.js";
@@ -255,7 +257,7 @@ const codecForPublicAccount = (): Codec<TalerCorebankApi.PublicAccount> =>
buildCodecForObject<TalerCorebankApi.PublicAccount>()
.property("account_name", codecForString())
.property("balance", codecForBalance())
- .property("payto_uri", codecForPaytoURI())
+ .property("payto_uri", codecForPaytoString())
.build("TalerCorebankApi.PublicAccount")
export const codecForPublicAccountsResponse =
@@ -285,10 +287,10 @@ export const codecForAccountData =
buildCodecForObject<TalerCorebankApi.AccountData>()
.property("name", codecForString())
.property("balance", codecForBalance())
- .property("payto_uri", codecForPaytoURI())
+ .property("payto_uri", codecForPaytoString())
.property("debit_threshold", codecForAmountString())
.property("contact_data", codecOptional(codecForChallengeContactData()))
- .property("cashout_payto_uri", codecOptional(codecForPaytoURI()))
+ .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
.build("TalerCorebankApi.AccountData")
@@ -309,9 +311,9 @@ export const codecForBankAccountTransactionInfo =
(): Codec<TalerCorebankApi.BankAccountTransactionInfo> =>
buildCodecForObject<TalerCorebankApi.BankAccountTransactionInfo>()
.property("amount", codecForAmountString())
- .property("creditor_payto_uri", codecForPaytoURI())
+ .property("creditor_payto_uri", codecForPaytoString())
.property("date", codecForTimestamp)
- .property("debtor_payto_uri", codecForPaytoURI())
+ .property("debtor_payto_uri", codecForPaytoString())
.property("direction", codecForEither(codecForConstString("debit"), codecForConstString("credit")))
.property("row_id", codecForNumber())
.property("subject", codecForString())
@@ -320,7 +322,7 @@ export const codecForBankAccountTransactionInfo =
export const codecForBankAccountCreateWithdrawalResponse =
(): Codec<TalerCorebankApi.BankAccountCreateWithdrawalResponse> =>
buildCodecForObject<TalerCorebankApi.BankAccountCreateWithdrawalResponse>()
- .property("taler_withdraw_uri", codecForTalerWithdrawalURI())
+ .property("taler_withdraw_uri", codecForTalerActionString())
.property("withdrawal_id", codecForString())
.build("TalerCorebankApi.BankAccountCreateWithdrawalResponse");
@@ -330,7 +332,7 @@ export const codecForBankAccountGetWithdrawalResponse =
.property("aborted", codecForBoolean())
.property("amount", codecForAmountString())
.property("confirmation_done", codecForBoolean())
- .property("selected_exchange_account", codecOptional(codecForString()))
+ .property("selected_exchange_account", codecOptional(codecForPaytoString()))
.property("selected_reserve_pub", codecOptional(codecForString()))
.property("selection_done", (codecForBoolean()))
.build("TalerCorebankApi.BankAccountGetWithdrawalResponse");
@@ -382,7 +384,7 @@ export const codecForCashoutStatusResponse =
.property("amount_debit", codecForAmountString())
.property("confirmation_time", codecForTimestamp)
.property("creation_time", codecForTimestamp)
- .property("credit_payto_uri", codecForPaytoURI())
+ .property("credit_payto_uri", codecForPaytoString())
.property("status", codecForEither(codecForConstString("pending"), codecForConstString("confirmed")))
.property("subject", codecForString())
.build("TalerCorebankApi.CashoutStatusResponse");
@@ -423,7 +425,7 @@ export const codecForBankWithdrawalOperationStatus =
.property("amount", codecForAmountString())
.property("confirm_transfer_url", codecOptional(codecForURL()))
.property("selection_done", codecForBoolean())
- .property("sender_wire", codecForPaytoURI())
+ .property("sender_wire", codecForPaytoString())
.property("suggested_exchange", codecOptional(codecForString()))
.property("transfer_done", codecForBoolean())
.property("wire_types", codecForList(codecForString()))
@@ -439,7 +441,7 @@ export const codecForBankWithdrawalOperationPostResponse =
export const codecForMerchantIncomingHistory =
(): Codec<TalerRevenueApi.MerchantIncomingHistory> =>
buildCodecForObject<TalerRevenueApi.MerchantIncomingHistory>()
- .property("credit_account", codecForPaytoURI())
+ .property("credit_account", codecForPaytoString())
.property("incoming_transactions", codecForList(codecForMerchantIncomingBankTransaction()))
.build("TalerRevenueApi.MerchantIncomingHistory");
@@ -448,7 +450,7 @@ export const codecForMerchantIncomingBankTransaction =
buildCodecForObject<TalerRevenueApi.MerchantIncomingBankTransaction>()
.property("amount", codecForAmountString())
.property("date", codecForTimestamp)
- .property("debit_account", codecForPaytoURI())
+ .property("debit_account", codecForPaytoString())
.property("exchange_url", codecForURL())
.property("row_id", codecForNumber())
.property("wtid", codecForString())
@@ -464,7 +466,7 @@ export const codecForTransferResponse =
export const codecForIncomingHistory =
(): Codec<TalerWireGatewayApi.IncomingHistory> =>
buildCodecForObject<TalerWireGatewayApi.IncomingHistory>()
- .property("credit_account", codecForString())
+ .property("credit_account", codecForPaytoString())
.property("incoming_transactions", codecForList(codecForIncomingBankTransaction()))
.build("TalerWireGatewayApi.IncomingHistory");
@@ -479,7 +481,7 @@ export const codecForIncomingReserveTransaction =
buildCodecForObject<TalerWireGatewayApi.IncomingReserveTransaction>()
.property("amount", codecForAmountString())
.property("date", codecForTimestamp)
- .property("debit_account", codecForPaytoURI())
+ .property("debit_account", codecForPaytoString())
.property("reserve_pub", codecForString())
.property("row_id", codecForNumber())
.property("type", codecForConstString("RESERVE"))
@@ -489,9 +491,9 @@ export const codecForIncomingWadTransaction =
(): Codec<TalerWireGatewayApi.IncomingWadTransaction> =>
buildCodecForObject<TalerWireGatewayApi.IncomingWadTransaction>()
.property("amount", codecForAmountString())
- .property("credit_account", codecForPaytoURI())
+ .property("credit_account", codecForPaytoString())
.property("date", codecForTimestamp)
- .property("debit_account", codecForPaytoURI())
+ .property("debit_account", codecForPaytoString())
.property("origin_exchange_url", codecForURL())
.property("row_id", codecForNumber())
.property("type", codecForConstString("WAD"))
@@ -501,7 +503,7 @@ export const codecForIncomingWadTransaction =
export const codecForOutgoingHistory =
(): Codec<TalerWireGatewayApi.OutgoingHistory> =>
buildCodecForObject<TalerWireGatewayApi.OutgoingHistory>()
- .property("debit_account", codecForString())
+ .property("debit_account", codecForPaytoString())
.property("outgoing_transactions", codecForList(codecForOutgoingBankTransaction()))
.build("TalerWireGatewayApi.OutgoingHistory");
@@ -509,7 +511,7 @@ export const codecForOutgoingBankTransaction =
(): Codec<TalerWireGatewayApi.OutgoingBankTransaction> =>
buildCodecForObject<TalerWireGatewayApi.OutgoingBankTransaction>()
.property("amount", codecForAmountString())
- .property("credit_account", codecForPaytoURI())
+ .property("credit_account", codecForPaytoString())
.property("date", codecForTimestamp)
.property("exchange_base_url", codecForURL())
.property("row_id", codecForNumber())
@@ -537,7 +539,6 @@ type DecimalNumber = number;
const codecForURL = codecForString
const codecForLibtoolVersion = codecForString
const codecForCurrencyName = codecForString
-const codecForPaytoURI = codecForString
const codecForTalerWithdrawalURI = codecForString
const codecForDecimalNumber = codecForNumber
@@ -583,7 +584,7 @@ export namespace TalerWireGatewayApi {
wtid: ShortHashCode;
// The recipient's account identifier as a payto URI.
- credit_account: string;
+ credit_account: PaytoString;
}
export interface IncomingHistory {
@@ -595,7 +596,7 @@ export namespace TalerWireGatewayApi {
// This must be one of the exchange's bank accounts.
// Credit account is shared by all incoming transactions
// as per the nature of the request.
- credit_account: string;
+ credit_account: PaytoString;
}
@@ -617,7 +618,7 @@ export namespace TalerWireGatewayApi {
amount: AmountString;
// Payto URI to identify the sender of funds.
- debit_account: string;
+ debit_account: PaytoString;
// The reserve public key extracted from the transaction details.
reserve_pub: EddsaPublicKey;
@@ -638,10 +639,10 @@ export namespace TalerWireGatewayApi {
// Payto URI to identify the receiver of funds.
// This must be one of the exchange's bank accounts.
- credit_account: string;
+ credit_account: PaytoString;
// Payto URI to identify the sender of funds.
- debit_account: string;
+ debit_account: PaytoString;
// Base URL of the exchange that originated the wad.
origin_exchange_url: string;
@@ -660,7 +661,7 @@ export namespace TalerWireGatewayApi {
// This must be one of the exchange's bank accounts.
// Credit account is shared by all incoming transactions
// as per the nature of the request.
- debit_account: string;
+ debit_account: PaytoString;
}
@@ -676,7 +677,7 @@ export namespace TalerWireGatewayApi {
amount: AmountString;
// Payto URI to identify the receiver of funds.
- credit_account: string;
+ credit_account: PaytoString;
// The wire transfer ID in the outgoing transaction.
wtid: ShortHashCode;
@@ -697,7 +698,7 @@ export namespace TalerWireGatewayApi {
// Usually this account must be created by the test harness before this API is
// used. An exception is the "exchange-fakebank", where any debit account can be
// specified, as it is automatically created.
- debit_account: string;
+ debit_account: PaytoString;
}
export interface AddIncomingResponse {
@@ -729,7 +730,7 @@ export namespace TalerRevenueApi {
// This must be one of the merchant's bank accounts.
// Credit account is shared by all incoming transactions
// as per the nature of the request.
- credit_account: string;
+ credit_account: PaytoString;
}
@@ -745,7 +746,7 @@ export namespace TalerRevenueApi {
amount: AmountString;
// Payto URI to identify the sender of funds.
- debit_account: string;
+ debit_account: PaytoString;
// Base URL of the exchange where the transfer originated form.
exchange_url: string;
@@ -791,7 +792,7 @@ export namespace TalerBankIntegrationApi {
// Bank account of the customer that is withdrawing, as a
// payto URI.
- sender_wire?: string;
+ sender_wire?: PaytoString;
// Suggestion for an exchange given by the bank.
suggested_exchange?: string;
@@ -811,7 +812,7 @@ export namespace TalerBankIntegrationApi {
reserve_pub: string;
// Payto address of the exchange selected for the withdrawal.
- selected_exchange: string;
+ selected_exchange: PaytoString;
}
export interface BankWithdrawalOperationPostResponse {
@@ -867,7 +868,7 @@ export namespace TalerCorebankApi {
withdrawal_id: string;
// URI that can be passed to the wallet to initiate the withdrawal.
- taler_withdraw_uri: string;
+ taler_withdraw_uri: TalerActionString;
}
export interface BankAccountGetWithdrawalResponse {
// Amount that will be withdrawn with this withdrawal operation.
@@ -891,7 +892,7 @@ export namespace TalerCorebankApi {
// Exchange account selected by the wallet, or by the bank
// (with the default exchange) in case the wallet did not provide one
// through the Integration API.
- selected_exchange_account: string | undefined;
+ selected_exchange_account: PaytoString | undefined;
}
export interface BankAccountTransactionsResponse {
@@ -899,8 +900,8 @@ export namespace TalerCorebankApi {
}
export interface BankAccountTransactionInfo {
- creditor_payto_uri: string;
- debtor_payto_uri: string;
+ creditor_payto_uri: PaytoString;
+ debtor_payto_uri: PaytoString;
amount: AmountString;
direction: "debit" | "credit";
@@ -916,13 +917,13 @@ export namespace TalerCorebankApi {
export interface CreateBankAccountTransactionCreate {
// Address in the Payto format of the wire transfer receiver.
// It needs at least the 'message' query string parameter.
- payto_uri: string;
+ payto_uri: PaytoString;
// Transaction amount (in the $currency:x.y format), optional.
// However, when not given, its value must occupy the 'amount'
// query string parameter of the 'payto' field. In case it
// is given in both places, the paytoUri's takes the precedence.
- amount?: string;
+ amount?: AmountString;
}
export interface RegisterAccountRequest {
@@ -958,11 +959,11 @@ export namespace TalerCorebankApi {
// Payments will be sent to this bank account
// when the user wants to convert the local currency
// back to fiat currency outside libeufin-bank.
- cashout_payto_uri?: string;
+ cashout_payto_uri?: PaytoString;
// Internal payto URI of this bank account.
// Used mostly for testing.
- internal_payto_uri?: string;
+ internal_payto_uri?: PaytoString;
}
export interface ChallengeContactData {
@@ -987,7 +988,7 @@ export namespace TalerCorebankApi {
// Payments will be sent to this bank account
// when the user wants to convert the local currency
// back to fiat currency outside libeufin-bank.
- cashout_address?: string;
+ cashout_address?: PaytoString;
// Legal name associated with $username.
// When missing, the old name is kept.
@@ -1011,7 +1012,7 @@ export namespace TalerCorebankApi {
public_accounts: PublicAccount[];
}
export interface PublicAccount {
- payto_uri: string;
+ payto_uri: PaytoString;
balance: Balance;
@@ -1049,7 +1050,7 @@ export namespace TalerCorebankApi {
balance: Balance;
// payto://-URI of the account.
- payto_uri: string;
+ payto_uri: PaytoString;
// Number indicating the max debit allowed for the requesting user.
debit_threshold: AmountString;
@@ -1062,7 +1063,7 @@ export namespace TalerCorebankApi {
// in the merchants' circuit. One example is the exchange:
// that never cashouts. Registering these accounts can
// be done via the access API.
- cashout_payto_uri?: string;
+ cashout_payto_uri?: PaytoString;
}
@@ -1151,7 +1152,7 @@ export namespace TalerCorebankApi {
// Fiat bank account that will receive the cashed out amount.
// Specified as a payto URI.
- credit_payto_uri: string;
+ credit_payto_uri: PaytoString;
// Time when the cashout was created.
creation_time: Timestamp;
diff --git a/packages/taler-util/src/index.node.ts b/packages/taler-util/src/index.node.ts
index 018b4767f..619da0127 100644
--- a/packages/taler-util/src/index.node.ts
+++ b/packages/taler-util/src/index.node.ts
@@ -21,3 +21,4 @@ initNodePrng();
export * from "./index.js";
export * from "./talerconfig.js";
export * from "./globbing/minimatch.js";
+export { setPrintHttpRequestAsCurl } from "./http-impl.node.js"; \ No newline at end of file
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 85870afcd..3df174944 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,6 +15,7 @@
*/
import { generateFakeSegwitAddress } from "./bitcoin.js";
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -23,6 +24,27 @@ export type PaytoUri =
| PaytoUriTalerBank
| PaytoUriBitcoin;
+declare const __payto_str: unique symbol;
+export type PaytoString = string & { [__payto_str]: true };
+
+export function codecForPaytoString(): Codec<PaytoString> {
+ return {
+ decode(x: any, c?: Context): PaytoString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (!x.startsWith(paytoPfx)) {
+ throw new DecodingError(
+ `expected start with payto at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as PaytoString;
+ },
+ };
+}
+
export interface PaytoUriGeneric {
targetType: PaytoType | string;
targetPath: string;
@@ -143,13 +165,13 @@ export function addPaytoQueryParams(
* @param p
* @returns
*/
-export function stringifyPaytoUri(p: PaytoUri): string {
+export function stringifyPaytoUri(p: PaytoUri): PaytoString {
const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
const paramList = !p.params ? [] : Object.entries(p.params);
paramList.forEach(([key, value]) => {
url.searchParams.set(key, value);
});
- return url.href;
+ return url.href as PaytoString;
}
/**
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 9568636b8..cf5d3f413 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { canonicalizeBaseUrl } from "./helpers.js";
import { AmountString } from "./taler-types.js";
import { URLSearchParams, URL } from "./url.js";
@@ -32,6 +33,27 @@ export type TalerUri =
| WithdrawExchangeUri
| AuditorUri;
+declare const __action_str: unique symbol;
+export type TalerActionString = string & { [__action_str]: true };
+
+export function codecForTalerActionString(): Codec<TalerActionString> {
+ return {
+ decode(x: any, c?: Context): TalerActionString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (parseTalerUri(x) === undefined) {
+ throw new DecodingError(
+ `invalid taler action at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as TalerActionString;
+ },
+ };
+}
+
export interface PayUriResult {
type: TalerUriAction.Pay;
merchantBaseUrl: string;
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
index 8f9e0e835..ca67c5b9b 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -1,6 +1,6 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+import { TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
import { useEffect, useState } from "preact/hooks";
-import { memoryMap } from "../index.browser.js";
+import { memoryMap, useTranslationContext } from "../index.browser.js";
export type NotificationMessage = ErrorNotification | InfoNotification;
@@ -105,3 +105,97 @@ function hash(msg: NotificationMessage): string {
}
return hashCode(str);
}
+
+export function useLocalNotification(): [Notification | undefined, (n: NotificationMessage) => void, (cb: () => Promise<void>) => Promise<void>] {
+ const {i18n} = useTranslationContext();
+
+ const [value, setter] = useState<NotificationMessage>();
+ const notif = !value ? undefined : {
+ message: value,
+ remove: () => {
+ setter(undefined);
+ },
+ }
+
+ async function errorHandling(cb: () => Promise<void>) {
+ try {
+ return await cb()
+ } catch (error: unknown) {
+ if (error instanceof TalerError) {
+ notify(buildRequestErrorMessage(i18n, error))
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString
+ )
+ }
+
+ }
+ }
+ return [notif, setter, errorHandling]
+}
+
+type Translator = ReturnType<typeof useTranslationContext>["i18n"]
+
+function buildRequestErrorMessage( i18n: Translator, cause: TalerError): ErrorNotification {
+ let result: ErrorNotification;
+ switch (cause.errorDetail.code) {
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ result = {
+ type: "error",
+ title: i18n.str`Request throttled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ result = {
+ type: "error",
+ title: i18n.str`Malformed response`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Network error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected request error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ };
+ break;
+ }
+ default: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ };
+ break;
+ }
+ }
+ return result;
+}