From e57414de17e0c79ae3ea5b01fd6faa7e56d9cb2b Mon Sep 17 00:00:00 2001 From: rasputin Date: Sat, 13 Jun 2026 16:09:58 +0200 Subject: [PATCH] feat: send from contact details --- .../gudariwallet/ui/WalletScreen.kt | 17 ++++++++++++++--- .../gudariwallet/ui/WalletViewModel.kt | 11 +++++++++++ .../ui/contacts/ContactDetailScreen.kt | 12 +++++++++++- .../gudariwallet/ui/send/SendScreen.kt | 11 +++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt index 737a539..2252514 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt @@ -537,19 +537,30 @@ fun WalletScreen( composable( route = ROUTE_CONTACT_DETAIL, arguments = listOf(navArgument("contactId") { type = NavType.StringType }), - enterTransition = { fadeIn(animationSpec = tween(150)) }, + enterTransition = { fadeIn(animationSpec = tween(150)) }, popExitTransition = { fadeOut(animationSpec = tween(150)) } ) { backStackEntry -> val contactId = backStackEntry.arguments?.getString("contactId") ?: "" val detailVm: ContactDetailViewModel = viewModel( - key = contactId, + key = contactId, factory = ContactDetailViewModel.Factory(contactId, vm.contactRepo) ) ContactDetailScreen( viewModel = detailVm, - onBack = { navController.popBackStack() } + onBack = { navController.popBackStack() }, + onPayAddress = { address -> + vm.prefillSend(address) + navController.navigate(TabItem.Send.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = false // don't restore stale SendScreen state + } + } ) } + } // ── Clipboard banner — floats over content, slides in from top ──── diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt index 06554cf..ca8c187 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletViewModel.kt @@ -23,6 +23,7 @@ import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel import com.bitcointxoko.gudariwallet.ui.send.SendEvent import com.bitcointxoko.gudariwallet.ui.send.SendStrings import com.bitcointxoko.gudariwallet.ui.send.SendViewModel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -116,6 +117,16 @@ class WalletViewModel( val sendEvents: SharedFlow = sendVm.events + // ── Contact tap-to-pay prefill ──────────────────────────────────────────── + private val _pendingSend = MutableStateFlow(null) + val pendingSend: StateFlow get() = _pendingSend + + /** Called by ContactDetailScreen before navigating to the Send tab. */ + fun prefillSend(address: String) { _pendingSend.value = address } + + /** Called by SendScreen once it has consumed the prefilled address. */ + fun consumePendingSend() { _pendingSend.value = null } + fun handleWithdrawScanned(raw: LnurlScanResponse, lnurl: String) { receiveVm.handleWithdrawResponse(raw, lnurl) } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt index 4c58397..35839bc 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/contacts/ContactDetailScreen.kt @@ -1,6 +1,7 @@ package com.bitcointxoko.gudariwallet.ui.contacts import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -37,7 +38,8 @@ import com.bitcointxoko.gudariwallet.ui.common.DetailSection @Composable fun ContactDetailScreen( viewModel: ContactDetailViewModel, - onBack: () -> Unit + onBack: () -> Unit, + onPayAddress: (address: String) -> Unit = {} ) { val strings = LocalAppStrings.current val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -127,6 +129,7 @@ fun ContactDetailScreen( isEditing = isEditing, onRemoveAddress = { addressId -> pendingAddressDelete = addressId }, onSetDefault = { viewModel.setDefaultAddress(it) }, + onPayAddress = onPayAddress, onEditAlias = { viewModel.openEditAliasSheet() }, onDeleteContact = { viewModel.requestDelete() }, modifier = Modifier.padding(innerPadding) @@ -209,6 +212,7 @@ private fun ContactDetailContent( isEditing: Boolean, onRemoveAddress: (addressId: String) -> Unit, onSetDefault: (PaymentAddressEntity) -> Unit, + onPayAddress: (address: String) -> Unit, onEditAlias: () -> Unit, onDeleteContact: () -> Unit, modifier: Modifier = Modifier @@ -276,6 +280,7 @@ private fun ContactDetailContent( AddressRow( address = addr, isEditing = isEditing, + onPay = { onPayAddress(addr.address) }, onRemove = { onRemoveAddress(addr.id) }, onSetDefault = { onSetDefault(addr) } ) @@ -349,6 +354,7 @@ private fun ContactDetailContent( private fun AddressRow( address: PaymentAddressEntity, isEditing: Boolean, + onPay: () -> Unit, onRemove: () -> Unit, onSetDefault: () -> Unit, ) { @@ -365,6 +371,10 @@ private fun AddressRow( Row( modifier = Modifier .fillMaxWidth() + .clickable( + enabled = !isEditing, + onClickLabel = "Pay with this address" + ) { onPay() } .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt index dbe0028..525da0c 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt @@ -59,6 +59,17 @@ fun SendScreen( } val fiatCurrency = (balanceState as? BalanceState.Success)?.fiatCurrency + // ── Consume address prefilled from Contacts tap-to-pay ─────────────────── + val pendingSend by vm.pendingSend.collectAsStateWithLifecycle() + LaunchedEffect(pendingSend) { + val address = pendingSend + if (!address.isNullOrBlank() && sendState is SendState.Idle) { + input = SendInputDetector.normalize(address) + vm.scan(address, sendStrings) + vm.consumePendingSend() + } + } + // ── Collect NFC offer at top level — never inside a conditional ─────────── val nfcOffer by nfcVm.nfcOfferState.collectAsStateWithLifecycle()