1package com.peachpasswords.ui.vault234567891011object CardFieldUtils {1213 14 fun detectNetwork(rawNumber: String): String {15 val digits = rawNumber.filter { it.isDigit() }16 if (digits.isEmpty()) return "unknown"17 return when {18 digits.startsWith("4") -> "visa"19 digits.length >= 2 && digits.take(2).toIntOrNull() in 51..55 -> "mastercard"20 digits.length >= 2 && digits.take(2).toIntOrNull() in 22..27 -> "mastercard"21 digits.startsWith("34") || digits.startsWith("37") -> "amex"22 digits.startsWith("6011") || digits.startsWith("65") -> "discover"23 else -> "unknown"24 }25 }2627 28 fun networkLabel(network: String?): String = when (network) {29 "visa" -> "Visa"30 "mastercard" -> "Mastercard"31 "amex" -> "Amex"32 "discover" -> "Discover"33 else -> ""34 }3536 37 fun panMaxDigits(network: String?): Int = if (network == "amex") 15 else 163839 40 fun cvcMaxDigits(network: String?): Int = if (network == "amex") 4 else 34142 43444546 fun formatPan(rawNumber: String, network: String? = null): String {47 val net = network ?: detectNetwork(rawNumber)48 val digits = rawNumber.filter { it.isDigit() }.take(panMaxDigits(net))49 val groups = if (net == "amex") intArrayOf(4, 6, 5) else intArrayOf(4, 4, 4, 4)50 val out = StringBuilder()51 var i = 052 for (g in groups) {53 if (i >= digits.length) break54 if (out.isNotEmpty()) out.append(' ')55 out.append(digits.substring(i, minOf(i + g, digits.length)))56 i += g57 }58 return out.toString()59 }6061 62 fun digitsOnly(value: String): String = value.filter { it.isDigit() }6364 65 fun last4(rawNumber: String?): String {66 val digits = (rawNumber ?: "").filter { it.isDigit() }67 return if (digits.length >= 4) digits.takeLast(4) else digits68 }6970 7172737475 fun maskedPan(rawNumber: String?, network: String? = null, last4: String? = null): String {76 val digits = (rawNumber ?: "").filter { it.isDigit() }77 78 val storedTail = last4?.filter { it.isDigit() }?.takeIf { it.isNotEmpty() }79 if (digits.isEmpty() && storedTail == null) return ""80 val net = network ?: detectNetwork(digits)81 val tail = storedTail ?: if (digits.length >= 4) digits.takeLast(4) else digits82 val groups = if (net == "amex") intArrayOf(4, 6, 5) else intArrayOf(4, 4, 4, 4)83 84 val total = groups.sum()85 val visibleFrom = (total - tail.length).coerceAtLeast(0)86 val chars = CharArray(total) { idx -> if (idx >= visibleFrom) tail[idx - visibleFrom] else '•' }87 val out = StringBuilder()88 var i = 089 for (g in groups) {90 if (out.isNotEmpty()) out.append(' ')91 out.append(chars, i, g)92 i += g93 }94 return out.toString()95 }9697 98 fun formatExpiry(raw: String): String {99 val digits = raw.filter { it.isDigit() }.take(6)100 return when {101 digits.length <= 2 -> digits102 else -> digits.substring(0, 2) + "/" + digits.substring(2)103 }104 }105106 107 fun displayExpiry(expMonth: String?, expYear: String?): String {108 val mm = (expMonth ?: "").filter { it.isDigit() }.takeLast(2).padStart(2, '0').takeIf { expMonth?.isNotBlank() == true } ?: ""109 val year = (expYear ?: "").filter { it.isDigit() }.take(4)110 return if (mm.isNotBlank() && year.isNotBlank()) "$mm/$year" else (mm.takeIf { it.isNotBlank() } ?: year)111 }112113 fun expiryMonth(formatted: String): String =114 formatted.filter { it.isDigit() }.take(2)115116 fun expiryYear(formatted: String): String =117 formatted.filter { it.isDigit() }.drop(2).take(4)118119 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 }124125 126127128129 fun isExpiryValid(formatted: String, currentYear2: Int, currentMonth: Int): Boolean {130 val digits = formatted.filter { it.isDigit() }131 if (digits.length != 4 && digits.length != 6) return false132 val mm = digits.substring(0, 2).toIntOrNull() ?: return false133 val yy = digits.takeLast(2).toIntOrNull() ?: return false134 if (mm !in 1..12) return false135 if (yy < currentYear2) return false136 if (yy == currentYear2 && mm < currentMonth) return false137 return true138 }139}