From 7b394664202a54cbbc4c98c17174ed8d0ba39a30 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 25 Mar 2026 10:46:43 -0300 Subject: [PATCH 01/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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") ) } From c8da9c23705480aa03fe0f9e5bd9b8639dc11d5d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 07:22:00 -0300 Subject: [PATCH 20/57] fix: clean confirmedWarnings when switch to lighting --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 403ce9d85..e25a55e3b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1143,6 +1143,7 @@ class AppViewModel @Inject constructor( payMethod = SendMethod.LIGHTNING, fee = SendFee.Lightning(0), isAmountInputValid = validateAmount(it.amount, SendMethod.LIGHTNING), + confirmedWarnings = persistentListOf(), ) } estimateLightningRoutingFeesIfNeeded() From 85600dd237615581c251a5bf32279e4e190cc295 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 07:36:21 -0300 Subject: [PATCH 21/57] chore: lint --- .../java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3f1f8f77d..d819a1f43 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 @@ -632,7 +632,7 @@ private fun LightningDetails( if (!isLnurlPay && expirySeconds != null) { SendSectionView( caption = stringResource(R.string.wallet__send_invoice_expiration), - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) { Row( verticalAlignment = Alignment.CenterVertically, From 49ed09401a631c3db5a63569e0d4057e3940ef6c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 07:53:29 -0300 Subject: [PATCH 22/57] chore: remove magic number --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 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 d819a1f43..a31c79c1a 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 @@ -92,6 +92,7 @@ import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState private const val EXPIRY_REFRESH_INTERVAL = 60_000L +private const val SWIPE_ROTATION_DEGREES = 14f @Suppress("MagicNumber") @Composable @@ -254,7 +255,6 @@ private fun Content( } } -@Suppress("MagicNumber") @Composable fun ContentRunning( uiState: SendUiState, @@ -314,7 +314,7 @@ fun ContentRunning( .fillMaxWidth(0.8f) .align(Alignment.CenterHorizontally) .padding(bottom = 16.dp) - .graphicsLayer { rotationZ = swipeProgress.floatValue * 14f } + .graphicsLayer { rotationZ = swipeProgress.floatValue * SWIPE_ROTATION_DEGREES } ) } From 95f856c020f53130bb852407debbd3f5da031e3b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 08:23:02 -0300 Subject: [PATCH 23/57] chore: modifier rule lint --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 7 ++++--- .../to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt | 2 +- 2 files changed, 5 insertions(+), 4 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 a31c79c1a..43ab0e54f 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 @@ -90,8 +90,9 @@ import to.bitkit.viewmodels.SendEvent import to.bitkit.viewmodels.SendFee import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState +import kotlin.time.Duration.Companion.seconds -private const val EXPIRY_REFRESH_INTERVAL = 60_000L +private val EXPIRY_REFRESH_INTERVAL = 60.seconds private const val SWIPE_ROTATION_DEGREES = 14f @Suppress("MagicNumber") @@ -722,7 +723,7 @@ private fun PreviewOnChainDetails() { isLoading = false, showBiometrics = false, initialShowDetails = true, - modifier = Modifier.sheetHeight(), + modifier = Modifier.sheetHeight() ) } } @@ -745,7 +746,7 @@ private fun PreviewLightningDetails() { isLoading = false, showBiometrics = false, initialShowDetails = true, - modifier = Modifier.sheetHeight(), + modifier = Modifier.sheetHeight() ) } } 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 c784b6ace..d642ec256 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 @@ -264,7 +264,7 @@ private fun PreviewWithInstant() { isUnified = true, payMethod = SendMethod.LIGHTNING, estimatedRoutingFee = 43L, - modifier = Modifier.sheetHeight(), + modifier = Modifier.sheetHeight() ) } } From c95b053b77e5be1565e8af3b81583e8422214206 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 08:23:43 -0300 Subject: [PATCH 24/57] chore: magic number --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 43ab0e54f..faa21c3af 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 @@ -94,6 +94,7 @@ import kotlin.time.Duration.Companion.seconds private val EXPIRY_REFRESH_INTERVAL = 60.seconds private const val SWIPE_ROTATION_DEGREES = 14f +private const val IMAGE_FILL_PERCENTAGE = 0.8f @Suppress("MagicNumber") @Composable @@ -312,7 +313,7 @@ fun ContentRunning( contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier - .fillMaxWidth(0.8f) + .fillMaxWidth(IMAGE_FILL_PERCENTAGE) .align(Alignment.CenterHorizontally) .padding(bottom = 16.dp) .graphicsLayer { rotationZ = swipeProgress.floatValue * SWIPE_ROTATION_DEGREES } From 1bee2de34c59f62b3644a59e311a46d274a44170 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 08:42:55 -0300 Subject: [PATCH 25/57] feat: dashed add button --- .../screens/wallets/send/SendConfirmScreen.kt | 90 ++++++++++++------- 1 file changed, 59 insertions(+), 31 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 faa21c3af..40841132c 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 @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -33,6 +32,11 @@ 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.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag @@ -80,6 +84,7 @@ import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.rememberBiometricAuthSupported @@ -392,39 +397,62 @@ private fun TagsSection( onClickTag: (String) -> Unit, onClickAddTag: () -> Unit, ) { - Spacer(modifier = Modifier.height(16.dp)) - Caption13Up(text = stringResource(R.string.wallet__tags), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) { - uiState.selectedTags.map { tagText -> - TagButton( - text = tagText, - displayIconClose = true, - onClick = { onClickTag(tagText) }, + SendSectionView(caption = stringResource(R.string.wallet__tags)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + uiState.selectedTags.map { tagText -> + TagButton( + text = tagText, + displayIconClose = true, + onClick = { onClickTag(tagText) }, + ) + } + AddTagButton( + onClick = onClickAddTag, + modifier = Modifier.testTag("TagsAddSend") ) } } - PrimaryButton( - text = stringResource(R.string.wallet__tags_add), - size = ButtonSize.Small, - onClick = onClickAddTag, - icon = { - Icon( - painter = painterResource(R.drawable.ic_tag), - contentDescription = stringResource(R.string.wallet__tags_add), - tint = Colors.Brand, - ) - }, - fullWidth = false, - modifier = Modifier.testTag("TagsAddSend") - ) - HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) +} + +@Composable +private fun AddTagButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val shape = AppShapes.small + val cornerRadius = 8.dp + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = modifier + .clip(shape) + .drawBehind { + drawRoundRect( + color = Colors.White32, + style = Stroke( + width = 1.dp.toPx(), + pathEffect = PathEffect.dashPathEffect(floatArrayOf(4f, 4f)), + ), + cornerRadius = CornerRadius(cornerRadius.toPx()), + ) + } + .clickableAlpha(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + BodySSB( + text = stringResource(R.string.wallet__tags_add_button), + color = Colors.White64, + ) + Icon( + painter = painterResource(R.drawable.ic_plus), + contentDescription = null, + tint = Colors.White64, + modifier = Modifier.size(16.dp) + ) + } } @Composable From 3b80a36e593852d574ecf176d9b41761c4eb85d5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 09:31:45 -0300 Subject: [PATCH 26/57] feat: horizontal swipe --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 40841132c..7d22d1bd3 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,6 +1,7 @@ package to.bitkit.ui.screens.wallets.send import androidx.compose.foundation.Image +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -690,9 +691,11 @@ private fun LightningDetails( } } - if (!isLnurlPay && description != null) { + if (!isLnurlPay && !description.isNullOrEmpty()) { SendSectionView(caption = stringResource(R.string.wallet__note)) { - BodySSB(text = description, maxLines = 1) + Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { + BodySSB(text = description, maxLines = 1) + } } } } From 34f25032bf808b8150b21407dfbf7c167f7fe478 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 09:58:12 -0300 Subject: [PATCH 27/57] fix: dust validation on updateCanSwitchWallet --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index e25a55e3b..9ed5bd19d 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1102,7 +1102,7 @@ class AppViewModel @Inject constructor( } val amount = state.amount val balance = walletRepo.balanceState.value - val canSwitch = amount >= Defaults.dustLimit.toULong() && + val canSwitch = amount > Defaults.dustLimit.toULong() && amount <= balance.maxSendOnchainSats && amount <= balance.maxSendLightningSats _sendUiState.update { it.copy(canSwitchWallet = canSwitch) } From 21ff423a86e4b5bb854f8d5eb3f0662d7463643d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 13:47:47 -0300 Subject: [PATCH 28/57] fix: tags section padding --- .../screens/wallets/send/SendConfirmScreen.kt | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 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 7d22d1bd3..b92971d9f 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 @@ -302,16 +302,25 @@ fun ContentRunning( if (showDetails) { when (uiState.payMethod) { - SendMethod.ONCHAIN -> OnChainDetails(uiState = uiState, onEvent = onEvent) - SendMethod.LIGHTNING -> LightningDetails(uiState = uiState, onEvent = onEvent) + SendMethod.ONCHAIN -> { + OnChainDetails(uiState = uiState, onEvent = onEvent) + VerticalSpacer(16.dp) + TagsSection(uiState, onClickTag, onClickAddTag) + } + SendMethod.LIGHTNING -> { + LightningDetails( + uiState = uiState, + onEvent = onEvent, + onClickTag = onClickTag, + onClickAddTag = onClickAddTag, + ) + } } if (uiState.lnurl is LnurlParams.LnurlPay) { if (uiState.lnurl.data.commentAllowed()) { LnurlCommentSection(uiState, onEvent) } - } else { - TagsSection(uiState, onClickTag, onClickAddTag) } } else { Image( @@ -398,7 +407,20 @@ private fun TagsSection( onClickTag: (String) -> Unit, onClickAddTag: () -> Unit, ) { - SendSectionView(caption = stringResource(R.string.wallet__tags)) { + TagsSectionContent(uiState = uiState, onClickTag = onClickTag, onClickAddTag = onClickAddTag) +} + +@Composable +private fun TagsSectionContent( + uiState: SendUiState, + onClickTag: (String) -> Unit, + onClickAddTag: () -> Unit, + modifier: Modifier = Modifier, +) { + SendSectionView( + caption = stringResource(R.string.wallet__tags), + modifier = modifier + ) { FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -569,6 +591,8 @@ private fun OnChainDetails( private fun LightningDetails( uiState: SendUiState, onEvent: (SendEvent) -> Unit, + onClickTag: (String) -> Unit, + onClickAddTag: () -> Unit, ) { val isLnurlPay = uiState.lnurl is LnurlParams.LnurlPay val expirySeconds = uiState.decodedInvoice?.expirySeconds @@ -698,6 +722,14 @@ private fun LightningDetails( } } } + + if (!isLnurlPay) { + TagsSectionContent( + uiState = uiState, + onClickTag = onClickTag, + onClickAddTag = onClickAddTag, + ) + } } } From 0224d21b931a4e26cc88ceda9c3de438aff89ecd Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 13:59:05 -0300 Subject: [PATCH 29/57] fix: don't hide LNURL details --- .../screens/wallets/send/SendConfirmScreen.kt | 158 ++++++++++++++---- 1 file changed, 123 insertions(+), 35 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 b92971d9f..5e0e2a2aa 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 @@ -276,6 +276,7 @@ fun ContentRunning( ) { var showDetails by rememberSaveable { mutableStateOf(initialShowDetails) } val swipeProgress = remember { mutableFloatStateOf(0f) } + val isLnurlPay = uiState.lnurl is LnurlParams.LnurlPay val accentColor = when (uiState.payMethod) { SendMethod.ONCHAIN -> Colors.Brand @@ -300,7 +301,9 @@ fun ContentRunning( VerticalSpacer(44.dp) - if (showDetails) { + if (isLnurlPay) { + LnurlPayDetails(uiState = uiState, onEvent = onEvent) + } else if (showDetails) { when (uiState.payMethod) { SendMethod.ONCHAIN -> { OnChainDetails(uiState = uiState, onEvent = onEvent) @@ -316,12 +319,6 @@ fun ContentRunning( ) } } - - if (uiState.lnurl is LnurlParams.LnurlPay) { - if (uiState.lnurl.data.commentAllowed()) { - LnurlCommentSection(uiState, onEvent) - } - } } else { Image( painter = painterResource(R.drawable.coin_stack_4), @@ -335,36 +332,38 @@ fun ContentRunning( ) } - VerticalSpacer(16.dp) + if (!isLnurlPay) { + 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 + 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") - ) + ), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + }, + fullWidth = false, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .testTag("SendConfirmToggleDetails") + ) + } FillHeight(min = 16.dp) @@ -733,6 +732,61 @@ private fun LightningDetails( } } +@Composable +private fun LnurlPayDetails( + uiState: SendUiState, + onEvent: (SendEvent) -> Unit, +) { + val lnurlPay = uiState.lnurl as? LnurlParams.LnurlPay ?: return + Column(modifier = Modifier.fillMaxWidth()) { + SendSectionView(caption = stringResource(R.string.wallet__send_invoice)) { + BodySSB( + text = lnurlPay.data.uri, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + modifier = Modifier + .height(28.dp) + .clickableAlpha { onEvent(SendEvent.NavToAddress) } + .testTag("ReviewUri") + ) + } + + VerticalSpacer(16.dp) + + 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 (lnurlPay.data.commentAllowed()) { + LnurlCommentSection(uiState, onEvent) + } + } +} + @Suppress("SpellCheckingInspection") private fun sendUiState() = SendUiState( amount = 2_345u, @@ -909,6 +963,40 @@ private fun PreviewLnurl() { } } +@Suppress("MagicNumber") +@Preview(showSystemUi = true, group = "lnurl details") +@Composable +private fun PreviewLnurlDetails() { + AppThemeSurface { + BottomSheetPreview { + Content( + uiState = sendUiState().copy( + amount = 5_000u, + payMethod = SendMethod.LIGHTNING, + fee = SendFee.Lightning(12), + lnurl = LnurlParams.LnurlPay( + data = LnurlPayData( + uri = "veryLongLnurlPayUri12345677890123456789012345678901234567890", + callback = "", + metadataStr = "", + commentAllowed = 255u, + minSendable = 1000u, + maxSendable = 1000_000u, + allowsNostr = false, + nostrPubkey = null, + ), + ), + comment = "Thanks for the coffee!", + ), + isNodeRunning = true, + isLoading = false, + showBiometrics = false, + modifier = Modifier.sheetHeight() + ) + } + } +} + @Preview(showSystemUi = true) @Composable private fun PreviewBio() { From 764b71a25839bb14a19b0c9ea6a6b82182befb75 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 14:09:36 -0300 Subject: [PATCH 30/57] fix: replace gradient BG with White06 --- .../java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 2 ++ 1 file changed, 2 insertions(+) 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 5e0e2a2aa..9730bc079 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 @@ -359,6 +359,8 @@ fun ContentRunning( ) }, fullWidth = false, + color = Colors.White06, + enableGradient = false, modifier = Modifier .align(Alignment.CenterHorizontally) .testTag("SendConfirmToggleDetails") From 702863d1d0a44fda78dbbf0ab7fc7d74d60a8e88 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 14:13:09 -0300 Subject: [PATCH 31/57] doc: changelog entry --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358ebc984..9ebee3858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Show/hide details toggle on send confirmation screen with coin-stack animation +- "Send from" payment method switcher (Savings/Spending) for unified BIP21 payments +- "Instant" Lightning option on fee rate selection screen for unified payments +- Relative invoice expiry formatting on send confirmation screen + +### Changed + +- Custom fee rate defaults now fall back through settings default, slow rate, then 1 +- Sanity warnings reset when amount or payment method changes + [Unreleased]: https://github.com/synonymdev/bitkit-android/compare/v2.1.2...HEAD From 32610e267cfc3b16be4c61fcb59829d6db464f63 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 14:24:31 -0300 Subject: [PATCH 32/57] doc: changelog entry --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b5b7ea2..a68920e75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Show/hide details toggle on send confirmation screen with coin-stack animation -- "Send from" payment method switcher (Savings/Spending) for unified BIP21 payments -- "Instant" Lightning option on fee rate selection screen for unified payments -- Relative invoice expiry formatting on send confirmation screen +- Show/hide details toggle on send confirmation screen with coin-stack animation #863 +- "Send from" payment method switcher (Savings/Spending) for unified BIP21 payments #863 +- "Instant" Lightning option on fee rate selection screen for unified payments #863 +- Relative invoice expiry formatting on send confirmation screen #863 - Lightning Connections empty state with onboarding screen #857 - Unified PIN management screen (enable/disable/change in one place) #857 - Support entry in drawer menu #857 @@ -21,8 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Mnemonic warning text transitions on reveal #857 ### Changed -- Custom fee rate defaults now fall back through settings default, slow rate, then 1 -- Sanity warnings reset when amount or payment method changes +- Custom fee rate defaults now fall back through settings default, slow rate, then 1 #863 +- Sanity warnings reset when amount or payment method changes #863 - Settings redesigned with tabbed navigation (General/Security/Advanced) with swipe support #857 - Icons added to all settings rows for faster scanning #857 - Selected values displayed on right side of settings rows #857 From cc9d1f255d3c262758b7b776274a743cf552273f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 15:15:33 -0300 Subject: [PATCH 33/57] chore: code cleanup --- .../ui/screens/wallets/send/SendConfirmScreen.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 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 9730bc079..f4799f22a 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 @@ -310,6 +310,7 @@ fun ContentRunning( VerticalSpacer(16.dp) TagsSection(uiState, onClickTag, onClickAddTag) } + SendMethod.LIGHTNING -> { LightningDetails( uiState = uiState, @@ -407,15 +408,6 @@ private fun TagsSection( uiState: SendUiState, onClickTag: (String) -> Unit, onClickAddTag: () -> Unit, -) { - TagsSectionContent(uiState = uiState, onClickTag = onClickTag, onClickAddTag = onClickAddTag) -} - -@Composable -private fun TagsSectionContent( - uiState: SendUiState, - onClickTag: (String) -> Unit, - onClickAddTag: () -> Unit, modifier: Modifier = Modifier, ) { SendSectionView( @@ -725,7 +717,7 @@ private fun LightningDetails( } if (!isLnurlPay) { - TagsSectionContent( + TagsSection( uiState = uiState, onClickTag = onClickTag, onClickAddTag = onClickAddTag, From fc5a38e088536e6f0a0d82a68f87900b286a4bf9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 18:00:34 -0300 Subject: [PATCH 34/57] chore: set modifier as last parameter --- .../bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 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 f4799f22a..1e65f2180 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 @@ -475,11 +475,12 @@ private fun AddTagButton( private fun OnChainDetails( uiState: SendUiState, onEvent: (SendEvent) -> Unit, + modifier: Modifier = Modifier, ) { 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), @@ -586,6 +587,7 @@ private fun LightningDetails( onEvent: (SendEvent) -> Unit, onClickTag: (String) -> Unit, onClickAddTag: () -> Unit, + modifier: Modifier = Modifier, ) { val isLnurlPay = uiState.lnurl is LnurlParams.LnurlPay val expirySeconds = uiState.decodedInvoice?.expirySeconds @@ -597,7 +599,7 @@ private fun LightningDetails( Column( verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth() ) { Row( horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -730,9 +732,10 @@ private fun LightningDetails( private fun LnurlPayDetails( uiState: SendUiState, onEvent: (SendEvent) -> Unit, + modifier: Modifier = Modifier, ) { val lnurlPay = uiState.lnurl as? LnurlParams.LnurlPay ?: return - Column(modifier = Modifier.fillMaxWidth()) { + Column(modifier = modifier.fillMaxWidth()) { SendSectionView(caption = stringResource(R.string.wallet__send_invoice)) { BodySSB( text = lnurlPay.data.uri, From 057112c95c394284732e6b76f183be313af3219b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 30 Mar 2026 18:07:15 -0300 Subject: [PATCH 35/57] refactor: eliminate unsafe cast --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 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 1e65f2180..b9569ecc5 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 @@ -592,8 +592,8 @@ private fun LightningDetails( 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 + val destination = when (val lnurl = uiState.lnurl) { + is LnurlParams.LnurlPay -> lnurl.data.uri else -> uiState.decodedInvoice?.bolt11.orEmpty() } From 0e396021984ec35bc676cf1763d63cab18dfde1a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 08:26:47 -0300 Subject: [PATCH 36/57] fix: update text --- app/src/main/res/values-ar/strings.xml | 2 +- app/src/main/res/values-b+es+419/strings.xml | 2 +- app/src/main/res/values-ca/strings.xml | 2 +- app/src/main/res/values-cs/strings.xml | 2 +- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-el/strings.xml | 2 +- app/src/main/res/values-es-rES/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 2 +- app/src/main/res/values-pt/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values/strings.xml | 4 ++-- 16 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 840561096..672450be0 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -945,7 +945,7 @@ الرصيد الاحتياطي QuickPay جارٍ دفع\n<accent>الفاتورة...</accent> - مراجعة وإرسال + تأكيد تم إرسال Bitcoin اسحب للدفع إلى diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 9dbd3625a..67cb66c07 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -945,7 +945,7 @@ Saldo de reserva QuickPay Pagando\n<accent>factura...</accent> - Revisar y enviar + Confirmar Bitcoin Enviado Deslizar para pagar Para diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index c30193647..e3c83be4c 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -945,7 +945,7 @@ Saldo de reserva QuickPay Pagant\n<accent>factura...</accent> - Revisar i enviar + Confirmar Bitcoin enviat Desplaça per pagar A diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 8f9d457f2..606a15c4c 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -945,7 +945,7 @@ Zůstatek rezervy QuickPay Placení\n<accent>faktury...</accent> - Zkontrolovat a odeslat + Potvrdit bitcoin odeslán Zaplatit přejetím prstem Komu diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b3e55d72f..86a43b716 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -799,7 +799,7 @@ Verfügbar (sparen) Reserve-Saldo Der maximal auszahlbare Betrag ist etwas niedriger, da eine Reserve erforderlich ist. - Überprüfen & Senden + Bestätigen Bestätigung in Rechnungsverfall Zum Bezahlen wischen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 379545f73..279794cd3 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -945,7 +945,7 @@ Αποθεματικό υπόλοιπο QuickPay Πληρωμή\n<accent>τιμολογίου...</accent> - Έλεγχος & αποστολή + Επιβεβαίωση Τα Bitcoin στάλθηκαν Σύρε για πληρωμή Προς diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 252257879..699eefd9f 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -945,7 +945,7 @@ Saldo de reserva QuickPay Pagando\n<accent>factura...</accent> - Revisar y enviar + Confirmar Bitcoin Enviado Deslizar para pagar Para diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 8a8788259..f60beef35 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -796,7 +796,7 @@ MAX Disponible (para gasto) Disponible (ahorro) - Revisar y enviar + Confirmar Confirmando en Expiración de la factura Deslizar para pagar diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9b11dcd6a..a23a42aa2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -799,7 +799,7 @@ Disponible (épargne) Solde Réserve Le montant maximum pouvant être dépensé est un peu moins élevé en raison d\'un solde de Réserve requis. - Révision et envoi + Confirmer Confirmation en Expiration de la facture Glisser pour payer diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c54328363..e011944dd 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -945,7 +945,7 @@ Saldo di Riserva QuickPay Pagamento\n<accent>fattura...</accent> - Rivedi & Invia + Conferma Bitcoin Inviati Scorri per Pagare A diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 141fd00b9..3ce47fcd0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -945,7 +945,7 @@ Reservesaldo QuickPay Factuur\n<accent>betalen...</accent> - Controleren & verzenden + Bevestigen Bitcoin verzonden Veeg om te betalen Naar diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ad72a04c2..408035dc8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -799,7 +799,7 @@ Dostępne (oszczędności) Saldo rezerwy Maksymalna kwota do wydania jest nieco niższa ze względu na wymagane saldo rezerwowe. - Przejrzyj i wyślij + Potwierdź Potwierdzenie w ciągu Termin faktury Przesuń, aby zapłacić diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8bb66e6d9..2bce55e6a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -945,7 +945,7 @@ Saldo de reserva QuickPay Pagando \n<accent>invoice...</accent> - Revisar e Enviar + Confirmar Bitcoin Enviado Deslize para Pagar Para diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index dcf964b10..49f87acfa 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -766,7 +766,7 @@ Disponível (poupança) Saldo de reserva O valor máximo utilizável é um pouco menor devido a um saldo de reserva obrigatório. - Revisar e Enviar + Confirmar Confirmando em Vencimento do invoice Deslize para Pagar diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 56e7f7acf..8f60f3eea 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -973,7 +973,7 @@ Bitkit должен увеличить приёмную ёмкость ваше QuickPay Оплата <accent>счёта...</accent> - Проверить и Отправить + Подтвердить Биткоины отправлены Отправить Кому diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24f968538..f4f371024 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -962,7 +962,7 @@ Speed ₿ {feeSats} for this transaction ₿ {feeSats} for this transaction ({fiatSymbol}{fiatFormatted}) - Send from + From Invoice Invoice expiration MAX @@ -972,7 +972,7 @@ Payment Pending QuickPay Paying\n<accent>invoice...</accent> - Review & Send + Confirm Bitcoin Sent Swipe To Pay To From 684a46cb2c0f2ee0b038da42a971e81ef0397559 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 08:27:10 -0300 Subject: [PATCH 37/57] fix: AddTagButton color --- .../to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 b9569ecc5..21fc92604 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 @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator @@ -447,7 +448,7 @@ private fun AddTagButton( .clip(shape) .drawBehind { drawRoundRect( - color = Colors.White32, + color = Colors.White64, style = Stroke( width = 1.dp.toPx(), pathEffect = PathEffect.dashPathEffect(floatArrayOf(4f, 4f)), @@ -509,6 +510,7 @@ private fun OnChainDetails( overflow = TextOverflow.MiddleEllipsis, modifier = Modifier .height(28.dp) + .wrapContentHeight(Alignment.CenterVertically) .clickableAlpha { onEvent(SendEvent.NavToAddress) } .testTag("ReviewUri") ) @@ -628,6 +630,7 @@ private fun LightningDetails( overflow = TextOverflow.MiddleEllipsis, modifier = Modifier .height(28.dp) + .wrapContentHeight(Alignment.CenterVertically) .clickableAlpha { onEvent(SendEvent.NavToAddress) } .testTag("ReviewUri") ) @@ -743,6 +746,7 @@ private fun LnurlPayDetails( overflow = TextOverflow.MiddleEllipsis, modifier = Modifier .height(28.dp) + .wrapContentHeight(Alignment.CenterVertically) .clickableAlpha { onEvent(SendEvent.NavToAddress) } .testTag("ReviewUri") ) From f2cef8418b8c35f4192cd98bca19d47d7b892d28 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 08:27:37 -0300 Subject: [PATCH 38/57] fix: call updateCanSwitchWallet for fixed amount invoice --- 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 9ed5bd19d..d4355f0d6 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1185,6 +1185,7 @@ class AppViewModel @Inject constructor( refreshOnchainSendIfNeeded() estimateLightningRoutingFeesIfNeeded() _sendUiState.update { it.copy(isLoading = false) } + updateCanSwitchWallet() setSendEffect(SendEffect.NavigateToConfirm) } @@ -1330,6 +1331,7 @@ class AppViewModel @Inject constructor( payMethod = lnInvoice?.let { SendMethod.LIGHTNING } ?: SendMethod.ONCHAIN, ) } + updateCanSwitchWallet() val lnAmountSats = lnInvoice?.amountSatoshis ?: 0u if (lnAmountSats > 0u) { From 3a741c612faa47ea70e5ea9bfdf112ab3c4a6ae6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 08:42:29 -0300 Subject: [PATCH 39/57] fix: disable instant option when not possible to switch to lighting --- .../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 d642ec256..0f781df24 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 @@ -67,7 +67,7 @@ fun SendFeeRateScreen( Content( uiState = uiState, - isUnified = sendUiState.isUnified, + isUnified = sendUiState.canSwitchWallet, payMethod = sendUiState.payMethod, estimatedRoutingFee = sendUiState.estimatedRoutingFee.toLong(), onBack = onBack, From d155c4ddf791286f7913653a26a7a926335424e9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 09:00:30 -0300 Subject: [PATCH 40/57] fix: fee estimates not loaded when navigating from lightning --- .../main/java/to/bitkit/viewmodels/AppViewModel.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d4355f0d6..540fddfb3 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -787,7 +787,16 @@ class AppViewModel @Inject constructor( is SendEvent.CommentChange -> onCommentChange(it.value) - SendEvent.SpeedAndFee -> setSendEffect(SendEffect.NavigateToFee) + SendEvent.SpeedAndFee -> { + if (_sendUiState.value.fees.isEmpty()) { + viewModelScope.launch { + refreshFeeEstimates() + setSendEffect(SendEffect.NavigateToFee) + } + } else { + setSendEffect(SendEffect.NavigateToFee) + } + } SendEvent.SwipeToPay -> onSwipeToPay() is SendEvent.ConfirmAmountWarning -> onConfirmAmountWarning(it.warning) SendEvent.DismissAmountWarning -> onDismissAmountWarning() From f92708fbc3a5f58f32924f7ace382acb355d4d3c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 10:06:12 -0300 Subject: [PATCH 41/57] chore: make ContentRunning private --- .../java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 21fc92604..d6ccc449d 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 @@ -265,7 +265,7 @@ private fun Content( } @Composable -fun ContentRunning( +private fun ContentRunning( uiState: SendUiState, isLoading: Boolean, modifier: Modifier = Modifier, From 7a6c9ce3b5ba0b040167b18345e526a92dab1775 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 07:08:05 -0300 Subject: [PATCH 42/57] fix: tag text color --- .../java/to/bitkit/ui/screens/wallets/send/SendConfirmScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d6ccc449d..5aaf07d74 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 @@ -461,7 +461,7 @@ private fun AddTagButton( ) { BodySSB( text = stringResource(R.string.wallet__tags_add_button), - color = Colors.White64, + color = Colors.White, ) Icon( painter = painterResource(R.drawable.ic_plus), From af32125f7a6ca53418d9bef008c50d0865e411d2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 07:12:32 -0300 Subject: [PATCH 43/57] fix: instant icon --- app/src/main/java/to/bitkit/models/FeeRate.kt | 2 +- 1 file changed, 1 insertion(+), 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 6ebef7907..8cb69c3ad 100644 --- a/app/src/main/java/to/bitkit/models/FeeRate.kt +++ b/app/src/main/java/to/bitkit/models/FeeRate.kt @@ -20,7 +20,7 @@ enum class FeeRate( description = R.string.fee__instant__description, shortDescription = R.string.fee__instant__shortDescription, color = Colors.Purple, - icon = R.drawable.ic_lightning, + icon = R.drawable.ic_speed_fast, ), FAST( title = R.string.fee__fast__title, From fabfde30116101485a419ebc55fc2dc24e148b4a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 07:14:33 -0300 Subject: [PATCH 44/57] fix: fixed-amount path missing estimateLightningRoutingFeesIfNeeded --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 540fddfb3..3603300f7 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1353,6 +1353,7 @@ class AppViewModel @Inject constructor( if (quickPayHandled) return refreshOnchainSendIfNeeded() + estimateLightningRoutingFeesIfNeeded() if (isMainScanner) { showSheet(Sheet.Send(SendRoute.Confirm)) } else { From 2c5e2af8e7e3e80ecb4d60bc22b0ec601df5c985 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 07:23:55 -0300 Subject: [PATCH 45/57] fix: instant fee description --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4f371024..94462606a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,7 +75,7 @@ ±10-20 minutes ±10m Fast - Lightning Network + ±1-5 seconds Instant Instant +2 hours From 70d07bfa9098e7022d19ce00b73c889be38a03ba Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 07:33:56 -0300 Subject: [PATCH 46/57] fix: refreshFeeEstimates unconditionally overwriting fee with SendFee.OnChain --- .../to/bitkit/ui/screens/wallets/send/SendFeeRateScreen.kt | 3 ++- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 2 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 0f781df24..3f00dc4a8 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,6 +47,7 @@ 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.SendFee import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState @@ -69,7 +70,7 @@ fun SendFeeRateScreen( uiState = uiState, isUnified = sendUiState.canSwitchWallet, payMethod = sendUiState.payMethod, - estimatedRoutingFee = sendUiState.estimatedRoutingFee.toLong(), + estimatedRoutingFee = (sendUiState.fee as? SendFee.Lightning)?.value ?: 0L, onBack = onBack, onContinue = onContinue, onSelect = { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 3603300f7..7a963cada 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -2078,7 +2078,7 @@ class AppViewModel @Inject constructor( _sendUiState.update { it.copy( fees = feesMap.toImmutableMap(), - fee = SendFee.OnChain(currentFee), + fee = if (it.payMethod == SendMethod.ONCHAIN) SendFee.OnChain(currentFee) else it.fee, ) } } From da24a7eaff80aa12cfaf174e750c8100d2c4ce5a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 07:50:57 -0300 Subject: [PATCH 47/57] test: prevent regressions on send flow --- .../viewmodels/AppViewModelSendFlowTest.kt | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt new file mode 100644 index 000000000..275150872 --- /dev/null +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -0,0 +1,316 @@ +package to.bitkit.viewmodels + +import android.content.Context +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.advanceUntilIdle +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.mockito.kotlin.wheneverBlocking +import to.bitkit.data.AppCacheData +import to.bitkit.data.CacheStore +import to.bitkit.data.SettingsData +import to.bitkit.data.SettingsStore +import to.bitkit.data.keychain.Keychain +import to.bitkit.domain.commands.NotifyPaymentReceivedHandler +import to.bitkit.models.BalanceState +import to.bitkit.models.TransactionSpeed +import to.bitkit.repositories.ActivityRepo +import to.bitkit.repositories.BackupRepo +import to.bitkit.repositories.BlocktankRepo +import to.bitkit.repositories.ConnectivityRepo +import to.bitkit.repositories.ConnectivityState +import to.bitkit.repositories.CurrencyRepo +import to.bitkit.repositories.HealthRepo +import to.bitkit.repositories.LightningRepo +import to.bitkit.repositories.LightningState +import to.bitkit.repositories.PendingPaymentRepo +import to.bitkit.repositories.PreActivityMetadataRepo +import to.bitkit.repositories.TransferRepo +import to.bitkit.repositories.WalletRepo +import to.bitkit.repositories.WalletState +import to.bitkit.repositories.WidgetsRepo +import to.bitkit.services.AppUpdaterService +import to.bitkit.services.CoreService +import to.bitkit.services.MigrationService +import to.bitkit.test.BaseUnitTest +import to.bitkit.ui.shared.toast.ToastQueueManager +import to.bitkit.usecases.FormatMoneyValue +import to.bitkit.utils.timedsheets.TimedSheetManager +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class AppViewModelSendFlowTest : BaseUnitTest() { + + private lateinit var sut: AppViewModel + + private val context: Context = mock() + private val lightningRepo: LightningRepo = mock() + private val walletRepo: WalletRepo = mock() + private val settingsStore: SettingsStore = mock() + private val currencyRepo: CurrencyRepo = mock() + private val connectivityRepo: ConnectivityRepo = mock() + private val healthRepo: HealthRepo = mock() + private val pendingPaymentRepo: PendingPaymentRepo = mock() + private val backupRepo: BackupRepo = mock() + private val activityRepo: ActivityRepo = mock() + private val preActivityMetadataRepo: PreActivityMetadataRepo = mock() + private val blocktankRepo: BlocktankRepo = mock() + private val appUpdaterService: AppUpdaterService = mock() + private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler = mock() + private val cacheStore: CacheStore = mock() + private val transferRepo: TransferRepo = mock() + private val migrationService: MigrationService = mock() + private val coreService: CoreService = mock() + private val keychain: Keychain = mock() + private val widgetsRepo: WidgetsRepo = mock() + private val formatMoneyValue: FormatMoneyValue = mock() + + private val balanceState = MutableStateFlow(BalanceState()) + + private val timedSheetManager = mock() + + @Before + fun setUp() { + whenever(context.getString(any())).thenReturn("") + whenever(connectivityRepo.isOnline).thenReturn(MutableStateFlow(ConnectivityState.CONNECTED)) + whenever(healthRepo.healthState).thenReturn(MutableStateFlow(mock())) + whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(LightningState())) + whenever(lightningRepo.nodeEvents).thenReturn(MutableSharedFlow()) + whenever(walletRepo.balanceState).thenReturn(balanceState) + whenever(walletRepo.walletState).thenReturn(MutableStateFlow(WalletState())) + whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) + whenever(cacheStore.data).thenReturn(flowOf(AppCacheData())) + whenever(transferRepo.activeTransfers).thenReturn(flowOf(emptyList())) + whenever(timedSheetManager.currentSheet).thenReturn(MutableStateFlow(null)) + whenever(migrationService.isShowingMigrationLoading).thenReturn(MutableStateFlow(false)) + wheneverBlocking { migrationService.needsPostMigrationSync() }.thenReturn(false) + wheneverBlocking { migrationService.isMigrationChecked() }.thenReturn(true) + wheneverBlocking { widgetsRepo.refreshEnabledWidgets() }.thenReturn(Unit) + wheneverBlocking { lightningRepo.updateGeoBlockState() }.thenReturn(Unit) + wheneverBlocking { currencyRepo.convertSatsToFiat(any(), anyOrNull()) } + .thenReturn(Result.failure(Exception("not mocked"))) + wheneverBlocking { lightningRepo.calculateTotalFee(any(), anyOrNull(), any(), anyOrNull(), anyOrNull()) } + .thenReturn(Result.success(100uL)) + wheneverBlocking { lightningRepo.getFeeRateForSpeed(any(), anyOrNull()) } + .thenReturn(Result.success(2u)) + wheneverBlocking { lightningRepo.canSend(any(), any()) }.thenReturn(true) + + sut = AppViewModel( + connectivityRepo = connectivityRepo, + healthRepo = healthRepo, + toastManagerProvider = { mock() }, + timedSheetManagerProvider = { timedSheetManager }, + context = context, + bgDispatcher = testDispatcher, + keychain = keychain, + lightningRepo = lightningRepo, + pendingPaymentRepo = pendingPaymentRepo, + walletRepo = walletRepo, + backupRepo = backupRepo, + settingsStore = settingsStore, + currencyRepo = currencyRepo, + activityRepo = activityRepo, + preActivityMetadataRepo = preActivityMetadataRepo, + blocktankRepo = blocktankRepo, + appUpdaterService = appUpdaterService, + notifyPaymentReceivedHandler = notifyPaymentReceivedHandler, + cacheStore = cacheStore, + transferRepo = transferRepo, + migrationService = migrationService, + coreService = coreService, + appUpdateSheet = mock(), + backupSheet = mock(), + notificationsSheet = mock(), + quickPaySheet = mock(), + highBalanceSheet = mock(), + formatMoneyValue = formatMoneyValue, + widgetsRepo = widgetsRepo, + ) + } + + // -- updateCanSwitchWallet -- + + @Test + fun `canSwitchWallet is false when not unified`() = test { + sut.setSendEvent(SendEvent.AmountChange(1000u)) + advanceUntilIdle() + + assertFalse(sut.sendUiState.value.canSwitchWallet) + } + + @Test + fun `canSwitchWallet is false when amount is zero`() = test { + setUnifiedState(amount = 0u) + advanceUntilIdle() + + assertFalse(sut.sendUiState.value.canSwitchWallet) + } + + @Test + fun `canSwitchWallet is false when amount equals dust limit`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 100_000u, + maxSendLightningSats = 100_000u, + ) + setUnifiedState(amount = 546u) + advanceUntilIdle() + + assertFalse(sut.sendUiState.value.canSwitchWallet) + } + + @Test + fun `canSwitchWallet is true when amount above dust limit and within both balances`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 100_000u, + maxSendLightningSats = 100_000u, + ) + setUnifiedState(amount = 1000u) + advanceUntilIdle() + + assertTrue(sut.sendUiState.value.canSwitchWallet) + } + + @Test + fun `canSwitchWallet is false when amount exceeds onchain balance`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 500u, + maxSendLightningSats = 100_000u, + ) + setUnifiedState(amount = 1000u) + advanceUntilIdle() + + assertFalse(sut.sendUiState.value.canSwitchWallet) + } + + @Test + fun `canSwitchWallet is false when amount exceeds lightning balance`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 100_000u, + maxSendLightningSats = 500u, + ) + setUnifiedState(amount = 1000u) + advanceUntilIdle() + + assertFalse(sut.sendUiState.value.canSwitchWallet) + } + + // -- onPaymentMethodSwitch -- + + @Test + fun `switch from lightning to onchain resets confirmedWarnings`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 100_000u, + maxSendLightningSats = 100_000u, + ) + setUnifiedState(amount = 1000u, payMethod = SendMethod.LIGHTNING) + sut.setSendEvent(SendEvent.ConfirmAmountWarning(SanityWarning.VALUE_OVER_100_USD)) + advanceUntilIdle() + + assertTrue(sut.sendUiState.value.confirmedWarnings.isNotEmpty()) + + sut.setSendEvent(SendEvent.PaymentMethodSwitch) + advanceUntilIdle() + + assertTrue(sut.sendUiState.value.confirmedWarnings.isEmpty()) + } + + @Test + fun `switch from onchain to lightning sets fee to Lightning zero`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 100_000u, + maxSendLightningSats = 100_000u, + ) + setUnifiedState(amount = 1000u, payMethod = SendMethod.ONCHAIN) + advanceUntilIdle() + + sut.setSendEvent(SendEvent.PaymentMethodSwitch) + advanceUntilIdle() + + assertEquals(SendMethod.LIGHTNING, sut.sendUiState.value.payMethod) + assertEquals(SendFee.Lightning(0), sut.sendUiState.value.fee) + } + + @Test + fun `switch does nothing when not unified`() = test { + sut.setSendEvent(SendEvent.AmountChange(1000u)) + advanceUntilIdle() + + val before = sut.sendUiState.value.payMethod + sut.setSendEvent(SendEvent.PaymentMethodSwitch) + advanceUntilIdle() + + assertEquals(before, sut.sendUiState.value.payMethod) + } + + // -- onAmountChange -- + + @Test + fun `amount change clears confirmedWarnings`() = test { + setUnifiedState(amount = 1000u) + sut.setSendEvent(SendEvent.ConfirmAmountWarning(SanityWarning.VALUE_OVER_100_USD)) + advanceUntilIdle() + + assertTrue(sut.sendUiState.value.confirmedWarnings.isNotEmpty()) + + sut.setSendEvent(SendEvent.AmountChange(2000u)) + advanceUntilIdle() + + assertTrue(sut.sendUiState.value.confirmedWarnings.isEmpty()) + } + + // -- refreshFeeEstimates -- + + @Test + fun `refreshFeeEstimates preserves lightning fee when payMethod is LIGHTNING`() = test { + val lightningFee = SendFee.Lightning(42) + setUnifiedState(amount = 1000u, payMethod = SendMethod.LIGHTNING, fee = lightningFee) + advanceUntilIdle() + + sut.setSendEvent(SendEvent.SpeedAndFee) + advanceUntilIdle() + + val currentFee = sut.sendUiState.value.fee + assertEquals(lightningFee, currentFee) + } + + // -- helpers -- + + @Suppress("UNCHECKED_CAST") + private fun setSendState(state: SendUiState) { + val field = AppViewModel::class.java.getDeclaredField("_sendUiState") + field.isAccessible = true + (field.get(sut) as MutableStateFlow).value = state + } + + private fun setUnifiedState( + amount: ULong = 0u, + payMethod: SendMethod = SendMethod.LIGHTNING, + fee: SendFee? = null, + ) { + setSendState( + SendUiState( + address = "bcrt1qtest", + amount = amount, + isUnified = true, + payMethod = payMethod, + fee = fee, + confirmedWarnings = persistentListOf(), + speed = TransactionSpeed.Medium, + ) + ) + // Trigger updateCanSwitchWallet via reflection + val method = AppViewModel::class.java.getDeclaredMethod("updateCanSwitchWallet") + method.isAccessible = true + method.invoke(sut) + } +} From 39cd7a60a09162f682ba47df44d62e0bcd067f64 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 08:03:35 -0300 Subject: [PATCH 48/57] fix: lighting fee state override to zero when selecting on-chain --- .../screens/wallets/send/SendFeeRateScreen.kt | 3 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 5 +++- .../viewmodels/AppViewModelSendFlowTest.kt | 30 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 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 3f00dc4a8..40938e410 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,7 +47,6 @@ 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.SendFee import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.SendUiState @@ -70,7 +69,7 @@ fun SendFeeRateScreen( uiState = uiState, isUnified = sendUiState.canSwitchWallet, payMethod = sendUiState.payMethod, - estimatedRoutingFee = (sendUiState.fee as? SendFee.Lightning)?.value ?: 0L, + estimatedRoutingFee = sendUiState.lastLightningFee, onBack = onBack, onContinue = onContinue, onSelect = { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 7a963cada..55ac8e0a1 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1098,6 +1098,7 @@ class AppViewModel @Inject constructor( selectedUtxos = if (shouldResetUtxos) null else it.selectedUtxos, ) } + updateCanSwitchWallet() refreshOnchainSendIfNeeded() setSendEffect(SendEffect.PopBack(SendRoute.Confirm)) } @@ -2097,7 +2098,8 @@ class AppViewModel @Inject constructor( feeResult.onSuccess { fee -> _sendUiState.update { it.copy( - fee = SendFee.Lightning(fee.toLong()) + fee = SendFee.Lightning(fee.toLong()), + lastLightningFee = fee.toLong(), ) } } @@ -2540,6 +2542,7 @@ data class SendUiState( val fee: SendFee? = null, val fees: ImmutableMap = persistentMapOf(), val estimatedRoutingFee: ULong = 0uL, + val lastLightningFee: Long = 0L, ) enum class SanityWarning(@StringRes val message: Int, val testTag: String) { diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 275150872..b0f4ee1c5 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -283,6 +283,34 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(lightningFee, currentFee) } + // -- lastLightningFee -- + + @Test + fun `lastLightningFee persists after switching to onchain`() = test { + balanceState.value = BalanceState( + maxSendOnchainSats = 100_000u, + maxSendLightningSats = 100_000u, + ) + setUnifiedState( + amount = 1000u, + payMethod = SendMethod.LIGHTNING, + fee = SendFee.Lightning(42), + lastLightningFee = 42L, + ) + advanceUntilIdle() + + sut.setTransactionSpeed(TransactionSpeed.Medium) + advanceUntilIdle() + + assertEquals(SendMethod.ONCHAIN, sut.sendUiState.value.payMethod) + assertEquals(42L, sut.sendUiState.value.lastLightningFee) + } + + @Test + fun `lastLightningFee is zero initially`() = test { + assertEquals(0L, sut.sendUiState.value.lastLightningFee) + } + // -- helpers -- @Suppress("UNCHECKED_CAST") @@ -296,6 +324,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { amount: ULong = 0u, payMethod: SendMethod = SendMethod.LIGHTNING, fee: SendFee? = null, + lastLightningFee: Long = 0L, ) { setSendState( SendUiState( @@ -304,6 +333,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { isUnified = true, payMethod = payMethod, fee = fee, + lastLightningFee = lastLightningFee, confirmedWarnings = persistentListOf(), speed = TransactionSpeed.Medium, ) From 4eed0a335dc3b9ce5d204b764bb38f6f5ddab719 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 08:24:49 -0300 Subject: [PATCH 49/57] chore: replace wheneverBlocking --- .../viewmodels/AppViewModelSendFlowTest.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index b0f4ee1c5..83fbf9419 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -13,7 +13,6 @@ import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -import org.mockito.kotlin.wheneverBlocking import to.bitkit.data.AppCacheData import to.bitkit.data.CacheStore import to.bitkit.data.SettingsData @@ -93,17 +92,17 @@ class AppViewModelSendFlowTest : BaseUnitTest() { whenever(transferRepo.activeTransfers).thenReturn(flowOf(emptyList())) whenever(timedSheetManager.currentSheet).thenReturn(MutableStateFlow(null)) whenever(migrationService.isShowingMigrationLoading).thenReturn(MutableStateFlow(false)) - wheneverBlocking { migrationService.needsPostMigrationSync() }.thenReturn(false) - wheneverBlocking { migrationService.isMigrationChecked() }.thenReturn(true) - wheneverBlocking { widgetsRepo.refreshEnabledWidgets() }.thenReturn(Unit) - wheneverBlocking { lightningRepo.updateGeoBlockState() }.thenReturn(Unit) - wheneverBlocking { currencyRepo.convertSatsToFiat(any(), anyOrNull()) } + whenever { migrationService.needsPostMigrationSync() }.thenReturn(false) + whenever { migrationService.isMigrationChecked() }.thenReturn(true) + whenever { widgetsRepo.refreshEnabledWidgets() }.thenReturn(Unit) + whenever { lightningRepo.updateGeoBlockState() }.thenReturn(Unit) + whenever { currencyRepo.convertSatsToFiat(any(), anyOrNull()) } .thenReturn(Result.failure(Exception("not mocked"))) - wheneverBlocking { lightningRepo.calculateTotalFee(any(), anyOrNull(), any(), anyOrNull(), anyOrNull()) } + whenever { lightningRepo.calculateTotalFee(any(), anyOrNull(), any(), anyOrNull(), anyOrNull()) } .thenReturn(Result.success(100uL)) - wheneverBlocking { lightningRepo.getFeeRateForSpeed(any(), anyOrNull()) } + whenever { lightningRepo.getFeeRateForSpeed(any(), anyOrNull()) } .thenReturn(Result.success(2u)) - wheneverBlocking { lightningRepo.canSend(any(), any()) }.thenReturn(true) + whenever { lightningRepo.canSend(any(), any()) }.thenReturn(true) sut = AppViewModel( connectivityRepo = connectivityRepo, From e70e9fc04b9c70ada03a6cfe504feff66fb6bd34 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 08:26:54 -0300 Subject: [PATCH 50/57] chore: lint --- app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt | 2 +- 1 file changed, 1 insertion(+), 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 3557e0105..9d7f06b5f 100644 --- a/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt +++ b/app/src/main/java/to/bitkit/ui/components/SwipeToConfirm.kt @@ -63,6 +63,7 @@ private val Padding = 8.dp @Composable fun SwipeToConfirm( + onConfirm: () -> Unit, modifier: Modifier = Modifier, text: String = stringResource(R.string.other__swipe), color: Color = Colors.Brand, @@ -72,7 +73,6 @@ fun SwipeToConfirm( loading: Boolean = false, confirmed: Boolean = false, progress: MutableFloatState? = null, - onConfirm: () -> Unit, ) { val scope = rememberCoroutineScope() val trailColor = remember(color) { color.copy(alpha = 0.24f) } From bffaa57413f210ec6ea8f8f31b608e0e1927bca1 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 13:12:45 -0300 Subject: [PATCH 51/57] chore: update changelog rule --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 7ac8f687f..b7fef0e82 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -240,7 +240,8 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { ### Changelog -- ALWAYS add an entry under `## [Unreleased]` in `CHANGELOG.md` for `feat:` and `fix:` PRs; skip for `chore:`, `ci:`, `refactor:`, `test:`, `docs:` unless the change is user-facing +- ALWAYS add exactly ONE entry per PR under `## [Unreleased]` in `CHANGELOG.md` for `feat:` and `fix:` PRs; skip for `chore:`, `ci:`, `refactor:`, `test:`, `docs:` unless the change is user-facing +- NEVER add multiple changelog lines for the same PR — summarize all changes in a single concise entry - USE standard Keep a Changelog categories: `### Added`, `### Changed`, `### Deprecated`, `### Removed`, `### Fixed`, `### Security` - ALWAYS append `#PR_NUMBER` at the end of each changelog entry when the PR number is known - ALWAYS place new entries at the top of their category section (newest first) From 7312e09c8ac76871b2309c2e79298756dffd7112 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 13:12:58 -0300 Subject: [PATCH 52/57] chore: update changelog entrie to match Agentes rule --- CHANGELOG.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ff3030b6..c3c090d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Show loading state on Spending tab when node is not running #875 ### Added -- Show/hide details toggle on send confirmation screen with coin-stack animation #863 -- "Send from" payment method switcher (Savings/Spending) for unified BIP21 payments #863 -- "Instant" Lightning option on fee rate selection screen for unified payments #863 -- Relative invoice expiry formatting on send confirmation screen #863 +- Unified send flow with payment method switcher, details toggle, and Lightning support for BIP21 payments #863 - Lightning Connections empty state with onboarding screen #857 - Unified PIN management screen (enable/disable/change in one place) #857 - Support entry in drawer menu #857 @@ -26,8 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Mnemonic warning text transitions on reveal #857 ### Changed -- Custom fee rate defaults now fall back through settings default, slow rate, then 1 #863 -- Sanity warnings reset when amount or payment method changes #863 +- Improved fee rate defaults and sanity warning resets for send flow #863 - Settings redesigned with tabbed navigation (General/Security/Advanced) with swipe support #857 - Icons added to all settings rows for faster scanning #857 - Selected values displayed on right side of settings rows #857 From 9dbc7b7875ab8c0899e2f39dbdc4b081258e5184 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 13:15:09 -0300 Subject: [PATCH 53/57] refactor: remove section comments --- .../to/bitkit/viewmodels/AppViewModelSendFlowTest.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 83fbf9419..2dbd9bc38 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -137,8 +137,6 @@ class AppViewModelSendFlowTest : BaseUnitTest() { ) } - // -- updateCanSwitchWallet -- - @Test fun `canSwitchWallet is false when not unified`() = test { sut.setSendEvent(SendEvent.AmountChange(1000u)) @@ -203,8 +201,6 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertFalse(sut.sendUiState.value.canSwitchWallet) } - // -- onPaymentMethodSwitch -- - @Test fun `switch from lightning to onchain resets confirmedWarnings`() = test { balanceState.value = BalanceState( @@ -251,8 +247,6 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(before, sut.sendUiState.value.payMethod) } - // -- onAmountChange -- - @Test fun `amount change clears confirmedWarnings`() = test { setUnifiedState(amount = 1000u) @@ -267,8 +261,6 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertTrue(sut.sendUiState.value.confirmedWarnings.isEmpty()) } - // -- refreshFeeEstimates -- - @Test fun `refreshFeeEstimates preserves lightning fee when payMethod is LIGHTNING`() = test { val lightningFee = SendFee.Lightning(42) @@ -282,8 +274,6 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(lightningFee, currentFee) } - // -- lastLightningFee -- - @Test fun `lastLightningFee persists after switching to onchain`() = test { balanceState.value = BalanceState( @@ -310,8 +300,6 @@ class AppViewModelSendFlowTest : BaseUnitTest() { assertEquals(0L, sut.sendUiState.value.lastLightningFee) } - // -- helpers -- - @Suppress("UNCHECKED_CAST") private fun setSendState(state: SendUiState) { val field = AppViewModel::class.java.getDeclaredField("_sendUiState") From 5a567e0a1fdac944184de7059256af2f261867d6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 13:17:06 -0300 Subject: [PATCH 54/57] refactor: mock declaration --- .../viewmodels/AppViewModelSendFlowTest.kt | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 2dbd9bc38..cadaaae43 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -52,27 +52,27 @@ class AppViewModelSendFlowTest : BaseUnitTest() { private lateinit var sut: AppViewModel - private val context: Context = mock() - private val lightningRepo: LightningRepo = mock() - private val walletRepo: WalletRepo = mock() - private val settingsStore: SettingsStore = mock() - private val currencyRepo: CurrencyRepo = mock() - private val connectivityRepo: ConnectivityRepo = mock() - private val healthRepo: HealthRepo = mock() - private val pendingPaymentRepo: PendingPaymentRepo = mock() - private val backupRepo: BackupRepo = mock() - private val activityRepo: ActivityRepo = mock() - private val preActivityMetadataRepo: PreActivityMetadataRepo = mock() - private val blocktankRepo: BlocktankRepo = mock() - private val appUpdaterService: AppUpdaterService = mock() - private val notifyPaymentReceivedHandler: NotifyPaymentReceivedHandler = mock() - private val cacheStore: CacheStore = mock() - private val transferRepo: TransferRepo = mock() - private val migrationService: MigrationService = mock() - private val coreService: CoreService = mock() - private val keychain: Keychain = mock() - private val widgetsRepo: WidgetsRepo = mock() - private val formatMoneyValue: FormatMoneyValue = mock() + private val context = mock() + private val lightningRepo = mock() + private val walletRepo = mock() + private val settingsStore = mock() + private val currencyRepo = mock() + private val connectivityRepo = mock() + private val healthRepo = mock() + private val pendingPaymentRepo = mock() + private val backupRepo = mock() + private val activityRepo = mock() + private val preActivityMetadataRepo = mock() + private val blocktankRepo = mock() + private val appUpdaterService = mock() + private val notifyPaymentReceivedHandler = mock() + private val cacheStore = mock() + private val transferRepo = mock() + private val migrationService = mock() + private val coreService = mock() + private val keychain = mock() + private val widgetsRepo = mock() + private val formatMoneyValue = mock() private val balanceState = MutableStateFlow(BalanceState()) From a6641f4ca3360240a53a11f9e35db6470f43d4f8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 13:19:29 -0300 Subject: [PATCH 55/57] refactor: rename component and add preview --- .../{SendSectionView.kt => SendCell.kt} | 15 ++++++++++- .../screens/wallets/send/SendConfirmScreen.kt | 26 +++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) rename app/src/main/java/to/bitkit/ui/components/{SendSectionView.kt => SendCell.kt} (67%) diff --git a/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt b/app/src/main/java/to/bitkit/ui/components/SendCell.kt similarity index 67% rename from app/src/main/java/to/bitkit/ui/components/SendSectionView.kt rename to app/src/main/java/to/bitkit/ui/components/SendCell.kt index cf9c772fc..0d8f36376 100644 --- a/app/src/main/java/to/bitkit/ui/components/SendSectionView.kt +++ b/app/src/main/java/to/bitkit/ui/components/SendCell.kt @@ -3,13 +3,16 @@ 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.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable -fun SendSectionView( +fun SendCell( caption: String, modifier: Modifier = Modifier, content: @Composable () -> Unit, @@ -22,3 +25,13 @@ fun SendSectionView( HorizontalDivider(modifier = Modifier.fillMaxWidth()) } } + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + SendCell(caption = "AMOUNT") { + Text("0.001 BTC", color = Colors.White) + } + } +} 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 5aaf07d74..f5dde27c4 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 @@ -73,7 +73,7 @@ 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.SendCell import to.bitkit.ui.components.SwipeToConfirm import to.bitkit.ui.components.SyncNodeView import to.bitkit.ui.components.TagButton @@ -411,7 +411,7 @@ private fun TagsSection( onClickAddTag: () -> Unit, modifier: Modifier = Modifier, ) { - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__tags), modifier = modifier ) { @@ -487,7 +487,7 @@ private fun OnChainDetails( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) ) { - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__send_from), modifier = Modifier.weight(1f) ) { @@ -500,7 +500,7 @@ private fun OnChainDetails( modifier = Modifier.testTag("SendConfirmAssetButton") ) } - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__send_to), modifier = Modifier.weight(1f) ) { @@ -527,7 +527,7 @@ private fun OnChainDetails( .fillMaxHeight() .clickableAlpha { onEvent(SendEvent.SpeedAndFee) } ) { - SendSectionView(caption = stringResource(R.string.wallet__send_fee_and_speed)) { + SendCell(caption = stringResource(R.string.wallet__send_fee_and_speed)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -561,7 +561,7 @@ private fun OnChainDetails( } } } - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__send_confirming_in), modifier = Modifier.weight(1f) ) { @@ -607,7 +607,7 @@ private fun LightningDetails( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.height(IntrinsicSize.Min) ) { - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__send_from), modifier = Modifier.weight(1f) ) { @@ -620,7 +620,7 @@ private fun LightningDetails( modifier = Modifier.testTag("SendConfirmAssetButton") ) } - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__send_to), modifier = Modifier.weight(1f) ) { @@ -647,7 +647,7 @@ private fun LightningDetails( .fillMaxHeight() .let { if (uiState.canSwitchWallet) it.clickableAlpha { onEvent(SendEvent.SpeedAndFee) } else it } ) { - SendSectionView(caption = stringResource(R.string.wallet__send_fee_and_speed)) { + SendCell(caption = stringResource(R.string.wallet__send_fee_and_speed)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -683,7 +683,7 @@ private fun LightningDetails( } } if (!isLnurlPay && expirySeconds != null) { - SendSectionView( + SendCell( caption = stringResource(R.string.wallet__send_invoice_expiration), modifier = Modifier.weight(1f) ) { @@ -714,7 +714,7 @@ private fun LightningDetails( } if (!isLnurlPay && !description.isNullOrEmpty()) { - SendSectionView(caption = stringResource(R.string.wallet__note)) { + SendCell(caption = stringResource(R.string.wallet__note)) { Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { BodySSB(text = description, maxLines = 1) } @@ -739,7 +739,7 @@ private fun LnurlPayDetails( ) { val lnurlPay = uiState.lnurl as? LnurlParams.LnurlPay ?: return Column(modifier = modifier.fillMaxWidth()) { - SendSectionView(caption = stringResource(R.string.wallet__send_invoice)) { + SendCell(caption = stringResource(R.string.wallet__send_invoice)) { BodySSB( text = lnurlPay.data.uri, maxLines = 1, @@ -754,7 +754,7 @@ private fun LnurlPayDetails( VerticalSpacer(16.dp) - SendSectionView(caption = stringResource(R.string.wallet__send_fee_and_speed)) { + SendCell(caption = stringResource(R.string.wallet__send_fee_and_speed)) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), From e50948c6e8fc71a80108b61148ce87eb141b3a49 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 2 Apr 2026 13:34:18 -0300 Subject: [PATCH 56/57] doc: consolidate changelog entries --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3c090d70..a7b6d4bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Show loading state on Spending tab when node is not running #875 ### Added -- Unified send flow with payment method switcher, details toggle, and Lightning support for BIP21 payments #863 - Lightning Connections empty state with onboarding screen #857 - Unified PIN management screen (enable/disable/change in one place) #857 - Support entry in drawer menu #857 @@ -23,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Mnemonic warning text transitions on reveal #857 ### Changed -- Improved fee rate defaults and sanity warning resets for send flow #863 +- Unified send flow with payment method switcher, details toggle, Lightning support for BIP21 payments, and improved fee rate defaults #863 - Settings redesigned with tabbed navigation (General/Security/Advanced) with swipe support #857 - Icons added to all settings rows for faster scanning #857 - Selected values displayed on right side of settings rows #857 From 49518e8f43db2a85466631656636ef666b59b349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Sena?= Date: Thu, 2 Apr 2026 14:04:26 -0300 Subject: [PATCH 57/57] Update app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index cadaaae43..d1c479590 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -96,7 +96,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { whenever { migrationService.isMigrationChecked() }.thenReturn(true) whenever { widgetsRepo.refreshEnabledWidgets() }.thenReturn(Unit) whenever { lightningRepo.updateGeoBlockState() }.thenReturn(Unit) - whenever { currencyRepo.convertSatsToFiat(any(), anyOrNull()) } + whenever(currencyRepo.convertSatsToFiat(any(), anyOrNull())) .thenReturn(Result.failure(Exception("not mocked"))) whenever { lightningRepo.calculateTotalFee(any(), anyOrNull(), any(), anyOrNull(), anyOrNull()) } .thenReturn(Result.success(100uL))