Privacy-Period-Tracker/core
null 9b2b332a98 feat: give the lock a method, and every old install the one it had
Groundwork for offering a PIN, a fingerprint, or either. This commit is
the storage and the migration; the screens follow.

LockMethod is an enum rather than two booleans because BIOMETRIC is the
state with no fallback, and everything that has to be careful -- the
migration, the settings transitions, what happens when a sensor stops
working -- is careful specifically about the absence of a PIN. A boolean
pair spreads that condition across two fields nothing stops disagreeing.

It lives in the lock's own DataStore, not UserPreferences, and that
placement is the point: resetToDefaults() there is edit { clear() }, so a
fingerprint-only lock recorded beside the theme would be one "reset my
settings" away from silently vanishing.

resolve() decides what the stored state actually means, and each of its
three rules closes a way somebody could be locked out or wrongly let in. A
verifier with no recorded method reads as PIN, so the gate is shut from
the first frame of an old install rather than waiting for a migration. A
method needing a PIN with no verifier reads as NONE -- the same policy
VerifierRecord.decode already applies, because a lock nobody can open is
worse than no lock when the way out is behind it. An unrecognised name
falls back to the verifier, so a newer build's value cannot brick an older
one.

A PIN is never checked for a method that does not use one, even with a
stale record in the file.

The migration can never produce BIOMETRIC. recordMigratedMethod refuses it
at the API, and both its conditions -- nothing recorded, verifier agrees --
are evaluated inside the DataStore transaction, so a migration racing an
erase cannot resurrect a lock the user just removed. Getting this wrong
does not show a wrong number on a screen: it locks somebody out of their
own history on an update they did not ask for.

The migration lives in app because it reads two stores and core/security
depends on nothing.

24 repository tests and 9 migration tests, including all four legacy
combinations asserting the never-PIN-less invariant, proved by removing
the require and watching exactly one go red.

Part of #63

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 01:32:37 -05:00
..
data fix: make the learning loop keep the samples it earns 2026-08-20 16:13:53 -05:00
database fix: make the learning loop keep the samples it earns 2026-08-20 16:13:53 -05:00
datastore feat: give the lock a method, and every old install the one it had 2026-08-21 01:32:37 -05:00
designsystem feat: project the next year onto the calendar, and say what it assumes 2026-08-20 16:38:30 -05:00
export feat: export my data, as one plaintext file the user places 2026-08-19 22:13:07 -05:00
notifications feat: pin down which day, and let the reminder be any time 2026-08-21 01:09:33 -05:00
security feat: give the lock a method, and every old install the one it had 2026-08-21 01:32:37 -05:00