View Issue Details

IDProjectCategoryView StatusLast Update
0012034Talerlibeufin-bank webuipublic2026-11-10 23:42
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityhave not tried
Status newResolutionopen 
Summary0012034: Bank: multi-factor authentication dialog leaves focus on the button behind the overlay
DescriptionWhat happens:
Any MFA-protected action (sign in, send transfer, change password, delete account, confirm withdrawal, cashout) opens the 'Multi-factor authentication required' dialog (non-modal <dialog open>). SolveChallengeDialog passes `focus` to SolveMFAChallenges, but the prop was never used, so nothing in the dialog gets focus. Measured: after clicking 'Send transfer' activeElement stays on BUTTON[name=send] 'Send transfer' behind the grey overlay; after submitting the login form with Enter it stays in INPUT#password, and pressing Enter again re-submits the login instead of continuing. Returning from the code step with 'Back' leaves focus on BODY. Note: the TAN code step itself (`ref={doAutoFocus}` on SolveChallenge's input) does work in a rendered browser: the code input is focused after 'Send me a message', after 'I have a code', and after the automatic send for combi_and challenges. Nothing later in the render defeats it.

Expected:
Focus inside the dialog on the obvious next action, which is 'Send me a message' of the first challenge still to be done. When that code was already sent and the retransmission wait is running (the button is disabled, e.g. after 'Back'), focus 'I have a code' instead.

Where:
  - packages/libeufin-bank-webui/src/pages/SolveMFA.tsx:484
  - packages/libeufin-bank-webui/src/pages/SolveMFA.tsx:585
  - packages/libeufin-bank-webui/src/pages/SolveMFA.tsx:599

Measured in headless Chromium (document.activeElement):
  before: BUTTON[name=send] 'Send transfer' (behind the overlay); login flow: INPUT#password behind the overlay; after 'Back': BODY
  after: BUTTON[name='send again'] 'Send me a message'; after 'Back': BUTTON 'I have a code'; Enter on it -> INPUT#tan-ch-1 (code)

Proposed fix:
SolveMFAChallenges now uses its `focus` prop: the first challenge row not yet done passes `focus` to its AsyncButtons (web-util Button `focus` prop, i.e. doAutoFocus on mount). 'Send me a message' always gets it for that row, and 'I have a code' gets it only while the code is already sent. Because 'Send me a message' does not depend on alreadySent, its ref does not change when the retransmission wait ends, so focus is not taken away from the user later.

Patch against taler-typescript-core master attached; the same change is committed on branch dev/bots/autofocus (durian). Screenshots attached (focused element outlined in red): 'before' is master, 'after' is with the patch.

Verified: tsc: no new errors (the pre-existing BankFrame.tsx/ActiveWithdrawal.test.tsx errors are unrelated). Unit tests 94/94 pass (ActiveWithdrawal.test.tsx excluded because @testing-library/preact is not installed). Chromium via puppeteer: send-transfer, login (keyboard only: type password, Enter, Enter, type code), 2 OR challenges, 2 AND challenges (auto-send -> code input), 'I have a code', and 'Back'. Extra shots: shots/bank--mfa-login--{before,after}.png, shots/bank--mfa-back-to-list--{before,after}.png, shots/bank--mfa-code-entry--{before,after}.png (the code input is focused in both).
TagsNo tags attached.
Attached Files
bank--mfa-challenge-list.diff (2,640 bytes)   
diff --git a/packages/libeufin-bank-webui/src/pages/SolveMFA.tsx b/packages/libeufin-bank-webui/src/pages/SolveMFA.tsx
index b54f6a43d..4fcd390e6 100644
--- a/packages/libeufin-bank-webui/src/pages/SolveMFA.tsx
+++ b/packages/libeufin-bank-webui/src/pages/SolveMFA.tsx
@@ -478,6 +478,10 @@ function SolveMFAChallenges({
   const hasSolvedEnough = currentChallenge.combi_and
     ? currentSolved.length === currentChallenge.challenges.length
     : currentSolved.length > 0;
+  // The dialog opens while keyboard focus is still on the button that
+  // started the operation, behind the overlay, where Enter would submit it
+  // again.  Move focus to the first challenge that still has to be done.
+  let focusPending = !!focus;
 
   return (
     <Fragment>
@@ -543,6 +547,8 @@ function SolveMFAChallenges({
                 solved.indexOf(challenge.challenge_id) !== -1 ||
                 (currentChallenge.combi_and &&
                   nextAndChallenge?.challenge_id !== challenge.challenge_id);
+              const focusHere = focusPending && !noNeedToComplete;
+              if (focusHere) focusPending = false;
 
               return (
                 <div
@@ -576,15 +582,21 @@ function SolveMFAChallenges({
                             name="cancel"
                             class="text-sm font-semibold leading-6 text-onBackground"
                             disabled={noNeedToComplete}
+                            focus={focusHere && alreadySent}
                             onClick={() => selectChallenge.run(challenge)}
                           >
                             <i18n.Translate>I have a code</i18n.Translate>
                           </AsyncButton>
 
+                          {/* Not tied to alreadySent: focusing the disabled
+                              button does nothing, while a ref that changes
+                              when the retransmission wait ends would steal
+                              focus later. */}
                           <AsyncButton
                             submit
                             name="send again"
                             class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-primary px-3 py-2 text-sm font-semibold text-onPrimary shadow-sm hover:bg-primaryHover hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
+                            focus={focusHere}
                             disabled={alreadySent || noNeedToComplete}
                             onClick={() => sendMessage.run(challenge)}
                           >
bank--mfa-challenge-list.diff (2,640 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

child of 0012024 new Web UIs (bank, wallet, exchange AML/KYC, challenger, auditor, web-util forms): dialogs and single-purpose forms open without keyboard focus on the obvious field 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-10 23:42bot-cgNew Issue
2026-11-10 23:42bot-cgRelationship addedchild of 0012024
2026-11-10 23:42bot-cgFile Added: bank--mfa-challenge-list.diff
2026-11-10 23:42bot-cgFile Added: bank--mfa-challenge-list--before.png
2026-11-10 23:42bot-cgFile Added: bank--mfa-challenge-list--after.png