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 (digits.first()) {18 '4' -> "visa"19 '5' -> "mastercard"20 '3' -> "amex"21 '6' -> "discover"22 else -> "unknown"23 }24 }2526 27 fun networkLabel(network: String?): String = when (network) {28 "visa" -> "Visa"29 "mastercard" -> "Mastercard"30 "amex" -> "Amex"31 "discover" -> "Discover"32 else -> ""33 }3435 36 fun panMaxDigits(network: String?): Int = if (network == "amex") 15 else 163738 39 fun cvcMaxDigits(network: String?): Int = if (network == "amex") 4 else 34041 42434445 fun formatPan(rawNumber: String, network: String? = null): String {46 val net = network ?: detectNetwork(rawNumber)47 val digits = rawNumber.filter { it.isDigit() }.take(panMaxDigits(net))48 val groups = if (net == "amex") intArrayOf(4, 6, 5) else intArrayOf(4, 4, 4, 4)49 val out = StringBuilder()50 var i = 051 for (g in groups) {52 if (i >= digits.length) break53 if (out.isNotEmpty()) out.append(' ')54 out.append(digits.substring(i, minOf(i + g, digits.length)))55 i += g56 }57 return out.toString()58 }5960 61 fun digitsOnly(value: String): String = value.filter { it.isDigit() }6263 64 fun last4(rawNumber: String?): String {65 val digits = (rawNumber ?: "").filter { it.isDigit() }66 return if (digits.length >= 4) digits.takeLast(4) else digits67 }6869 7071727374 fun maskedPan(rawNumber: String?, network: String? = null, last4: String? = null): String {75 val digits = (rawNumber ?: "").filter { it.isDigit() }76 77 val storedTail = last4?.filter { it.isDigit() }?.takeIf { it.isNotEmpty() }78 if (digits.isEmpty() && storedTail == null) return ""79 val net = network ?: detectNetwork(digits)80 val tail = storedTail ?: if (digits.length >= 4) digits.takeLast(4) else digits81 val groups = if (net == "amex") intArrayOf(4, 6, 5) else intArrayOf(4, 4, 4, 4)82 83 val total = groups.sum()84 val visibleFrom = (total - tail.length).coerceAtLeast(0)85 val chars = CharArray(total) { idx -> if (idx >= visibleFrom) tail[idx - visibleFrom] else '•' }86 val out = StringBuilder()87 var i = 088 for (g in groups) {89 if (out.isNotEmpty()) out.append(' ')90 out.append(chars, i, g)91 i += g92 }93 return out.toString()94 }9596 97 fun formatExpiry(raw: String): String {98 val digits = raw.filter { it.isDigit() }.take(4)99 return when {100 digits.length <= 2 -> digits101 else -> digits.substring(0, 2) + "/" + digits.substring(2)102 }103 }104105 106 fun displayExpiry(expMonth: String?, expYear: String?): String {107 val mm = (expMonth ?: "").filter { it.isDigit() }.takeLast(2).padStart(2, '0').takeIf { expMonth?.isNotBlank() == true } ?: ""108 val yy = (expYear ?: "").filter { it.isDigit() }.takeLast(2)109 return if (mm.isNotBlank() && yy.isNotBlank()) "$mm/$yy" else (mm.takeIf { it.isNotBlank() } ?: yy)110 }111112 113114115116 fun isExpiryValid(formatted: String, currentYear2: Int, currentMonth: Int): Boolean {117 val digits = formatted.filter { it.isDigit() }118 if (digits.length != 4) return false119 val mm = digits.substring(0, 2).toIntOrNull() ?: return false120 val yy = digits.substring(2, 4).toIntOrNull() ?: return false121 if (mm !in 1..12) return false122 if (yy < currentYear2) return false123 if (yy == currentYear2 && mm < currentMonth) return false124 return true125 }126}