Repository navigation
Password manager: restore from it, and key the credential by pubky - #475
Conversation
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
left a comment
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
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:
Four new strings, in all eleven Android languages. iOS gets them in the next PR of the stack.
Verification
./gradlew :shared:jvmTest detektAll :androidApp:assembleDebugpass. New tests inRestorePhraseViewModelTest,BackupPhraseViewModelTestand a newBackupQuizViewModelTest.wm sizefor the two tablet width classes:restore_method_managerraises the picker on entry, which returnsNoCredentialException; the screen showsrestore_phrase_manager_emptyand the field is editable.restore_phrase_from_managerraises it again. Rotating does not. Typing clears the note.restore_method_phraseoffers the button and raises nothing on entry.backup_phrase_save_managerfails withCreateCredentialNoCreateOptionException, showsbackup_phrase_manager_failed, and the account stays un-backed-up.journeys/28-password-manager-backup.xmlis the script, andjourneys/README.mdlists it as undriven. Whether Google Password Manager honoursallowedUserIdsis 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