commit 5d5ab36686029d4876fbc24171503d606273a767
parent 9f8c7effba512afb932a2976c28e166ceabbf03d
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 16 Oct 2025 08:18:17 -0300
wip: fix error handling
Diffstat:
5 files changed, 212 insertions(+), 16 deletions(-)
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -29,6 +29,8 @@ import {
LocalNotificationBanner,
makeSafeCall,
notifyInfo,
+ MfaHandler,
+ safeFunctionCall,
useBankCoreApiContext,
useChallengeHandler,
useLocalNotificationBetter,
@@ -83,6 +85,54 @@ export function WithdrawalConfirmationQuestion({
? Amounts.zeroOfCurrency(config.currency)
: Amounts.parseOrThrow(config.wire_transfer_fees);
+ /**
+ * Continue here:
+ *
+ * Make the useNotification hook take "handler|undefine"
+ * to cover the most common use case.
+ *
+ * Usenotification hook also should take the new SafeHandler interface
+ *
+ * safeFunctionCall should also include the default error handler that shows
+ * the generic erros (talererror and http errors)
+ */
+ // const fn = safeFunctionCall(
+ // (mfa: MfaHandler, creds: LoggedIn, opId: string) =>
+ // api.confirmWithdrawalById(creds, {}, opId, mfa),
+ // );
+
+ // fn.onSuccess = () => {
+ // mutate(() => true); // clean any info that we have
+ // if (!settings.showWithdrawalSuccess) {
+ // notifyInfo(i18n.str`Wire transfer completed!`);
+ // }
+ // };
+
+ // fn.onFail = (fail, mfa, a, b) => {
+ // 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 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.`;
+ // case HttpStatusCode.Accepted: {
+ // mfa.onChallengeRequired(fail.body);
+ // return i18n.str`A second factor authentication is required.`;
+ // }
+ // }
+ // };
+
+ // const [doConfirm2, repeatConfirm2] = mfa.addMfaHandler( fn )
+
const [doConfirm, repeatConfirm] = mfa.withMfaHandler(
({ challengeIds, onChallengeRequired }) =>
makeSafeCall(
@@ -192,7 +242,7 @@ export function WithdrawalConfirmationQuestion({
<dl class="divide-y divide-gray-100">
{((): VNode => {
switch (details.account.targetType) {
- case undefined:
+ case undefined:
case PaytoType.TalerReserveHttp:
case PaytoType.TalerReserve: {
// FIXME: support wire transfer to wallet
diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts
@@ -35,13 +35,6 @@ export function useComponentState({
const { i18n } = useTranslationContext();
const { safely } = useAlertContext();
- // const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined;
- // const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam);
- // const currency = parsedAmount ? parsedAmount.currency : amountParam;
-
- // const initialAmount =
- // parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined);
-
const [newOrder, setNewOrder] = useState("");
const hook = useAsyncAsHook(async () => {
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
@@ -17,7 +17,7 @@ export {
useFormMeta,
} from "./useForm.js";
export { useLang } from "./useLang.js";
-export { useChallengeHandler } from "./useChallenge.js";
+export { useChallengeHandler, MfaHandler, MfaState } from "./useChallenge.js";
export {
buildStorageKey,
StorageKey,
diff --git a/packages/web-util/src/hooks/useChallenge.ts b/packages/web-util/src/hooks/useChallenge.ts
@@ -26,7 +26,7 @@ import { useCallback, useRef, useState } from "preact/hooks";
* the state and retry.
*
*/
-interface MfaState {
+export interface MfaState {
/**
* If a mfa has been started this will contain
* the challenge response.
@@ -40,6 +40,31 @@ interface MfaState {
*/
doCancelChallenge: () => void;
+ /**
+ * Similar to withMfaHandler.
+ * Take a function that expects an MfaHanlder on the first argument.
+ * Returns the original function for the first call, another to repeat with the new credentials
+ * and the list of pending challenges.
+ * @param d
+ * @returns
+ */
+ addMfaHandler: <Type extends Array<any>, R>(
+ fn: (mfa: MfaHandler, ...args: Type) => Promise<R>,
+ ) => [
+ (...args: Type) => Promise<R>, // function for the first call
+ (newChallenges: string[]) => Promise<R>, // function to repeat with new chIds
+ Type,
+ ];
+
+ /**
+ * Similar to addMfaHandler.
+ * Takes a function that receive an MFA handler all execute business logic that
+ * may require a MFA.
+ * Returns the original function for the first call, another to repeat with the new credentials
+ * and the list of pending challenges.
+ * @param builder
+ * @returns
+ */
withMfaHandler: <Type extends Array<any>, R>(
builder: CallbackFactory<Type, R>,
) => [
@@ -53,7 +78,7 @@ interface MfaState {
* Handler to be used by the function performing the MFA
* guarded operation
*/
-interface MfaHandler {
+export interface MfaHandler {
/**
* Callback handler to use when the operation fails with MFA required
* @param challenge
@@ -71,7 +96,7 @@ interface MfaHandler {
/**
* asd
*/
-type CallbackFactory<T extends any[] ,R> = (
+type CallbackFactory<T extends any[], R> = (
h: MfaHandler,
) => (...args: T) => Promise<R>;
@@ -90,15 +115,15 @@ export function useChallengeHandler(): MfaState {
/**
* This have the same machanism that useEffect, needs to be called always on order
- * @param builder
- * @returns
+ * @param builder
+ * @returns
*/
function withMfaHandler<T extends any[], R>(
builder: CallbackFactory<T, R>,
): [
ReturnType<CallbackFactory<T, R>>,
(newChallenges: string[]) => Promise<R>,
- T
+ T,
] {
const thisIdx = exeOrder;
exeOrder = exeOrder + 1;
@@ -124,12 +149,45 @@ export function useChallengeHandler(): MfaState {
return [saveArgsAndProceed, repeatCall, ref.current[thisIdx]];
}
+ function addMfaHandler<T extends any[], R>(
+ fn: (mfa: MfaHandler, ...args: T) => Promise<R>,
+ ): [
+ ReturnType<CallbackFactory<T, R>>,
+ (newChallenges: string[]) => Promise<R>,
+ T,
+ ] {
+ const thisIdx = exeOrder;
+ exeOrder = exeOrder + 1;
+
+ async function saveArgsAndProceed(...currentArgs: T): Promise<R> {
+ ref.current[thisIdx] = currentArgs;
+ const mfa = {
+ challengeIds: undefined,
+ onChallengeRequired,
+ };
+ return fn(mfa, ...currentArgs);
+ }
+
+ async function repeatCall(challengeIds: string[]): Promise<R> {
+ if (!ref.current[thisIdx])
+ throw Error("calling repeat function without doing the first call");
+ const mfa = {
+ challengeIds,
+ onChallengeRequired,
+ };
+ return fn(mfa, ...ref.current[thisIdx]);
+ }
+
+ return [saveArgsAndProceed, repeatCall, ref.current[thisIdx]];
+ }
+
function reset() {
onChallengeRequired(undefined);
}
return {
withMfaHandler,
+ addMfaHandler,
doCancelChallenge: reset,
pendingChallenge: current,
};
diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts
@@ -546,7 +546,7 @@ export function makeSafeCall<
doAction: (...args: K) => Promise<T>,
onOperationSuccess: OnOperationSuccesReturnType<T, K>,
onOperationFail?: OnOperationFailReturnType<T, K>,
-): (...args: K) => Promise<NotificationMessage | undefined> {
+): FunctionThatMayFail<K> {
return async (...args: K): Promise<NotificationMessage | undefined> => {
try {
const resp = await doAction(...args);
@@ -598,6 +598,96 @@ export function makeSafeCall<
}
};
}
+
+export interface SafeHandler<
+ K extends any[],
+ T extends OperationResult<A, B>,
+ A,
+ B,
+> {
+ (...args: K): Promise<NotificationMessage | undefined>;
+ onSuccess: OnOperationSuccesReturnType<T, K>;
+ onFail: OnOperationFailReturnType<T, K>;
+ onUnexpectedFailure: OnOperationUnexpectedFailReturnType<K>;
+}
+
+/**
+ * Convert an function that return an operation into a function that return
+ * a notification if it fail.
+ *
+ * @returns
+ */
+export function safeFunctionCall<
+ K extends any[],
+ T extends OperationResult<A, B>,
+ A,
+ B,
+>(doAction: (...args: K) => Promise<T>): SafeHandler<K, T, A, B> {
+ const handler = (async (
+ ...args: K
+ ): Promise<NotificationMessage | undefined> => {
+ try {
+ const resp = await doAction(...args);
+ switch (resp.type) {
+ case "ok": {
+ const result: OperationOk<any> = resp;
+ const msg = handler.onSuccess(result as any, ...args);
+ if (msg) {
+ notifyInfo(msg);
+ }
+ return undefined;
+ }
+ case "fail": {
+ const d = "detail" in resp ? resp.detail : undefined;
+ const title = handler.onFail(resp as any, ...args);
+ return {
+ title,
+ type: "error",
+ description: d && d.hint ? (d.hint as TranslatedString) : undefined,
+ debug: d,
+ when: AbsoluteTime.now(),
+ };
+ }
+ default: {
+ assertUnreachable(resp);
+ }
+ }
+ } catch (error: unknown) {
+ // This functions should not throw, this is a problem.
+ console.error(`Error: `, error);
+
+ if (error instanceof TalerError) {
+ return {
+ title: handler.onUnexpectedFailure(error, ...args),
+ type: "error",
+ description:
+ error && error.errorDetail.hint
+ ? (error.errorDetail.hint as TranslatedString)
+ : undefined,
+ debug: error,
+ when: AbsoluteTime.now(),
+ };
+ } else {
+ const description = (
+ error instanceof Error ? error.message : String(error)
+ ) as TranslatedString;
+
+ return {
+ title: `Operation failed` as TranslatedString,
+ type: "error",
+ description,
+ when: AbsoluteTime.now(),
+ };
+ }
+ }
+ }) as SafeHandler<K, T, A, B>;
+ handler.onFail = () => "<unhandled failure>" as TranslatedString;
+ handler.onSuccess = () => {};
+ handler.onUnexpectedFailure = () =>
+ "<unhandled unexpected failure>" as TranslatedString;
+ return handler;
+}
+
export type OnOperationSuccesReturnType<T, K extends any[]> = (
result: T extends OperationOk<any> ? T : never,
...args: K
@@ -609,3 +699,8 @@ export type OnOperationFailReturnType<T, K extends any[]> = (
| (T extends OperationAlternative<any, any> ? T : never),
...args: K
) => TranslatedString;
+
+export type OnOperationUnexpectedFailReturnType<K extends any[]> = (
+ e: TalerError,
+ ...args: K
+) => TranslatedString;