Skip to content

fix(ui): keep backup codes across device rotation - #1084

Merged
swolfand merged 4 commits into
mainfrom
fix/backup-codes-rotation
Oct 13, 2026
Merged

swolfand merged 4 commits into
mainfrom
fix/backup-codes-rotation

Conversation

@claude

@claude claude Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Requested via Slack thread

What & why

Before: Rotating the device while the backup codes were on screen lost them. In the setup-MFA session task, the view went back to the method list and the task then completed, so the user never got to save their codes. The profile Security screen's backup codes sheet (after adding an authenticator app or SMS verification) closed on rotation, and so did the one on the profile detail screen (after verifying an email or phone).

After: The backup codes stay on screen through rotation until the user dismisses them. In the session task, the task no longer completes while the codes are showing; it completes once they are dismissed.

How: The codes were held in plain remember state, which a configuration change drops. They now live in ViewModels. SessionTaskMfaViewModel holds the session task's flow step (including the codes). PendingBackupCodesViewModel holds the codes for the profile Security and profile detail sheets, and each sheet is shown whenever codes are pending. The codes are cleared when the user dismisses the sheet or step. The session task's completion effect now also waits until the backup codes step is dismissed. Without that, a session refresh after recreation could finish the task while the codes were showing.

Why a ViewModel and not rememberSaveable: rememberSaveable would write the backup codes into saved instance state (the Bundle), which can be persisted and is exposed in places secrets shouldn't be. The prebuilt UI ViewModels are already scoped to the component or nav entry (#1032), so they survive rotation and are cleared when the component or entry goes away. The codes stay in memory only.

What to focus on

  • The showBottomSheet || pendingBackupCodes != null handling in UserProfileSecurityView and UserProfileDetailView.
  • The !isShowingBackupCodes guard on onAuthComplete in SessionTaskMfaView. I added it from reading the code, not from reproducing the rotation case.
  • UserProfileDetailView has the same bug and the same fix, which goes beyond the two screens named in the issue.
  • UserProfileDestination.BackupCodeView (the regenerate and authenticator-app navigation entry) keeps the codes in a @Serializable nav key, so they already survive rotation but are written to saved state. I left that unchanged. It is pinned by existing navigation tests and is a separate design question.

Not run: I could not build or run tests locally (the sandbox can't reach the Android/Gradle hosts), so I reviewed the diff by hand. CI is the check. I added unit tests for both new ViewModels, but there is no Compose rotation test.

Fixes #1082

🤖 Generated with Claude Code

https://claude.ai/code/session_01Apqjw4sSkTw2Kb3TYvHQJf


Generated by Claude Code

Backup codes (and the setup-MFA flow step that holds them) lived in
`remember` state, so a configuration change dropped them. Hold them in
ViewModels instead, which survive rotation without writing the codes into
saved instance state.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Apqjw4sSkTw2Kb3TYvHQJf
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: clerk/clerk-android/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f124dabc-2941-45c0-a337-b131a1781cec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@swolfand
swolfand marked this pull request as ready for review October 9, 2026 18:44
claude added 3 commits October 9, 2026 18:51
The baselined ComposeParameterOrder message embeds the composable's
parameter list, so adding the backup codes ViewModel parameter changed the
message and made the existing baseline entry stop matching.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Apqjw4sSkTw2Kb3TYvHQJf
SessionTaskMfaView now reads its flow step from a ViewModel. Paparazzi has
no ViewModelStoreOwner, so the snapshot test supplies one explicitly, as the
other snapshot tests do.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Apqjw4sSkTw2Kb3TYvHQJf
Constructing a ViewModel inside the snapshot lambda trips the
ViewModelConstructorInComposable lint check, which also covers test
sources. Create it before the lambda instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Apqjw4sSkTw2Kb3TYvHQJf
@swolfand
swolfand merged commit 0d8e1d5 into main Oct 13, 2026
12 checks passed
@swolfand
swolfand deleted the fix/backup-codes-rotation branch October 13, 2026 17:41
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.

Rotating the device on the backup codes screen loses the codes

2 participants