098f165bba88

fix: Stop post-unlock nav clobbering the autofill new-entry redirect (v2.14.9)

- LockScreen's unlock LaunchedEffect was keyed on both (state.unlocked, autofillPending). The NavHost's autofill effect consumes newEntryRequest synchronously, which flipped autofillPending from true to false mid-unlock and re-fired the LockScreen effect — routing the user to the vault list with popUpTo(0) and overwriting the new-entry editor navigation. - Key the effect on state.unlocked only so autofillPending is captured once at the unlock transition; the NavHost's consume no longer retriggers LockScreen. - 3 AutofillNavRepositoryTest cases (request surfaces payload, blank optional fields are dropped, consume clears so the post-unlock branch cannot re-fire). - VERSION 2.14.8 -> 2.14.9. - gates: testDebugUnitTest 980/0, lintDebug clean, assembleRelease both ABIs.

time
2026-08-06 00:33 UTC · 2 months ago
machine
koh2
parent
ba1cd3cf8da3
4 files VERSION+1 −1app/src/main/java/com/peachpasswords/ui/lock/LockScreen.kt+8 −2app/src/test/java/com/peachpasswords/autofill/AutofillNavRepositoryTest.kt+50 −0releases/v2.14.9.md+2 −0
VERSION+1 −1
@@ -1 +1 @@1-2.14.81+2.14.9

view file

app/src/main/java/com/peachpasswords/ui/lock/LockScreen.kt+8 −2
@@ -102,12 +102,18 @@102102     val createBlocker = if (state.hasVault || password.isEmpty()) null103103                         else createValidationError(password, confirm, createScore)104104 105-    LaunchedEffect(state.unlocked, autofillPending) {105+    // Keyed on `state.unlocked` only: the autofill nav path in PeachAppNavHost consumes106+    // `newEntryRequest` synchronously, which would otherwise flip `autofillPending` to107+    // false mid-unlock and re-fire this effect — landing the user on the vault list108+    // instead of the new-entry editor. Capturing `autofillPending` at the unlock109+    // transition (rather than re-reading it on every key change) makes the two effects110+    // cooperative instead of racing.111+    LaunchedEffect(state.unlocked) {106112         if (state.unlocked) {107113             // A freshly created vault routes through recovery-word setup before the list;108114             // an ordinary unlock lands on the last-viewed entry (brief Fix 3). The autofill109115             // nav host handles its own destinations when autofillPending is set; stepping in110-            // here would race with it (this LaunchedEffect's popUpTo(0) wins by ordering).116+            // here would clobber that navigation with `popUpTo(0)`.111117             if (state.created) onNavigate(PeachNav.RecoverySetup)112118             else if (autofillPending) Unit113119             else onUnlocked(vm.postUnlockDestination())

view file

app/src/test/java/com/peachpasswords/autofill/AutofillNavRepositoryTest.kt+50 −0
@@ -0,0 +1,50 @@1+package com.peachpasswords.autofill2+3+import org.junit.Assert.assertEquals4+import org.junit.Assert.assertNull5+import org.junit.Test6+7+/**8+ * AutofillNavRepository carries the one-shot "open the new-entry editor" request9+ * from the autofill picker through MainActivity to PeachAppNavHost. The NavHost10+ * consumes the request as part of its LaunchedEffect, so the repository must11+ * surface the request exactly once before clearing — re-firing the LockScreen12+ * post-unlock branch after the consume is what made the new-entry redirect13+ * land on the vault list instead of the editor.14+ */15+class AutofillNavRepositoryTest {16+17+    @Test fun request_makes_payload_visible_to_collectors() {18+        val repo = AutofillNavRepository()19+        assertNull(repo.newEntryRequest.value)20+        repo.requestNewEntry(url = "https://github.com/login", username = "june", email = null)21+        val req = repo.newEntryRequest.value22+        assertEquals("https://github.com/login", req?.url)23+        assertEquals("june", req?.username)24+        assertNull(req?.email)25+    }26+27+    @Test fun blank_optional_fields_are_dropped_from_the_payload() {28+        // Hosts that don't surface a typed username/email pass empty strings;29+        // these must not pollute the new-entry editor's prefill.30+        val repo = AutofillNavRepository()31+        repo.requestNewEntry(url = "", username = "", email = "")32+        val req = repo.newEntryRequest.value33+        assertEquals(null, req?.url)34+        assertEquals(null, req?.username)35+        assertEquals(null, req?.email)36+    }37+38+    @Test fun consume_clears_the_request_so_lock_screen_does_not_re_fire_on_post_unlock() {39+        // Regression: the v2.14.5 guard in LockScreen was keyed on `autofillPending`,40+        // so when the NavHost's LaunchedEffect consumed the request the key41+        // flipped and LockScreen re-fired `onUnlocked(...)`, dropping the user42+        // on the vault list. The repository must surface exactly one non-null43+        // value across the request/consume cycle, never two.44+        val repo = AutofillNavRepository()45+        repo.requestNewEntry(url = "https://example.com", username = null, email = null)46+        assertEquals("https://example.com", repo.newEntryRequest.value?.url)47+        repo.consumeNewEntryRequest()48+        assertNull(repo.newEntryRequest.value)49+    }50+}

view file

releases/v2.14.9.md+2 −0
@@ -0,0 +1,2 @@1+## Fixes2+- Tapping "+ Create new entry" from the autofill picker used to land you back in the vault list after unlocking instead of opening the new-entry editor with the URL pre-filled. LockScreen and the NavHost were both reacting to the unlock transition; the NavHost consumed the autofill request first, which flipped the LockScreen guard's key and re-fired its "where to land?" branch — overwriting the editor with a `popUpTo(0)`.

view file