Skip to content

Password manager: restore from it, and key the credential by pubky - #475

Merged
jvsena42 merged 4 commits into
mainfrom
feat/password-manager-restore
Oct 9, 2026
Merged

jvsena42 merged 4 commits into
mainfrom
feat/password-manager-restore

Conversation

@jvsena42

@jvsena42 jvsena42 commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Closes #471.

What changed

The credential id is the pubky. It was the app's name, so two accounts saved from one device shared an entry and the read-back accepted whichever one came back under that name. The save and read-back effects now carry the account's full pubky. Entries saved before this keep the old id and are still accepted: the read-back asks for both ids, and the ViewModel decides by comparing the words, as it already did.

Restore from the password manager. The restore menu has a third card, restore_method_manager. It opens the phrase screen with the credential picker already raised. The phrase screen also offers the picker as a secondary button. What the picker returns goes through the existing phrase path (derive, look up, sign in), so every outcome is the one typed words get. A cancel or an empty provider leaves the field usable and says nothing came back.

Two details in the Android route:

  • The on-entry picker is raised directly, not through the effect flow. An effect emitted as the screen opens was dropped before the collector subscribed and left the field locked. Found on the emulator.
  • A rotation while the picker is up answers null, because the coroutine waiting on it is cancelled while the ViewModel survives.

Four new strings, in all eleven Android languages. iOS gets them in the next PR of the stack.

Verification

  • ./gradlew :shared:jvmTest detektAll :androidApp:assembleDebug pass. New tests in RestorePhraseViewModelTest, BackupPhraseViewModelTest and a new BackupQuizViewModelTest.
  • Pixel_9 emulator (API 37, no Google account, no credential provider), phone size, plus 2560x1600 and 1600x2560 via wm size for the two tablet width classes:
    • Restore menu shows three cards, centred in the column at both tablet sizes.
    • restore_method_manager raises the picker on entry, which returns NoCredentialException; the screen shows restore_phrase_manager_empty and the field is editable.
    • restore_phrase_from_manager raises it again. Rotating does not. Typing clears the note.
    • restore_method_phrase offers the button and raises nothing on entry.
    • On a signed-in local-key account, backup_phrase_save_manager fails with CreateCredentialNoCreateOptionException, shows backup_phrase_manager_failed, and the account stays un-backed-up.
  • Not run: a save, read-back or restore that succeeds. No emulator here has a credential provider. journeys/28-password-manager-backup.xml is the script, and journeys/README.md lists it as undriven. Whether Google Password Manager honours allowedUserIds is also unverified; nothing depends on it.

The agent skill under plugins/loopky/ needs no change: nothing here touches CLI, import, card or deck logic.

🤖 Generated with Claude Code

jvsena42 and others added 3 commits October 8, 2026 20:35
The credential's id was the app's name, so two accounts saved from one
device shared an entry and the read-back accepted whichever one the
provider returned under that name. The ViewModels now carry the account's
full pubky on the save and read-back effects.

Entries saved before this keep the app's name as their id and still hold a
good phrase, so the read-back asks for both ids. Neither is treated as
proof: the ViewModel already compares the words that come back, which is
what decides. The sheet also gains an unfiltered pick for the restore path.

Refs #471

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Someone who saved the phrase to a password manager and lost the phone had
to open the manager, copy twelve words and paste them in. The restore menu
gains a third card that opens the phrase screen with the credential picker
already raised, and the phrase screen offers the same picker as a secondary
action.

What the picker returns goes through the existing phrase path: derived,
looked up and signed in like typed words, with the same outcomes. A cancel
or an empty provider leaves the field usable and says nothing came back.

The on-entry picker is raised directly instead of through the effect flow,
which drops an effect emitted before the screen's collector has subscribed
and left the field locked. A rotation while the picker is up answers null
for the same reason.

Refs #471

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the script, lists what has not been driven for want of a credential
provider, and updates the Architecture entry now that restore exists.

Refs #471

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42
jvsena42 added this pull request to stack #477 October 8, 2026 23:45
@jvsena42 jvsena42 changed the title feat/password manager restore Password manager: restore from it, and key the credential by pubky Oct 8, 2026
@jvsena42
jvsena42 marked this pull request as ready for review October 8, 2026 23:45

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

One finding. The restore path, the pubky-keyed id, the legacy-id read and the eleven translations read correctly against the code.

viewModel.onPasswordManagerSaveResult(sheet.save(account, effect.secret))
BackupPhraseEffect.ReadBackFromPasswordManager ->
viewModel.onPasswordManagerReadBack(sheet.read(account))
viewModel.onPasswordManagerSaveResult(sheet.save(effect.account, effect.secret))

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Medium, already on main: a rotation while the save or read-back sheet is up locks this screen.

sheet is remember(context), so a configuration change restarts this LaunchedEffect and cancels the coroutine suspended in sheet.save / sheet.readBack. LeaveEffect skips onLeave on a configuration change, so the retained ViewModel keeps isSavingToPasswordManager = true: backup_phrase_save_manager stays disabled and neither the saved nor the failed line appears until the user leaves the screen.

BackupQuizScreen has the same shape with isChecking: onCheckSavedClick returns early while it is set, so backup_quiz_saved_check is dead after a rotation.

This PR fixes exactly this on the restore screen with answerFrom's finally. The same wrapper here would answer onPasswordManagerSaveResult(false) / onPasswordManagerReadBack(null) on cancellation.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 6c65850. answerEvenIfCancelled in PasswordManagerSheet.kt delivers a fallback when the wait is cancelled, and the phrase, confirm and restore screens all use it. The ViewModels now ignore an answer nobody is waiting on, because the same fallback fires when the screen is left with the sheet up, after onLeave has reset the state; there is a test for that. Not driven: the emulator has no credential provider, so the sheet never stays up long enough to rotate under. Journey 28 has the step.

…cks the screen

The effect suspended on the save or read-back sheet is cancelled by a
configuration change while the ViewModel survives it, so the saving and
checking flags stayed set and the buttons stayed disabled until the screen
was left. The restore screen already answered null on cancellation; the
backup phrase and confirm screens now share that wrapper.

The ViewModels ignore an answer nobody is waiting on, because the same
fallback also fires when the screen is left with the sheet up, after
onLeave has reset the state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jvsena42
jvsena42 merged commit 0a601fe into main Oct 9, 2026
11 checks passed
@jvsena42
jvsena42 deleted the feat/password-manager-restore branch October 9, 2026 00:16
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.

Password-manager backup: restore from it, and key the credential by pubky

1 participant