package com.peachpasswords.ui.vault import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.em import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.vault.VaultEntry /** * D2 — EntryDetail CARD display block. Drop a single call to [CardDetailFields] * where the login username/password/notes rows are rendered, gated by * `e.isCard()`. * * This file defines its own row primitives so it does not depend on the private * `FieldShell`/`CopyButton` in EntryDetailScreen.kt. The orchestrator may instead * pass the existing `CopyButton`/`FieldShell` in (see INTEGRATION.md) — the * preferred wiring — but these mirrors keep the draft compilable in isolation. * * Reveal/biometric gating mirrors the password row exactly: CARD NUMBER reveal is * a plain local toggle (full PAN is shown), while CVC reveal is biometric-gated * via the same `onRevealCvc` callback the host wires to Biometrics + vm.reveal(). */ @Composable fun CardDetailFields( entry: VaultEntry, cardRevealed: Boolean, cvcRevealed: Boolean, copiedField: String?, onToggleCardReveal: () -> Unit, onRevealCvc: () -> Unit, onMaskCvc: () -> Unit, onCopy: (field: String, value: String?) -> Unit, onSaveCard: (cardNickname: String, cardNumber: String, cardholderName: String, expiry: String, cvc: String) -> Unit, copyButton: @Composable (copied: Boolean, onCopy: () -> Unit) -> Unit, ) { val accent = LocalPeachAccent.current val rawNumber = entry.cardNumber val network = entry.cardNetwork ?: CardFieldUtils.detectNetwork(rawNumber.orEmpty()) var editing by remember(entry.id, entry.modifiedAt) { mutableStateOf(false) } if (editing) { var cardNumber by remember(entry.id, entry.modifiedAt) { mutableStateOf(CardFieldUtils.formatPan(entry.cardNumber.orEmpty(), network)) } var cardNickname by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cardNickname.orEmpty()) } var cardholder by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cardholderName.orEmpty()) } var expiry by remember(entry.id, entry.modifiedAt) { mutableStateOf(CardFieldUtils.displayExpiry(entry.expMonth, entry.expYear)) } var cvc by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cvc.orEmpty()) } var showCvc by remember { mutableStateOf(false) } val draftNetwork = CardFieldUtils.detectNetwork(cardNumber) CardEditFields( cardNickname = cardNickname, cardNumber = cardNumber, cardholderName = cardholder, expiry = expiry, cvc = cvc, showCvc = showCvc, network = draftNetwork, onCardNickname = { cardNickname = it }, onCardNumber = { cardNumber = it }, onCardholder = { cardholder = it }, onExpiry = { expiry = it }, onCvc = { cvc = it }, onToggleShowCvc = { showCvc = !showCvc }, ) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) { Text( "✓ save", color = accent, style = PeachType.labelMono, modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { onSaveCard(cardNickname, cardNumber, cardholder, expiry, cvc) editing = false }.padding(vertical = 4.dp), ) Text( "✗", color = peachColors.T2, style = PeachType.labelMono, modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { editing = false }.padding(horizontal = 6.dp, vertical = 4.dp), ) } return } Text( "edit card", color = accent, style = PeachType.labelMono, modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { editing = true }.padding(vertical = 4.dp), ) // CARD NUMBER — masked w/ last-4 by default; reveal shows full grouped PAN; copy = full unmasked. if (!rawNumber.isNullOrBlank()) { CardShell( label = "card number", labelTrailing = { val net = CardFieldUtils.networkLabel(network) if (net.isNotEmpty()) { Text(net, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp) } }, ) { Text( text = if (cardRevealed) CardFieldUtils.formatPan(rawNumber, network) else CardFieldUtils.maskedPan(rawNumber, network, entry.last4), color = peachColors.T0, style = PeachType.bodyMono, modifier = Modifier.weight(1f), ) Text( if (cardRevealed) "hide" else "show", color = accent, style = PeachType.labelMono, modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onToggleCardReveal).padding(8.dp), ) copyButton(copiedField == "cardNumber") { onCopy("cardNumber", CardFieldUtils.digitsOnly(rawNumber)) } } } entry.cardholderName?.takeIf { it.isNotBlank() }?.let { name -> CardShell("cardholder") { Text(name, color = peachColors.T0, style = PeachType.body, modifier = Modifier.weight(1f)) copyButton(copiedField == "cardholderName") { onCopy("cardholderName", name) } } } val expiry = CardFieldUtils.displayExpiry(entry.expMonth, entry.expYear) if (expiry.isNotBlank()) { CardShell("expiry") { Text(expiry, color = peachColors.T0, style = PeachType.bodyMono, modifier = Modifier.weight(1f)) copyButton(copiedField == "expiry") { onCopy("expiry", expiry) } } } // CVC — masked; reveal is biometric-gated (same path as password reveal). entry.cvc?.takeIf { it.isNotBlank() }?.let { cvc -> CardShell("cvc") { Text( text = if (cvcRevealed) cvc else "•".repeat(cvc.length.coerceIn(3, 4)), color = peachColors.T0, style = PeachType.bodyMono, modifier = Modifier.weight(1f), ) Text( if (cvcRevealed) "hide" else "show", color = accent, style = PeachType.labelMono, modifier = Modifier.clip(RoundedCornerShape(6.dp)) .clickable { if (cvcRevealed) onMaskCvc() else onRevealCvc() } .padding(8.dp), ) copyButton(copiedField == "cvc") { onCopy("cvc", cvc) } } } } /** Mirror of EntryDetailScreen's private `FieldShell` (label + value row + t3 hairline). */ @Composable private fun CardShell( label: String, labelTrailing: (@Composable () -> Unit)? = null, content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit, ) { val hairline = peachColors.T3 Column( Modifier.fillMaxWidth().padding(vertical = 10.dp).drawBehind { drawRect( color = hairline, topLeft = Offset(0f, size.height - 1.dp.toPx()), size = Size(size.width, 1.dp.toPx()), ) }, ) { Row(verticalAlignment = Alignment.CenterVertically) { Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em)) if (labelTrailing != null) { Spacer(Modifier.width(8.dp)) labelTrailing() } } Spacer(Modifier.height(6.dp)) Row(verticalAlignment = Alignment.CenterVertically, content = content) } }