Skip to content

Add a phone code (text or voice call) as an MFA factor - #61

Merged
pacific-ring merged 6 commits into
mainfrom
feature/mfa-sms-otp
Nov 6, 2026
Merged

pacific-ring merged 6 commits into
mainfrom
feature/mfa-sms-otp

Conversation

@pacific-ring

@pacific-ring pacific-ring commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds a code sent to a phone (text message or voice call) as an MFA factor, next to push.

  • A phone the server offers becomes one factor per way it can be reached (text, voice), each its own tile in the list.
  • MfaReducer holds the phone rules and the engine sends and verifies the code. CHALLENGE_PHONE is added to the capabilities. "One more step" rounds work as for push.
  • A wrong code stays on the screen with an inline line. Resend opens after 60 seconds. Verify another way and Back go to the list. Back is ignored while a code is being sent or checked. An expired code or any other failure goes to the error screen with Start over.
  • Code screen and list tiles ("SMS OTP" / "Voice OTP", masked number on a second line) follow the Figma OTP update. A number the server hides completely is not named in the subtitle.
  • Public API, same shapes as push: MfaFactorId.Phone, PhoneChallengeModel, PhoneDelivery, MfaScreens { phoneChallenge { } }, MfaMessages { phoneChallenge { } }, and the headless step.
  • Shared components touched: OTPInputField (cursor stays after the last digit) and FlowScreenHeader (optional subtitleFontFeatures, off by default).
  • EXAMPLES.md updated.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test improvement

References

Testing

  • Unit tests
  • Manual testing
  • Sample app testing

Test Configuration:

  • Android version(s):
  • Device(s):

Screenshots (if appropriate)

Checklist

🤖 Generated with Claude Code

@pacific-ring
pacific-ring requested a review from a team as a code owner November 5, 2026 07:38
@pacific-ring
pacific-ring force-pushed the feature/mfa-public-api branch from 9fb6fcf to 292091b Compare November 5, 2026 11:15
Comment thread EXAMPLES.md Resolved
@pacific-ring
pacific-ring changed the base branch from feature/mfa-public-api to main November 5, 2026 17:16
pacific-ring and others added 2 commits November 5, 2026 23:22
The server's MFA menu can offer a code sent to a phone. MFA now runs it next to push:
- one factor per way a phone can be reached, each its own row in the list ("SMS OTP" / "Voice OTP")
- a code screen ("Enter the 6-digit code") with an inline wrong-code line, Resend after 60 seconds and Verify another way
- phone rules live in MfaReducer; the engine sends and verifies the code
- public MfaFactorId.Phone, PhoneChallengeModel, PhoneDelivery, MfaScreens.phoneChallenge and
  MfaMessages.phoneChallenge, so the screen can be reworded, replaced or drawn headless

Two fixes found on the way, both in screens the first factor shares:
- OTPInputField keeps its cursor after the last digit; after a rotation it started at the front and Backspace did nothing
- a dismissed wrong-code line no longer comes back after a rotation (MFA phone screen and LoginCodeScreen)

EXAMPLES.md: the embedded login section is shorter and follows the iOS guide's shape.

Co-Authored-By: Claude <noreply@anthropic.com>
- Code screen: "We’ve sent a code to *********0128" (voice: "We’re calling …") and a "One-Time Passcode" label above the boxes
- A number the server hid completely is not named: "We’ve sent a code to your phone"
- List tiles: "SMS OTP" / "Voice OTP" with the masked number on a second line, like the device name under a push tile; Voice uses a handset icon (Lucide phone, the same set as the SMS icon)
- Hidden digits are one character (`*`): anything the server uses to hide a digit is shown as the same `*`
- Inter's contextual alternates swapped the five characters nearest the digits for a raised, larger form, so the number showed two sizes; they are turned off for the lines that show a number (FlowScreenHeader gets an optional subtitleFontFeatures)
- OTPInputField remembers its TextFieldValue instead of building one on every recomposition
- Docs for rowText/rowVoice say what the default words do

Co-Authored-By: Claude <noreply@anthropic.com>
…quest neutrally

- A push comes before a code sent to a phone (the more phishing-resistant
  first); within each kind the server's order is kept
- The "request expired" message no longer says it "wasn't approved in time",
  which made no sense for an expired phone code

Co-Authored-By: Claude <noreply@anthropic.com>
A phone code waiting behind an in-flight push check was not tracked, so if the
check ended the round the code still went out for a screen the user never sees.
It is now tracked like the push send and cancelled with the other challenge
work (CancelPushWork is renamed CancelChallengeWork to match).

Also correct the Back description in EXAMPLES.md: from a factor screen Back
goes to the list in later rounds even with a single factor.

Co-Authored-By: Claude <noreply@anthropic.com>
@pacific-ring

Copy link
Copy Markdown
Contributor Author
MFA_SMS_OTP.mp4

Co-Authored-By: Claude <noreply@anthropic.com>
NandanPrabhu
NandanPrabhu previously approved these changes Nov 6, 2026
Comment thread EXAMPLES.md Resolved
pmathew92
pmathew92 previously approved these changes Nov 6, 2026
…e MFA code field

A send queued behind a push check got its turn from the SDK lock before the check's answer reached the engine,
so cancelling it on a new round came too late and a text or call went out for a screen the user never sees.
Sends now wait for the check to be answered and applied first, so the new round can drop them.

The push and phone sends share one job instead of one per factor, since only one send is ever out at a time.

The code field on the MFA code screen gets a content description for TalkBack, and the EXAMPLES.md note on
Back and the capabilities KDoc are corrected.

Co-Authored-By: Claude <noreply@anthropic.com>
@pacific-ring
pacific-ring merged commit 1a95719 into main Nov 6, 2026
5 checks passed
@pmathew92 pmathew92 mentioned this pull request Nov 6, 2026
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.

3 participants