From 7b394664202a54cbbc4c98c17174ed8d0ba39a30 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 10:46:43 -0300 Subject: [PATCH 01/19] feat: string resources --- app/src/main/res/values/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df7cd85b1..9489937ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,6 +41,7 @@ Yes, Delete No, Cancel Edit + Hide Details Empty Error Unknown error @@ -60,6 +61,7 @@ Save Search Share + Show Details Skip Success Try Again @@ -73,6 +75,8 @@ ±10-20 minutes ±10m Fast + Lightning Network + Instant Instant +2 hours +2h @@ -950,6 +954,7 @@ Unable to broadcast the transaction. Please try again. Transaction Failed Speed and fee + Send from Set Custom Fee Fee Invalid Unable to increase the fee any further. Otherwise, it will exceed half the current input balance. From 77cd683c312c61b5eac91f59eca78b1afb27cab9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 10:49:19 -0300 Subject: [PATCH 02/19] feat: SendSectionView --- .../bitkit/ui/components/SendSectionView.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/src/main/java/to/bitkit/ui/components/SendSectionView.kt diff --git a/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt b/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt new file mode 100644 index 000000000..2ff05411d --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt @@ -0,0 +1,23 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import to.bitkit.ui.theme.Colors + +@Composable +fun SendSectionView( + caption: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column(modifier = modifier.fillMaxWidth()) { + Caption13Up(text = caption, color = Colors.White64) + VerticalSpacer(8.dp) + content() + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } +} From ad62cea0f805e426d86c7fcb6699a74d7a106aeb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 10:53:56 -0300 Subject: [PATCH 03/19] feat: add swipe progress callback to SwipeToConfirm --- .../java/to/bitkit/ui/components/SwipeToConfirm.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt index 698020593..3c8376241 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -61,6 +62,7 @@ private val Padding = 8.dp @Composable fun SwipeToConfirm( + modifier: Modifier = Modifier, text: String = stringResource(R.string.other__swipe), color: Color = Colors.Brand, icon: ImageVector = Icons.AutoMirrored.Default.ArrowForward, @@ -68,8 +70,8 @@ fun SwipeToConfirm( endIconTint: Color = Colors.Black, loading: Boolean = false, confirmed: Boolean = false, + onProgressChange: ((Float) -> Unit)? = null, onConfirm: () -> Unit, - modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() val trailColor = remember(color) { color.copy(alpha = 0.24f) } @@ -94,6 +96,12 @@ fun SwipeToConfirm( ) } + LaunchedEffect(onProgressChange) { + if (onProgressChange == null) return@LaunchedEffect + snapshotFlow { panX.value / maxPanX } + .collect { onProgressChange(it.coerceIn(0f, 1f)) } + } + Box( modifier = modifier .requiredHeight(CircleSize + Padding * 2) From ef35e2cfae106eed7f20ca1aaf36d6bcc6733b33 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 10:55:40 -0300 Subject: [PATCH 04/19] feat: add Instant enum type --- app/src/main/java/to/bitkit/models/FeeRate.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/models/FeeRate.kt b/app/src/main/java/to/bitkit/models/FeeRate.kt index d4a8785de..6ebef7907 100644 --- a/app/src/main/java/to/bitkit/models/FeeRate.kt +++ b/app/src/main/java/to/bitkit/models/FeeRate.kt @@ -15,6 +15,13 @@ enum class FeeRate( @DrawableRes val icon: Int, val color: Color, ) { + INSTANT( + title = R.string.fee__instant__title, + description = R.string.fee__instant__description, + shortDescription = R.string.fee__instant__shortDescription, + color = Colors.Purple, + icon = R.drawable.ic_lightning, + ), FAST( title = R.string.fee__fast__title, description = R.string.fee__fast__description, @@ -53,7 +60,7 @@ enum class FeeRate( fun toSpeed(): TransactionSpeed { return when (this) { - FAST -> TransactionSpeed.Fast + INSTANT, FAST -> TransactionSpeed.Fast NORMAL -> TransactionSpeed.Medium MINIMUM, SLOW -> TransactionSpeed.Slow CUSTOM -> TransactionSpeed.Custom(0u) From 7f37c8c9b7c52b12821ea98edea49fa1dedb2cbc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 11:01:04 -0300 Subject: [PATCH 05/19] feat: update SendFeeViewModel Custom Fee Defaults. to: current custom speed -> settings default (if custom) -> slow fee rate -> 1 --- .../screens/wallets/send/SendFeeViewModel.kt | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index fd3e4cf6e..e5d46ae87 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R @@ -20,6 +21,7 @@ import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed +import to.bitkit.data.SettingsStore import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo @@ -37,6 +39,7 @@ class SendFeeViewModel @Inject constructor( private val lightningRepo: LightningRepo, private val currencyRepo: CurrencyRepo, private val walletRepo: WalletRepo, + private val settingsStore: SettingsStore, @ApplicationContext private val context: Context, ) : ViewModel() { private val _uiState = MutableStateFlow(SendFeeUiState()) @@ -52,25 +55,31 @@ class SendFeeViewModel @Inject constructor( val selected = FeeRate.fromSpeed(sendUiState.speed) val fees = sendUiState.fees - val custom = when (val speed = sendUiState.speed) { - is TransactionSpeed.Custom -> speed - else -> { - val satsPerVByte = sendUiState.feeRates?.getSatsPerVByteFor(speed) ?: 0u - TransactionSpeed.Custom(satsPerVByte) + viewModelScope.launch { + val custom = when (val speed = sendUiState.speed) { + is TransactionSpeed.Custom -> speed + else -> { + val settingsSpeed = settingsStore.data.first().defaultTransactionSpeed + val satsPerVByte = when (settingsSpeed) { + is TransactionSpeed.Custom -> settingsSpeed.satsPerVByte + else -> sendUiState.feeRates?.slow ?: 1u + } + TransactionSpeed.Custom(satsPerVByte) + } } + calculateMaxSatPerVByte() + val disabledRates = fees.filter { it.value.toULong() > maxFee }.keys.toImmutableSet() + _uiState.update { + it.copy( + selected = selected, + fees = fees, + custom = custom, + input = custom.satsPerVByte.toString().takeIf { custom.satsPerVByte > 0u } ?: "", + disabledRates = disabledRates, + ) + } + updateTotalFeeText() } - calculateMaxSatPerVByte() - val disabledRates = fees.filter { it.value.toULong() > maxFee }.keys.toImmutableSet() - _uiState.update { - it.copy( - selected = selected, - fees = fees, - custom = custom, - input = custom.satsPerVByte.toString().takeIf { custom.satsPerVByte > 0u } ?: "", - disabledRates = disabledRates, - ) - } - updateTotalFeeText() } private fun getFeeLimit(): ULong { From 7ccab2d8731b56e51ebc18ad1d8f0324534250c8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 12:26:57 -0300 Subject: [PATCH 06/19] feat: switch to lighting when selecting instant --- .../screens/wallets/send/SendFeeRateScreen.kt | 52 +++++++++++++++++-- .../java/to/bitkit/ui/sheets/SendSheet.kt | 4 ++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 34 ++++++++++-- 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index fe3326848..b5ef62a32 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -47,15 +47,17 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState @Composable fun SendFeeRateScreen( sendUiState: SendUiState, + viewModel: SendFeeViewModel, onBack: () -> Unit, onContinue: () -> Unit, onSelect: (TransactionSpeed) -> Unit, - viewModel: SendFeeViewModel, + onSelectInstant: () -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -65,9 +67,14 @@ fun SendFeeRateScreen( Content( uiState = uiState, + isUnified = sendUiState.isUnified, + payMethod = sendUiState.payMethod, + estimatedRoutingFee = sendUiState.estimatedRoutingFee.toLong(), onBack = onBack, onContinue = onContinue, - onSelect = { onSelect(it.toSpeed()) }, + onSelect = { + if (it == FeeRate.INSTANT) onSelectInstant() else onSelect(it.toSpeed()) + }, ) } @@ -75,6 +82,9 @@ fun SendFeeRateScreen( private fun Content( uiState: SendFeeUiState, modifier: Modifier = Modifier, + isUnified: Boolean = false, + payMethod: SendMethod = SendMethod.ONCHAIN, + estimatedRoutingFee: Long = 0L, onBack: () -> Unit = {}, onContinue: () -> Unit = {}, onSelect: (FeeRate) -> Unit = {}, @@ -103,11 +113,22 @@ private fun Content( title = stringResource(R.string.wallet__send_fee_and_speed), modifier = Modifier.padding(horizontal = 16.dp) ) + + if (isUnified) { + FeeItem( + feeRate = FeeRate.INSTANT, + sats = estimatedRoutingFee, + isSelected = payMethod == SendMethod.LIGHTNING, + onClick = { onSelect(FeeRate.INSTANT) }, + modifier = Modifier.testTag("fee_INSTANT_button"), + ) + } + uiState.fees.map { (feeRate, sats) -> FeeItem( feeRate = feeRate, sats = sats, - isSelected = uiState.selected == feeRate, + isSelected = uiState.selected == feeRate && payMethod == SendMethod.ONCHAIN, isDisabled = feeRate in uiState.disabledRates, onClick = { if (feeRate !in uiState.disabledRates) onSelect(feeRate) }, modifier = Modifier.testTag("fee_${feeRate.name}_button"), @@ -224,6 +245,31 @@ private fun PreviewCustom() { } } +@Suppress("MagicNumber") +@Preview(showSystemUi = true) +@Composable +private fun PreviewWithInstant() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = SendFeeUiState( + fees = persistentMapOf( + FeeRate.FAST to 4000L, + FeeRate.NORMAL to 3000L, + FeeRate.SLOW to 2000L, + FeeRate.CUSTOM to 0L, + ), + selected = FeeRate.NORMAL, + ), + isUnified = true, + payMethod = SendMethod.LIGHTNING, + estimatedRoutingFee = 43L, + modifier = Modifier.sheetHeight(), + ) + } + } +} + @Preview(showSystemUi = true) @Composable private fun PreviewEmpty() { diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 54f590811..9b9431d65 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -164,6 +164,10 @@ fun SendSheet( onBack = { navController.popBackStack() }, onContinue = { navController.popBackStack() }, onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, + onSelectInstant = { + appViewModel.switchToLightning() + navController.popBackStack() + }, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index cbec471c1..5479d5a81 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1080,6 +1080,7 @@ class AppViewModel @Inject constructor( } _sendUiState.update { it.copy( + payMethod = SendMethod.ONCHAIN, speed = speed, fee = SendFee.OnChain(fee), selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos, @@ -1091,16 +1092,43 @@ class AppViewModel @Inject constructor( } private suspend fun onPaymentMethodSwitch() { - val nextPaymentMethod = when (_sendUiState.value.payMethod) { + val current = _sendUiState.value + if (!current.isUnified) return + + val nextMethod = when (current.payMethod) { SendMethod.ONCHAIN -> SendMethod.LIGHTNING SendMethod.LIGHTNING -> SendMethod.ONCHAIN } _sendUiState.update { it.copy( - payMethod = nextPaymentMethod, - isAmountInputValid = validateAmount(it.amount, nextPaymentMethod), + payMethod = nextMethod, + isAmountInputValid = validateAmount(it.amount, nextMethod), ) } + when (nextMethod) { + SendMethod.ONCHAIN -> { + val defaultSpeed = settingsStore.data.first().defaultTransactionSpeed + _sendUiState.update { it.copy(speed = defaultSpeed) } + refreshFeeEstimates() + } + SendMethod.LIGHTNING -> { + _sendUiState.update { it.copy(fee = SendFee.Lightning(0)) } + estimateLightningRoutingFeesIfNeeded() + } + } + } + + fun switchToLightning() { + viewModelScope.launch { + _sendUiState.update { + it.copy( + payMethod = SendMethod.LIGHTNING, + fee = SendFee.Lightning(0), + isAmountInputValid = validateAmount(it.amount, SendMethod.LIGHTNING), + ) + } + estimateLightningRoutingFeesIfNeeded() + } } private suspend fun onAmountContinue() { From fe98cdc7af1123c77e5f7f3005c3db9e56eb90c1 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 12:47:03 -0300 Subject: [PATCH 07/19] feat: relative expire date for invoice --- app/src/main/java/to/bitkit/ext/DateTime.kt | 28 +++++++++++++++++++ .../screens/wallets/send/SendConfirmScreen.kt | 12 ++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/ext/DateTime.kt b/app/src/main/java/to/bitkit/ext/DateTime.kt index 530de7f4f..516944bcb 100644 --- a/app/src/main/java/to/bitkit/ext/DateTime.kt +++ b/app/src/main/java/to/bitkit/ext/DateTime.kt @@ -109,6 +109,34 @@ fun Long.toRelativeTimeString( } } +fun formatInvoiceExpiryRelative( + expirySeconds: ULong, + locale: Locale = Locale.getDefault(), +): String { + val seconds = expirySeconds.toLong() + if (seconds <= 0) return "" + + val uLocale = ULocale.forLocale(locale) + val numberFormat = NumberFormat.getNumberInstance(uLocale)?.apply { maximumFractionDigits = 0 } + val formatter = RelativeDateTimeFormatter.getInstance( + uLocale, + numberFormat, + RelativeDateTimeFormatter.Style.LONG, + DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE, + ) ?: return "" + + val minutes = seconds / Factor.SECONDS_TO_MINUTES.toLong() + val hours = minutes / Factor.MINUTES_TO_HOURS.toLong() + val days = hours / Factor.HOURS_TO_DAYS.toLong() + + return when { + minutes < 1 -> formatter.format(seconds.toDouble(), Direction.NEXT, RelativeUnit.SECONDS) + hours < 1 -> formatter.format(minutes.toDouble(), Direction.NEXT, RelativeUnit.MINUTES) + days < 1 -> formatter.format(hours.toDouble(), Direction.NEXT, RelativeUnit.HOURS) + else -> formatter.format(days.toDouble(), Direction.NEXT, RelativeUnit.DAYS) + } +} + fun getDaysInMonth(month: LocalDate): List { val firstDayOfMonth = LocalDate(month.year, month.month, Constants.FIRST_DAY_OF_MONTH) val daysInMonth = month.month.toJavaMonth().length(isLeapYear(month.year)) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index e65076a24..26d6d458e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -49,9 +49,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.ext.DatePattern import to.bitkit.ext.commentAllowed -import to.bitkit.ext.formatted +import to.bitkit.ext.formatInvoiceExpiryRelative import to.bitkit.models.FeeRate import to.bitkit.models.TransactionSpeed import to.bitkit.ui.components.BalanceHeaderView @@ -84,7 +83,6 @@ import to.bitkit.viewmodels.SendEvent import to.bitkit.viewmodels.SendFee import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState -import java.time.Instant @Suppress("MagicNumber") @Composable @@ -563,10 +561,10 @@ private fun LightningDescription( tint = Colors.Purple, modifier = Modifier.size(16.dp) ) - val invoiceExpiryTimestamp = Instant.now().plusSeconds(expirySeconds.toLong()) - .formatted(DatePattern.INVOICE_EXPIRY) - - BodySSB(text = invoiceExpiryTimestamp) + val invoiceExpiryText = remember(expirySeconds) { + formatInvoiceExpiryRelative(expirySeconds) + } + BodySSB(text = invoiceExpiryText) } Spacer(modifier = Modifier.weight(1f)) HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) From 8a67ab0864cb7fbfd3f6f8cdccc3c6db21ab0a4e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 12:57:29 -0300 Subject: [PATCH 08/19] feat: hide details on send confirmation screen --- .../screens/wallets/send/SendConfirmScreen.kt | 333 +++++++++++------- 1 file changed, 199 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 26d6d458e..dab5bc54e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -1,5 +1,6 @@ package to.bitkit.ui.screens.wallets.send +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -22,6 +23,7 @@ import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -30,6 +32,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -60,7 +64,9 @@ import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.NumberPadActionButton import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SendSectionView import to.bitkit.ui.components.SwipeToConfirm import to.bitkit.ui.components.SyncNodeView import to.bitkit.ui.components.TagButton @@ -243,16 +249,25 @@ private fun Content( } } +@Suppress("MagicNumber") @Composable fun ContentRunning( uiState: SendUiState, - onEvent: (SendEvent) -> Unit, isLoading: Boolean, - onClickAddTag: () -> Unit, - onClickTag: (String) -> Unit, - onSwipeToConfirm: () -> Unit, modifier: Modifier = Modifier, + onEvent: (SendEvent) -> Unit = {}, + onClickAddTag: () -> Unit = {}, + onClickTag: (String) -> Unit = {}, + onSwipeToConfirm: () -> Unit = {}, ) { + var showDetails by rememberSaveable { mutableStateOf(false) } + var swipeProgress by remember { mutableFloatStateOf(0f) } + + val accentColor = when (uiState.payMethod) { + SendMethod.ONCHAIN -> Colors.Brand + SendMethod.LIGHTNING -> Colors.Purple + } + Column( modifier = modifier .padding(horizontal = 16.dp) @@ -269,32 +284,70 @@ fun ContentRunning( .testTag("ReviewAmount") ) - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(44.dp) - when (uiState.payMethod) { - SendMethod.ONCHAIN -> OnChainDescription(uiState = uiState, onEvent = onEvent) - SendMethod.LIGHTNING -> LightningDescription(uiState = uiState, onEvent = onEvent) - } + if (showDetails) { + when (uiState.payMethod) { + SendMethod.ONCHAIN -> OnChainDetails(uiState = uiState, onEvent = onEvent) + SendMethod.LIGHTNING -> LightningDetails(uiState = uiState, onEvent = onEvent) + } - if (uiState.lnurl is LnurlParams.LnurlPay) { - if (uiState.lnurl.data.commentAllowed()) { - LnurlCommentSection(uiState, onEvent) + if (uiState.lnurl is LnurlParams.LnurlPay) { + if (uiState.lnurl.data.commentAllowed()) { + LnurlCommentSection(uiState, onEvent) + } + } else { + TagsSection(uiState, onClickTag, onClickAddTag) } } else { - TagsSection(uiState, onClickTag, onClickAddTag) + Image( + painter = painterResource(R.drawable.coin_stack_4), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth(0.8f) + .align(Alignment.CenterHorizontally) + .padding(bottom = 16.dp) + .graphicsLayer { rotationZ = swipeProgress * 14f } + ) } - FillHeight() VerticalSpacer(16.dp) + PrimaryButton( + text = stringResource( + if (showDetails) R.string.common__hide_details else R.string.common__show_details + ), + size = ButtonSize.Small, + onClick = { showDetails = !showDetails }, + icon = { + Icon( + painter = painterResource( + if (showDetails) R.drawable.ic_eye_slash + else when (uiState.payMethod) { + SendMethod.ONCHAIN -> R.drawable.ic_speed_normal + SendMethod.LIGHTNING -> R.drawable.ic_lightning + } + ), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + }, + fullWidth = false, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag("SendConfirmToggleDetails") + ) + + FillHeight(min = 16.dp) + SwipeToConfirm( text = stringResource(R.string.wallet__send_swipe), - color = when (uiState.payMethod) { - SendMethod.ONCHAIN -> Colors.Brand - SendMethod.LIGHTNING -> Colors.Purple - }, + color = accentColor, loading = isLoading, confirmed = isLoading, + onProgressChange = { swipeProgress = it }, onConfirm = onSwipeToConfirm, ) VerticalSpacer(16.dp) @@ -364,41 +417,57 @@ private fun TagsSection( } @Composable -private fun OnChainDescription( +private fun OnChainDetails( uiState: SendUiState, onEvent: (SendEvent) -> Unit, ) { - val fee by remember(uiState.speed) { mutableStateOf(FeeRate.fromSpeed(uiState.speed)) } + val fee = remember(uiState.speed) { FeeRate.fromSpeed(uiState.speed) } Column(modifier = Modifier.fillMaxWidth()) { - Caption13Up(text = stringResource(R.string.wallet__send_to), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - BodySSB( - text = uiState.address, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - modifier = Modifier - .clickableAlpha { onEvent(SendEvent.NavToAddress) } - .testTag("ReviewUri") - ) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) + // Row 1: Send from | Send to + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.height(IntrinsicSize.Min) + ) { + SendSectionView( + caption = stringResource(R.string.wallet__send_from), + modifier = Modifier.weight(1f), + ) { + NumberPadActionButton( + text = stringResource(R.string.wallet__savings__title), + color = Colors.Brand, + enabled = uiState.isUnified, + icon = R.drawable.ic_transfer.takeIf { uiState.isUnified }, + onClick = { onEvent(SendEvent.PaymentMethodSwitch) }, + modifier = Modifier.testTag("SendConfirmAssetButton") + ) + } + SendSectionView( + caption = stringResource(R.string.wallet__send_to), + modifier = Modifier.weight(1f), + ) { + BodySSB( + text = uiState.address, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier + .clickableAlpha { onEvent(SendEvent.NavToAddress) } + .testTag("ReviewUri") + ) + } + } + // Row 2: Fee & Speed | Confirming in Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) ) { Column( modifier = Modifier - .fillMaxHeight() .weight(1f) + .fillMaxHeight() + .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } - ) { - VerticalSpacer(16.dp) - Caption13Up(stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) - VerticalSpacer(8.dp) + SendSectionView(caption = stringResource(R.string.wallet__send_fee_and_speed)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -430,127 +499,127 @@ private fun OnChainDescription( modifier = Modifier.size(16.dp) ) } - FillHeight() - VerticalSpacer(16.dp) } - HorizontalDivider() } - Column( - modifier = Modifier - .fillMaxHeight() - .weight(1f) + SendSectionView( + caption = stringResource(R.string.wallet__send_confirming_in), + modifier = Modifier.weight(1f), ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - VerticalSpacer(16.dp) - Caption13Up(text = stringResource(R.string.wallet__send_confirming_in), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - painterResource(R.drawable.ic_clock), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(16.dp) - ) - BodySSB(stringResource(fee.description)) - } - FillHeight() - VerticalSpacer(16.dp) + Icon( + painterResource(R.drawable.ic_clock), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(16.dp) + ) + BodySSB(stringResource(fee.description)) } - HorizontalDivider() } } } } @Composable -private fun LightningDescription( +private fun LightningDetails( uiState: SendUiState, onEvent: (SendEvent) -> Unit, ) { val isLnurlPay = uiState.lnurl is LnurlParams.LnurlPay val expirySeconds = uiState.decodedInvoice?.expirySeconds val description = uiState.decodedInvoice?.description + val destination = when { + isLnurlPay -> (uiState.lnurl as LnurlParams.LnurlPay).data.uri + else -> uiState.decodedInvoice?.bolt11.orEmpty() + } Column(modifier = Modifier.fillMaxWidth()) { - Caption13Up( - text = stringResource(R.string.wallet__send_invoice), - color = Colors.White64, - ) - val destination = when { - isLnurlPay -> uiState.lnurl.data.uri - else -> uiState.decodedInvoice?.bolt11.orEmpty() + // Row 1: Send from | Send to + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.height(IntrinsicSize.Min) + ) { + SendSectionView( + caption = stringResource(R.string.wallet__send_from), + modifier = Modifier.weight(1f), + ) { + NumberPadActionButton( + text = stringResource(R.string.wallet__spending__title), + color = Colors.Purple, + enabled = uiState.isUnified, + icon = R.drawable.ic_transfer.takeIf { uiState.isUnified }, + onClick = { onEvent(SendEvent.PaymentMethodSwitch) }, + modifier = Modifier.testTag("SendConfirmAssetButton") + ) + } + SendSectionView( + caption = stringResource(R.string.wallet__send_to), + modifier = Modifier.weight(1f), + ) { + BodySSB( + text = destination, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier + .clickableAlpha { onEvent(SendEvent.NavToAddress) } + .testTag("ReviewUri") + ) + } } - Spacer(modifier = Modifier.height(8.dp)) - BodySSB( - text = destination, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - modifier = Modifier - .clickableAlpha { onEvent(SendEvent.NavToAddress) } - .testTag("ReviewUri") - ) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) - + // Row 2: Fee & Speed | Invoice expiration Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) ) { Column( modifier = Modifier - .fillMaxHeight() .weight(1f) + .fillMaxHeight() + .let { if (uiState.isUnified) it.clickableAlpha { onEvent(SendEvent.SpeedAndFee) } else it } ) { - VerticalSpacer(16.dp) - Caption13Up(text = stringResource(R.string.wallet__send_fee_and_speed), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Icon( - painterResource(R.drawable.ic_lightning), - contentDescription = null, - tint = Colors.Purple, - modifier = Modifier.size(16.dp) - ) - (uiState.fee as? SendFee.Lightning)?.value - ?.takeIf { it > 0 } - ?.let { feeSat -> - val feeText = let { - val prefix = stringResource(R.string.fee__instant__title) - val value = rememberMoneyText(feeSat, showSymbol = true) - "$prefix (± $value)" - } - BodySSB( - text = feeText.withAccent(accentColor = Colors.White), - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, + SendSectionView(caption = stringResource(R.string.wallet__send_fee_and_speed)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painterResource(R.drawable.ic_lightning), + contentDescription = null, + tint = Colors.Purple, + modifier = Modifier.size(16.dp) + ) + (uiState.fee as? SendFee.Lightning)?.value + ?.takeIf { it > 0 } + ?.let { feeSat -> + val feeText = let { + val prefix = stringResource(R.string.fee__instant__title) + val value = rememberMoneyText(feeSat, showSymbol = true) + "$prefix (± $value)" + } + BodySSB( + text = feeText.withAccent(accentColor = Colors.White), + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } ?: BodySSB(text = stringResource(R.string.fee__instant__title)) + if (uiState.isUnified) { + Icon( + painterResource(R.drawable.ic_pencil_simple), + contentDescription = null, + modifier = Modifier.size(16.dp) ) - } ?: BodySSB(text = stringResource(R.string.fee__instant__title)) + } + } } - Spacer(modifier = Modifier.weight(1f)) - HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) } if (!isLnurlPay && expirySeconds != null) { - Column( - modifier = Modifier - .fillMaxHeight() - .weight(1f) - .padding(top = 16.dp) + SendSectionView( + caption = stringResource(R.string.wallet__send_invoice_expiration), + modifier = Modifier.weight(1f), ) { - Caption13Up( - text = stringResource(R.string.wallet__send_invoice_expiration), - color = Colors.White64, - ) - Spacer(modifier = Modifier.height(8.dp)) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -566,18 +635,14 @@ private fun LightningDescription( } BodySSB(text = invoiceExpiryText) } - Spacer(modifier = Modifier.weight(1f)) - HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) } } } + // Optional note if (!isLnurlPay && description != null) { - Column { - Caption13Up(text = stringResource(R.string.wallet__note), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - BodySSB(text = description) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) + SendSectionView(caption = stringResource(R.string.wallet__note)) { + BodySSB(text = description, maxLines = 1) } } } From 204e1dd5c50647902962c94b867ff6e458ace6c3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 13:08:47 -0300 Subject: [PATCH 09/19] test: update test --- .../bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt index 9db528317..4d2529018 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt @@ -5,12 +5,15 @@ import com.synonym.bitkitcore.FeeRates import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.mockito.kotlin.wheneverBlocking +import to.bitkit.data.SettingsData +import to.bitkit.data.SettingsStore import to.bitkit.models.BalanceState import to.bitkit.models.FeeRate import to.bitkit.models.TransactionSpeed @@ -29,6 +32,7 @@ class SendFeeViewModelTest : BaseUnitTest() { private val lightningRepo: LightningRepo = mock() private val currencyRepo: CurrencyRepo = mock() private val walletRepo: WalletRepo = mock() + private val settingsStore: SettingsStore = mock() private val context: Context = mock() private val balance = 100_000uL @@ -44,7 +48,8 @@ class SendFeeViewModelTest : BaseUnitTest() { whenever(walletRepo.balanceState) .thenReturn(MutableStateFlow(BalanceState(totalOnchainSats = balance))) - sut = SendFeeViewModel(lightningRepo, currencyRepo, walletRepo, context) + whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) + sut = SendFeeViewModel(lightningRepo, currencyRepo, walletRepo, settingsStore, context) } @Test From d62e6cd131ab97a9485041106789ede474802120 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 13:08:57 -0300 Subject: [PATCH 10/19] chore: lint --- .../ui/screens/wallets/send/SendConfirmScreen.kt | 11 +++++++---- .../ui/screens/wallets/send/SendFeeViewModel.kt | 3 +-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index dab5bc54e..b481c2b0f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -323,10 +323,13 @@ fun ContentRunning( icon = { Icon( painter = painterResource( - if (showDetails) R.drawable.ic_eye_slash - else when (uiState.payMethod) { - SendMethod.ONCHAIN -> R.drawable.ic_speed_normal - SendMethod.LIGHTNING -> R.drawable.ic_lightning + if (showDetails) { + R.drawable.ic_eye_slash + } else { + when (uiState.payMethod) { + SendMethod.ONCHAIN -> R.drawable.ic_speed_normal + SendMethod.LIGHTNING -> R.drawable.ic_lightning + } } ), contentDescription = null, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index e5d46ae87..ecf43247d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -17,11 +17,10 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.ext.getSatsPerVByteFor +import to.bitkit.data.SettingsStore import to.bitkit.models.FeeRate import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed -import to.bitkit.data.SettingsStore import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo From bd59b6eb6ad7cd0befc868eb523ab0724f8039c8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 13:41:15 -0300 Subject: [PATCH 11/19] fix: spacing --- .../bitkit/ui/components/SendSectionView.kt | 1 + .../screens/wallets/send/SendConfirmScreen.kt | 64 +++++++++++++++++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt b/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt index 2ff05411d..cf9c772fc 100644 --- a/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt +++ b/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt @@ -18,6 +18,7 @@ fun SendSectionView( Caption13Up(text = caption, color = Colors.White64) VerticalSpacer(8.dp) content() + VerticalSpacer(16.dp) HorizontalDivider(modifier = Modifier.fillMaxWidth()) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index b481c2b0f..1d51a7abb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -177,6 +177,7 @@ private fun Content( showBiometrics: Boolean, modifier: Modifier = Modifier, canGoBack: Boolean = true, + initialShowDetails: Boolean = false, onBack: () -> Unit = {}, onEvent: (SendEvent) -> Unit = {}, onClickAddTag: () -> Unit = {}, @@ -207,8 +208,9 @@ private fun Content( if (isNodeRunning) { ContentRunning( uiState = uiState, - onEvent = onEvent, isLoading = isLoading, + initialShowDetails = initialShowDetails, + onEvent = onEvent, onClickAddTag = onClickAddTag, onClickTag = onClickTag, onSwipeToConfirm = onSwipeToConfirm, @@ -255,12 +257,13 @@ fun ContentRunning( uiState: SendUiState, isLoading: Boolean, modifier: Modifier = Modifier, + initialShowDetails: Boolean = false, onEvent: (SendEvent) -> Unit = {}, onClickAddTag: () -> Unit = {}, onClickTag: (String) -> Unit = {}, onSwipeToConfirm: () -> Unit = {}, ) { - var showDetails by rememberSaveable { mutableStateOf(false) } + var showDetails by rememberSaveable { mutableStateOf(initialShowDetails) } var swipeProgress by remember { mutableFloatStateOf(0f) } val accentColor = when (uiState.payMethod) { @@ -425,7 +428,10 @@ private fun OnChainDetails( onEvent: (SendEvent) -> Unit, ) { val fee = remember(uiState.speed) { FeeRate.fromSpeed(uiState.speed) } - Column(modifier = Modifier.fillMaxWidth()) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { // Row 1: Send from | Send to Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -453,6 +459,7 @@ private fun OnChainDetails( maxLines = 1, overflow = TextOverflow.MiddleEllipsis, modifier = Modifier + .height(28.dp) .clickableAlpha { onEvent(SendEvent.NavToAddress) } .testTag("ReviewUri") ) @@ -538,7 +545,10 @@ private fun LightningDetails( else -> uiState.decodedInvoice?.bolt11.orEmpty() } - Column(modifier = Modifier.fillMaxWidth()) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { // Row 1: Send from | Send to Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -566,6 +576,7 @@ private fun LightningDetails( maxLines = 1, overflow = TextOverflow.MiddleEllipsis, modifier = Modifier + .height(28.dp) .clickableAlpha { onEvent(SendEvent.NavToAddress) } .testTag("ReviewUri") ) @@ -689,6 +700,51 @@ private fun PreviewOnChain() { } } +@Suppress("MagicNumber") +@Preview(showSystemUi = true, group = "onchain details") +@Composable +private fun PreviewOnChainDetails() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = sendUiState().copy( + selectedTags = persistentListOf("car", "house", "uber"), + fee = SendFee.OnChain(1_234), + speed = TransactionSpeed.Medium, + ), + isNodeRunning = true, + isLoading = false, + showBiometrics = false, + initialShowDetails = true, + modifier = Modifier.sheetHeight(), + ) + } + } +} + +@Suppress("MagicNumber") +@Preview(showSystemUi = true, group = "lightning details") +@Composable +private fun PreviewLightningDetails() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = sendUiState().copy( + amount = 6_543u, + payMethod = SendMethod.LIGHTNING, + selectedTags = persistentListOf("coffee"), + fee = SendFee.Lightning(43), + ), + isNodeRunning = true, + isLoading = false, + showBiometrics = false, + initialShowDetails = true, + modifier = Modifier.sheetHeight(), + ) + } + } +} + @Suppress("MagicNumber") @Preview(showSystemUi = true, group = "onchain", device = Devices.NEXUS_5) @Composable From d5fc1a1f703008a8bd5df2e128200ee6941f6e6e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 14:18:56 -0300 Subject: [PATCH 12/19] fix: switch logic --- .../screens/wallets/send/SendConfirmScreen.kt | 12 ++++++------ .../java/to/bitkit/viewmodels/AppViewModel.kt | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 1d51a7abb..db0ce54f9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -444,8 +444,8 @@ private fun OnChainDetails( NumberPadActionButton( text = stringResource(R.string.wallet__savings__title), color = Colors.Brand, - enabled = uiState.isUnified, - icon = R.drawable.ic_transfer.takeIf { uiState.isUnified }, + enabled = uiState.canSwitchWallet, + icon = R.drawable.ic_transfer.takeIf { uiState.canSwitchWallet }, onClick = { onEvent(SendEvent.PaymentMethodSwitch) }, modifier = Modifier.testTag("SendConfirmAssetButton") ) @@ -561,8 +561,8 @@ private fun LightningDetails( NumberPadActionButton( text = stringResource(R.string.wallet__spending__title), color = Colors.Purple, - enabled = uiState.isUnified, - icon = R.drawable.ic_transfer.takeIf { uiState.isUnified }, + enabled = uiState.canSwitchWallet, + icon = R.drawable.ic_transfer.takeIf { uiState.canSwitchWallet }, onClick = { onEvent(SendEvent.PaymentMethodSwitch) }, modifier = Modifier.testTag("SendConfirmAssetButton") ) @@ -592,7 +592,7 @@ private fun LightningDetails( modifier = Modifier .weight(1f) .fillMaxHeight() - .let { if (uiState.isUnified) it.clickableAlpha { onEvent(SendEvent.SpeedAndFee) } else it } + .let { if (uiState.canSwitchWallet) it.clickableAlpha { onEvent(SendEvent.SpeedAndFee) } else it } ) { SendSectionView(caption = stringResource(R.string.wallet__send_fee_and_speed)) { Row( @@ -619,7 +619,7 @@ private fun LightningDetails( overflow = TextOverflow.MiddleEllipsis, ) } ?: BodySSB(text = stringResource(R.string.fee__instant__title)) - if (uiState.isUnified) { + if (uiState.canSwitchWallet) { Icon( painterResource(R.drawable.ic_pencil_simple), contentDescription = null, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index dbef63a11..583cdb617 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -931,6 +931,7 @@ class AppViewModel @Inject constructor( payMethod = SendMethod.LIGHTNING, ) } + updateCanSwitchWallet() return } @@ -1043,6 +1044,7 @@ class AppViewModel @Inject constructor( isAmountInputValid = validateAmount(amount), ) } + updateCanSwitchWallet() } private fun onCommentChange(comment: String) { @@ -1091,6 +1093,20 @@ class AppViewModel @Inject constructor( } } + private fun updateCanSwitchWallet() { + val state = _sendUiState.value + if (!state.isUnified) { + _sendUiState.update { it.copy(canSwitchWallet = false) } + return + } + val amount = state.amount + val balance = walletRepo.balanceState.value + val canSwitch = amount >= Defaults.dustLimit.toULong() && + amount <= balance.maxSendOnchainSats && + amount <= balance.maxSendLightningSats + _sendUiState.update { it.copy(canSwitchWallet = canSwitch) } + } + private suspend fun onPaymentMethodSwitch() { val current = _sendUiState.value if (!current.isUnified) return @@ -2493,6 +2509,7 @@ data class SendUiState( val amount: ULong = 0u, val isAmountInputValid: Boolean = false, val isUnified: Boolean = false, + val canSwitchWallet: Boolean = false, val payMethod: SendMethod = SendMethod.ONCHAIN, val selectedTags: ImmutableList = persistentListOf(), val decodedInvoice: LightningInvoice? = null, From d5687db41a9579568a047bbfd124e705356a4813 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 14:55:35 -0300 Subject: [PATCH 13/19] fix: coin animation --- .../java/to/bitkit/ui/components/SwipeToConfirm.kt | 10 +++------- .../ui/screens/wallets/send/SendConfirmScreen.kt | 6 +++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt index 3c8376241..b0f50b490 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -24,12 +24,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -70,7 +70,7 @@ fun SwipeToConfirm( endIconTint: Color = Colors.Black, loading: Boolean = false, confirmed: Boolean = false, - onProgressChange: ((Float) -> Unit)? = null, + progress: MutableFloatState? = null, onConfirm: () -> Unit, ) { val scope = rememberCoroutineScope() @@ -96,11 +96,7 @@ fun SwipeToConfirm( ) } - LaunchedEffect(onProgressChange) { - if (onProgressChange == null) return@LaunchedEffect - snapshotFlow { panX.value / maxPanX } - .collect { onProgressChange(it.coerceIn(0f, 1f)) } - } + progress?.floatValue = (panX.value / maxPanX).coerceIn(0f, 1f) Box( modifier = modifier diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index db0ce54f9..b147f5e6d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -264,7 +264,7 @@ fun ContentRunning( onSwipeToConfirm: () -> Unit = {}, ) { var showDetails by rememberSaveable { mutableStateOf(initialShowDetails) } - var swipeProgress by remember { mutableFloatStateOf(0f) } + val swipeProgress = remember { mutableFloatStateOf(0f) } val accentColor = when (uiState.payMethod) { SendMethod.ONCHAIN -> Colors.Brand @@ -311,7 +311,7 @@ fun ContentRunning( .fillMaxWidth(0.8f) .align(Alignment.CenterHorizontally) .padding(bottom = 16.dp) - .graphicsLayer { rotationZ = swipeProgress * 14f } + .graphicsLayer { rotationZ = swipeProgress.floatValue * 14f } ) } @@ -353,7 +353,7 @@ fun ContentRunning( color = accentColor, loading = isLoading, confirmed = isLoading, - onProgressChange = { swipeProgress = it }, + progress = swipeProgress, onConfirm = onSwipeToConfirm, ) VerticalSpacer(16.dp) From 0b8e70a9ac465521fe44b501a7f4da7bbad92003 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 15:09:40 -0300 Subject: [PATCH 14/19] fix: reset warnings on amount change and payment method switches --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 583cdb617..403ce9d85 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1042,6 +1042,7 @@ class AppViewModel @Inject constructor( it.copy( amount = amount, isAmountInputValid = validateAmount(amount), + confirmedWarnings = persistentListOf(), ) } updateCanSwitchWallet() @@ -1119,6 +1120,7 @@ class AppViewModel @Inject constructor( it.copy( payMethod = nextMethod, isAmountInputValid = validateAmount(it.amount, nextMethod), + confirmedWarnings = persistentListOf(), ) } when (nextMethod) { From 2bbad48331f7d07d457e2a5742c170f3e678ea36 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 15:17:18 -0300 Subject: [PATCH 15/19] fix: strings order --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 11139d923..24f968538 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,10 +41,10 @@ Yes, Delete No, Cancel Edit - Hide Details Empty Error Unknown error + Hide Details Later Learn More Max @@ -955,7 +955,6 @@ Unable to broadcast the transaction. Please try again. Transaction Failed Speed and fee - Send from Set Custom Fee Fee Invalid Unable to increase the fee any further. Otherwise, it will exceed half the current input balance. @@ -963,6 +962,7 @@ Speed ₿ {feeSats} for this transaction ₿ {feeSats} for this transaction ({fiatSymbol}{fiatFormatted}) + Send from Invoice Invoice expiration MAX From 9c84c60804def3a916762c1790b6356b98bb6322 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 15:24:23 -0300 Subject: [PATCH 16/19] fix: expire text update --- .../to/bitkit/ui/components/SwipeToConfirm.kt | 2 +- .../screens/wallets/send/SendConfirmScreen.kt | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt index b0f50b490..28247fb84 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -23,8 +23,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.MutableFloatState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index b147f5e6d..8fbedac0c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState @@ -90,6 +91,8 @@ import to.bitkit.viewmodels.SendFee import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState +private const val EXPIRY_REFRESH_INTERVAL = 60_000L + @Suppress("MagicNumber") @Composable fun SendConfirmScreen( @@ -432,7 +435,6 @@ private fun OnChainDetails( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(), ) { - // Row 1: Send from | Send to Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) @@ -466,7 +468,6 @@ private fun OnChainDetails( } } - // Row 2: Fee & Speed | Confirming in Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) @@ -532,6 +533,7 @@ private fun OnChainDetails( } } +@Suppress("CyclomaticComplexMethod") @Composable private fun LightningDetails( uiState: SendUiState, @@ -549,7 +551,6 @@ private fun LightningDetails( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth(), ) { - // Row 1: Send from | Send to Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) @@ -583,7 +584,6 @@ private fun LightningDetails( } } - // Row 2: Fee & Speed | Invoice expiration Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) @@ -644,8 +644,15 @@ private fun LightningDetails( tint = Colors.Purple, modifier = Modifier.size(16.dp) ) - val invoiceExpiryText = remember(expirySeconds) { - formatInvoiceExpiryRelative(expirySeconds) + val timestampSeconds = uiState.decodedInvoice?.timestampSeconds ?: 0uL + val invoiceExpiryText by produceState("", timestampSeconds, expirySeconds) { + val expiryMoment = timestampSeconds + expirySeconds + while (true) { + val now = System.currentTimeMillis() / 1000 + val remaining = (expiryMoment.toLong() - now).coerceAtLeast(0) + value = formatInvoiceExpiryRelative(remaining.toULong()) + delay(EXPIRY_REFRESH_INTERVAL) + } } BodySSB(text = invoiceExpiryText) } @@ -653,7 +660,6 @@ private fun LightningDetails( } } - // Optional note if (!isLnurlPay && description != null) { SendSectionView(caption = stringResource(R.string.wallet__note)) { BodySSB(text = description, maxLines = 1) From b183d311a479a8080e8dcc92f262591a923da4cf Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 15:36:13 -0300 Subject: [PATCH 17/19] fix: apply side effect --- app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt index 28247fb84..3557e0105 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableFloatState +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -96,7 +97,9 @@ fun SwipeToConfirm( ) } - progress?.floatValue = (panX.value / maxPanX).coerceIn(0f, 1f) + SideEffect { + progress?.floatValue = (panX.value / maxPanX).coerceIn(0f, 1f) + } Box( modifier = modifier From f1cdb2c9aacef323f4cfbc4b4bf4e8a45f8f8036 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 15:39:27 -0300 Subject: [PATCH 18/19] fix: remove trailing comma --- .../ui/screens/wallets/send/SendConfirmScreen.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt index 8fbedac0c..3f1f8f77d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt @@ -433,7 +433,7 @@ private fun OnChainDetails( val fee = remember(uiState.speed) { FeeRate.fromSpeed(uiState.speed) } Column( verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) { Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -441,7 +441,7 @@ private fun OnChainDetails( ) { SendSectionView( caption = stringResource(R.string.wallet__send_from), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { NumberPadActionButton( text = stringResource(R.string.wallet__savings__title), @@ -454,7 +454,7 @@ private fun OnChainDetails( } SendSectionView( caption = stringResource(R.string.wallet__send_to), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { BodySSB( text = uiState.address, @@ -514,7 +514,7 @@ private fun OnChainDetails( } SendSectionView( caption = stringResource(R.string.wallet__send_confirming_in), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -549,7 +549,7 @@ private fun LightningDetails( Column( verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) { Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -557,7 +557,7 @@ private fun LightningDetails( ) { SendSectionView( caption = stringResource(R.string.wallet__send_from), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { NumberPadActionButton( text = stringResource(R.string.wallet__spending__title), @@ -570,7 +570,7 @@ private fun LightningDetails( } SendSectionView( caption = stringResource(R.string.wallet__send_to), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { BodySSB( text = destination, From e2fb696f335f0df650b0bbc65e42c4f798cb413b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 27 Mar 2026 15:45:57 -0300 Subject: [PATCH 19/19] fix: remove trailing comma --- .../java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt index b5ef62a32..c784b6ace 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt @@ -120,7 +120,7 @@ private fun Content( sats = estimatedRoutingFee, isSelected = payMethod == SendMethod.LIGHTNING, onClick = { onSelect(FeeRate.INSTANT) }, - modifier = Modifier.testTag("fee_INSTANT_button"), + modifier = Modifier.testTag("fee_INSTANT_button") ) }