taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit c0cf3c03f69a47b3713fdba5eadecae30e3bf22b
parent 4bea2e24c5a587631c4af835c0539ac2d00c52b0
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed, 22 Oct 2025 12:40:19 -0300

wip

Diffstat:
Mpackages/bank-ui/src/Routing.tsx | 18++++++++----------
Mpackages/bank-ui/src/pages/ConversionRateClassDetails.tsx | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mpackages/bank-ui/src/pages/LoginForm.tsx | 123+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/bank-ui/src/pages/NewConversionRateClass.tsx | 76++++++++++++++++++++++++++++++++--------------------------------------------
Mpackages/bank-ui/src/pages/OperationState/views.tsx | 172++++++++++++++++++++++++++++++++++++-------------------------------------------
Mpackages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 164+++++++++++++++++++++++++++++++------------------------------------------------
Mpackages/bank-ui/src/pages/QrCodeSection.tsx | 51+++++++++++++++++++++++++++------------------------
Mpackages/bank-ui/src/pages/RegistrationPage.tsx | 206+++++++++++++++++++++++++++----------------------------------------------------
Mpackages/bank-ui/src/pages/SolveMFA.tsx | 21+++++++--------------
Mpackages/bank-ui/src/pages/WalletWithdrawForm.tsx | 125++++++++++++++++++++++++++++++-------------------------------------------------
Mpackages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 18++++++++----------
Mpackages/bank-ui/src/pages/WithdrawalQRCode.tsx | 4----
Mpackages/bank-ui/src/pages/account/ShowAccountDetails.tsx | 102+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/bank-ui/src/pages/account/UpdateAccountPassword.tsx | 10+++-------
Mpackages/bank-ui/src/pages/admin/CreateNewAccount.tsx | 199+++++++++++++++++++++----------------------------------------------------------
Mpackages/bank-ui/src/pages/admin/DownloadStats.tsx | 63++++++++++++++++++++++++++++++++++-----------------------------
Mpackages/bank-ui/src/pages/admin/RemoveAccount.tsx | 74++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/bank-ui/src/pages/regional/ConversionConfig.tsx | 10++++++----
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 63++++++++++++++++++++++++++++++---------------------------------
Mpackages/web-util/src/components/Button.tsx | 5++---
Mpackages/web-util/src/hooks/useNotifications.ts | 1+
21 files changed, 710 insertions(+), 931 deletions(-)

diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx @@ -16,14 +16,13 @@ import { LocalNotificationBanner, - safeFunctionHandler, urlPattern, useBankCoreApiContext, useChallengeHandler, useCurrentLocation, useLocalNotificationBetter, useNavigationContext, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -118,8 +117,7 @@ function PublicRounting({ const { navigateTo } = useNavigationContext(); const { config, lib } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = - useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); @@ -152,9 +150,7 @@ function PublicRounting({ AbsoluteTime.fromProtocolTimestamp(success.body.expiration), ); - login.onUnexpectedFailure = defaultUnexpectedFailureMessages; - - login.onFail = saveNotification((fail, username) => { + login.onFail = (fail, username) => { switch (fail.case) { case HttpStatusCode.Accepted: { mfa.onChallengeRequired(fail.body); @@ -169,7 +165,7 @@ function PublicRounting({ case HttpStatusCode.NotFound: return i18n.str`Account not found`; } - }); + }; const repeatLogin = login.lambda((ids: string[]) => { return [login.args![0], login.args![1], ids]; @@ -181,7 +177,7 @@ function PublicRounting({ currentChallenge={mfa.pendingChallenge} description={i18n.str`New web session`} onCancel={mfa.doCancelChallenge} - username={lastCallingArgs[0]} + username={login.args![0]} onCompleted={repeatLogin} /> ); @@ -217,7 +213,9 @@ function PublicRounting({ <Fragment> <LocalNotificationBanner notification={notification} /> <RegistrationPage - onRegistrationSuccesful={notifyOnError(doAutomaticLogin)} + onRegistrationSuccesful={(usr, pwd) => { + login.withArgs(usr,pwd, []).call() + }} routeCancel={publicPages.login} /> </Fragment> diff --git a/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx b/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx @@ -10,6 +10,7 @@ import { } from "@gnu-taler/taler-util"; import { Attention, + ButtonBetter, ErrorLoading, InputText, InputToggle, @@ -19,11 +20,11 @@ import { RouteDefinition, ShowInputErrorLabel, useBankCoreApiContext, - useLocalNotification, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; -import { useState, useEffect } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { FormErrors, @@ -35,9 +36,7 @@ import { revalidateConversionRateClassDetails, revalidateConversionRateClassUsers, TransferCalculation, - useCashinEstimator, useCashinEstimatorForClass, - useCashoutEstimator, useCashoutEstimatorForClass, useConversionInfo, useConversionRateClassDetails, @@ -45,10 +44,10 @@ import { } from "../hooks/regional.js"; import { useSessionState } from "../hooks/session.js"; import { RecursivePartial, undefinedIfEmpty } from "../utils.js"; +import { DescribeConversion } from "./admin/ConversionClassList.js"; import { doAutoFocus, InputAmount } from "./PaytoWireTransferForm.js"; import { ConversionForm } from "./regional/ConversionConfig.js"; -import { AmountJson } from "@gnu-taler/taler-util"; -import { DescribeConversion } from "./admin/ConversionClassList.js"; +import { AccessToken } from "@gnu-taler/taler-util"; interface Props { classId: number; @@ -136,9 +135,7 @@ function Form({ lib: { bank }, config, } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; - + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [section, setSection] = useState< "detail" | "cashout" | "cashin" | "users" | "test" | "delete" >("detail"); @@ -167,18 +164,27 @@ function Form({ ), ); - async function doDeleteClass() { - if (!creds) return; - await bank.deleteConversionRateClass(creds.token, classId); - onClassDeleted(); - } - - const doDelete = + const deleteClass = safeFunctionHandler( + (token: AccessToken) => bank.deleteConversionRateClass(token, classId), !creds || section !== "delete" || detailsResult.num_users > 0 ? undefined - : doDeleteClass; + : [creds.token], + ); + deleteClass.onSuccess = onClassDeleted; + deleteClass.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str``; + case HttpStatusCode.Forbidden: + return i18n.str``; + case HttpStatusCode.NotFound: + return i18n.str``; + case HttpStatusCode.NotImplemented: + return i18n.str``; + } + }; - async function doUpdateClass() { + async function doUpdateClass1() { if (!creds) return; if (status.status !== "ok") { console.log("can submit due to form error", status.errors); @@ -202,7 +208,55 @@ function Form({ setSection("detail"); } - const doUpdateDetails = + const updateRequest: TalerCorebankApi.ConversionRateClassInput | undefined = + status.status === "fail" + ? undefined + : { + name: status.result.name, + description: status.result.description, + + cashin_fee: status.result.conv.cashin_fee, + cashin_min_amount: status.result.conv.cashin_min_amount, + cashin_ratio: status.result.conv.cashin_ratio, + cashin_rounding_mode: status.result.conv.cashin_rounding_mode, + + cashout_fee: status.result.conv.cashout_fee, + cashout_min_amount: status.result.conv.cashout_min_amount, + cashout_ratio: status.result.conv.cashout_ratio, + cashout_rounding_mode: status.result.conv.cashout_rounding_mode, + }; + + const updateClassTemplate = safeFunctionHandler( + ( + token: AccessToken, + updateRequest: TalerCorebankApi.ConversionRateClassInput, + ) => bank.updateConversionRateClass(token, classId, updateRequest), + ); + + updateClassTemplate.onSuccess = () => { + setSection("detail"); + }; + updateClassTemplate.onFail = (fail) => { + switch (fail.case) { + default: + return i18n.str``; + } + }; + + const updateDetails = updateClassTemplate.lambda( + (t: AccessToken, r: TalerCorebankApi.ConversionRateClassInput) => [t, r], + !creds || + !updateRequest || + section !== "detail" || + status.errors?.name || + status.errors?.description || + (status.result.name === initalState.name && + status.result.description === initalState.description) + ? undefined + : [creds.token, updateRequest], + ); + + const doUpdateDetails1 = !creds || section !== "detail" || status.errors?.name || @@ -210,9 +264,9 @@ function Form({ (status.result.name === initalState.name && status.result.description === initalState.description) ? undefined - : doUpdateClass; + : doUpdateClass2; - const doUpdateCashin = + const doUpdateCashin1 = !creds || section !== "cashin" || status.errors?.conv?.cashin_fee || @@ -220,9 +274,9 @@ function Form({ status.errors?.conv?.cashin_ratio || status.errors?.conv?.cashin_rounding_mode ? undefined - : doUpdateClass; + : doUpdateClass2; - const doUpdateCashout = + const doUpdateCashout1 = !creds || section !== "cashout" || // no errors on fields @@ -238,7 +292,7 @@ function Form({ status.result?.conv?.cashout_rounding_mode === initalState.conv.cashout_rounding_mode) ? undefined - : doUpdateClass; + : doUpdateClass2; const default_rate = conversionInfo.conversion_rate; @@ -603,54 +657,50 @@ function Form({ </a> {section == "cashin" ? ( <Fragment> - <button + <ButtonBetter type="submit" name="update conversion" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!doUpdateCashin} - onClick={doUpdateCashin} + onClick={updateCashin} > <i18n.Translate>Update</i18n.Translate> - </button> + </ButtonBetter> </Fragment> ) : undefined} {section == "cashout" ? ( <Fragment> - <button + <ButtonBetter type="submit" name="update conversion" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!doUpdateCashout} - onClick={doUpdateCashout} + onClick={updateCashout} > <i18n.Translate>Update</i18n.Translate> - </button> + </ButtonBetter> </Fragment> ) : undefined} {section == "detail" ? ( <Fragment> - <button + <ButtonBetter type="submit" name="update conversion" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!doUpdateDetails} - onClick={doUpdateDetails} + onClick={updateDelete} > <i18n.Translate>Update</i18n.Translate> - </button> + </ButtonBetter> </Fragment> ) : undefined} {section == "delete" ? ( <Fragment> - <button + <ButtonBetter type="submit" name="update conversion" class="disabled:opacity-50 disabled:cursor-default cursor-pointer 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-600" - disabled={!doDelete} - onClick={doDelete} + onClick={deleteClass} > <i18n.Translate>Delete</i18n.Translate> - </button> + </ButtonBetter> </Fragment> ) : undefined} </div> @@ -778,9 +828,7 @@ export function createFormValidator( function TestConversionClass({ classId }: { classId: number }): VNode { const { i18n } = useTranslationContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; - + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const result = useConversionInfo(); const info = result && !(result instanceof TalerError) && result.type === "ok" @@ -840,7 +888,7 @@ function TestConversionClass({ classId }: { classId: number }): VNode { setCalc(undefined); // silent failure return; } - + setCalc({ cashin, cashout }); }); } diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx @@ -18,19 +18,18 @@ import { AbsoluteTime, Duration, HttpStatusCode, + TalerErrorCode, + TokenRequest, createRFC8959AccessTokenEncoded, } from "@gnu-taler/taler-util"; import { - Button, ButtonBetter, LocalNotificationBanner, - makeSafeCall, RouteDefinition, ShowInputErrorLabel, useBankCoreApiContext, useChallengeHandler, useLocalNotificationBetter, - useLocalNotificationHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -39,9 +38,6 @@ import { useSessionState } from "../hooks/session.js"; import { undefinedIfEmpty } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; import { USERNAME_REGEX } from "./RegistrationPage.js"; -import { TalerErrorCode } from "@gnu-taler/taler-util"; -import { useBankState } from "../hooks/bank-state.js"; -import { TokenRequest } from "@gnu-taler/taler-util"; import { SolveMFAChallenges } from "./SolveMFA.js"; const TALER_SCREEN_ID = 104; @@ -82,7 +78,7 @@ export function LoginForm({ const { lib: { bank: api }, } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const { config } = useBankCoreApiContext(); @@ -96,12 +92,17 @@ export function LoginForm({ password: !password ? i18n.str`Missing password` : undefined, }); - async function doLogout() { - if (sessionState) { - await api.deleteAccessToken(sessionState.username, sessionState.token); + const logout = safeFunctionHandler( + api.deleteAccessToken, + !sessionState ? undefined : [sessionState.username, sessionState.token], + ); + logout.onSuccess = session.logOut; + logout.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.NotFound: + return i18n.str`User doesn't exist anymore.`; } - session.logOut(); - } + }; const tokenRequest = { scope: "readwrite", @@ -109,57 +110,56 @@ export function LoginForm({ refreshable: true, } as TokenRequest; - const [doLogin, repeatLogin] = mfa.withMfaHandler( - ({ ids: challengeIds, onChallengeRequired }) => - makeSafeCall( - i18n, - (username: string, password: string) => - api.createAccessToken( - username, - { type: "basic", password }, - tokenRequest, - { challengeIds }, - ), - (result, username) => { - session.logIn({ - username, - token: createRFC8959AccessTokenEncoded(result.body.access_token), - expiration: AbsoluteTime.fromProtocolTimestamp( - result.body.expiration, - ), - }); - }, - (fail, username) => { - switch (fail.case) { - case HttpStatusCode.Accepted: { - onChallengeRequired(fail.body); - return i18n.str`A second factor authentication is required.`; - } - case TalerErrorCode.GENERIC_FORBIDDEN: - return i18n.str`You have no permission to this account.`; - case TalerErrorCode.BANK_ACCOUNT_LOCKED: - return i18n.str`You have no permission to this account.`; - case HttpStatusCode.Unauthorized: - return i18n.str`Wrong credentials for "${username}"`; - case HttpStatusCode.NotFound: - return i18n.str`Account not found`; - } - }, + const login = safeFunctionHandler( + (username: string, password: string, challengeIds: string[]) => + api.createAccessToken( + username, + { type: "basic", password }, + tokenRequest, + { challengeIds }, ), + !!errors ? undefined : [username!, password!, []], ); - const loginHandler = - !username || !password || !!errors - ? undefined - : () => notifyOnError(doLogin)(username, password); - if (mfa.pendingChallenge && repeatLogin && username) { + login.onSuccess = (result, username) => { + session.logIn({ + username, + token: createRFC8959AccessTokenEncoded(result.body.access_token), + expiration: AbsoluteTime.fromProtocolTimestamp(result.body.expiration), + }); + }; + + login.onFail = (fail, username) => { + switch (fail.case) { + case HttpStatusCode.Accepted: { + mfa.onChallengeRequired(fail.body); + return i18n.str`A second factor authentication is required.`; + } + case TalerErrorCode.GENERIC_FORBIDDEN: + return i18n.str`You have no permission to this account.`; + case TalerErrorCode.BANK_ACCOUNT_LOCKED: + return i18n.str`You have no permission to this account.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Wrong credentials for "${username}"`; + case HttpStatusCode.NotFound: + return i18n.str`Account not found`; + } + }; + + const retryLogin = login.lambda((ids: string[]) => [ + login.args![0], + login.args![1], + ids, + ]); + + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} description={i18n.str`Account login.`} onCancel={mfa.doCancelChallenge} - username={username} - onCompleted={repeatLogin} + username={username!} + onCompleted={retryLogin} /> ); } @@ -243,23 +243,20 @@ export function LoginForm({ {session.state.status !== "loggedOut" ? ( <div class="flex justify-between"> - <button + <ButtonBetter type="submit" name="cancel" 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) => { - e.preventDefault(); - doLogout(); - }} + onClick={logout} > <i18n.Translate>Cancel</i18n.Translate> - </button> + </ButtonBetter> <ButtonBetter type="submit" name="check" class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - onClick={loginHandler} + onClick={login} > <i18n.Translate>Check</i18n.Translate> </ButtonBetter> @@ -270,7 +267,7 @@ export function LoginForm({ type="submit" name="login" class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - onClick={loginHandler} + onClick={login} > <i18n.Translate>Log in</i18n.Translate> </ButtonBetter> diff --git a/packages/bank-ui/src/pages/NewConversionRateClass.tsx b/packages/bank-ui/src/pages/NewConversionRateClass.tsx @@ -1,16 +1,17 @@ import { - assertUnreachable, + AccessToken, HttpStatusCode, TalerCorebankApi, - TalerErrorCode, + TalerErrorCode } from "@gnu-taler/taler-util"; import { + ButtonBetter, LocalNotificationBanner, notifyInfo, RouteDefinition, useBankCoreApiContext, useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -33,44 +34,35 @@ export function NewConversionRateClass({ lib: { bank: api }, } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = - useLocalNotificationBetter(); - + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const [submitData, setSubmitData] = useState< TalerCorebankApi.ConversionRateClassInput | undefined >(); - async function doCreate() { - if (!submitData || !token) return; - await handleError(async () => { - const resp = await api.createConversionRateClass(token, submitData); - if (resp.type === "ok") { - notifyInfo(i18n.str`Conversion rate class created.`); - onCreated(resp.body.conversion_rate_class_id); - return; - } - switch (resp.case) { - case HttpStatusCode.Unauthorized: { - break; - } - case TalerErrorCode.BANK_NAME_REUSE: { - break; - } - case HttpStatusCode.Forbidden: { - break; - } - case HttpStatusCode.NotFound: { - break; - } - case HttpStatusCode.NotImplemented: { - break; - } - default: { - assertUnreachable(resp); - } - } - }); - } + const create = safeFunctionHandler( + (token: AccessToken, data: TalerCorebankApi.ConversionRateClassInput) => + api.createConversionRateClass(token, data), + !submitData || !token ? undefined : [token, submitData], + ); + create.onSuccess = (success) => { + notifyInfo(i18n.str`Conversion rate class created.`); + onCreated(success.body.conversion_rate_class_id); + }; + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`The rights to change the account are not sufficient`; + case HttpStatusCode.Forbidden: + return i18n.str`Wrong credentials`; + case HttpStatusCode.NotFound: + return i18n.str`Account not found`; + case HttpStatusCode.NotImplemented: + return i18n.str`Not implemented`; + case TalerErrorCode.BANK_NAME_REUSE: + return i18n.str`The name is already used`; + } + }; return ( <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg"> @@ -91,18 +83,14 @@ export function NewConversionRateClass({ > <i18n.Translate>Cancel</i18n.Translate> </a> - <button + <ButtonBetter type="submit" name="create" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!submitData} - onClick={(e) => { - e.preventDefault(); - doCreate(); - }} + onClick={create} > <i18n.Translate>Create</i18n.Translate> - </button> + </ButtonBetter> </div> </ConversionRateClassForm> </div> diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx @@ -17,6 +17,7 @@ import { Amounts, HttpStatusCode, + PaytoType, TalerErrorCode, TalerUris, assertUnreachable, @@ -25,7 +26,6 @@ import { Attention, ButtonBetter, LocalNotificationBanner, - makeSafeCall, notifyInfo, useBankCoreApiContext, useChallengeHandler, @@ -37,12 +37,11 @@ import { Fragment, VNode, h } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../../components/QR.js"; import { usePreferences } from "../../hooks/preferences.js"; -import { useSessionState } from "../../hooks/session.js"; +import { LoggedIn, useSessionState } from "../../hooks/session.js"; import { RenderAmount } from "../PaytoWireTransferForm.js"; import { SolveMFAChallenges } from "../SolveMFA.js"; import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js"; import { State } from "./index.js"; -import { PaytoType } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 5; @@ -69,7 +68,7 @@ export function NeedConfirmationView({ }: State.NeedConfirmation) { const { i18n } = useTranslationContext(); const [settings] = usePreferences(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; @@ -83,68 +82,59 @@ export function NeedConfirmationView({ ? Amounts.zeroOfCurrency(config.currency) : Amounts.parseOrThrow(config.wire_transfer_fees); - const doAbort = !creds - ? undefined - : notifyOnError( - makeSafeCall( - i18n, - () => bank.abortWithdrawalById(creds, operationId), - (suc) => { - onAbort(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Conflict: - return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; - case HttpStatusCode.BadRequest: - return i18n.str`The operation ID is invalid.`; - case HttpStatusCode.NotFound: - return i18n.str`The operation was not found.`; - } - }, - ), - ); + const abort = safeFunctionHandler( + (creds: LoggedIn) => bank.abortWithdrawalById(creds, operationId), + !creds ? undefined : [creds], + ); + abort.onSuccess = onAbort; + abort.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Conflict: + return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; + case HttpStatusCode.BadRequest: + return i18n.str`The operation ID is invalid.`; + case HttpStatusCode.NotFound: + return i18n.str`The operation was not found.`; + } + }; - const [doConfirm, repeatConfirm] = !creds - ? [undefined, undefined] - : mfa.withMfaHandler(({ ids: challengeIds, onChallengeRequired }) => - makeSafeCall( - i18n, - () => - bank.confirmWithdrawalById(creds, {}, operationId, { - challengeIds, - }), - (suc) => { - if (!settings.showWithdrawalSuccess) { - notifyInfo(i18n.str`Wire transfer completed!`); - } - }, - (fail) => { - switch (fail.case) { - case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: - return i18n.str`The withdrawal has been aborted previously and can't be confirmed`; - case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: - return i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`; - case HttpStatusCode.BadRequest: - return i18n.str`The operation ID is invalid.`; - case HttpStatusCode.NotFound: - return i18n.str`The operation was not found.`; - case TalerErrorCode.BANK_UNALLOWED_DEBIT: - return i18n.str`Your balance is not sufficient for the operation.`; - case HttpStatusCode.Accepted: { - onChallengeRequired(fail.body); - return i18n.str`A second factor authentication is required.`; - } - case TalerErrorCode.BANK_AMOUNT_DIFFERS: - return i18n.str`The starting withdrawal amount and the confirmation amount differs.`; - case TalerErrorCode.BANK_AMOUNT_REQUIRED: - return i18n.str`The bank requires a bank account which has not been specified yet.`; - } - }, - ), - ); + const confirm = safeFunctionHandler( + (creds: LoggedIn, challengeIds: string[]) => + bank.confirmWithdrawalById(creds, {}, operationId, { challengeIds }), + !creds ? undefined : [creds, []], + ); + confirm.onSuccess = () => { + if (!settings.showWithdrawalSuccess) { + notifyInfo(i18n.str`Wire transfer completed!`); + } + }; + confirm.onFail = (fail) => { + switch (fail.case) { + case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: + return i18n.str`The withdrawal has been aborted previously and can't be confirmed`; + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + return i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`; + case HttpStatusCode.BadRequest: + return i18n.str`The operation ID is invalid.`; + case HttpStatusCode.NotFound: + return i18n.str`The operation was not found.`; + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return i18n.str`Your balance is not sufficient for the operation.`; + case HttpStatusCode.Accepted: { + mfa.onChallengeRequired(fail.body); + return i18n.str`A second factor authentication is required.`; + } + case TalerErrorCode.BANK_AMOUNT_DIFFERS: + return i18n.str`The starting withdrawal amount and the confirmation amount differs.`; + case TalerErrorCode.BANK_AMOUNT_REQUIRED: + return i18n.str`The bank requires a bank account which has not been specified yet.`; + } + }; - if (mfa.pendingChallenge && repeatConfirm) { + const repeatConfirm = confirm.lambda((ids: string[]) => { + return [confirm.args![0], ids]; + }); + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} @@ -358,7 +348,7 @@ export function NeedConfirmationView({ type="button" name="cancel" class="text-sm font-semibold leading-6 text-gray-900" - onClick={doAbort} + onClick={abort} > <i18n.Translate>Cancel</i18n.Translate> </ButtonBetter> @@ -366,7 +356,7 @@ export function NeedConfirmationView({ type="submit" name="transfer" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - onClick={!doConfirm ? undefined : notifyOnError(doConfirm)} + onClick={confirm} > <i18n.Translate>Transfer</i18n.Translate> </ButtonBetter> @@ -519,7 +509,7 @@ export function ReadyView({ }: State.Ready): VNode { const { i18n } = useTranslationContext(); const walletInegrationApi = useTalerWalletIntegrationAPI(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; @@ -537,25 +527,21 @@ export function ReadyView({ walletInegrationApi.publishTalerAction(uri); }, []); - const doAbort = !creds - ? undefined - : makeSafeCall( - i18n, - () => bank.abortWithdrawalById(creds, operationId), - (suc) => { - onAbort(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Conflict: - return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; - case HttpStatusCode.BadRequest: - return i18n.str`The operation ID is invalid.`; - case HttpStatusCode.NotFound: - return i18n.str`The operation was not found.`; - } - }, - ); + const abort = safeFunctionHandler( + (creds: LoggedIn) => bank.abortWithdrawalById(creds, operationId), + !creds ? undefined : [creds], + ); + abort.onSuccess = onAbort; + abort.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Conflict: + return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; + case HttpStatusCode.BadRequest: + return i18n.str`The operation ID is invalid.`; + case HttpStatusCode.NotFound: + return i18n.str`The operation was not found.`; + } + }; return ( <Fragment> @@ -586,15 +572,15 @@ export function ReadyView({ </p> </div> <div class="flex items-center justify-between gap-x-6 pt-2 mt-2 "> - <button + <ButtonBetter type="button" name="cancel" class="text-sm font-semibold leading-6 text-gray-900" // class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-black shadow-sm " - onClick={onAbort} + onClick={abort} > <i18n.Translate>Cancel</i18n.Translate> - </button> + </ButtonBetter> <a href={talerWithdrawUri} @@ -624,15 +610,13 @@ export function ReadyView({ </div> </div> <div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> - <button + <ButtonBetter type="button" - // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" class="text-sm font-semibold leading-6 text-gray-900" - // handler={onAbortHandler} - onClick={doAbort} + onClick={abort} > <i18n.Translate>Cancel</i18n.Translate> - </button> + </ButtonBetter> </div> </div> </Fragment> diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -25,7 +25,6 @@ import { IbanString, PaytoType, Paytos, - TalerCorebankApi, TalerErrorCode, TranslatedString, assertUnreachable @@ -36,12 +35,11 @@ import { LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, - makeSafeCall, notifyInfo, useBankCoreApiContext, useChallengeHandler, useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -107,7 +105,7 @@ export function PaytoWireTransferForm({ const parsedAmount = Amounts.parse( `${limitWithFee.currency}:${trimmedAmountStr}`, ); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); @@ -149,130 +147,97 @@ export function PaytoWireTransferForm({ ), }); - let payto_uri: Paytos.FullPaytoString | undefined; + let parsedURI: Paytos.URI | undefined; let sendingAmount: AmountString | undefined; - - let acName: string | undefined; + if (isRawPayto) { const res = Paytos.fromString(rawPaytoInput!); if (res && res.type === "ok") { - const p = res.body; - sendingAmount = p.params.amount as AmountString; - delete p.params.amount; - // if this payto is valid then it already have message - payto_uri = Paytos.toFullString(p); - acName = - p.targetType === undefined - ? undefined - : p.targetType === PaytoType.IBAN - ? p.iban - : p.targetType === PaytoType.TalerReserve || - p.targetType === PaytoType.TalerReserveHttp - ? undefined // FIXME: unsupported payto:// - : p.targetType === PaytoType.Bitcoin - ? p.address - : p.targetType === PaytoType.Ethereum - ? p.address - : p.targetType === PaytoType.TalerBank - ? p.account - : assertUnreachable(p); - } + parsedURI = res.body; + sendingAmount = parsedURI.params.amount as AmountString; + delete parsedURI.params.amount; // we don't want to send twice in the request + } } else if (account && subject) { - // if (!account || !subject) return; - let payto; - acName = account; switch (paytoType) { case "x-taler-bank": { - payto = Paytos.createTalerBank(url.host as HostPortPath, account); + parsedURI = Paytos.createTalerBank(url.host as HostPortPath, account); break; } case "iban": { - payto = Paytos.createIban(account as IbanString, undefined); + parsedURI = Paytos.createIban(account as IbanString, undefined); break; } default: assertUnreachable(paytoType); } - payto.params.message = encodeURIComponent(subject); - payto_uri = Paytos.toFullString(payto); + parsedURI.params.message = encodeURIComponent(subject); sendingAmount = `${limitWithFee.currency}:${trimmedAmountStr}` as AmountString; } - const puri = payto_uri; const sAmount = sendingAmount; - const request: TalerCorebankApi.CreateTransactionRequest | undefined = - !payto_uri + const send = safeFunctionHandler( + ( + creds: LoggedIn, + amount: AmountString, + uri: Paytos.URI, + challengeIds: string[], + ) => api.createTransaction(creds, {payto_uri: Paytos.toFullString(uri), amount}, { challengeIds }), + (isRawPayto ? !!errorsPayto : !!errorsWire) || !sAmount || !parsedURI || credentials.status !== "loggedIn" ? undefined - : { - payto_uri: puri!, - amount: sAmount, - }; - - type reqType = TalerCorebankApi.CreateTransactionRequest; - - const [doTransfer, repeatTransfer, lastCallingArgs] = mfa.withMfaHandler( - ({ ids: challengeIds, onChallengeRequired }) => - makeSafeCall( - i18n, - (credentials: LoggedIn, request: reqType) => - api.createTransaction(credentials, request, { - challengeIds, - }), - (success) => { - notifyInfo(i18n.str`The wire transfer was successfully completed!`); - onSuccess(); - setAmount(undefined); - setAccount(undefined); - setSubject(undefined); - rawPaytoInputSetter(undefined); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.BadRequest: - return i18n.str`The request was invalid or the payto://-URI used unacceptable features.`; - case HttpStatusCode.Unauthorized: - return i18n.str`Not enough permission to complete the operation.`; - case TalerErrorCode.BANK_ADMIN_CREDITOR: - return i18n.str`The bank administrator cannot be the transfer creditor.`; - case TalerErrorCode.BANK_UNKNOWN_CREDITOR: - return i18n.str`The destination account "${ - acName ?? puri - }" was not found.`; - case TalerErrorCode.BANK_SAME_ACCOUNT: - return i18n.str`The origin and the destination of the transfer can't be the same.`; - case TalerErrorCode.BANK_UNALLOWED_DEBIT: - return i18n.str`Your balance is not sufficient for the operation.`; - case HttpStatusCode.NotFound: - return i18n.str`The origin account "${puri}" was not found.`; - case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: { - return i18n.str`The attempt to create the transaction has failed. Please try again.`; - } - case HttpStatusCode.Accepted: { - onChallengeRequired(fail.body); - return i18n.str`A second factor authentication is required.`; - } - default: - assertUnreachable(fail); - } - }, - ), + : [credentials, sAmount, parsedURI, []], ); - const sendHandler = - !request || credentials.status !== "loggedIn" - ? undefined - : () => notifyOnError(doTransfer)(credentials, request); - if (mfa.pendingChallenge && repeatTransfer) { + send.onSuccess = (success) => { + notifyInfo(i18n.str`The wire transfer was successfully completed!`); + onSuccess(); + setAmount(undefined); + setAccount(undefined); + setSubject(undefined); + rawPaytoInputSetter(undefined); + }; + + send.onFail = (fail, creds, amount, uri) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`The request was invalid or the payto://-URI used unacceptable features.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Not enough permission to complete the operation.`; + case TalerErrorCode.BANK_ADMIN_CREDITOR: + return i18n.str`The bank administrator cannot be the transfer creditor.`; + case TalerErrorCode.BANK_UNKNOWN_CREDITOR: + return i18n.str`The destination account "${uri.displayName}" was not found.`; + case TalerErrorCode.BANK_SAME_ACCOUNT: + return i18n.str`The origin and the destination of the transfer can't be the same.`; + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return i18n.str`Your balance is not sufficient for the operation.`; + case HttpStatusCode.NotFound: + return i18n.str`The origin account "${uri.displayName}" was not found.`; + case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: { + return i18n.str`The attempt to create the transaction has failed. Please try again.`; + } + case HttpStatusCode.Accepted: { + mfa.onChallengeRequired(fail.body); + return i18n.str`A second factor authentication is required.`; + } + default: + assertUnreachable(fail); + } + }; + const repeatSend = send.lambda((ids:string[]) => { + return [send.args![0],send.args![1],send.args![2], ids] + }) + + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} description={i18n.str`Confirm wire transfer.`} onCancel={mfa.doCancelChallenge} - username={lastCallingArgs[0].username} - onCompleted={repeatTransfer} + username={send.args![0].username} + onCompleted={repeatSend} /> ); } @@ -676,8 +641,7 @@ export function PaytoWireTransferForm({ type="submit" name="send" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={isRawPayto ? !!errorsPayto : !!errorsWire} - onClick={sendHandler} + onClick={send} > <i18n.Translate>Send</i18n.Translate> </ButtonBetter> diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx @@ -17,13 +17,14 @@ import { HttpStatusCode, TalerUris, - WithdrawUriResult + WithdrawUriResult, } from "@gnu-taler/taler-util"; import { Button, + ButtonBetter, LocalNotificationBanner, useBankCoreApiContext, - useLocalNotificationHandler, + useLocalNotificationBetter, useTalerWalletIntegrationAPI, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -31,6 +32,7 @@ import { Fragment, h, VNode } from "preact"; import { useEffect } from "preact/hooks"; import { QR } from "../components/QR.js"; import { useSessionState } from "../hooks/session.js"; +import { UserAndToken } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 109; @@ -51,29 +53,30 @@ export function QrCodeSection({ walletInegrationApi.publishTalerAction(withdrawUri); }, []); - const [notification, handleError] = useLocalNotificationHandler(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { lib: { bank: api }, } = useBankCoreApiContext(); - const onAbortHandler = !creds ? undefined : handleError( - async () => { - return api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId); - }, - onAborted, - (fail) => { - switch (fail.case) { - case HttpStatusCode.BadRequest: - return i18n.str`The operation ID is invalid.`; - case HttpStatusCode.NotFound: - return i18n.str`The operation was not found.`; - case HttpStatusCode.Conflict: - return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; - } - }, + const abort = safeFunctionHandler( + (creds: UserAndToken) => + api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId), + !creds ? undefined : [creds], ); + abort.onSuccess = onAborted; + abort.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`The operation ID is invalid.`; + case HttpStatusCode.NotFound: + return i18n.str`The operation was not found.`; + case HttpStatusCode.Conflict: + return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; + } + }; + return ( <Fragment> <LocalNotificationBanner notification={notification} /> @@ -103,14 +106,14 @@ export function QrCodeSection({ </p> </div> <div class="flex items-center justify-between gap-x-6 pt-2 mt-2 "> - <Button + <ButtonBetter type="button" name="cancel" class="text-sm font-semibold leading-6 text-gray-900" - handler={onAbortHandler} + onClick={abort} > <i18n.Translate>Cancel</i18n.Translate> - </Button> + </ButtonBetter> <a href={talerWithdrawUri} name="withdraw" @@ -139,14 +142,14 @@ export function QrCodeSection({ </div> </div> <div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8"> - <Button + <ButtonBetter type="button" // class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600" class="text-sm font-semibold leading-6 text-gray-900" - handler={onAbortHandler} + onClick={abort} > <i18n.Translate>Cancel</i18n.Translate> - </Button> + </ButtonBetter> </div> </div> </Fragment> diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx @@ -15,19 +15,21 @@ */ import { HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util"; import { + ButtonBetter, LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, useBankCoreApiContext, - useLocalNotification, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useSettingsContext } from "../context/settings.js"; +import { usePreferences } from "../hooks/preferences.js"; import { undefinedIfEmpty } from "../utils.js"; import { getRandomPassword, getRandomUsername } from "./rnd.js"; -import { usePreferences } from "../hooks/preferences.js"; +import { TalerCorebankApi } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 110; @@ -76,8 +78,7 @@ function RegistrationForm({ // const [phone, setPhone] = useState<string | undefined>(); // const [email, setEmail] = useState<string | undefined>(); const [repeatPassword, setRepeatPassword] = useState<string | undefined>(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const settings = useSettingsContext(); const [pref] = usePreferences(); @@ -106,68 +107,63 @@ function RegistrationForm({ : undefined, }); - async function doRegistrationAndLogin( - name: string, - username: string, - password: string, - onComplete: () => void, - ) { - await handleError(async (onError) => { - const resp = await api.createAccount(undefined, { - name, - username, - password, - }); - if (resp.type === "ok") { - onComplete(); - } else { - onError(resp, (_case) => { - switch (_case) { - case HttpStatusCode.BadRequest: - return i18n.str`Server replied with invalid phone or email.`; - case HttpStatusCode.Unauthorized: - return i18n.str`You are not authorised to create this account.`; - case TalerErrorCode.BANK_UNALLOWED_DEBIT: - return i18n.str`Registration is disabled because the bank ran out of bonus credit.`; - case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: - return i18n.str`That username can't be used because is reserved.`; - case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: - return i18n.str`That username is already taken.`; - case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: - return i18n.str`That account ID is already taken.`; - case TalerErrorCode.BANK_MISSING_TAN_INFO: - return i18n.str`No information for the selected authentication channel.`; - case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: - return i18n.str`Authentication channel is not supported.`; - case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: - return i18n.str`Only an administrator is allowed to set the debt limit.`; - case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: - return i18n.str`Only the administrator can change the conversion rate.`; - case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: - return i18n.str`The conversion rate class doesn't exist.`; - case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: - return i18n.str`Only admin can create accounts with second factor authentication.`; - case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: - return i18n.str`The password is too short. Can't have less than 8 characters.`; - case TalerErrorCode.BANK_PASSWORD_TOO_LONG: - return i18n.str`The password is too long. Can't have more than 64 characters.`; - } - }); - } - }); - } + const reg: TalerCorebankApi.RegisterAccountRequest | undefined = + !name || !username || !password + ? undefined + : { + name, + username, + password, + }; - async function doRegistrationStep() { - if (!username || !password || !name) return; - await doRegistrationAndLogin(name, username, password, () => { - setUsername(undefined); - setPassword(undefined); - setRepeatPassword(undefined); - onRegistrationSuccesful(username, password); - }); - } + const register = safeFunctionHandler( + (account: TalerCorebankApi.RegisterAccountRequest) => + api.createAccount(undefined, account), + !!errors || !reg ? undefined : [reg], + ); - async function doRandomRegistration() { + register.onSuccess = (succes, acc) => { + setUsername(undefined); + setPassword(undefined); + setRepeatPassword(undefined); + onRegistrationSuccesful(acc.username, acc.password); + }; + + register.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`Server replied with invalid phone or email.`; + case HttpStatusCode.Unauthorized: + return i18n.str`You are not authorised to create this account.`; + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return i18n.str`Registration is disabled because the bank ran out of bonus credit.`; + case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: + return i18n.str`That username can't be used because is reserved.`; + case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: + return i18n.str`That username is already taken.`; + case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: + return i18n.str`That account ID is already taken.`; + case TalerErrorCode.BANK_MISSING_TAN_INFO: + return i18n.str`No information for the selected authentication channel.`; + case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: + return i18n.str`Authentication channel is not supported.`; + case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: + return i18n.str`Only an administrator is allowed to set the debt limit.`; + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: + return i18n.str`Only the administrator can change the conversion rate.`; + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: + return i18n.str`The conversion rate class doesn't exist.`; + case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: + return i18n.str`Only admin can create accounts with second factor authentication.`; + case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: + return i18n.str`The password is too short. Can't have less than 8 characters.`; + case TalerErrorCode.BANK_PASSWORD_TOO_LONG: + return i18n.str`The password is too long. Can't have more than 64 characters.`; + } + + }; + + const registerRandom = register.lambda(() => { const user = getRandomUsername(); const password = settings.simplePasswordForRandomAccounts @@ -179,10 +175,9 @@ function RegistrationForm({ const name = `${capitalizeFirstLetter(user.first)} ${capitalizeFirstLetter( user.second, )}`; - await doRegistrationAndLogin(name, username, password, () => { - onRegistrationSuccesful(username, password); - }); - } + return [{name, username, password}] + },[]) + return ( <Fragment> @@ -334,63 +329,8 @@ function RegistrationForm({ setName(e.currentTarget.value); }} /> - {/* <ShowInputErrorLabel - message={errors?.name} - isDirty={name !== undefined} - /> */} - </div> - </div> - - {/* <div> - <label for="phone" class="block text-sm font-medium leading-6 text-gray-900"> - <i18n.Translate>Phone</i18n.Translate> - </label> - <div class="mt-2"> - <input - autoFocus - type="text" - name="phone" - id="phone" - class="block w-full 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={phone ?? ""} - enterkeyhint="next" - placeholder="your phone" - autocomplete="none" - onInput={(e): void => { - setPhone(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.phone} - isDirty={phone !== undefined} - /> </div> </div> - <div> - <label for="email" class="block text-sm font-medium leading-6 text-gray-900"> - <i18n.Translate>Email</i18n.Translate> - </label> - <div class="mt-2"> - <input - autoFocus - type="text" - name="email" - id="email" - class="block w-full 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={email ?? ""} - enterkeyhint="next" - placeholder="your email" - autocomplete="email" - onInput={(e): void => { - setEmail(e.currentTarget.value); - }} - /> - <ShowInputErrorLabel - message={errors?.email} - isDirty={email !== undefined} - /> - </div> - </div> */} <div class="flex w-full justify-between"> <a @@ -400,35 +340,27 @@ function RegistrationForm({ > <i18n.Translate>Cancel</i18n.Translate> </a> - <button + <ButtonBetter type="submit" name="register" class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!!errors} - onClick={async (e) => { - e.preventDefault(); - - doRegistrationStep(); - }} + onClick={register} > <i18n.Translate>Register</i18n.Translate> - </button> + </ButtonBetter> </div> </form> {settings.allowRandomAccountCreation && ( <p class="mt-10 text-center text-sm text-gray-500 border-t"> - <button + <ButtonBetter type="submit" name="create random" class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600" - onClick={(e) => { - e.preventDefault(); - doRandomRegistration(); - }} + onClick={registerRandom} > <i18n.Translate>Create a random temporary user</i18n.Translate> - </button> + </ButtonBetter> </p> )} </div> diff --git a/packages/bank-ui/src/pages/SolveMFA.tsx b/packages/bank-ui/src/pages/SolveMFA.tsx @@ -11,8 +11,6 @@ import { import { ButtonBetter, LocalNotificationBanner, - NotificationMessage, - safeFunctionHandler, SafeHandlerTemplate, ShowInputErrorLabel, Time, @@ -51,8 +49,7 @@ function SolveChallenge({ const { lib: { bank: api }, } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = - useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [showExpired, setExpired] = useState( expiration !== undefined && AbsoluteTime.isExpired(expiration), @@ -79,8 +76,7 @@ function SolveChallenge({ api.confirmChallenge(username, challenge.challenge_id, { tan }), !errors ? [tanCode!] : undefined, ); - doVerification.onUnexpectedFailure = defaultUnexpectedFailureMessages; - doVerification.onFail = saveNotification((resp) => { + doVerification.onFail = (resp) => { switch (resp.case) { case TalerErrorCode.BANK_TRANSACTION_NOT_FOUND: return i18n.str`Unknown challenge.`; @@ -93,7 +89,7 @@ function SolveChallenge({ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return i18n.str`Expired challenge.`; } - }); + }; doVerification.onSuccess = onSolved; return ( @@ -233,8 +229,7 @@ export function SolveMFAChallenges({ ch: Challenge; expiration: AbsoluteTime; }>(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = - useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { lib: { bank: api }, @@ -273,7 +268,6 @@ export function SolveMFAChallenges({ const sendMessage = safeFunctionHandler((ch: Challenge) => api.sendChallenge(username, ch.challenge_id), ); - sendMessage.onUnexpectedFailure = defaultUnexpectedFailureMessages; sendMessage.onSuccess = (success, ch) => { if (success.body.earliest_retransmission) { setRetransmission({ @@ -291,7 +285,7 @@ export function SolveMFAChallenges({ }); }; - sendMessage.onFail = saveNotification((fail) => { + sendMessage.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Failed to send the verification code.`; @@ -304,7 +298,7 @@ export function SolveMFAChallenges({ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return i18n.str`Code transmission failed.`; } - }); + }; const doComplete = onCompleted.withArgs(solved); @@ -315,8 +309,7 @@ export function SolveMFAChallenges({ }); return opEmptySuccess(); }); - selectChallenge.onUnexpectedFailure = defaultUnexpectedFailureMessages; - + return ( <Fragment> <LocalNotificationBanner notification={notification} /> diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx @@ -15,24 +15,24 @@ */ import { - AbsoluteTime, AmountJson, Amounts, HttpStatusCode, TalerCorebankApi, TalerUriAction, TalerUris, - TranslatedString, + UserAndToken, assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, + ButtonBetter, LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, notifyError, useBankCoreApiContext, - useLocalNotification, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -50,6 +50,7 @@ import { doAutoFocus, } from "./PaytoWireTransferForm.js"; import { IntAmountJson } from "./regional/CreateCashout.js"; +import { AmountString } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 112; @@ -111,9 +112,6 @@ function OldWithdrawalForm({ const settings = useSettingsContext(); const [preference] = usePreferences(); - // const walletInegrationApi = useTalerWalletIntegrationAPI() - // const { navigateTo } = useNavigationContext(); - const [, updateBankState] = useBankState(); const { lib: { bank: api }, @@ -126,8 +124,7 @@ function OldWithdrawalForm({ const [amountStr, setAmountStr] = useState<string | undefined>( `${settings.defaultSuggestedAmount ?? 1}`, ); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const trimmedAmountStr = amountStr?.trim(); @@ -146,70 +143,47 @@ function OldWithdrawalForm({ : undefined, }); - async function doStart() { - if (!parsedAmount || !creds) return; - await handleError(async () => { - const params: TalerCorebankApi.BankAccountCreateWithdrawalRequest = + const start = safeFunctionHandler( + (creds: UserAndToken, amount: AmountString) => + api.createWithdrawal( + creds, preference.fastWithdrawalForm - ? { - suggested_amount: Amounts.stringify(parsedAmount), - } - : { - amount: Amounts.stringify(parsedAmount), - }; - const resp = await api.createWithdrawal(creds, params); - if (resp.type === "ok") { - const uri = TalerUris.fromString(resp.body.taler_withdraw_uri); - if (uri.type === "fail" || uri.body.type !== TalerUriAction.Withdraw) { - return notifyError( - i18n.str`The server replied with an invalid taler://withdraw URI`, - i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`, - ); - } else { - updateBankState( - "currentWithdrawalOperationId", - uri.body.withdrawalOperationId, - ); - onOperationCreated(uri.body.withdrawalOperationId); - } - } else { - switch (resp.case) { - case HttpStatusCode.Conflict: { - notify({ - type: "error", - title: i18n.str`The operation was rejected due to insufficient funds`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - break; - } - case HttpStatusCode.Unauthorized: { - notify({ - type: "error", - title: i18n.str`The operation was rejected due to insufficient funds`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - break; - } - case HttpStatusCode.NotFound: { - notify({ - type: "error", - title: i18n.str`Account not found`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - break; - } - default: - assertUnreachable(resp); - } - } - }); - } + ? { suggested_amount: amount } + : { amount: amount }, + ), + !parsedAmount || !creds + ? undefined + : [creds, Amounts.stringify(parsedAmount)], + ); + + start.onSuccess = (success) => { + const uri = TalerUris.fromString(success.body.taler_withdraw_uri); + if (uri.type === "fail" || uri.body.type !== TalerUriAction.Withdraw) { + return notifyError( + i18n.str`The server replied with an invalid taler://withdraw URI`, + i18n.str`Withdraw URI: ${success.body.taler_withdraw_uri}`, + ); + } else { + updateBankState( + "currentWithdrawalOperationId", + uri.body.withdrawalOperationId, + ); + onOperationCreated(uri.body.withdrawalOperationId); + } + }; + + start.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Conflict: + return i18n.str`The operation was rejected due to insufficient funds`; + case HttpStatusCode.Unauthorized: + return i18n.str`The operation was rejected due to insufficient funds`; + case HttpStatusCode.NotFound: + return i18n.str`Account not found`; + default: + assertUnreachable(fail); + } + }; return ( <form @@ -320,18 +294,15 @@ function OldWithdrawalForm({ > <i18n.Translate>Cancel</i18n.Translate> </a> - <button + <ButtonBetter type="submit" name="continue" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" // disabled={isRawPayto ? !!errorsPayto : !!errorsWire} - onClick={(e) => { - e.preventDefault(); - doStart(); - }} + onClick={start} > <i18n.Translate>Continue</i18n.Translate> - </button> + </ButtonBetter> </div> </form> ); diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -28,7 +28,6 @@ import { Attention, ButtonBetter, LocalNotificationBanner, - safeFunctionHandler, useBankCoreApiContext, useChallengeHandler, useLocalNotificationBetter, @@ -56,6 +55,7 @@ interface Props { function useComponentState(opid: string) { const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); @@ -93,6 +93,7 @@ function useComponentState(opid: string) { const spec = config.currency_specification; return { + notification, mfa, wireFee, spec, @@ -111,13 +112,10 @@ export function WithdrawalConfirmationQuestion({ withdrawUri, }: Props): VNode { const { i18n } = useTranslationContext(); - const { mfa, wireFee, spec, abort, confirm, repeat } = + const { notification, mfa, wireFee, spec, abort, confirm, repeat } = useComponentState(withdrawUri.withdrawalOperationId); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); - confirm.onUnexpectedFailure = defaultUnexpectedFailureMessages; - - confirm.onFail = saveNotification((fail) => { + confirm.onFail = (fail) => { switch (fail.case) { case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return i18n.str`The withdrawal has been aborted previously and can't be confirmed`; @@ -138,9 +136,9 @@ export function WithdrawalConfirmationQuestion({ return i18n.str`A second factor authentication is required.`; } } - }); + }; - abort.onFail = saveNotification((fail) => { + abort.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: return i18n.str``; @@ -149,8 +147,8 @@ export function WithdrawalConfirmationQuestion({ case HttpStatusCode.Conflict: return i18n.str`The withdrawal operation has been confirmed previously and can’t be aborted.`; } - }); - + }; + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges diff --git a/packages/bank-ui/src/pages/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx @@ -251,10 +251,6 @@ export function WithdrawalQRCode({ reserve: data.selected_reserve_pub, amount: !data.amount ? undefined : Amounts.parseOrThrow(data.amount), }} - onAborted={() => { - notifyInfo(i18n.str`Operation aborted`); - onOperationAborted(); - }} /> ); } diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -18,7 +18,7 @@ import { TalerCorebankApi, TalerError, TalerErrorCode, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, @@ -28,7 +28,6 @@ import { Loading, LocalNotificationBanner, RouteDefinition, - makeSafeCall, notifyInfo, useBankCoreApiContext, useChallengeHandler, @@ -71,7 +70,6 @@ export function ShowAccountDetails({ account: string; }): VNode { const { i18n } = useTranslationContext(); - const [preferences] = usePreferences(); const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; const { @@ -85,7 +83,7 @@ export function ShowAccountDetails({ const [submitAccount, setSubmitAccount] = useState< TalerCorebankApi.AccountReconfiguration | undefined >(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); @@ -111,53 +109,54 @@ export function ShowAccountDetails({ } } - const [doUpdate, repeatUpdate] = mfa.withMfaHandler( - ({ ids: challengeIds, onChallengeRequired }) => - makeSafeCall( - i18n, - (creds: LoggedIn, account: TalerCorebankApi.AccountReconfiguration) => - bank.updateAccount(creds, account, { challengeIds }), - (success) => { - notifyInfo(i18n.str`Account updated`); - onUpdateSuccess(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Unauthorized: - return i18n.str`The rights to change the account are not sufficient`; - case HttpStatusCode.NotFound: - return i18n.str`The username was not found`; - case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME: - return i18n.str`You can't change the legal name, please contact the your account administrator.`; - case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: - return i18n.str`You can't change the debt limit, please contact the your account administrator.`; - case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT: - return i18n.str`You can't change the cashout address, please contact the your account administrator.`; - case TalerErrorCode.BANK_MISSING_TAN_INFO: - return i18n.str`No information for the selected authentication channel.`; - case HttpStatusCode.Accepted: { - onChallengeRequired(fail.body); - return i18n.str`A second factor authentication is required.`; - } - case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: - return i18n.str`Authentication channel is not supported.`; - case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: - return i18n.str`Only the administrator can change the conversion rate.`; - case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: - return i18n.str`The conversion rate class doesn't exist.`; - case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: - return i18n.str`The password is too short. Can't have less than 8 characters.`; - case TalerErrorCode.BANK_PASSWORD_TOO_LONG: - return i18n.str`The password is too long. Can't have more than 64 characters.`; - } - }, - ), + const update = safeFunctionHandler( + ( + creds: LoggedIn, + account: TalerCorebankApi.AccountReconfiguration, + challengeIds: string[], + ) => bank.updateAccount(creds, account, { challengeIds }), + !creds || !submitAccount ? undefined : [creds, submitAccount, []], ); - const updateHandler = - !creds || !submitAccount - ? undefined - : () => notifyOnError(doUpdate)(creds, submitAccount); + update.onSuccess = (success) => { + notifyInfo(i18n.str`Account updated`); + onUpdateSuccess(); + }; + + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`The rights to change the account are not sufficient`; + case HttpStatusCode.NotFound: + return i18n.str`The username was not found`; + case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME: + return i18n.str`You can't change the legal name, please contact the your account administrator.`; + case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: + return i18n.str`You can't change the debt limit, please contact the your account administrator.`; + case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT: + return i18n.str`You can't change the cashout address, please contact the your account administrator.`; + case TalerErrorCode.BANK_MISSING_TAN_INFO: + return i18n.str`No information for the selected authentication channel.`; + case HttpStatusCode.Accepted: { + mfa.onChallengeRequired(fail.body); + return i18n.str`A second factor authentication is required.`; + } + case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: + return i18n.str`Authentication channel is not supported.`; + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: + return i18n.str`Only the administrator can change the conversion rate.`; + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: + return i18n.str`The conversion rate class doesn't exist.`; + case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: + return i18n.str`The password is too short. Can't have less than 8 characters.`; + case TalerErrorCode.BANK_PASSWORD_TOO_LONG: + return i18n.str`The password is too long. Can't have more than 64 characters.`; + } + }; + + const repeatUpdate = update.lambda((ids: string[]) => { + return [update.args![0], update.args![1], []]; + }); const url = bank.getRevenueAPI(account); const baseURL = url.href; @@ -167,7 +166,7 @@ export function ShowAccountDetails({ const ac = Paytos.fromString(result.body.payto_uri); const payto = ac.type === "fail" || !ac.body.targetType ? undefined : ac.body; - if (mfa.pendingChallenge && repeatUpdate) { + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} @@ -238,8 +237,7 @@ export function ShowAccountDetails({ type="submit" name="update" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!submitAccount} - onClick={updateHandler} + onClick={update} > <i18n.Translate>Update</i18n.Translate> </ButtonBetter> diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -20,7 +20,6 @@ import { RouteDefinition, ShowInputErrorLabel, notifyInfo, - safeFunctionHandler, useBankCoreApiContext, useChallengeHandler, useLocalNotificationBetter, @@ -91,9 +90,7 @@ export function UpdateAccountPassword({ ? i18n.str`Repeated password doesn't match` : undefined, }); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = - useLocalNotificationBetter(); - + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const update = safeFunctionHandler( @@ -117,12 +114,11 @@ export function UpdateAccountPassword({ ], ); - update.onUnexpectedFailure = defaultUnexpectedFailureMessages; update.onSuccess = (success) => { notifyInfo(i18n.str`Password changed`); onUpdateSuccess(); }; - update.onFail = saveNotification((fail) => { + update.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Not authorized to change the password, maybe the session is invalid.`; @@ -143,7 +139,7 @@ export function UpdateAccountPassword({ case TalerErrorCode.BANK_PASSWORD_TOO_LONG: return i18n.str`The password is too long. Can't have more than 64 characters.`; } - }); + }; const repeatUpdate = update.lambda((ids: string[]) => { return [update.args![0], update.args![1], ids]; }); diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx @@ -11,7 +11,7 @@ A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + GNU Taler; see the file COPYING. If not, see <http: */ import { AbsoluteTime, @@ -23,16 +23,17 @@ import { } from "@gnu-taler/taler-util"; import { Attention, + ButtonBetter, LocalNotificationBanner, + RouteDefinition, notifyInfo, - useLocalNotification, + useBankCoreApiContext, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useSessionState } from "../../hooks/session.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { AccountForm } from "./AccountForm.js"; const TALER_SCREEN_ID = 123; @@ -55,142 +56,52 @@ export function CreateNewAccount({ const [submitAccount, setSubmitAccount] = useState< TalerCorebankApi.RegisterAccountRequest | undefined >(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; - async function doCreate() { - if (!submitAccount || !token) return; - await handleError(async () => { - const resp = await api.createAccount(token, submitAccount); - if (resp.type === "ok") { - notifyInfo( - i18n.str`Account created with password "${submitAccount.password}".`, - ); - onCreateSuccess(); - } else { - switch (resp.case) { - case HttpStatusCode.BadRequest: - return notify({ - type: "error", - title: i18n.str`Server replied that phone or email is invalid`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case HttpStatusCode.Unauthorized: - return notify({ - type: "error", - title: i18n.str`The rights to perform the operation are not sufficient`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: - return notify({ - type: "error", - title: i18n.str`Account username is already taken`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: - return notify({ - type: "error", - title: i18n.str`Account ID is already taken`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_UNALLOWED_DEBIT: - return notify({ - type: "error", - title: i18n.str`Bank ran out of bonus credit.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: - return notify({ - type: "error", - title: i18n.str`Account username can't be used because is reserved`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: - return notify({ - type: "error", - title: i18n.str`Only an administrator is allowed to set the debt limit.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_MISSING_TAN_INFO: - return notify({ - type: "error", - title: i18n.str`No information for the selected authentication channel.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: - return notify({ - type: "error", - title: i18n.str`Authentication channel is not supported.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: - return notify({ - type: "error", - title: i18n.str`Only admin can create accounts with second factor authentication.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: { - return notify({ - type: "error", - title: i18n.str`Only the administrator can change the conversion rate.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - } - case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: { - return notify({ - type: "error", - title: i18n.str`The conversion rate class doesn't exist.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - } - case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: { - return notify({ - type: "error", - title: i18n.str`The password is too short. Can't have less than 8 characters.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - } - case TalerErrorCode.BANK_PASSWORD_TOO_LONG: { - return notify({ - type: "error", - title: i18n.str`The password is too long. Can't have more than 64 characters.`, - description: resp.detail?.hint as TranslatedString, - debug: resp.detail, - when: AbsoluteTime.now(), - }); - } - default: - assertUnreachable(resp); - } - } - }); - } + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const create = safeFunctionHandler( + api.createAccount, + !submitAccount || !token ? undefined : [token, submitAccount], + ); + create.onSuccess = (success, token, account) => { + notifyInfo(i18n.str`Account created with password "${account.password}".`); + onCreateSuccess(); + }; + + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + return i18n.str`Server replied that phone or email is invalid`; + case HttpStatusCode.Unauthorized: + return i18n.str`The rights to perform the operation are not sufficient`; + case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: + return i18n.str`Account username is already taken`; + case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: + return i18n.str`Account ID is already taken`; + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + return i18n.str`Bank ran out of bonus credit.`; + case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: + return i18n.str`Account username can't be used because is reserved`; + case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: + return i18n.str`Only an administrator is allowed to set the debt limit.`; + case TalerErrorCode.BANK_MISSING_TAN_INFO: + return i18n.str`No information for the selected authentication channel.`; + case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: + return i18n.str`Authentication channel is not supported.`; + case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: + return i18n.str`Only admin can create accounts with second factor authentication.`; + case TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS: + return i18n.str`Only the administrator can change the conversion rate.`; + case TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN: + return i18n.str`The conversion rate class doesn't exist.`; + case TalerErrorCode.BANK_PASSWORD_TOO_SHORT: + return i18n.str`The password is too short. Can't have less than 8 characters.`; + case TalerErrorCode.BANK_PASSWORD_TOO_LONG: + return i18n.str`The password is too long. Can't have more than 64 characters.`; + default: + assertUnreachable(fail); + } + }; if (!(credentials.status === "loggedIn" && credentials.isUserAdministrator)) { return ( @@ -237,18 +148,14 @@ export function CreateNewAccount({ > <i18n.Translate>Cancel</i18n.Translate> </a> - <button + <ButtonBetter type="submit" name="create" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={!submitAccount} - onClick={(e) => { - e.preventDefault(); - doCreate(); - }} + onClick={create} > <i18n.Translate>Create</i18n.Translate> - </button> + </ButtonBetter> </div> </AccountForm> </div> diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx @@ -17,21 +17,23 @@ import { AccessToken, AmountString, + OperationOk, TalerCoreBankHttpClient, TalerCorebankApi, - TalerError, + opFixedSuccess } from "@gnu-taler/taler-util"; import { Attention, + ButtonBetter, LocalNotificationBanner, - useLocalNotification, - useTranslationContext, + RouteDefinition, + useBankCoreApiContext, + useLocalNotificationBetter, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { useBankCoreApiContext } from "@gnu-taler/web-util/browser"; import { useSessionState } from "../../hooks/session.js"; -import { RouteDefinition } from "@gnu-taler/web-util/browser"; import { getTimeframesForDate } from "./AdminHome.js"; const TALER_SCREEN_ID = 124; @@ -77,11 +79,30 @@ export function DownloadStats({ routeCancel }: Props): VNode { const [lastStep, setLastStep] = useState<{ step: number; total: number }>(); const [downloaded, setDownloaded] = useState<string>(); const referenceDates = [new Date()]; - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const download = safeFunctionHandler( + async (token) => { + setDownloaded(undefined); + return fetchAllStatus( + api, + token, + options, + referenceDates, + (step, total) => { + setLastStep({ step, total }); + }, + ); + }, + lastStep !== undefined || !creds ? undefined : [creds.token], + ); + download.onSuccess = (success) => { + setDownloaded(success.body); + setLastStep(undefined); + }; if (!creds) { - return <div>only admin can download stats</div>; + return <i18n.Translate>only admin can download stats</i18n.Translate>; } return ( @@ -353,30 +374,14 @@ export function DownloadStats({ routeCancel }: Props): VNode { > <i18n.Translate>Cancel</i18n.Translate> </a> - <button + <ButtonBetter type="submit" name="download" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - disabled={lastStep !== undefined} - onClick={async () => { - setDownloaded(undefined); - await handleError(async () => { - const csv = await fetchAllStatus( - api, - creds.token, - options, - referenceDates, - (step, total) => { - setLastStep({ step, total }); - }, - ); - setDownloaded(csv); - }); - setLastStep(undefined); - }} + onClick={download} > <i18n.Translate>Download</i18n.Translate> - </button> + </ButtonBetter> </div> </form> </div> @@ -427,7 +432,7 @@ async function fetchAllStatus( options: Options, references: Date[], progress: (current: number, total: number) => void, -): Promise<string> { +): Promise<OperationOk<string>> { const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = []; if (options.hourMetric) { allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour); @@ -544,7 +549,7 @@ async function fetchAllStatus( return acc + row.join(",") + "\n"; }, ""); - return csv; + return opFixedSuccess(csv); } type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">; diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx @@ -28,7 +28,6 @@ import { LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, - makeSafeCall, notifyInfo, useBankCoreApiContext, useChallengeHandler, @@ -44,6 +43,7 @@ import { undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; import { doAutoFocus } from "../PaytoWireTransferForm.js"; import { SolveMFAChallenges } from "../SolveMFA.js"; +import { UserAndToken } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 125; @@ -69,7 +69,7 @@ export function RemoveAccount({ const { lib: { bank: api }, } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); @@ -97,7 +97,7 @@ export function RemoveAccount({ const balance = Amounts.parse(result.body.balance.amount); if (!balance) { - return <div>there was an error reading the balance</div>; + return <i18n.Translate>there was an error reading the balance</i18n.Translate> } const isBalanceEmpty = Amounts.isZero(balance); if (!isBalanceEmpty) { @@ -130,45 +130,47 @@ export function RemoveAccount({ : undefined, }); - const [doDelete, repeatDelete] = - !token || !!errors - ? [undefined, undefined] - : mfa.withMfaHandler(({ ids: challengeIds, onChallengeRequired }) => - makeSafeCall( - i18n, - () => - api.deleteAccount({ username: account, token }, { challengeIds }), - (success) => { - notifyInfo(i18n.str`Account removed`); - onUpdateSuccess(); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Unauthorized: - return i18n.str`No enough permission to delete the account.`; - case HttpStatusCode.NotFound: - return i18n.str`The username was not found.`; - case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: - return i18n.str`Can't delete a reserved username.`; - case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO: - return i18n.str`Can't delete an account with balance different than zero.`; - case HttpStatusCode.Accepted: { - onChallengeRequired(fail.body); - return i18n.str`A second factor authentication is required.`; - } - } - }, - ), - ); + const deleteAccount = safeFunctionHandler( + (auth: UserAndToken, challengeIds: string[]) => + api.deleteAccount(auth, { challengeIds }), + !!errors || !token ? undefined : [{ username: account, token }, []], + ); + + deleteAccount.onSuccess = (success) => { + notifyInfo(i18n.str`Account removed`); + onUpdateSuccess(); + }; + + deleteAccount.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`No enough permission to delete the account.`; + case HttpStatusCode.NotFound: + return i18n.str`The username was not found.`; + case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: + return i18n.str`Can't delete a reserved username.`; + case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO: + return i18n.str`Can't delete an account with balance different than zero.`; + case HttpStatusCode.Accepted: { + mfa.onChallengeRequired(fail.body); + return i18n.str`A second factor authentication is required.`; + } + } + }; + + const retryDeleteAccount = deleteAccount.lambda((ids: string[]) => [ + deleteAccount.args![0], + ids, + ]); - if (mfa.pendingChallenge && repeatDelete) { + if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} description={i18n.str`Remove account.`} username={account} onCancel={mfa.doCancelChallenge} - onCompleted={repeatDelete} + onCompleted={retryDeleteAccount} /> ); } @@ -249,7 +251,7 @@ export function RemoveAccount({ type="submit" name="delete" class="disabled:opacity-50 disabled:cursor-default cursor-pointer 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-600" - onClick={!doDelete ? undefined : notifyOnError(doDelete)} + onClick={deleteAccount} > <i18n.Translate>Delete</i18n.Translate> </ButtonBetter> diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx @@ -33,7 +33,7 @@ import { RouteDefinition, ShowInputErrorLabel, useBankCoreApiContext, - useLocalNotification, + useLocalNotificationBetter, useTranslationContext, utils, } from "@gnu-taler/web-util/browser"; @@ -128,8 +128,7 @@ function useComponentState({ lib: { conversion }, } = useBankCoreApiContext(); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); -; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const initalState: FormValues<FormType> = { amount: "100", @@ -202,7 +201,7 @@ function useComponentState({ : respCashout.case === HttpStatusCode.Conflict ? ("amount-is-too-small" as const) : undefined; - + if (!cashout) { setCalc(undefined); // silent failure return; @@ -229,6 +228,9 @@ function useComponentState({ calculationResult?.cashout === "amount-is-too-small" ? undefined : calculationResult?.cashout; + + + async function doUpdate() { if (!creds) return; await handleError(async () => { diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -34,8 +34,6 @@ import { RouteDefinition, ShowInputErrorLabel, notifyInfo, - repeatLastCall, - safeFunctionHandler, useBankCoreApiContext, useChallengeHandler, useLocalNotificationBetter, @@ -44,6 +42,11 @@ import { import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { + Paytos, + TalerCorebankApi, + opFixedSuccess, +} from "@gnu-taler/taler-util"; import { useAccountDetails } from "../../hooks/account.js"; import { TransCalc, @@ -61,9 +64,6 @@ import { doAutoFocus, } from "../PaytoWireTransferForm.js"; import { SolveMFAChallenges } from "../SolveMFA.js"; -import { TalerCorebankApi } from "@gnu-taler/taler-util"; -import { Paytos } from "@gnu-taler/taler-util"; -import { opFixedSuccess } from "@gnu-taler/taler-util"; const TALER_SCREEN_ID = 127; @@ -223,7 +223,7 @@ function CreateCashoutInternal({ estimateByDebit: calculateFromDebit, } = useCashoutEstimatorByUser(accountData.name); const [form, setForm] = useState<Partial<FormType>>({ isDebit: true }); - const [notification, saveNotification, defaultUnexpectedFailureMessages] = useLocalNotificationBetter(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const { i18n } = useTranslationContext(); @@ -279,9 +279,9 @@ function CreateCashoutInternal({ return opFixedSuccess(zeroCalc); } }, + [form.isDebit ?? false, inputAmount, sellFee], ); conversionCalculator.onSuccess = (success) => setCalculation(success.body); - conversionCalculator.args = [form.isDebit ?? false, inputAmount, sellFee]; conversionCalculator.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Conflict: @@ -294,10 +294,7 @@ function CreateCashoutInternal({ }; useEffect(() => { - async function doAsync() { - await conversionCalculator.call(); - } - doAsync(); + conversionCalculator.call(); }, [form.amount, form.isDebit, notZero, higerThanMin, rate.cashout_fee]); const calc = @@ -338,28 +335,27 @@ function CreateCashoutInternal({ }); const trimmedAmountStr = form.amount?.trim(); - const challengeIds = mfa.pendingChallenge?.challenges.map( - (d) => d.challenge_id, - ); const subject = form.subject; - const apiCashout = safeFunctionHandler((calc: TransCalc, subject: string) => - api.createCashout( - session, - { - request_uid: RANDOM_STRING, - amount_credit: Amounts.stringify(calc.credit), - amount_debit: Amounts.stringify(calc.debit), - subject, - }, - { challengeIds }, - ), + const cashout = safeFunctionHandler( + (calc: TransCalc, subject: string, challengeIds: string[]) => + api.createCashout( + session, + { + request_uid: RANDOM_STRING, + amount_credit: Amounts.stringify(calc.credit), + amount_debit: Amounts.stringify(calc.debit), + subject, + }, + { challengeIds }, + ), + !!errors || !subject ? undefined : [calc, subject, []], ); - apiCashout.onSuccess = (success) => { + cashout.onSuccess = (success) => { notifyInfo(i18n.str`Cashout created`); onCashout(); }; - apiCashout.onFail = (fail) => { + cashout.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: { mfa.onChallengeRequired(fail.body); @@ -387,10 +383,11 @@ function CreateCashoutInternal({ } }; - apiCashout.args = !errors && subject ? [calc, subject] : undefined; - - const cashoutHandler = notifyOnError(apiCashout); - const repeatCashout = repeatLastCall(apiCashout); + const retryCashout = cashout.lambda((ids: string[]) => [ + cashout.args![0], + cashout.args![1], + ids, + ]); const cashoutDisabled = !accountData.cashout_payto_uri; @@ -414,7 +411,7 @@ function CreateCashoutInternal({ username={accountData.name} description={i18n.str`Create cashout.`} onCancel={mfa.doCancelChallenge} - onCompleted={repeatCashout} + onCompleted={retryCashout} /> ); } @@ -745,7 +742,7 @@ function CreateCashoutInternal({ type="submit" name="cashout" class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - onClick={cashoutHandler} + onClick={cashout} > <i18n.Translate>Cashout</i18n.Translate> </ButtonBetter> diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx @@ -69,7 +69,7 @@ export function Button({ } -type PropsBetter = Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> & { +type PropsBetter = Omit<Omit<HTMLAttributes<HTMLButtonElement>, "onClick">,"disabled"> & { onClick: SafeHandlerTemplate<any, any> | undefined } /** @@ -79,7 +79,6 @@ type PropsBetter = Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> & { */ export function ButtonBetter({ children, - disabled, onClick, ...rest }: PropsBetter): VNode { @@ -87,7 +86,7 @@ export function ButtonBetter({ return ( <button {...rest} - disabled={disabled || running || !onClick || !onClick.args} + disabled={running || !onClick || !onClick.args} onClick={(e) => { e.preventDefault(); if (!onClick || !onClick.args) { diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts @@ -342,6 +342,7 @@ export interface SafeHandlerTemplate<Args extends any[], Errors> { */ lambda<OtherArgs extends any[]>( e: (...d: OtherArgs) => Args, + init?: OtherArgs ): SafeHandlerTemplate<OtherArgs, Error>; /** * creates another handler with new arguements