f9ec7f8479b4

fix: Match browser card expiry entry (v2.3.16)

time
2026-06-26 01:27 UTC · 4 months ago
machine
koh2
parent
53183c79f084
7 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 −2
app/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 {

view file

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)

view file

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 false

view file

app/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                     ),

view file

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 },

view file

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 }

view file

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     }

view file