diff options
Diffstat (limited to 'packages')
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; +} |