1package com.peachpasswords.ui.vault23import androidx.compose.foundation.clickable4import androidx.compose.foundation.layout.Arrangement5import androidx.compose.foundation.layout.Column6import androidx.compose.foundation.layout.Row7import androidx.compose.foundation.layout.Spacer8import androidx.compose.foundation.layout.fillMaxWidth9import androidx.compose.foundation.layout.height10import androidx.compose.foundation.layout.padding11import androidx.compose.foundation.layout.width12import androidx.compose.foundation.shape.RoundedCornerShape13import androidx.compose.material3.Text14import androidx.compose.runtime.Composable15import androidx.compose.runtime.getValue16import androidx.compose.runtime.mutableStateOf17import androidx.compose.runtime.remember18import androidx.compose.runtime.setValue19import androidx.compose.ui.Alignment20import androidx.compose.ui.Modifier21import androidx.compose.ui.draw.clip22import androidx.compose.ui.draw.drawBehind23import androidx.compose.ui.geometry.Offset24import androidx.compose.ui.geometry.Size25import androidx.compose.ui.text.TextStyle26import androidx.compose.ui.unit.em27import androidx.compose.ui.unit.dp28import androidx.compose.ui.unit.sp29import com.peachpasswords.ui.theme.LocalPeachAccent30import com.peachpasswords.ui.theme.peachColors31import com.peachpasswords.ui.theme.PeachType32import com.peachpasswords.vault.VaultEntry33343536373839404142434445464748@Composable49fun CardDetailFields(50 entry: VaultEntry,51 cardRevealed: Boolean,52 cvcRevealed: Boolean,53 copiedField: String?,54 onToggleCardReveal: () -> Unit,55 onRevealCvc: () -> Unit,56 onMaskCvc: () -> Unit,57 onCopy: (field: String, value: String?) -> Unit,58 onSaveCard: (cardNickname: String, cardNumber: String, cardholderName: String, expiry: String, cvc: String) -> Unit,59 copyButton: @Composable (copied: Boolean, onCopy: () -> Unit) -> Unit,60) {61 val accent = LocalPeachAccent.current62 val rawNumber = entry.cardNumber63 val network = entry.cardNetwork ?: CardFieldUtils.detectNetwork(rawNumber.orEmpty())64 var editing by remember(entry.id, entry.modifiedAt) { mutableStateOf(false) }6566 if (editing) {67 var cardNumber by remember(entry.id, entry.modifiedAt) { mutableStateOf(CardFieldUtils.formatPan(entry.cardNumber.orEmpty(), network)) }68 var cardNickname by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cardNickname.orEmpty()) }69 var cardholder by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cardholderName.orEmpty()) }70 var expiry by remember(entry.id, entry.modifiedAt) {71 mutableStateOf(CardFieldUtils.displayExpiry(entry.expMonth, entry.expYear))72 }73 var cvc by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cvc.orEmpty()) }74 var showCvc by remember { mutableStateOf(false) }75 val draftNetwork = CardFieldUtils.detectNetwork(cardNumber)76 CardEditFields(77 cardNickname = cardNickname,78 cardNumber = cardNumber,79 cardholderName = cardholder,80 expiry = expiry,81 cvc = cvc,82 showCvc = showCvc,83 network = draftNetwork,84 onCardNickname = { cardNickname = it },85 onCardNumber = { cardNumber = it },86 onCardholder = { cardholder = it },87 onExpiry = { expiry = it },88 onCvc = { cvc = it },89 onToggleShowCvc = { showCvc = !showCvc },90 )91 Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {92 Text(93 "✓ save", color = accent, style = PeachType.labelMono,94 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable {95 onSaveCard(cardNickname, cardNumber, cardholder, expiry, cvc)96 editing = false97 }.padding(vertical = 4.dp),98 )99 Text(100 "✗", color = peachColors.T2, style = PeachType.labelMono,101 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { editing = false }.padding(horizontal = 6.dp, vertical = 4.dp),102 )103 }104 return105 }106107 Text(108 "edit card",109 color = accent,110 style = PeachType.labelMono,111 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { editing = true }.padding(vertical = 4.dp),112 )113114 115 if (!rawNumber.isNullOrBlank()) {116 CardShell(117 label = "card number",118 labelTrailing = {119 val net = CardFieldUtils.networkLabel(network)120 if (net.isNotEmpty()) {121 Text(net, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp)122 }123 },124 ) {125 Text(126 text = if (cardRevealed) CardFieldUtils.formatPan(rawNumber, network)127 else CardFieldUtils.maskedPan(rawNumber, network, entry.last4),128 color = peachColors.T0,129 style = PeachType.bodyMono,130 modifier = Modifier.weight(1f),131 )132 Text(133 if (cardRevealed) "hide" else "show", color = accent, style = PeachType.labelMono,134 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onToggleCardReveal).padding(8.dp),135 )136 copyButton(copiedField == "cardNumber") { onCopy("cardNumber", CardFieldUtils.digitsOnly(rawNumber)) }137 }138 }139140 entry.cardholderName?.takeIf { it.isNotBlank() }?.let { name ->141 CardShell("cardholder") {142 Text(name, color = peachColors.T0, style = PeachType.body, modifier = Modifier.weight(1f))143 copyButton(copiedField == "cardholderName") { onCopy("cardholderName", name) }144 }145 }146147 val expiry = CardFieldUtils.displayExpiry(entry.expMonth, entry.expYear)148 if (expiry.isNotBlank()) {149 CardShell("expiry") {150 Text(expiry, color = peachColors.T0, style = PeachType.bodyMono, modifier = Modifier.weight(1f))151 copyButton(copiedField == "expiry") { onCopy("expiry", expiry) }152 }153 }154155 156 entry.cvc?.takeIf { it.isNotBlank() }?.let { cvc ->157 CardShell("cvc") {158 Text(159 text = if (cvcRevealed) cvc else "•".repeat(cvc.length.coerceIn(3, 4)),160 color = peachColors.T0,161 style = PeachType.bodyMono,162 modifier = Modifier.weight(1f),163 )164 Text(165 if (cvcRevealed) "hide" else "show", color = accent, style = PeachType.labelMono,166 modifier = Modifier.clip(RoundedCornerShape(6.dp))167 .clickable { if (cvcRevealed) onMaskCvc() else onRevealCvc() }168 .padding(8.dp),169 )170 copyButton(copiedField == "cvc") { onCopy("cvc", cvc) }171 }172 }173}174175176@Composable177private fun CardShell(178 label: String,179 labelTrailing: (@Composable () -> Unit)? = null,180 content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit,181) {182 val hairline = peachColors.T3183 Column(184 Modifier.fillMaxWidth().padding(vertical = 10.dp).drawBehind {185 drawRect(186 color = hairline,187 topLeft = Offset(0f, size.height - 1.dp.toPx()),188 size = Size(size.width, 1.dp.toPx()),189 )190 },191 ) {192 Row(verticalAlignment = Alignment.CenterVertically) {193 Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))194 if (labelTrailing != null) {195 Spacer(Modifier.width(8.dp))196 labelTrailing()197 }198 }199 Spacer(Modifier.height(6.dp))200 Row(verticalAlignment = Alignment.CenterVertically, content = content)201 }202}