taler-typescript-core

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

commit 5d5ab36686029d4876fbc24171503d606273a767
parent 9f8c7effba512afb932a2976c28e166ceabbf03d
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 16 Oct 2025 08:18:17 -0300

wip: fix error handling

Diffstat:
Mpackages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts | 7-------
Mpackages/web-util/src/hooks/index.ts | 2+-
Mpackages/web-util/src/hooks/useChallenge.ts | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mpackages/web-util/src/hooks/useNotifications.ts | 97++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
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;