diff --git a/app/src/main/java/to/bitkit/ui/components/Spacers.kt b/app/src/main/java/to/bitkit/ui/components/Spacers.kt index 724409f2a..8d54132a1 100644 --- a/app/src/main/java/to/bitkit/ui/components/Spacers.kt +++ b/app/src/main/java/to/bitkit/ui/components/Spacers.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,6 +19,15 @@ fun VerticalSpacer(height: Dp) { Spacer(modifier = Modifier.height(height)) } +@Composable +fun ColumnScope.VerticalSpacer(minHeight: Dp, maxHeight: Dp) { + Spacer( + modifier = Modifier + .weight(1f) + .sizeIn(minHeight = minHeight, maxHeight = maxHeight) + ) +} + @Composable fun HorizontalSpacer(width: Dp) { Spacer(modifier = Modifier.width(width)) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index 62638cb1e..0da240830 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -34,6 +34,7 @@ import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.NumberPadActionButton import to.bitkit.ui.components.NumberPadTextField import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SyncNodeView import to.bitkit.ui.components.Text13Up import to.bitkit.ui.components.UnitButton import to.bitkit.ui.components.VerticalSpacer @@ -59,9 +60,10 @@ fun SpendingAmountScreen( ) { val currencies = LocalCurrencies.current val uiState by viewModel.spendingUiState.collectAsStateWithLifecycle() + val isNodeRunning by viewModel.isNodeRunning.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.updateLimits(retry = true) + viewModel.updateLimits() } LaunchedEffect(Unit) { @@ -86,6 +88,7 @@ fun SpendingAmountScreen( ) Content( + isNodeRunning = isNodeRunning, uiState = uiState, currencies = currencies, onBackClick = onBackClick, @@ -99,6 +102,7 @@ fun SpendingAmountScreen( @Composable private fun Content( + isNodeRunning: Boolean, uiState: TransferToSpendingUiState, currencies: CurrencyUiState, onBackClick: () -> Unit, @@ -114,92 +118,123 @@ private fun Content( onBackClick = onBackClick, actions = { CloseNavIcon(onCloseClick) }, ) - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxSize() - .imePadding() - .testTag("SpendingAmount") - ) { - VerticalSpacer(16.dp) - Display( - text = stringResource(R.string.lightning__spending_amount__title) - .withAccent(accentColor = Colors.Purple) - ) - NumberPadTextField( - input = uiState.input, - displayUnit = currencies.displayUnit, - showSecondaryField = false, - primaryDisplay = currencies.primaryDisplay, + if (isNodeRunning) { + SpendingAmountNodeRunning( + uiState = uiState, + currencies = currencies, + onClickQuarter = onClickQuarter, + onClickMaxAmount = onClickMaxAmount, + onConfirmAmount = onConfirmAmount, + onInputChange = onInputChange, + ) + } else { + SyncNodeView( modifier = Modifier .fillMaxWidth() - .testTag("SpendingAmountNumberField") + .weight(1f) ) + } + } +} - FillHeight() +@Composable +private fun SpendingAmountNodeRunning( + uiState: TransferToSpendingUiState, + currencies: CurrencyUiState, + onClickQuarter: () -> Unit, + onClickMaxAmount: () -> Unit, + onConfirmAmount: () -> Unit, + onInputChange: (String) -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize() + .imePadding() + .testTag("SpendingAmount") + ) { + VerticalSpacer(minHeight = 16.dp, maxHeight = 32.dp) - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .padding(vertical = 8.dp) - .testTag("SendAmountNumberPad") - ) { - Column { - Text13Up( - text = stringResource(R.string.wallet__send_available), - color = Colors.White64, - modifier = Modifier.testTag("SpendingAmountAvailable") - ) - Spacer(modifier = Modifier.height(8.dp)) - MoneySSB(sats = uiState.balanceAfterFee, modifier = Modifier.testTag("SpendingAmountUnit")) - } - FillWidth() - UnitButton(color = Colors.Purple) - // 25% Button - NumberPadActionButton( - text = stringResource(R.string.lightning__spending_amount__quarter), - color = Colors.Purple, - onClick = onClickQuarter, - modifier = Modifier.testTag("SpendingAmountQuarter") - ) - // Max Button - NumberPadActionButton( - text = stringResource(R.string.common__max), - color = Colors.Purple, - onClick = onClickMaxAmount, - modifier = Modifier.testTag("SpendingAmountMax") + Display( + text = stringResource(R.string.lightning__spending_amount__title) + .withAccent(accentColor = Colors.Purple) + ) + + FillHeight() + + NumberPadTextField( + input = uiState.input, + displayUnit = currencies.displayUnit, + showSecondaryField = false, + primaryDisplay = currencies.primaryDisplay, + modifier = Modifier + .fillMaxWidth() + .testTag("SpendingAmountNumberField") + ) + + FillHeight() + + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(vertical = 8.dp) + .testTag("SendAmountNumberPad") + ) { + Column { + Text13Up( + text = stringResource(R.string.wallet__send_available), + color = Colors.White64, + modifier = Modifier.testTag("SpendingAmountAvailable") ) + Spacer(modifier = Modifier.height(8.dp)) + MoneySSB(sats = uiState.balanceAfterFee, modifier = Modifier.testTag("SpendingAmountUnit")) } - HorizontalDivider() - - VerticalSpacer(16.dp) - - Keyboard( - onClick = { number -> - onInputChange(if (uiState.input == "0") number else uiState.input + number) - }, - onClickBackspace = { - onInputChange(if (uiState.input.length > 1) uiState.input.dropLast(1) else "0") - }, - isDecimal = currencies.primaryDisplay == PrimaryDisplay.FIAT, - modifier = Modifier - .fillMaxWidth() + FillWidth() + UnitButton(color = Colors.Purple) + // 25% Button + NumberPadActionButton( + text = stringResource(R.string.lightning__spending_amount__quarter), + color = Colors.Purple, + onClick = onClickQuarter, + modifier = Modifier.testTag("SpendingAmountQuarter") + ) + // Max Button + NumberPadActionButton( + text = stringResource(R.string.common__max), + color = Colors.Purple, + onClick = onClickMaxAmount, + modifier = Modifier.testTag("SpendingAmountMax") ) + } + HorizontalDivider() - VerticalSpacer(8.dp) + VerticalSpacer(16.dp) - PrimaryButton( - text = stringResource(R.string.common__continue), - onClick = onConfirmAmount, - enabled = uiState.satsAmount != 0L && uiState.satsAmount <= uiState.maxAllowedToSend, - isLoading = uiState.isLoading, - modifier = Modifier.testTag("SpendingAmountContinue") - ) + Keyboard( + onClick = { number -> + onInputChange(if (uiState.input == "0") number else uiState.input + number) + }, + onClickBackspace = { + onInputChange(if (uiState.input.length > 1) uiState.input.dropLast(1) else "0") + }, + isDecimal = currencies.primaryDisplay == PrimaryDisplay.FIAT, + modifier = Modifier + .fillMaxWidth() + ) - VerticalSpacer(16.dp) - } + VerticalSpacer(8.dp) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onConfirmAmount, + enabled = uiState.satsAmount != 0L && uiState.satsAmount <= uiState.maxAllowedToSend, + isLoading = uiState.isLoading, + modifier = Modifier.testTag("SpendingAmountContinue") + ) + + VerticalSpacer(16.dp) } } @@ -208,6 +243,7 @@ private fun Content( private fun Preview() { AppThemeSurface { Content( + isNodeRunning = true, uiState = TransferToSpendingUiState(input = "5 000"), currencies = CurrencyUiState(), onBackClick = {}, @@ -225,6 +261,25 @@ private fun Preview() { private fun Preview2() { AppThemeSurface { Content( + isNodeRunning = true, + uiState = TransferToSpendingUiState(input = "5 000"), + currencies = CurrencyUiState(), + onBackClick = {}, + onCloseClick = {}, + onClickQuarter = {}, + onClickMaxAmount = {}, + onConfirmAmount = {}, + onInputChange = {}, + ) + } +} + +@Preview(showBackground = true, device = NEXUS_5) +@Composable +private fun Preview3() { + AppThemeSurface { + Content( + isNodeRunning = false, uiState = TransferToSpendingUiState(input = "5 000"), currencies = CurrencyUiState(), onBackClick = {}, diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index d07517886..6ad4a665b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -18,11 +18,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R import to.bitkit.data.CacheStore @@ -41,6 +43,7 @@ import javax.inject.Inject import kotlin.math.max import kotlin.math.min import kotlin.math.roundToLong +import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds const val RETRY_INTERVAL_MS = 1 * 60 * 1000L // 1 minutes in ms @@ -63,6 +66,9 @@ class TransferViewModel @Inject constructor( val lightningSetupStep: StateFlow = settingsStore.data.map { it.lightningSetupStep } .stateIn(viewModelScope, SharingStarted.Lazily, 0) + val isNodeRunning = lightningRepo.lightningState.map { it.nodeStatus?.isRunning ?: false } + .stateIn(viewModelScope, SharingStarted.Lazily, false) + private val _selectedChannelIdsState = MutableStateFlow>(emptySet()) val selectedChannelIdsState = _selectedChannelIdsState.asStateFlow() @@ -71,7 +77,6 @@ class TransferViewModel @Inject constructor( val transferEffects = MutableSharedFlow() fun setTransferEffect(effect: TransferEffect) = viewModelScope.launch { transferEffects.emit(effect) } - var retryTimes = 0 var maxLspFee = 0uL // region Spending @@ -83,7 +88,7 @@ class TransferViewModel @Inject constructor( overrideSats = it.maxAllowedToSend, ) } - updateLimits(false) + updateLimits() } fun onClickQuarter() { @@ -106,7 +111,7 @@ class TransferViewModel @Inject constructor( overrideSats = min(quarter, it.maxAllowedToSend), ) } - updateLimits(false) + updateLimits() } fun onConfirmAmount() { @@ -138,6 +143,10 @@ class TransferViewModel @Inject constructor( return@launch } + withTimeoutOrNull(1.minutes) { + isNodeRunning.first { it } + } + blocktankRepo.createOrder(_spendingUiState.value.satsAmount.toULong()) .onSuccess { order -> settingsStore.update { it.copy(lightningSetupStep = 0) } @@ -172,13 +181,12 @@ class TransferViewModel @Inject constructor( _spendingUiState.update { it.copy(satsAmount = sats, overrideSats = null) } - retryTimes = 0 - updateLimits(retry = false) + updateLimits() } - fun updateLimits(retry: Boolean) { + fun updateLimits() { updateTransferValues(_spendingUiState.value.satsAmount.toULong()) - updateAvailableAmount(retry = retry) + updateAvailableAmount() } fun onAdvancedOrderCreated(order: IBtOrder) { @@ -260,19 +268,22 @@ class TransferViewModel @Inject constructor( setTransferEffect(TransferEffect.OnOrderCreated) } - private fun updateAvailableAmount(retry: Boolean) { + private fun updateAvailableAmount() { viewModelScope.launch { _spendingUiState.update { it.copy(isLoading = true) } // Get the max available balance discounting onChain fee val availableAmount = walletRepo.getMaxSendAmount() + withTimeoutOrNull(1.minutes) { + isNodeRunning.first { it } + } + // Calculate the LSP fee to the total balance blocktankRepo.estimateOrderFee( spendingBalanceSats = availableAmount, receivingBalanceSats = _transferValues.value.maxLspBalance ).onSuccess { estimate -> - retryTimes = 0 maxLspFee = estimate.feeSat // Calculate the available balance to send after LSP fee @@ -290,17 +301,9 @@ class TransferViewModel @Inject constructor( ) } }.onFailure { exception -> - if (exception is ServiceError.NodeNotStarted && retry) { - // Retry after delay - Logger.warn("Error getting the available amount. Node not started. trying again in 2 seconds") - delay(2.seconds) - updateAvailableAmount(retry = retryTimes <= RETRY_LIMIT) - retryTimes++ - } else { - _spendingUiState.update { it.copy(isLoading = false) } - Logger.error("Failure", exception) - setTransferEffect(TransferEffect.ToastException(exception)) - } + _spendingUiState.update { it.copy(isLoading = false) } + Logger.error("Failure", exception) + setTransferEffect(TransferEffect.ToastException(exception)) } } }