789289291437

add the four create-vault pieces missing vs Lock.tsx: blocker message on the submit button (reuses the existing createValidationError, suppressed while the field is empty) so a disabled button explains itself instead of silently doing nothing; show/hide reveal toggle via a new masked param on UnderlineField; static At-least-12-characters hint that the strength bar replaces once typing starts; and back-to-onboarding by keeping Onboarding on the nav backstack (system back gesture, no custom button) with the four post-vault escape points switched from popUpTo(Lock) to the popUpTo(0) pattern already used by the panic route, so a returning user with no Onboarding entry still clears correctly. Live-verified all three blocker states, reveal toggle, hint-to-strength swap, back-to-card-3, and full create to recovery-words to vault-list with the pre-vault stack cleared.

time
2026-07-24 18:52 UTC · 3 months ago
machine
koh2
agent
claude-opus-5
session
create-vault surface parity
parent
7e62540bd7b5
2 files app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+5 −7app/src/main/java/com/peachpasswords/ui/lock/LockScreen.kt+41 −5
app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+5 −7
@@ -129,9 +129,7 @@129129         composable(PeachNav.Onboarding.route) {130130             OnboardingScreen(131131                 onFinished = {132-                    navController.navigate(PeachNav.Lock.route) {133-                        popUpTo(PeachNav.Onboarding.route) { inclusive = true }134-                    }132+                    navController.navigate(PeachNav.Lock.route)135133                 },136134                 onRestore = { nav(PeachNav.Import) },137135             )@@ -140,7 +138,7 @@140138             LockScreen(141139                 onUnlocked = { dest ->142140                     navController.navigate(dest.route) {143-                        popUpTo(PeachNav.Lock.route) { inclusive = true }141+                        popUpTo(0) { inclusive = true }144142                     }145143                 },146144                 onNavigate = nav,@@ -232,7 +230,7 @@232230                 onBack = back,233231                 onVaultCreated = {234232                     navController.navigate(PeachNav.RecoverySetup.route) {235-                        popUpTo(PeachNav.Lock.route) { inclusive = true }233+                        popUpTo(0) { inclusive = true }236234                     }237235                 },238236             )@@ -253,7 +251,7 @@253251                 requireConfirm = true,254252                 onDone = {255253                     navController.navigate(PeachNav.VaultList.route) {256-                        popUpTo(PeachNav.Lock.route) { inclusive = true }254+                        popUpTo(0) { inclusive = true }257255                     }258256                 },259257             )@@ -265,7 +263,7 @@265263             RecoveryWordEntryScreen(266264                 onSuccess = {267265                     navController.navigate(PeachNav.VaultList.route) {268-                        popUpTo(PeachNav.Lock.route) { inclusive = true }266+                        popUpTo(0) { inclusive = true }269267                     }270268                 },271269                 onBack = back,

view file

app/src/main/java/com/peachpasswords/ui/lock/LockScreen.kt+41 −5
@@ -44,6 +44,7 @@4444 import androidx.compose.ui.text.input.ImeAction4545 import androidx.compose.ui.text.input.KeyboardType4646 import androidx.compose.ui.text.input.PasswordVisualTransformation47+import androidx.compose.ui.text.input.VisualTransformation4748 import androidx.compose.ui.text.style.TextAlign4849 import androidx.compose.ui.unit.IntOffset4950 import androidx.compose.ui.unit.dp@@ -80,7 +81,13 @@8081 8182     var password by remember { mutableStateOf("") }8283     var confirm by remember { mutableStateOf("") }84+    var reveal by remember { mutableStateOf(false) }8385     val createScore = remember(password) { scorePassword(password) }86+    // Why the create button is disabled, shown on the button itself. Without87+    // it the button just silently does nothing (browser parity: Lock.tsx's88+    // createBlockerMessage). Null before typing — no scolding on an empty form.89+    val createBlocker = if (state.hasVault || password.isEmpty()) null90+                        else createValidationError(password, confirm, createScore)8491 8592     LaunchedEffect(state.unlocked) {8693         if (state.unlocked) {@@ -164,7 +171,21 @@164171             GlassPanel(165172                 modifier = Modifier.offset { IntOffset(shake.value.dp.roundToPx(), 0) },166173             ) {167-                FieldLabel(if (state.hasVault) "master password" else "create a master password")174+                Row(verticalAlignment = Alignment.CenterVertically) {175+                    FieldLabel(if (state.hasVault) "master password" else "create a master password")176+                    if (!state.hasVault) {177+                        Spacer(Modifier.weight(1f))178+                        Text(179+                            text = if (reveal) "hide" else "show",180+                            color = accent,181+                            style = PeachType.labelMono.copy(fontSize = 11.sp),182+                            modifier = Modifier183+                                .clip(RoundedCornerShape(6.dp))184+                                .clickable { reveal = !reveal }185+                                .padding(horizontal = 6.dp, vertical = 4.dp),186+                        )187+                    }188+                }168189                 Spacer(Modifier.height(4.dp))169190                 UnderlineField(170191                     value = password,@@ -173,6 +194,7 @@173194                     textStyle = if (state.hasVault) PeachType.unlockInput else mono18Spaced(),174195                     imeAction = if (state.hasVault) ImeAction.Go else ImeAction.Next,175196                     onImeAction = { if (state.hasVault) vm.unlock(password) },197+                    masked = state.hasVault || !reveal,176198                 )177199 178200                 if (!state.hasVault) {@@ -186,10 +208,18 @@186208                         textStyle = mono18Spaced(),187209                         imeAction = ImeAction.Go,188210                         onImeAction = { vm.create(password, confirm) },211+                        masked = !reveal,189212                     )190213 191-                    if (password.isNotEmpty()) {192-                        Spacer(Modifier.height(14.dp))214+                    Spacer(Modifier.height(14.dp))215+                    if (password.isEmpty()) {216+                        Text(217+                            text = "At least $MIN_PASSWORD_LENGTH characters. The vault is created once both fields match and pass the strength check.",218+                            color = peachColors.T2,219+                            style = PeachType.hint,220+                            textAlign = TextAlign.Center,221+                        )222+                    } else {193223                         StrengthBar(score = createScore)194224                         Spacer(Modifier.height(6.dp))195225                         Text(@@ -212,7 +242,12 @@212242 213243                 Spacer(Modifier.height(4.dp))214244                 AccentButton(215-                    label = if (state.hasVault) "unlock" else "create vault",245+                    label = when {246+                        state.hasVault -> "unlock"247+                        state.loading -> "creating..."248+                        createBlocker != null -> createBlocker249+                        else -> "create vault"250+                    },216251                     enabled = !state.loading && (state.hasVault || canSubmitCreateVault(password, confirm, createScore)),217252                     onClick = { if (state.hasVault) vm.unlock(password) else vm.create(password, confirm) },218253                 )@@ -283,6 +318,7 @@283318     textStyle: TextStyle,284319     imeAction: ImeAction,285320     onImeAction: () -> Unit,321+    masked: Boolean = true,286322 ) {287323     val accent = LocalPeachAccent.current288324     val interaction = remember { MutableInteractionSource() }@@ -294,7 +330,7 @@294330         singleLine = true,295331         textStyle = textStyle.copy(color = peachColors.T0, textAlign = TextAlign.Center),296332         cursorBrush = SolidColor(accent),297-        visualTransformation = PasswordVisualTransformation(),333+        visualTransformation = if (masked) PasswordVisualTransformation() else VisualTransformation.None,298334         keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = imeAction),299335         keyboardActions = KeyboardActions(onGo = { onImeAction() }, onNext = { onImeAction() }),300336         interactionSource = interaction,

view file