Skip to content

fix(auth): iOS keyboard loss on password reset + render login for stale reset links#16

Merged
jaraf3330 merged 1 commit into
mainfrom
fix/reset-password-hydration-and-stale-link
Jun 18, 2026
Merged

fix(auth): iOS keyboard loss on password reset + render login for stale reset links#16
jaraf3330 merged 1 commit into
mainfrom
fix/reset-password-hydration-and-stale-link

Conversation

@jaraf3330

Copy link
Copy Markdown
Contributor

Summary

Fixes two bugs in the password-reset flow, reported on iPhone 13 / Chrome.

Bug 1 — soft keyboard disappears after the first character

The set-password view is server-rendered, so the password <input>s are real, focusable HTML before the JS bundle hydrates. On mobile the user can tap and type the first character into a not-yet-hydrated input; when hydrateRoot then attaches the controlled input (initial state ''), React resets the DOM value (dropping the char) and the focus reconciliation dismisses the iOS soft keyboard.

Fix: gate the controls behind a <fieldset disabled={!hydrated}> where hydrated flips to true in a useEffect. The controls are non-focusable until React owns them, so the pre-hydration typing race is structurally impossible. SSR and the first client render both emit disabled controls → no hydration mismatch. (Auth/src/components/form/SetPasswordForm.tsx)

Bug 2 — refreshing the reset page throws a hard error

GET /auth/email/reset-password re-validates a single-use, 30-min-expiry token on every load and had no graceful fallback, so a used/expired token threw AppError → hard error page. Commit 89ef6f7 already added a "render login for stale email links" fallback to the registration link route, but the same fix was never applied to the reset route.

Fix: mirror 89ef6f7try/catch around validatePasswordResetToken; on a recognised token error (INVALID_TOKEN/INVALID_TOKEN_CONFIG_URL/INVALID_TOKEN_TYPE/TOKEN_ALREADY_USED/TOKEN_EXPIRED) render login instead of erroring; re-throw everything else. (API/src/routes/auth/email-reset-password.ts)

Tests

  • API/tests/unit/email-reset-password.route.test.ts — used→login, expired→login (the refresh case), and infra error (DATABASE_DISABLED, 500) is not swallowed.
  • Auth/src/components/form/SetPasswordForm.test.tsx — SSR renders the form inside a disabled, aria-busy fieldset (pre-hydration gate).

Process

Designed by two independent architect passes (Codex + Claude), reviewed by two independent reviewers (Codex + Claude).

Known limitation (by design, not a regression)

The stale-link fallback renders login without PKCE, because reset email links deliberately carry no PKCE/redirect_url and the existing post-reset success path already returns to a PKCE-less login (setView('login')). Actual password login from this view needs PKCE, but the relevant recovery path — "forgot password" → request a new reset link (POST /auth/reset-password/request, email-only, no PKCE) — works. Threading PKCE through the whole reset flow would redesign the reset link and is out of scope for these bug fixes. Before this change a refresh produced a hard error; after it, login with a working recovery path — strictly better.

🤖 Generated with Claude Code

…e reset links

Two bugs in the password-reset flow:

1. iPhone Chrome: typing the first character of the new password dismissed the
   soft keyboard. The set-password view is SSR-rendered, so the password inputs
   were focusable before hydration. A pre-hydration keystroke is wiped when React
   hydrates the controlled input (initial value ''), and the focus reconciliation
   dismisses the iOS keyboard. Gate the controls behind a disabled <fieldset>
   until a post-hydration flag flips, so they are never focusable before React
   owns them. SSR and first client render both emit disabled controls (no
   hydration mismatch).

2. Refreshing the reset landing threw a hard error: GET /auth/email/reset-password
   re-validates a single-use/expiring token on every load and had no graceful
   fallback. Mirror commit 89ef6f7 (registration link route): on a recognised
   token error, render login instead of erroring; re-throw everything else.

Adds unit tests for the reset route fallback (used/expired -> login, infra error
-> not swallowed) and an SSR test asserting the set-password form renders a
disabled fieldset pre-hydration.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jaraf3330 jaraf3330 merged commit 4ffd82b into main Jun 18, 2026
1 check failed
@jaraf3330 jaraf3330 deleted the fix/reset-password-hydration-and-stale-link branch June 18, 2026 17:35
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.

1 participant