Skip to content

Fix login-screen UX: resend countdown, validation, demo-code mask, phone layout (T3: AUTH-03, 04, 05, 09, 10, 11, 12, 14, VS-02, AX-06) - #210

Merged
Programcode merged 1 commit into
mainfrom
fix/t3-login-ux
Oct 23, 2026
Merged

Programcode merged 1 commit into
mainfrom
fix/t3-login-ux

Conversation

@Programcode

@Programcode Programcode commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Login-screen fixes from the 22 Oct QA report (batch T3), built on T1 (#194) and T2 (#202). There are no migrations, no Edge Function changes and no Supabase settings changes. Merging deploys the frontend only.

Issues

AUTH-03, wrong code reported as expired. Main already had this fix before the batch started. #184 (b23b5d7) changed login.errors.codeExpired to "That code is wrong or has expired. Check it or send a new one." in all three languages, and src/test/auth/login-otp.test.tsx covers the otp_expired mapping. b23b5d7 is an ancestor of the QA build d749051, so the prod log entry at 15:57 came from an older deploy. This PR adds a comment at the mapping and a recipe step that sends a wrong code to the real local Auth (403 otp_expired) and checks the message. The digits-only filter is unchanged.

AUTH-04, Resend right away hits the rate limit. After each send, Resend is disabled and counts down ("Resend in 1:00", "0:59" and so on). The deadline is stored in sessionStorage under the trimmed, lowercased address, so a reload of /login keeps it. Sending to the same address again before it runs out opens the code step with "We sent a code to this address a moment ago…" and makes no request. When Auth refuses with "you can only request this after N seconds", the alert says "Too many attempts. Try again in 0:42." once and the button counts down from N. The hourly email cap names no wait. In that case the alert keeps the generic message and the button waits a minute. The PKCE verifier code is untouched.

AUTH-05, stale errors. Each send, resend, demo sign-in and "Use a different email" clears the old field error, the failed-link notice and the session-expired notice. A resend says "New code sent".

AUTH-09, no progress feedback. Send, Resend, Verify and the demo sign-in show a spinner. After 4 seconds a status line says "This can take a few seconds." A ref blocks a second request while one is running; before this, Enter in the email field could send another. The email field is disabled during a send, and verify and resend use the address the code went to.

AUTH-10, browser bubble next to the app error. All three forms are noValidate. type="email", autocomplete and the OTP inputMode stay.

AUTH-11, empty and over-long email. Empty input says "Email is required." Input over 254 characters says the address is too long. The field no longer has maxLength, so nothing is cut.

AUTH-12, demo code format. The field formats typed and pasted input as DEMO-XXXX-XXXX. It uppercases, drops anything that isn't a letter or digit, and adds the prefix and dashes, so ab12cd34 becomes DEMO-AB12-CD34. The caret stays after the character it followed, and Backspace over a trailing dash works. The input has pattern, autoCapitalize="characters" and no spellcheck. An incomplete code says "Demo codes look like DEMO-AB12-CD34." and an empty one says "Enter your demo code." A well-formed unknown code still gets "Demo code not found." from the server. No real demo codes were created or used; the recipes mint and delete local ones.

AUTH-14, signed-out deep links. Current main already redirects. RequireAuth sent signed-out /app routes to /login with state.from at d749051 too, and T2 carries the deep link through Google and the email link as ?next=. A probe of signed-out /app/generate before this change showed URL /login and history.state.usr.from.pathname = "/app/generate". I couldn't reproduce an in-place card. What was missing was the <main> landmark (AX-06 below). The new recipe auth-deep-link-email-code proves the round trip on the local stack. It goes from /app/generate to /login, sends a code, enters a wrong one, resends after the countdown, signs in, and lands back on /app/generate. T2's per-account landing (AUTH-06) is unchanged, and auth-switch-account still passes.

VS-02, gap above the card on phones. Below 768 px (md) the page starts at the top with 16 px padding. From 768 px up it stays centred as before. The top of the back link moved as follows (EN; HU shows the same pattern): 375 px from 133 to 16, 360 px from 97 to 16. At 768 px it stays at 249 and at 1280 px at 137.

AX-06, no <main> on signed-out views. /login, RequireAuth's loading view and both AuthCallbackPage views now render one <main>. The callback's timed-out view gets an <h1>. RequireAuth's spinner has a role="status" wrapper with a hidden "Loading" label. The callback spinner's role="status" moved to an inner element so it doesn't replace the landmark role. The repo has no axe dependency, so I checked with role queries: /login and signed-out /app/generate (which redirects to /login) have exactly one main and one h1, in both the recipe and a Playwright probe at four widths. The route's own Suspense skeleton, shown while the /login chunk loads, still has no <main>. That was already the case on main and is not changed here.

Verification

GitHub Actions minutes are used up, so CI did not run. Every result below is local.

npm run verify passed in full on this PR's head (4f1d0c4, rebased onto b0b2ae7):

  • docs check, lint (eslint, knip, repo rules) and typecheck passed.
  • Unit tests: 78 files, 574 tests passed. The new src/test/auth/login-form.test.tsx covers the resend countdown with fake timers across a remount, the 429 wait at both the email step and the resend step, noValidate, the empty and over-long email errors, the demo-code mask and caret, and authRetryAfterSeconds.
  • Collaboration tests: 5 files, 30 tests passed.
  • Edge tests: 457 passed, 0 failed.
  • The build (with prerender, sitemap and check-site) passed: "48 HTML files, sitemap and llms.txt resolve; no problems".
  • test:sw: all offline checks true.
  • e2e smoke: 43 passed.

I ran the local stack (verify.sh up, fake provider, local Supabase with Mailpit, never prod mail) on 6c4a08d6. That is this commit before the rebase; the rebase only brought in unrelated T4, T9 and live-stream changes and one README conflict. Results on desktop-chromium and mobile-chromium:

  • recipes/auth.spec.ts: 5 of 5 passed on each. These were auth-email-otp, auth-session-expired, auth-email-link (now waits out the countdown before its 429 resend), the new auth-deep-link-email-code, and auth-switch-account.
  • e2e/full-stack/auth.spec.ts: 3 of 3 passed on each, including the unknown demo code going through the mask to the real function.

Then verify.sh down released the lock.

Screenshots of /login at 375 px in EN and HU, before and after, plus 360, 768 and 1280 px, were taken locally and shown in the T3 thread; they are not uploaded here. The plain dev server only rendered pages; sign-in ran only against the local stack.

Review rounds

Codex was rate-limited, so each round was one fresh-context Claude subagent (claude-opus-5-5 on claudeAgent_cpa) reading the full diff against main.

Round 1 found one Medium. The email field stayed editable during a send, and the countdown, resend and verify read the live field instead of the address the code went to. I fixed it with a sentTo state, and the field is now disabled while sending. The Lows and Nits I fixed:

  • The 429 alert didn't follow the countdown.
  • The demo-mask caret jumped to the end.
  • RequireAuth's loading <main> had no content.
  • The breakpoint was sm (640 px) instead of md.
  • The 429 path at the email step had no test.
  • Two ES phrasings and one HU phrasing.
  • The clock kept ticking after the countdown ended.

I kept the noValidate attribute test because the brief asks for one.

Round 2 checked every round-1 fix and found nothing Medium or above. I fixed these:

  • The live alert re-announced every second. It now keeps the time fixed when shown and goes away when the wait ends.
  • A shadowed next variable.
  • A wrong layout comment.
  • Expired cooldown keys stayed in sessionStorage. They are now removed once they run out.

Left as is:

  • authStatus stays at error with no message after a wait ends. The reviewer confirmed this is harmless.
  • Two keystrokes within one animation frame can misplace the caret. Pastes are one event, so this needs typing faster than a frame.
  • The countdown starts at "1:00" rather than "0:59".

Needs a real browser

  • Google sign-in returning to a deep link. Automation must not sign in to real Google accounts. The code path is T2's next, which this PR doesn't change.
  • Safari and iOS behaviour of the sessionStorage countdown, autoCapitalize on the demo field, and one-time-code autofill.
  • Prod's Auth max_frequency. If it is above 60 seconds, a resend after the client's minute gets a 429 that names the wait, and the UI shows it. Local is 1 second.
  • AUTH-13 (the prod email template) is held for Adam as briefed.

Deploy notes

Migrations: none. Edge Functions to deploy: none.

🤖 Generated with Claude Code

…one layout (T3)

Resend is disabled with a countdown after each code is sent, kept in
sessionStorage per address so a reload of /login keeps it. A 429 that names
a wait says how long. Sending to the same address again within the wait
opens the code step without a request. Each send clears old errors and
notices. Sending shows a spinner and, after 4 s, says it can take a few
seconds; one click makes one request, and verify and resend use the address
the code went to.

The forms are noValidate, so only the app's localized messages show. An
empty email says it is required, and an over-long one says so instead of
being cut. The demo-code field formats input as DEMO-XXXX-XXXX and gives a
format error. On phones the card starts near the top. /login, RequireAuth's
loading view and the auth callback views each render one <main>.

Fixes AUTH-04, AUTH-05, AUTH-09, AUTH-10, AUTH-11, AUTH-12, VS-02, AX-06.
AUTH-03 and AUTH-14 were already fixed on main; a verify recipe now covers
both on the local stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 23, 2026 02:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 32 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between b0b2ae7 and 4f1d0c4.

📒 Files selected for processing: 13
  • .claude/skills/verify-debatelab/features/README.md
  • .claude/skills/verify-debatelab/features/auth-demo-login.md
  • .claude/skills/verify-debatelab/recipes/auth.spec.ts
  • src/components/auth/RequireAuth.tsx
  • src/lib/auth-errors.ts
  • src/lib/login-form.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/pages/AuthCallbackPage.tsx
  • src/pages/LoginPage.tsx
  • src/test/auth/login-form.test.tsx
  • src/test/auth/login-otp.test.tsx
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Programcode
Programcode merged commit 5ea2275 into main Oct 23, 2026
3 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants