View Issue Details

IDProjectCategoryView StatusLast Update
0012043Talerchallengerpublic2026-11-10 23:43
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 
Summary0012043: Challenger: code entry pulls focus back to the PIN field every second during the resend countdown
DescriptionWhat happens:
The PIN input used an inline ref callback, `ref={(el) => { pinInput.current = el; if (focus) doAutoFocus(el); }}`. Preact re-applies a ref whose identity changed on every render, so doAutoFocus ran on every re-render, not just on mount. While a resend cooldown is active, useRemainingSeconds re-renders the page every second. A keyboard user who tabs from the PIN to 'Use a different email address' (the only enabled action during the cooldown) is pulled back to #pin within about a second. Measured in a rendered browser (real App, fake backend, retransmission_time 90 s in the future): focus after Tab = A 'Use a different email address'; 2.2 s later = INPUT#pin. Initial mount, resend, 'Back to code entry' and ask -> answer did focus #pin correctly.

Expected:
The PIN is focused once, when the input appears: initial load, return from the change-address form, and after the attempts-exhausted screen is replaced by a fresh code. Focus then stays wherever the user moves it.

Where:
  - packages/challenger-webui/src/pages/AnswerChallenge.tsx:99
  - packages/challenger-webui/src/pages/AnswerChallenge.tsx:401

Measured in headless Chromium (document.activeElement):
  before: 2.2 s after tabbing to the link during the cooldown: INPUT#pin (focus stolen back)
  after: A 'Use a different email address' (focus kept). Regression checks still give INPUT#pin: answer-initial, answer-resend, change-back-to-code, ask-send-to-answer (screenshots challenger--{answer-initial,answer-resend,change-back-to-code,ask-send-to-answer}--{before,after}.png)

Proposed fix:
The ref callback is wrapped in useCallback([focus]). Its identity is now stable, so Preact calls it only when the input mounts. The existing useEffect that refocuses the PIN after a successful resend is unchanged.

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 --noEmit ok; eslint + prettier clean; challenger-flow.test.js 12/12 pass (the `pnpm test` glob does not expand on node 20, so the built file was run directly); real App rendered in Chromium against an in-page fake Challenger backend, before and after
TagsNo tags attached.
Attached Files
challenger--answer-pin-refocus.diff (1,777 bytes)   
diff --git a/packages/challenger-webui/src/pages/AnswerChallenge.tsx b/packages/challenger-webui/src/pages/AnswerChallenge.tsx
index ae12b7167..4f0646065 100644
--- a/packages/challenger-webui/src/pages/AnswerChallenge.tsx
+++ b/packages/challenger-webui/src/pages/AnswerChallenge.tsx
@@ -22,7 +22,13 @@ import {
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import { VNode, h } from "preact";
-import { useEffect, useMemo, useRef, useState } from "preact/hooks";
+import {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "preact/hooks";
 import {
   revalidateChallengeSession,
   useChallengeSession,
@@ -86,6 +92,17 @@ export function AnswerChallenge({
   const [deliveryErrorDetails, setDeliveryErrorDetails] = useState<string>();
   const [newCodeSent, setNewCodeSent] = useState(false);
   const pinInput = useRef<HTMLInputElement | null>(null);
+  // Keep the ref callback stable so it focuses the PIN only when the input
+  // appears.  An inline callback runs again on every render, and the resend
+  // countdown re-renders every second, pulling focus back from the other
+  // actions on this page.
+  const pinRef = useCallback(
+    (element: HTMLInputElement | null) => {
+      pinInput.current = element;
+      if (focus) doAutoFocus(element);
+    },
+    [focus],
+  );
   const { result, retry, isRefreshing } = useChallengeSession(session);
 
   const lastStatus =
@@ -381,10 +398,7 @@ export function AnswerChallenge({
           {verificationCodeLabel}
         </label>
         <input
-          ref={(element) => {
-            pinInput.current = element;
-            if (focus) doAutoFocus(element);
-          }}
+          ref={pinRef}
           id="pin"
           name="pin"
           type="text"

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:43bot-cgFile Added: challenger--answer-pin-refocus.diff
2026-11-10 23:43bot-cgFile Added: challenger--answer-cooldown-tab--before.png
2026-11-10 23:43bot-cgFile Added: challenger--answer-cooldown-tab--after.png