bee916190cd6

fix: Show adoption password validation feedback (v2.7.3)

time
2026-07-27 21:53 UTC · 2 months ago
machine
koh2
parent
8d6cf9d4e33b
4 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/pairing/AdoptVaultScreen.kt+43 −6app/src/main/java/com/peachpasswords/ui/pairing/CodePairScreen.kt+23 −4app/src/test/java/com/peachpasswords/ui/pairing/AdoptVaultPasswordValidationTest.kt+49 −0
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 24012920-        versionName "2.7.2"19+        versionCode 24013020+        versionName "2.7.3"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/pairing/AdoptVaultScreen.kt+43 −6
@@ -20,6 +20,9 @@2020 import androidx.compose.runtime.setValue2121 import androidx.compose.ui.Alignment2222 import androidx.compose.ui.Modifier23+import androidx.compose.ui.semantics.LiveRegionMode24+import androidx.compose.ui.semantics.liveRegion25+import androidx.compose.ui.semantics.semantics2326 import androidx.compose.ui.text.input.PasswordVisualTransformation2427 import androidx.compose.ui.text.input.VisualTransformation2528 import androidx.compose.ui.text.style.TextAlign@@ -50,8 +53,14 @@5053     var reveal by remember { mutableStateOf(false) }5154     var showBackgroundSyncPrompt by remember { mutableStateOf(false) }5255     val score = scorePassword(password)53-    val passwordReady =54-        password.length >= 12 && password == confirmation && score >= 256+    val passwordBlocker = adoptionPasswordBlocker(password, confirmation, score)57+    val passwordReady = passwordBlocker == null58+    val passwordFeedback = if (password.isEmpty() && confirmation.isEmpty()) {59+        null60+    } else {61+        passwordBlocker62+    }63+    val confirmationMismatch = confirmation.isNotEmpty() && password != confirmation5564 5665     ShellBackground {5766         Column(@@ -117,6 +126,17 @@117126                         singleLine = true,118127                         visualTransformation =119128                             if (reveal) VisualTransformation.None else PasswordVisualTransformation(),129+                        isError = confirmationMismatch,130+                        supportingText = {131+                            if (confirmationMismatch) {132+                                Text(133+                                    "Passwords don’t match.",134+                                    modifier = Modifier.semantics {135+                                        liveRegion = LiveRegionMode.Polite136+                                    },137+                                )138+                            }139+                        },120140                         modifier = Modifier.fillMaxWidth(),121141                     )122142                     Spacer(Modifier.height(PeachSpacing.sm))@@ -152,20 +172,24 @@152172                         "Continue",153173                         LocalPeachAccent.current,154174                         filled = true,175+                        enabled = passwordReady,155176                     ) {156-                        if (passwordReady && vm.acceptPassword(password, confirmation)) {177+                        if (vm.acceptPassword(password, confirmation)) {157178                             password = ""158179                             confirmation = ""159180                         }160181                     }161-                    if (!passwordReady) {182+                    if (passwordFeedback != null && !confirmationMismatch) {162183                         Spacer(Modifier.height(PeachSpacing.xs))163184                         Text(164-                            "Continue unlocks when both passwords match and pass the strength check.",165-                            color = peachColors.T2,185+                            passwordFeedback,186+                            color = peachColors.Breach,166187                             fontFamily = PeachType.Mono,167188                             fontSize = 10.sp,168189                             textAlign = TextAlign.Center,190+                            modifier = Modifier.semantics {191+                                liveRegion = LiveRegionMode.Polite192+                            },169193                         )170194                     }171195                 }@@ -260,6 +284,19 @@260284 internal const val ADOPT_RELAY_COPY =261285     "Scan the QR on the device that already has your vault. That device chooses " +262286         "Peach's public relay or your self-hosted relay, and this phone joins the same one."287+288+internal fun adoptionPasswordBlocker(289+    password: String,290+    confirmation: String,291+    score: Int,292+): String? = when {293+    password.isEmpty() -> "Enter a password."294+    password.length < 12 -> "Use at least 12 characters."295+    confirmation.isEmpty() -> "Confirm your password."296+    password != confirmation -> "Passwords don’t match."297+    score < 2 -> "Use a stronger password or longer passphrase."298+    else -> null299+}263300 264301 @Composable265302 private fun Body(value: String) {

view file

app/src/main/java/com/peachpasswords/ui/pairing/CodePairScreen.kt+23 −4
@@ -26,6 +26,7 @@2626 import androidx.compose.ui.platform.LocalContext2727 import androidx.compose.ui.Alignment2828 import androidx.compose.ui.Modifier29+import androidx.compose.ui.semantics.Role2930 import androidx.compose.ui.text.font.FontStyle3031 import androidx.compose.ui.text.style.TextAlign3132 import androidx.compose.ui.unit.dp@@ -208,18 +209,36 @@208209 209210 /** Full-width pill used for every action on the pairing screens. */210211 @Composable211-internal fun PillButton(label: String, color: Color, filled: Boolean, onClick: () -> Unit) {212+internal fun PillButton(213+    label: String,214+    color: Color,215+    filled: Boolean,216+    enabled: Boolean = true,217+    onClick: () -> Unit,218+) {212219     val accent = LocalPeachAccent.current220+    val buttonColor = if (filled) accent else color213221     Box(214222         modifier = Modifier215223             .fillMaxWidth()216224             .clip(RoundedCornerShape(14.dp))217-            .background(if (filled) accent.copy(alpha = 0.14f) else Color.Transparent)218-            .clickable(onClick = onClick)225+            .background(226+                if (filled) {227+                    accent.copy(alpha = if (enabled) 0.14f else 0.05f)228+                } else {229+                    Color.Transparent230+                },231+            )232+            .clickable(enabled = enabled, role = Role.Button, onClick = onClick)219233             .padding(vertical = PeachSpacing.md),220234         contentAlignment = Alignment.Center,221235     ) {222-        Text(label, color = if (filled) accent else color, fontFamily = PeachType.Mono, fontSize = 13.sp)236+        Text(237+            label,238+            color = buttonColor.copy(alpha = if (enabled) 1f else 0.38f),239+            fontFamily = PeachType.Mono,240+            fontSize = 13.sp,241+        )223242     }224243 }225244 

view file

app/src/test/java/com/peachpasswords/ui/pairing/AdoptVaultPasswordValidationTest.kt+49 −0
@@ -0,0 +1,49 @@1+package com.peachpasswords.ui.pairing2+3+import org.junit.Assert.assertEquals4+import org.junit.Assert.assertNull5+import org.junit.Test6+7+class AdoptVaultPasswordValidationTest {8+    @Test fun empty_password_is_blocked() {9+        assertEquals("Enter a password.", adoptionPasswordBlocker("", "", score = 0))10+    }11+12+    @Test fun short_password_names_the_length_requirement() {13+        assertEquals(14+            "Use at least 12 characters.",15+            adoptionPasswordBlocker("too-short", "too-short", score = 3),16+        )17+    }18+19+    @Test fun missing_confirmation_is_named() {20+        assertEquals(21+            "Confirm your password.",22+            adoptionPasswordBlocker("long enough password", "", score = 3),23+        )24+    }25+26+    @Test fun typo_names_the_mismatch() {27+        assertEquals(28+            "Passwords don’t match.",29+            adoptionPasswordBlocker("long enough password", "long enough passw0rd", score = 3),30+        )31+    }32+33+    @Test fun weak_matching_password_is_blocked() {34+        assertEquals(35+            "Use a stronger password or longer passphrase.",36+            adoptionPasswordBlocker("aaaaaaaaaaaa", "aaaaaaaaaaaa", score = 1),37+        )38+    }39+40+    @Test fun strong_matching_password_is_ready() {41+        assertNull(42+            adoptionPasswordBlocker(43+                "a long varied passphrase 42!",44+                "a long varied passphrase 42!",45+                score = 3,46+            ),47+        )48+    }49+}

view file