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: (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 cardholder by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cardholderName.orEmpty()) }69 var expiry by remember(entry.id, entry.modifiedAt) {70 mutableStateOf(CardFieldUtils.displayExpiry(entry.expMonth, entry.expYear))71 }72 var cvc by remember(entry.id, entry.modifiedAt) { mutableStateOf(entry.cvc.orEmpty()) }73 var showCvc by remember { mutableStateOf(false) }74 val draftNetwork = CardFieldUtils.detectNetwork(cardNumber)75 CardEditFields(76 cardNumber = cardNumber,77 cardholderName = cardholder,78 expiry = expiry,79 cvc = cvc,80 showCvc = showCvc,81 network = draftNetwork,82 onCardNumber = { cardNumber = it },83 onCardholder = { cardholder = it },84 onExpiry = { expiry = it },85 onCvc = { cvc = it },86 onToggleShowCvc = { showCvc = !showCvc },87 )88 Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {89 Text(90 "✓ save", color = accent, style = PeachType.labelMono,91 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable {92 onSaveCard(cardNumber, cardholder, expiry, cvc)93 editing = false94 }.padding(vertical = 4.dp),95 )96 Text(97 "✗", color = peachColors.T2, style = PeachType.labelMono,98 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { editing = false }.padding(horizontal = 6.dp, vertical = 4.dp),99 )100 }101 return102 }103104 Text(105 "edit card",106 color = accent,107 style = PeachType.labelMono,108 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { editing = true }.padding(vertical = 4.dp),109 )110111 112 if (!rawNumber.isNullOrBlank()) {113 CardShell(114 label = "card number",115 labelTrailing = {116 val net = CardFieldUtils.networkLabel(network)117 if (net.isNotEmpty()) {118 Text(net, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp)119 }120 },121 ) {122 Text(123 text = if (cardRevealed) CardFieldUtils.formatPan(rawNumber, network)124 else CardFieldUtils.maskedPan(rawNumber, network, entry.last4),125 color = peachColors.T0,126 style = PeachType.bodyMono,127 modifier = Modifier.weight(1f),128 )129 Text(130 if (cardRevealed) "hide" else "show", color = accent, style = PeachType.labelMono,131 modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable(onClick = onToggleCardReveal).padding(8.dp),132 )133 copyButton(copiedField == "cardNumber") { onCopy("cardNumber", CardFieldUtils.digitsOnly(rawNumber)) }134 }135 }136137 entry.cardholderName?.takeIf { it.isNotBlank() }?.let { name ->138 CardShell("cardholder") {139 Text(name, color = peachColors.T0, style = PeachType.body, modifier = Modifier.weight(1f))140 copyButton(copiedField == "cardholderName") { onCopy("cardholderName", name) }141 }142 }143144 val expiry = CardFieldUtils.displayExpiry(entry.expMonth, entry.expYear)145 if (expiry.isNotBlank()) {146 CardShell("expiry") {147 Text(expiry, color = peachColors.T0, style = PeachType.bodyMono, modifier = Modifier.weight(1f))148 copyButton(copiedField == "expiry") { onCopy("expiry", expiry) }149 }150 }151152 153 entry.cvc?.takeIf { it.isNotBlank() }?.let { cvc ->154 CardShell("cvc") {155 Text(156 text = if (cvcRevealed) cvc else "•".repeat(cvc.length.coerceIn(3, 4)),157 color = peachColors.T0,158 style = PeachType.bodyMono,159 modifier = Modifier.weight(1f),160 )161 Text(162 if (cvcRevealed) "hide" else "show", color = accent, style = PeachType.labelMono,163 modifier = Modifier.clip(RoundedCornerShape(6.dp))164 .clickable { if (cvcRevealed) onMaskCvc() else onRevealCvc() }165 .padding(8.dp),166 )167 copyButton(copiedField == "cvc") { onCopy("cvc", cvc) }168 }169 }170}171172173@Composable174private fun CardShell(175 label: String,176 labelTrailing: (@Composable () -> Unit)? = null,177 content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit,178) {179 val hairline = peachColors.T3180 Column(181 Modifier.fillMaxWidth().padding(vertical = 10.dp).drawBehind {182 drawRect(183 color = hairline,184 topLeft = Offset(0f, size.height - 1.dp.toPx()),185 size = Size(size.width, 1.dp.toPx()),186 )187 },188 ) {189 Row(verticalAlignment = Alignment.CenterVertically) {190 Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))191 if (labelTrailing != null) {192 Spacer(Modifier.width(8.dp))193 labelTrailing()194 }195 }196 Spacer(Modifier.height(6.dp))197 Row(verticalAlignment = Alignment.CenterVertically, content = content)198 }199}