bee916190cd6
fix: Show adoption password validation feedback (v2.7.3)
- time
- 2026-07-27 21:53 UTC · 2 months ago
- machine
- koh2
- parent
- 8d6cf9d4e33b
↓ peach-android-bee91619.tar.gzatlas at this save
files changed4 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 −0app/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 {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) {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 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+}