commit c0cf3c03f69a47b3713fdba5eadecae30e3bf22b
parent 4bea2e24c5a587631c4af835c0539ac2d00c52b0
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 22 Oct 2025 12:40:19 -0300
wip
Diffstat:
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