Make OTP widgets act in place: copy TOTP, advance HOTP - #576
Merged
Merged
Conversation
Bring the rebased tap behaviour in line with the architecture that landed while this branch sat unmerged. - Grant the widget process read plus HOTP increment only, via a WidgetStore = VaultStoreReader & VaultStoreHOTPIncrementer alias. It still cannot insert, update, delete, reorder, or export, and the store is opened .openOnly so an extension can never hit the recovery path (#526). - Keep the lock-screen accessory families non-interactive. Their buttons would be reachable on a locked device and advancing an HOTP counter cannot be undone, so they stay plain deep links. - Follow the branch in dropping the stored HOTP code: the persisted counter may be stale, so every family masks the digits until the user advances it. - Cover the intent-facing loader paths, including that an ineligible item yields no code and never advances the counter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Revives a branch that sat unmerged since May and rebases it onto current
main(it was 55 commits behind). It makes OTP widgets act in place instead of bouncing through the app:IncrementAndCopyHOTPCodeIntentincrements, renders the new code, copies it, and reloads the timeline — without launching the app. Previously the only path was thevault://otp/{id}/incrementdeep link, which opened the app to do it (Add opt-in OTP code home/lock-screen widget #517).CopyTOTPCodeIntent, also without launching the app.openItemDetaildeep link. This is how the app stays reachable from a widget whose code area is now a button.Security decisions
Home screen only. The interactive buttons are confined to
systemSmall. TheaccessoryCircularandaccessoryRectangularfamilies render on the Lock Screen, where a button would be reachable on a locked device, and advancing an HOTP counter cannot be undone. Those families keep the existing non-interactive deep link.Narrow write capability. #526 deliberately narrowed the widget's store to read-only so an extension could never trigger the recovery path and archive or move the shared SQLite store. That protection is preserved: the store is still opened
.openOnly. What changed is the capability type —WidgetStore = VaultStoreReader & VaultStoreHOTPIncrementer, which grants exactly the counter increment and nothing else. The extension still cannot insert, update, delete, reorder, or export.Eligibility still gates every action. Both intents route through
eligibleItem(id:), so a locked, hidden, passphrase-only, or killphrase-bearing item yields no code — and, for HOTP, no counter advance. There are tests for each of those cases.Manifesto
Reviewed against
MANIFESTO.md. The corollary that actually bites here is C8 — this reduces the steps needed to obtain a code, which is exactly what C8 says to evaluate rather than wave through. The judgement: the widget already renders a live TOTP code to anyone looking at the screen, so tapping to copy discloses nothing the screen did not already show, and the step being removed is an app launch, not an authentication. Nothing moved out from behind device auth, because nothing here was ever behind it. The irreversible action (HOTP increment) is kept off the Lock Screen for that reason.C4 — no auth gate is removed; widget eligibility has always derived from item state, never from authentication. C5 — the widget shows one user-chosen item and enumerates nothing. C2 — missing, deleted, and newly-ineligible items still resolve to the same
.unavailablestate; the intents return an empty result in every failure case, so a tap reveals nothing about why. C7 — the copy is.localOnlywith the concealed-type marker, matching the app's default posture. C1/C3/C6/C9/C10 — untouched.One gap worth recording: the app applies the user's
pasteTimeToLiveto copies, and the widget cannot read it. Settings live in standardUserDefaults, which an extension does not share, andPasteTTLsits inVaultSettings, which the widget target does not depend on. SincePasteTTL.defaultisnil(no expiry), the widget matches the app's default behaviour — but a user who has chosen an expiry will not get it on widget copies. Closing this needs an App Group settings suite; it is deliberately not in this PR.Rebase notes
Three conflicts, all resolved toward main's current architecture:
VaultMainScene(the store-failure screen from #567 now wraps the navigation view),WidgetVaultLoader(main's lazy, retry-safe store handling kept, capability widened), andOTPWidgetSmallView(main's Dynamic Type fonts kept over the branch's fixed sizes). The accessory views were taken wholesale from main to keep them non-interactive.The pre-rebase tip is preserved locally as
backup/hotp-in-widget-pre-rebase(1749dad6).Testing
iOSAllTestsplan passes locally on iPhone 18 Pro Max / iOS 27.0.WidgetVaultLoaderCodeActionTests(11 tests) covers both intent-facing loader paths: the counter advances exactly once and renders the next counter, a TOTP item is rejected by the HOTP path and vice versa, an unknown id is inert, and locked/killphrase/hidden items produce no code and no increment.OTPWidgetLoadingTestsupdated for the widened store type.Not yet verified on device — the interactive widget path needs a manual run: place a
systemSmallwidget on the home screen for an HOTP item, tap the code, and confirm the counter advances once, the code lands on the clipboard, and the timeline reloads. I have not done this, and it is the thing most worth checking before merge.🤖 Generated with Claude Code