f9ec7f8479b4
fix: Match browser card expiry entry (v2.3.16)
- time
- 2026-06-26 01:27 UTC · 4 months ago
- machine
- koh2
- parent
- 53183c79f084
↓ peach-android-f9ec7f84.tar.gzatlas at this save
files changed7 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/vault/CardEditFields.kt+19 −7app/src/main/java/com/peachpasswords/ui/vault/CardFieldUtils.kt+21 −9app/src/main/java/com/peachpasswords/ui/vault/EntryDetailViewModel.kt+1 −1app/src/main/java/com/peachpasswords/ui/vault/EntryEditViewModel.kt+3 −3app/src/test/java/com/peachpasswords/ui/vault/EntryDetailScreenTest.kt+2 −1app/src/test/java/com/peachpasswords/ui/vault/EntryEditScreenTest.kt+6 −2app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23150020- versionName "2.3.15"19+ versionCode 23160020+ versionName "2.3.16"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/ui/vault/CardEditFields.kt+19 −7
@@ -1,6 +1,8 @@11 package com.peachpasswords.ui.vault22 33 import androidx.compose.foundation.clickable4+import androidx.compose.foundation.layout.Arrangement5+import androidx.compose.foundation.layout.Row46 import androidx.compose.foundation.layout.padding57 import androidx.compose.foundation.shape.RoundedCornerShape68 import androidx.compose.material3.Text@@ -72,13 +74,23 @@7274 mono = false,7375 )7476 75- // EXPIRY — auto "MM/YY".76- PeachField(77- label = "expiry",78- value = expiry,79- onChange = { raw -> onExpiry(CardFieldUtils.formatExpiry(raw)) },80- keyboardType = KeyboardType.Number,81- )77+ // EXPIRY — browser-style split MM / YYYY fields.78+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {79+ PeachField(80+ label = "expiry mm",81+ value = CardFieldUtils.expiryMonth(expiry),82+ onChange = { raw -> onExpiry(CardFieldUtils.combineExpiry(raw, CardFieldUtils.expiryYear(expiry))) },83+ keyboardType = KeyboardType.Number,84+ modifier = Modifier.weight(1f),85+ )86+ PeachField(87+ label = "expiry year",88+ value = CardFieldUtils.expiryYear(expiry),89+ onChange = { raw -> onExpiry(CardFieldUtils.combineExpiry(CardFieldUtils.expiryMonth(expiry), raw)) },90+ keyboardType = KeyboardType.Number,91+ modifier = Modifier.weight(1f),92+ )93+ }8294 8395 // CVC — masked by default, eye toggle, maxLength by network.8496 val cvcMax = CardFieldUtils.cvcMaxDigits(network)app/src/main/java/com/peachpasswords/ui/vault/CardFieldUtils.kt+21 −9
@@ -94,31 +94,43 @@9494 return out.toString()9595 }9696 97- /** Auto-format expiry as "MM/YY": inserts "/" after 2 digits, caps at 4 digits. */97+ /** Auto-format expiry as "MM/YY" or "MM/YYYY": inserts "/" after 2 digits. */9898 fun formatExpiry(raw: String): String {99- val digits = raw.filter { it.isDigit() }.take(4)99+ val digits = raw.filter { it.isDigit() }.take(6)100100 return when {101101 digits.length <= 2 -> digits102102 else -> digits.substring(0, 2) + "/" + digits.substring(2)103103 }104104 }105105 106- /** Combine stored expMonth/expYear into "MM/YY" for display. */106+ /** Combine stored expMonth/expYear into "MM/YY" or browser-style "MM/YYYY". */107107 fun displayExpiry(expMonth: String?, expYear: String?): String {108108 val mm = (expMonth ?: "").filter { it.isDigit() }.takeLast(2).padStart(2, '0').takeIf { expMonth?.isNotBlank() == true } ?: ""109- val yy = (expYear ?: "").filter { it.isDigit() }.takeLast(2)110- return if (mm.isNotBlank() && yy.isNotBlank()) "$mm/$yy" else (mm.takeIf { it.isNotBlank() } ?: yy)109+ val year = (expYear ?: "").filter { it.isDigit() }.take(4)110+ return if (mm.isNotBlank() && year.isNotBlank()) "$mm/$year" else (mm.takeIf { it.isNotBlank() } ?: year)111111 }112112 113+ fun expiryMonth(formatted: String): String =114+ formatted.filter { it.isDigit() }.take(2)115+116+ fun expiryYear(formatted: String): String =117+ formatted.filter { it.isDigit() }.drop(2).take(4)118+119+ fun combineExpiry(month: String, year: String): String {120+ val mm = month.filter { it.isDigit() }.take(2)121+ val yy = year.filter { it.isDigit() }.take(4)122+ return if (mm.isNotBlank() && yy.isNotBlank()) "$mm/$yy" else mm.ifBlank { yy }123+ }124+113125 /**114- * Validate a "MM/YY" expiry: month 01–12 and 2-digit year >= current 2-digit115- * year. `currentYear2`/`currentMonth` are injected for testability.126+ * Validate a "MM/YY" or "MM/YYYY" expiry: month 01-12 and year >= current127+ * 2-digit year. `currentYear2`/`currentMonth` are injected for testability.116128 */117129 fun isExpiryValid(formatted: String, currentYear2: Int, currentMonth: Int): Boolean {118130 val digits = formatted.filter { it.isDigit() }119- if (digits.length != 4) return false131+ if (digits.length != 4 && digits.length != 6) return false120132 val mm = digits.substring(0, 2).toIntOrNull() ?: return false121- val yy = digits.substring(2, 4).toIntOrNull() ?: return false133+ val yy = digits.takeLast(2).toIntOrNull() ?: return false122134 if (mm !in 1..12) return false123135 if (yy < currentYear2) return false124136 if (yy == currentYear2 && mm < currentMonth) return falseapp/src/main/java/com/peachpasswords/ui/vault/EntryDetailViewModel.kt+1 −1
@@ -201,7 +201,7 @@201201 cardNumber = digits.ifBlank { null },202202 cardholderName = cardholderName.ifBlank { null },203203 expMonth = expDigits.take(2).ifBlank { null },204- expYear = expDigits.drop(2).take(2).ifBlank { null },204+ expYear = expDigits.drop(2).take(4).ifBlank { null },205205 cvc = cvc.ifBlank { null },206206 cardNetwork = network,207207 ),app/src/main/java/com/peachpasswords/ui/vault/EntryEditViewModel.kt+3 −3
@@ -31,7 +31,7 @@3131 val cardNickname: String = "",3232 val cardNumber: String = "",3333 val cardholderName: String = "",34- val expiry: String = "", // "MM/YY"; split into expMonth/expYear on save34+ val expiry: String = "", // "MM/YY" or "MM/YYYY"; split into expMonth/expYear on save3535 val cvc: String = "",3636 val showCvc: Boolean = false,3737 val cardNetwork: String = "unknown",@@ -220,7 +220,7 @@220220 cardNumber = f.cardNumber.filter { it.isDigit() }.ifBlank { null },221221 cardholderName = f.cardholderName.ifBlank { null },222222 expMonth = f.expiry.filter { it.isDigit() }.take(2).ifBlank { null },223- expYear = f.expiry.filter { it.isDigit() }.drop(2).take(2).ifBlank { null },223+ expYear = f.expiry.filter { it.isDigit() }.drop(2).take(4).ifBlank { null },224224 cvc = f.cvc.ifBlank { null },225225 cardNetwork = f.cardNetwork.takeIf { it != "unknown" },226226 tags = f.tags.ifEmpty { null },@@ -244,7 +244,7 @@244244 cardNumber = f.cardNumber.filter { it.isDigit() }.ifBlank { null },245245 cardholderName = f.cardholderName.ifBlank { null },246246 expMonth = f.expiry.filter { it.isDigit() }.take(2).ifBlank { null },247- expYear = f.expiry.filter { it.isDigit() }.drop(2).take(2).ifBlank { null },247+ expYear = f.expiry.filter { it.isDigit() }.drop(2).take(4).ifBlank { null },248248 cvc = f.cvc.ifBlank { null },249249 cardNetwork = f.cardNetwork.takeIf { it != "unknown" },250250 tags = f.tags.ifEmpty { null },app/src/test/java/com/peachpasswords/ui/vault/EntryDetailScreenTest.kt+2 −1
@@ -90,6 +90,7 @@9090 }9191 9292 @Test fun display_expiry_combines_month_and_year() {93- assertEquals("04/30", CardFieldUtils.displayExpiry("04", "2030"))93+ assertEquals("04/2030", CardFieldUtils.displayExpiry("04", "2030"))94+ assertEquals("04/30", CardFieldUtils.displayExpiry("04", "30"))9495 }9596 }app/src/test/java/com/peachpasswords/ui/vault/EntryEditScreenTest.kt+6 −2
@@ -127,10 +127,13 @@127127 assertEquals("unknown", CardFieldUtils.detectNetwork("6000000000000000"))128128 }129129 130- @Test fun new_card_expiry_auto_formats_mm_yy() {130+ @Test fun new_card_expiry_auto_formats_browser_mm_year() {131131 assertEquals("12", CardFieldUtils.formatExpiry("12"))132132 assertEquals("12/28", CardFieldUtils.formatExpiry("1228"))133- assertEquals("12/28", CardFieldUtils.formatExpiry("122899")) // caps at 4 digits133+ assertEquals("12/2899", CardFieldUtils.formatExpiry("122899")) // caps year at 4 digits134+ assertEquals("12", CardFieldUtils.expiryMonth("12/2028"))135+ assertEquals("2028", CardFieldUtils.expiryYear("12/2028"))136+ assertEquals("12/2028", CardFieldUtils.combineExpiry("12", "2028"))134137 }135138 136139 @Test fun new_card_expiry_rejects_invalid_month() {@@ -138,6 +141,7 @@138141 assertFalse(CardFieldUtils.isExpiryValid("13/28", currentYear2 = 26, currentMonth = 6))139142 assertFalse(CardFieldUtils.isExpiryValid("00/28", currentYear2 = 26, currentMonth = 6))140143 assertTrue(CardFieldUtils.isExpiryValid("12/28", currentYear2 = 26, currentMonth = 6))144+ assertTrue(CardFieldUtils.isExpiryValid("12/2028", currentYear2 = 26, currentMonth = 6))141145 // Past year rejected.142146 assertFalse(CardFieldUtils.isExpiryValid("12/24", currentYear2 = 26, currentMonth = 6))143147 }