recoveryAvailable: true)Enter one of the recovery codes you saved when you turned on two-factor authentication for amara@rowan.co.
One text field, not OTP cells: the code is opaque (up to 64 characters; today's look like
XXXX-XXXX-XXXX-XXXX) and the client must not check its format. Disable “Verify” only while the trimmed value is
empty. Send exactly what was typed as recoveryCode; the server strips spaces and dashes and
ignores case. No numeric keyboard, no auto-capitalise, no spellcheck.
Enter one of the recovery codes you saved when you turned on two-factor authentication for amara@rowan.co.
That didn't work. Enter one of your saved recovery codes.
One message for wrong, already used and replaced codes — the API makes them look the same on purpose, so the UI never says which. Nothing about time (“codes change every 30 seconds” is authenticator-app copy and is wrong here). The challenge is still live: keep the typed value, focus and select it, and allow another try.
Same limiter, same copy as frame 05. Wait comes from the 429's Retry-After header (seconds): show a live m:ss countdown in the
callout and on the disabled button, and enable “Try again” when it reaches zero. If the header is missing or not a
positive integer, show “Wait a few minutes, then try again.” with the button enabled. The entered code is kept
and not marked red (it wasn't judged wrong). A retry that still gets 429 restarts the countdown from the new header.
No promise about the recovery code here. NOT_ACTIVE also covers an
already-completed challenge — e.g. a successful verify whose reply was lost, then retried — in which case the
code was consumed. The old challengeId is dead: no field and no “Try again” on this screen.
This copy does not say the code is still unused: if the server accepted it but the reply was lost, the code is gone. Never describe this as a wrong code.