commit b8df370f71a002b50adf38820a04cbf813a71515
parent 70cb3f785671fac02b01e9b3aff1fccc4cfcfb55
Author: Sebastian <sebasjm@gmail.com>
Date: Thu, 9 Oct 2025 13:35:57 -0300
fixes #10250 support new info
Diffstat:
4 files changed, 58 insertions(+), 14 deletions(-)
diff --git a/packages/bank-ui/src/pages/SolveMFA.tsx b/packages/bank-ui/src/pages/SolveMFA.tsx
@@ -12,6 +12,7 @@ import {
makeSafeCall,
NotificationMessage,
ShowInputErrorLabel,
+ Time,
undefinedIfEmpty,
useBankCoreApiContext,
useLocalNotificationBetter,
@@ -21,6 +22,7 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useSessionState } from "../hooks/session.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
export interface Props {
onCompleted(challenges: string[]): Promise<NotificationMessage | undefined>;
@@ -66,6 +68,8 @@ function SolveChallenge({
},
(resp) => {
switch (resp.case) {
+ case TalerErrorCode.BANK_TRANSACTION_NOT_FOUND:
+ return i18n.str`Unknown challenge.`;
case HttpStatusCode.Unauthorized:
return i18n.str`Failed to validate the verification code.`;
case HttpStatusCode.TooManyRequests:
@@ -199,6 +203,14 @@ export function SolveMFAChallenges({
const {
lib: { bank: api },
} = useBankCoreApiContext();
+ // FIXME: we should save here also the expiration of the
+ // tan channel to be used when the user press "i have the code"
+ const [retransmission, setRetransmission] = useState<
+ Record<TanChannel, AbsoluteTime>
+ >({
+ email: AbsoluteTime.now(),
+ sms: AbsoluteTime.now(),
+ });
if (selected) {
return (
@@ -226,6 +238,15 @@ export function SolveMFAChallenges({
i18n,
(user: string, ch: Challenge) => api.sendChallenge(user, ch.challenge_id),
(success, user, ch) => {
+ if (success.body.earliest_retransmission) {
+ setRetransmission({
+ ...retransmission,
+ [ch.tan_channel]: AbsoluteTime.now(),
+ // AbsoluteTime.fromProtocolTimestamp(
+ // success.body.earliest_retransmission,
+ // ),
+ });
+ }
setSelected(ch);
},
(fail) => {
@@ -233,13 +254,13 @@ export function SolveMFAChallenges({
case HttpStatusCode.Unauthorized:
return i18n.str`Failed to send the verification code.`;
case HttpStatusCode.Forbidden:
- return i18n.str`Failed to send the verification code.`;
+ return i18n.str`The request was valid, but the server is refusing action.`;
case HttpStatusCode.NotFound:
- return i18n.str`Failed to send the verification code.`;
+ return i18n.str`The backend is not aware of the specified MFA challenge.`;
case HttpStatusCode.TooManyRequests:
- return i18n.str`Failed to send the verification code.`;
+ return i18n.str`It is too early to request another transmission of the challenge.`;
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
- return i18n.str`Failed to send the verification code.`;
+ return i18n.str`Code transmission failed.`;
}
},
),
@@ -307,6 +328,10 @@ export function SolveMFAChallenges({
</span>
</h2>
{currentChallenge.challenges.map((challenge) => {
+ const time = AbsoluteTime.now();
+ const alreadySent = AbsoluteTime.isExpired(time);
+ // const time = retransmission[challenge.tan_channel];
+ // const alreadySent = !AbsoluteTime.isExpired(time);
const noNeedToComplete =
hasSolvedEnough ||
solved.indexOf(challenge.challenge_id) !== -1;
@@ -361,12 +386,21 @@ export function SolveMFAChallenges({
type="submit"
name="send again"
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={doSend}
+ onClick={alreadySent ? undefined : doSend}
>
<i18n.Translate>Send me a message</i18n.Translate>
</ButtonBetter>
</div>
</dd>
+ {alreadySent && time.t_ms !== "never" ? (
+ <p class="text-sm text-gray-600">
+ <i18n.Translate>
+ You have to wait until{" "}
+ <Time format="HH:mm" timestamp={time} /> to send a
+ new code.
+ </i18n.Translate>
+ </p>
+ ) : undefined}
</div>
</dl>
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx
@@ -307,7 +307,7 @@ export function SolveMFAChallenges({
}
case HttpStatusCode.Forbidden: {
setNotif({
- message: i18n.str`Failed to send the verification code.`,
+ message: i18n.str`The request was valid, but the server is refusing action.`,
type: "ERROR",
description: resp.detail?.hint,
});
@@ -315,7 +315,7 @@ export function SolveMFAChallenges({
}
case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN: {
setNotif({
- message: i18n.str`Failed to send the verification code.`,
+ message: i18n.str`The backend is not aware of the specified MFA challenge.`,
type: "ERROR",
description: resp.detail?.hint,
});
@@ -323,7 +323,7 @@ export function SolveMFAChallenges({
}
case TalerErrorCode.MERCHANT_TAN_MFA_HELPER_EXEC_FAILED: {
setNotif({
- message: i18n.str`Failed to send the verification code.`,
+ message: i18n.str`The backend failed to launch a helper process required for the multi-factor authentication step.`,
type: "ERROR",
description: resp.detail?.hint,
});
@@ -331,7 +331,7 @@ export function SolveMFAChallenges({
}
case TalerErrorCode.MERCHANT_TAN_CHALLENGE_SOLVED: {
setNotif({
- message: i18n.str`Failed to send the verification code.`,
+ message: i18n.str`The challenge was already solved.`,
type: "ERROR",
description: resp.detail?.hint,
});
@@ -339,7 +339,7 @@ export function SolveMFAChallenges({
}
case TalerErrorCode.MERCHANT_TAN_TOO_EARLY: {
setNotif({
- message: i18n.str`Failed to send the verification code.`,
+ message: i18n.str`It is too early to request another transmission of the challenge.`,
type: "ERROR",
description: resp.detail?.hint,
});
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
@@ -32,6 +32,8 @@ import {
codecForTalerCommonConfigResponse,
codecForTokenInfoList,
codecForTokenSuccessResponse,
+ codecOptional,
+ codecOptionalDefault,
opKnownAlternativeHttpFailure,
opKnownHttpFailure,
opKnownTalerFailure,
@@ -82,7 +84,9 @@ import {
codecForWithdrawalPublicInfo,
} from "../types-taler-corebank.js";
import {
+ ChallengeRequestResponse,
ChallengeSolveRequest,
+ codecForChallengeRequestResponse,
codecForChallengeResponse,
} from "../types-taler-merchant.js";
import {
@@ -1296,8 +1300,10 @@ export class TalerCoreBankHttpClient {
method: "POST",
});
switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeRequestResponse());
case HttpStatusCode.NoContent:
- return opEmptySuccess();
+ return opFixedSuccess<ChallengeRequestResponse>({});
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden:
@@ -1347,6 +1353,8 @@ export class TalerCoreBankHttpClient {
switch (details.code) {
case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
+ return opKnownTalerFailure(details.code, details);
default:
return opUnknownHttpFailure(resp, details);
}
@@ -1354,7 +1362,7 @@ export class TalerCoreBankHttpClient {
case HttpStatusCode.NotFound: {
const details = await readTalerErrorResponse(resp);
switch (details.code) {
- case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
+ case TalerErrorCode.BANK_TRANSACTION_NOT_FOUND:
return opKnownTalerFailure(details.code, details);
case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
return opKnownTalerFailure(details.code, details);
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -17,6 +17,7 @@
import {
AccessToken,
CancellationToken,
+ ChallengeRequestResponse,
ChallengeSolveRequest,
FailCasesByMethod,
HttpStatusCode,
@@ -71,6 +72,7 @@ import {
codecForWalletTemplateDetails,
codecForWebhookDetails,
codecForWebhookSummaryResponse,
+ codecOptionalDefault,
opEmptySuccess,
opFixedSuccess,
opKnownAlternativeHttpFailure,
@@ -2529,10 +2531,10 @@ export class TalerMerchantInstanceHttpClient {
body: {},
});
switch (resp.status) {
- case HttpStatusCode.NoContent:
- return opSuccessFromHttp(resp, codecForChallengeRequestResponse());
case HttpStatusCode.Ok:
return opSuccessFromHttp(resp, codecForChallengeRequestResponse());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess<ChallengeRequestResponse>({});
case HttpStatusCode.Unauthorized:
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.Forbidden: