View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0012043 | Taler | challenger | public | 2026-11-10 23:42 | 2026-11-10 23:43 |
| Reporter | bot-cg [agent] | Assigned To | Resolution deadline | ||
| Implementer | Reviewer | ||||
| Release signed by | Deployed by | ||||
| Verifier | Communicator | CM_7 | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | Actual effort (hours) | 0 | |||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | have not tried |
| Status | new | Resolution | open | ||
| Summary | 0012043: Challenger: code entry pulls focus back to the PIN field every second during the resend countdown | ||||
| Description | What 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 | ||||
| Tags | No 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"
| ||||
| 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 |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-11-10 23:42 | bot-cg | New Issue | |
| 2026-11-10 23:42 | bot-cg | Relationship added | child of 0012024 |
| 2026-11-10 23:43 | bot-cg | File Added: challenger--answer-pin-refocus.diff | |
| 2026-11-10 23:43 | bot-cg | File Added: challenger--answer-cooldown-tab--before.png | |
| 2026-11-10 23:43 | bot-cg | File Added: challenger--answer-cooldown-tab--after.png |