diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 9ece0cf15..1b66e4f1d 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -191,7 +191,7 @@ LongParameterList:CoreService.kt$OnchainService$( mnemonicPhrase: String, derivationPathStr: String?, network: Network?, bip39Passphrase: String?, isChange: Boolean?, startIndex: UInt?, count: UInt?, ) LongParameterList:DevSettingsViewModel.kt$DevSettingsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val firebaseMessaging: FirebaseMessaging, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val widgetsStore: WidgetsStore, private val currencyRepo: CurrencyRepo, private val logsRepo: LogsRepo, private val cacheStore: CacheStore, private val blocktankRepo: BlocktankRepo, ) LongParameterList:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$( @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, internal val blocktankRepo: BlocktankRepo, private val logsRepo: LogsRepo, private val addressChecker: AddressChecker, private val ldkNodeEventBus: LdkNodeEventBus, private val walletRepo: WalletRepo, ) - LongParameterList:LightningRepo.kt$LightningRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val coreService: CoreService, private val blocktankNotificationsService: BlocktankNotificationsService, private val firebaseMessaging: FirebaseMessaging, private val keychain: Keychain, private val lnurlService: LnurlService, private val cacheStore: CacheStore, ) + LongParameterList:LightningRepo.kt$LightningRepo$( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningService: LightningService, private val ldkNodeEventBus: LdkNodeEventBus, private val settingsStore: SettingsStore, private val coreService: CoreService, private val lspNotificationsService: LspNotificationsService, private val firebaseMessaging: FirebaseMessaging, private val keychain: Keychain, private val lnurlService: LnurlService, private val cacheStore: CacheStore, ) LongParameterList:LightningRepo.kt$LightningRepo$( address: Address, sats: ULong, speed: TransactionSpeed? = null, utxosToSpend: List<SpendableUtxo>? = null, feeRates: FeeRates? = null, isTransfer: Boolean = false, channelId: String? = null, ) LongParameterList:LightningRepo.kt$LightningRepo$( walletIndex: Int = 0, timeout: Duration? = null, shouldRetry: Boolean = true, eventHandler: NodeEventHandler? = null, customServer: ElectrumServer? = null, customRgsServerUrl: String? = null, ) LongParameterList:Notifications.kt$( title: String?, text: String?, extras: Bundle? = null, bigText: String? = null, id: Int = Random.nextInt(), context: Context, ) @@ -330,7 +330,6 @@ MagicNumber:QuickPaySettingsScreen.kt$20 MagicNumber:QuickPaySettingsScreen.kt$5 MagicNumber:QuickPaySettingsScreen.kt$50 - MagicNumber:ReceiveAmountScreen.kt$500 MagicNumber:ReceiveLiquidityScreen.kt$100.0 MagicNumber:ReceiveQrScreen.kt$17.33f MagicNumber:ReceiveQrScreen.kt$32 @@ -481,7 +480,6 @@ ModifierMissing:HighBalanceWarningSheet.kt$HighBalanceWarningContent ModifierMissing:HomeNav.kt$HomeNav ModifierMissing:InfoScreenContent.kt$InfoScreenContent - ModifierMissing:InfoTextField.kt$InfoTextField ModifierMissing:InitializingWalletView.kt$InitializingWalletView ModifierMissing:IntroScreen.kt$IntroScreen ModifierMissing:LocalCurrencySettingsScreen.kt$LocalCurrencySettingsContent @@ -498,7 +496,6 @@ ModifierMissing:QrScanningScreen.kt$QrScanningScreen ModifierMissing:QuickPayIntroScreen.kt$QuickPayIntroScreen ModifierMissing:QuickPaySettingsScreen.kt$QuickPaySettingsScreenContent - ModifierMissing:ReceiveAmountScreen.kt$ReceiveAmountScreen ModifierMissing:ReceiveSheet.kt$ReceiveSheet ModifierMissing:ReportIssueResultScreen.kt$ReportIssueResultScreen ModifierMissing:ReportIssueScreen.kt$ReportIssueContent @@ -596,7 +593,6 @@ ReturnCount:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$private fun findUpdatedChannel( currentChannel: ChannelDetails, allChannels: List<ChannelDetails>, ): ChannelDetails? SpreadOperator:RestoreWalletScreen.kt$(*Array(24) { "" }) SwallowedException:Crypto.kt$Crypto$e: Exception - SwallowedException:FcmService.kt$FcmService$e: SerializationException TooGenericExceptionCaught:ActivityDetailViewModel.kt$ActivityDetailViewModel$e: Exception TooGenericExceptionCaught:ActivityDetailViewModel.kt$ActivityDetailViewModel$e: Throwable TooGenericExceptionCaught:ActivityListViewModel.kt$ActivityListViewModel$e: Exception @@ -628,7 +624,6 @@ TooGenericExceptionCaught:NewTransactionSheetDetails.kt$NewTransactionSheetDetails.Companion$e: Exception TooGenericExceptionCaught:PriceService.kt$PriceService$e: Exception TooGenericExceptionCaught:QrScanningScreen.kt$e: Exception - TooGenericExceptionCaught:ReceiveAmountScreen.kt$e: Exception TooGenericExceptionCaught:SendCoinSelectionViewModel.kt$SendCoinSelectionViewModel$e: Throwable TooGenericExceptionCaught:ServiceQueue.kt$ServiceQueue$e: Exception TooGenericExceptionCaught:SettingUpScreen.kt$e: Throwable diff --git a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendAmountContentTest.kt b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendAmountContentTest.kt index 759ce22e5..840632ef6 100644 --- a/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendAmountContentTest.kt +++ b/app/src/androidTest/java/to/bitkit/ui/screens/wallets/send/SendAmountContentTest.kt @@ -51,8 +51,8 @@ class SendAmountContentTest { // composeTestRule.onNodeWithTag("amount_input_field").assertExists() doesn't displayed because of viewmodel injection composeTestRule.onNodeWithTag("available_balance").assertExists() composeTestRule.onNodeWithTag("AssetButton-switch").assertExists() - composeTestRule.onNodeWithTag("continue_button").assertExists() - composeTestRule.onNodeWithTag("amount_keyboard").assertExists() + composeTestRule.onNodeWithTag("ContinueAmount").assertExists() + composeTestRule.onNodeWithTag("SendAmountNumberPad").assertExists() } @Test diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index f4ffa19b5..a786fee65 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.Address import org.lightningdevkit.ldknode.BalanceDetails +import org.lightningdevkit.ldknode.ChannelConfig import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails @@ -646,8 +647,9 @@ class LightningRepo @Inject constructor( peer: LnPeer, channelAmountSats: ULong, pushToCounterpartySats: ULong? = null, + channelConfig: ChannelConfig? = null, ): Result = executeWhenNodeRunning("Open channel") { - val result = lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats) + val result = lightningService.openChannel(peer, channelAmountSats, pushToCounterpartySats, channelConfig) syncState() result } diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index d578a9633..b0b87dabf 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -18,6 +18,7 @@ import org.lightningdevkit.ldknode.Bolt11Invoice import org.lightningdevkit.ldknode.Bolt11InvoiceDescription import org.lightningdevkit.ldknode.BuildException import org.lightningdevkit.ldknode.Builder +import org.lightningdevkit.ldknode.ChannelConfig import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.CoinSelectionAlgorithm import org.lightningdevkit.ldknode.Config @@ -324,6 +325,7 @@ class LightningService @Inject constructor( peer: LnPeer, channelAmountSats: ULong, pushToCounterpartySats: ULong? = null, + channelConfig: ChannelConfig? = null, ): Result { val node = this.node ?: throw ServiceError.NodeNotSetup @@ -336,7 +338,7 @@ class LightningService @Inject constructor( address = peer.address, channelAmountSats = channelAmountSats, pushToCounterpartyMsat = pushToCounterpartySats?.let { it * 1000u }, - channelConfig = null, + channelConfig = channelConfig, ) Logger.info("Channel open initiated, userChannelId: $userChannelId") diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt index 445b12c78..3aa382112 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceScreen.kt @@ -12,13 +12,11 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -38,7 +36,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Devices.PIXEL_5 +import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -53,11 +51,13 @@ import to.bitkit.ui.components.BodySSB 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.Keyboard import to.bitkit.ui.components.NumberPadTextField import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.UnitButton +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight @@ -145,8 +145,14 @@ fun EditInvoiceScreen( tags = walletUiState.selectedTags, onBack = onBack, onTextChanged = onDescriptionUpdate, - numericKeyboardVisible = keyboardVisible, - onClickBalance = { keyboardVisible = true }, + keyboardVisible = keyboardVisible, + onClickBalance = { + if (keyboardVisible) { + currencyVM.togglePrimaryDisplay() + } else { + keyboardVisible = true + } + }, onInputChanged = onInputUpdated, onContinueKeyboard = { keyboardVisible = false }, onContinueGeneral = { @@ -164,7 +170,7 @@ fun EditInvoiceContent( input: String, noteText: String, isSoftKeyboardVisible: Boolean, - numericKeyboardVisible: Boolean, + keyboardVisible: Boolean, primaryDisplay: PrimaryDisplay, displayUnit: BitcoinDisplayUnit, tags: List, @@ -187,7 +193,7 @@ fun EditInvoiceContent( val maxHeight = this.maxHeight AnimatedVisibility( - visible = !numericKeyboardVisible && !isSoftKeyboardVisible, + visible = !keyboardVisible && !isSoftKeyboardVisible, enter = fadeIn(), exit = fadeOut(), modifier = Modifier @@ -210,16 +216,17 @@ fun EditInvoiceContent( .navigationBarsPadding() .testTag("edit_invoice_screen") ) { - SheetTopBar(stringResource(R.string.wallet__receive_specify)) { - onBack() - } + SheetTopBar( + titleText = stringResource(R.string.wallet__receive_specify), + onBack = onBack, + ) Column( modifier = Modifier .padding(horizontal = 16.dp) .testTag("ReceiveAmount") ) { - Spacer(Modifier.height(32.dp)) + VerticalSpacer(16.dp) NumberPadTextField( input = input, @@ -233,7 +240,7 @@ fun EditInvoiceContent( // Animated visibility for keyboard section AnimatedVisibility( - visible = numericKeyboardVisible, + visible = keyboardVisible, enter = slideInVertically( initialOffsetY = { fullHeight -> fullHeight }, animationSpec = tween(durationMillis = 300) @@ -246,7 +253,7 @@ fun EditInvoiceContent( Column( modifier = Modifier.testTag("ReceiveNumberPad") ) { - Spacer(modifier = Modifier.weight(1f)) + FillHeight(min = 12.dp) Row( verticalAlignment = Alignment.CenterVertically, @@ -276,34 +283,26 @@ fun EditInvoiceContent( .testTag("amount_keyboard") ) - Spacer( - modifier = Modifier - .weight(1f) - .sizeIn(minHeight = 16.dp, maxHeight = 41.dp) - ) - PrimaryButton( text = stringResource(R.string.common__continue), onClick = onContinueKeyboard, modifier = Modifier.testTag("ReceiveNumberPadSubmit") ) - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) } } // Animated visibility for note section AnimatedVisibility( - visible = !numericKeyboardVisible, + visible = !keyboardVisible, enter = fadeIn(animationSpec = tween(durationMillis = 300)), exit = fadeOut(animationSpec = tween(durationMillis = 300)) ) { Column { - Spacer(modifier = Modifier.height(44.dp)) - + VerticalSpacer(44.dp) Caption13Up(text = stringResource(R.string.wallet__note), color = Colors.White64) - - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) TextField( placeholder = { @@ -325,10 +324,10 @@ fun EditInvoiceContent( .testTag("ReceiveNote") ) - Spacer(modifier = Modifier.height(16.dp)) - + VerticalSpacer(16.dp) Caption13Up(text = stringResource(R.string.wallet__tags), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) + VerticalSpacer(8.dp) + FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), @@ -359,7 +358,7 @@ fun EditInvoiceContent( modifier = Modifier.testTag("TagsAdd") ) - Spacer(modifier = Modifier.weight(1f)) + FillHeight() PrimaryButton( text = stringResource(R.string.wallet__receive_show_qr), @@ -367,7 +366,7 @@ fun EditInvoiceContent( modifier = Modifier.testTag("ShowQrReceive") ) - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) } } } @@ -387,7 +386,7 @@ private fun Preview() { displayUnit = BitcoinDisplayUnit.MODERN, onBack = {}, onTextChanged = {}, - numericKeyboardVisible = false, + keyboardVisible = false, onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, @@ -404,7 +403,7 @@ private fun Preview() { @Preview(showSystemUi = true) @Composable -private fun Preview2() { +private fun PreviewWithTags() { AppThemeSurface { BottomSheetPreview { EditInvoiceContent( @@ -414,7 +413,7 @@ private fun Preview2() { displayUnit = BitcoinDisplayUnit.MODERN, onBack = {}, onTextChanged = {}, - numericKeyboardVisible = false, + keyboardVisible = false, onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, @@ -431,7 +430,7 @@ private fun Preview2() { @Preview(showSystemUi = true) @Composable -private fun Preview3() { +private fun PreviewWithKeyboard() { AppThemeSurface { BottomSheetPreview { EditInvoiceContent( @@ -441,7 +440,7 @@ private fun Preview3() { displayUnit = BitcoinDisplayUnit.MODERN, onBack = {}, onTextChanged = {}, - numericKeyboardVisible = true, + keyboardVisible = true, onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, @@ -456,9 +455,9 @@ private fun Preview3() { } } -@Preview(showSystemUi = true, device = PIXEL_5) +@Preview(showSystemUi = true, device = NEXUS_5) @Composable -private fun Preview4() { +private fun PreviewSmallScreen() { AppThemeSurface { BottomSheetPreview { EditInvoiceContent( @@ -468,7 +467,7 @@ private fun Preview4() { displayUnit = BitcoinDisplayUnit.MODERN, onBack = {}, onTextChanged = {}, - numericKeyboardVisible = true, + keyboardVisible = true, onClickBalance = {}, onInputChanged = {}, onContinueGeneral = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt index d17e6b1ec..c0cefed4d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt @@ -1,11 +1,10 @@ package to.bitkit.ui.screens.wallets.receive +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -18,12 +17,13 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -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.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.delay @@ -31,21 +31,33 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.NodeLifecycleState +import to.bitkit.models.PrimaryDisplay import to.bitkit.models.Toast import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel -import to.bitkit.ui.components.AmountInput +import to.bitkit.ui.components.AmountInputHandler +import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.FillHeight +import to.bitkit.ui.components.FillWidth +import to.bitkit.ui.components.Keyboard import to.bitkit.ui.components.MoneySSB +import to.bitkit.ui.components.NumberPadTextField import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.UnitButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger +import to.bitkit.viewmodels.CurrencyUiState +import kotlin.time.Duration.Companion.milliseconds @Composable fun ReceiveAmountScreen( @@ -56,11 +68,12 @@ fun ReceiveAmountScreen( val wallet = walletViewModel ?: return val blocktank = blocktankViewModel ?: return val walletState by wallet.uiState.collectAsStateWithLifecycle() + val currencyVM = currencyViewModel ?: return val currencies = LocalCurrencies.current - var satsAmount by rememberSaveable { mutableLongStateOf(0) } + var input: String by remember { mutableStateOf("0") } var overrideSats: Long? by remember { mutableStateOf(null) } - + var satsAmount by remember { mutableLongStateOf(0L) } var isCreatingInvoice by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() @@ -68,105 +81,205 @@ fun ReceiveAmountScreen( blocktank.refreshMinCjitSats() } + AmountInputHandler( + input = input, + overrideSats = overrideSats, + primaryDisplay = currencies.primaryDisplay, + displayUnit = currencies.displayUnit, + onInputChanged = { newInput -> input = newInput }, + onAmountCalculated = { sats -> + satsAmount = sats.toLongOrNull() ?: 0L + overrideSats = null + }, + currencyVM = currencyVM, + ) + + val minCjitSats by blocktank.minCjitSats.collectAsStateWithLifecycle() + + ReceiveAmountContent( + input = input, + satsAmount = satsAmount, + minCjitSats = minCjitSats, + currencyUiState = currencies, + isCreatingInvoice = isCreatingInvoice, + onInputChange = { input = it }, + onClickMin = { overrideSats = it }, + onClickAmount = { currencyVM.togglePrimaryDisplay() }, + onBack = onBack, + onContinue = { + val sats = satsAmount.toULong() + scope.launch { + isCreatingInvoice = true + + if (walletState.nodeLifecycleState == NodeLifecycleState.Starting) { + while (walletState.nodeLifecycleState == NodeLifecycleState.Starting && isActive) { + delay(5.milliseconds) + } + } + + if (walletState.nodeLifecycleState == NodeLifecycleState.Running) { + runCatching { + val entry = blocktank.createCjit(amountSats = sats) + onCjitCreated( + CjitEntryDetails( + networkFeeSat = entry.networkFeeSat.toLong(), + serviceFeeSat = entry.serviceFeeSat.toLong(), + channelSizeSat = entry.channelSizeSat.toLong(), + feeSat = entry.feeSat.toLong(), + receiveAmountSats = satsAmount, + invoice = entry.invoice.request, + ) + ) + }.onFailure { e -> + app.toast(e) + Logger.error("Failed to create CJIT", e) + } + + isCreatingInvoice = false + } else { + // TODO add missing localized texts + app.toast( + type = Toast.ToastType.WARNING, + title = "Lightning not ready", + description = "Lightning node must be running to create an invoice", + ) + isCreatingInvoice = false + } + } + } + ) +} + +@Composable +private fun ReceiveAmountContent( + input: String, + satsAmount: Long, + minCjitSats: Int?, + currencyUiState: CurrencyUiState, + isCreatingInvoice: Boolean, + modifier: Modifier = Modifier, + onInputChange: (String) -> Unit = {}, + onClickMin: (Long) -> Unit = {}, + onClickAmount: () -> Unit = {}, + onBack: () -> Unit = {}, + onContinue: () -> Unit = {}, +) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("ReceiveAmount") ) { - SheetTopBar(stringResource(R.string.wallet__receive_bitcoin), onBack = onBack) - Spacer(Modifier.height(24.dp)) - - Column( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - AmountInput( - primaryDisplay = currencies.primaryDisplay, - showConversion = true, - overrideSats = overrideSats, - onSatsChange = { sats -> - satsAmount = sats - overrideSats = null - }, - ) + SheetTopBar( + titleText = stringResource(R.string.wallet__receive_bitcoin), + onBack = onBack, + ) - Spacer(modifier = Modifier.weight(1f)) + BoxWithConstraints { + val maxHeight = this.maxHeight - // Actions - Row( - verticalAlignment = Alignment.Bottom, - modifier = Modifier.fillMaxWidth() + Column( + modifier = Modifier.padding(horizontal = 16.dp) ) { - // Min amount view - val minCjitSats by blocktank.minCjitSats.collectAsStateWithLifecycle() - minCjitSats?.let { minCjitSats -> - Column( - modifier = Modifier.clickableAlpha { overrideSats = minCjitSats.toLong() } - ) { - Caption13Up( - text = stringResource(R.string.wallet__minimum), - color = Colors.White64, - modifier = Modifier.testTag("ReceiveAmountMinimum") - ) - Spacer(modifier = Modifier.height(8.dp)) - MoneySSB(sats = minCjitSats.toLong()) - } - } ?: CircularProgressIndicator(modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.weight(1f)) - UnitButton(modifier = Modifier.testTag("ReceiveNumberPadUnit")) - } + VerticalSpacer(16.dp) + NumberPadTextField( + input = input, + displayUnit = currencyUiState.displayUnit, + primaryDisplay = currencyUiState.primaryDisplay, + modifier = Modifier + .fillMaxWidth() + .clickableAlpha(onClick = onClickAmount) + .testTag("ReceiveNumberPadTextField") + ) - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider() + FillHeight(min = 12.dp) - Spacer(modifier = Modifier.height(16.dp)) - PrimaryButton( - text = stringResource(R.string.common__continue), - onClick = { - val sats = satsAmount.toULong() + // Min amount row + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxWidth() + ) { + minCjitSats?.let { minCjitSats -> + Column( + modifier = Modifier + .clickableAlpha { onClickMin(minCjitSats.toLong()) } + .testTag("ReceiveAmountMin") + ) { + Caption13Up( + text = stringResource(R.string.wallet__minimum), + color = Colors.White64, + ) + VerticalSpacer(8.dp) + MoneySSB(sats = minCjitSats.toLong()) + } + } ?: CircularProgressIndicator(modifier = Modifier.size(18.dp)) - scope.launch { - isCreatingInvoice = true + FillWidth() + UnitButton(modifier = Modifier.testTag("ReceiveNumberPadUnit")) + } - if (walletState.nodeLifecycleState == NodeLifecycleState.Starting) { - while (walletState.nodeLifecycleState == NodeLifecycleState.Starting && isActive) { - delay(500) // 0.5 second delay - } - } + VerticalSpacer(16.dp) + HorizontalDivider() - if (walletState.nodeLifecycleState == NodeLifecycleState.Running) { - try { - val entry = blocktank.createCjit(amountSats = sats) - onCjitCreated( - CjitEntryDetails( - networkFeeSat = entry.networkFeeSat.toLong(), - serviceFeeSat = entry.serviceFeeSat.toLong(), - channelSizeSat = entry.channelSizeSat.toLong(), - feeSat = entry.feeSat.toLong(), - receiveAmountSats = satsAmount, - invoice = entry.invoice.request, - ) - ) - } catch (e: Exception) { - app.toast(e) - Logger.error("Failed to create cjit", e) - } finally { - isCreatingInvoice = false - } - } else { - app.toast( - type = Toast.ToastType.WARNING, - title = "Lightning not ready", - description = "Lightning node must be running to create an invoice", - ) - isCreatingInvoice = false - } - } - }, - enabled = !isCreatingInvoice && satsAmount != 0L, // TODO if amount is valid - isLoading = isCreatingInvoice, + Keyboard( + onClick = { number -> + onInputChange(if (input == "0") number else input + number) + }, + onClickBackspace = { + onInputChange(if (input.length > 1) input.dropLast(1) else "0") + }, + isDecimal = currencyUiState.primaryDisplay == PrimaryDisplay.FIAT, + availableHeight = maxHeight, + modifier = Modifier + .fillMaxWidth() + .testTag("ReceiveNumberPad") + ) + + PrimaryButton( + text = stringResource(R.string.common__continue), + enabled = !isCreatingInvoice && satsAmount != 0L, + isLoading = isCreatingInvoice, + onClick = onContinue, + modifier = Modifier.testTag("ContinueAmount") + ) + + VerticalSpacer(16.dp) + } + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + BottomSheetPreview { + ReceiveAmountContent( + input = "100", + satsAmount = 10000L, + minCjitSats = 5000, + currencyUiState = CurrencyUiState(), + isCreatingInvoice = false, + modifier = Modifier.sheetHeight(), + ) + } + } +} + +@Preview(showSystemUi = true, device = NEXUS_5) +@Composable +private fun PreviewSmallScreen() { + AppThemeSurface { + BottomSheetPreview { + ReceiveAmountContent( + input = "100", + satsAmount = 10000L, + minCjitSats = 5000, + currencyUiState = CurrencyUiState(), + isCreatingInvoice = false, + modifier = Modifier.sheetHeight(), ) - Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 61dd21de0..1bb53ae38 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -264,7 +264,7 @@ private fun SendAmountNodeRunning( onClick = { onClickMax(max) }, modifier = Modifier .height(28.dp) - .testTag("max_amount_button") + .testTag("SendAmountMax") ) } HorizontalSpacer(8.dp) diff --git a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt index 432632472..f5c891933 100644 --- a/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/LightningRepoTest.kt @@ -33,12 +33,12 @@ import to.bitkit.models.ElectrumServer import to.bitkit.models.LnPeer import to.bitkit.models.NodeLifecycleState import to.bitkit.models.TransactionSpeed -import to.bitkit.services.LspNotificationsService import to.bitkit.services.BlocktankService import to.bitkit.services.CoreService import to.bitkit.services.LdkNodeEventBus import to.bitkit.services.LightningService import to.bitkit.services.LnurlService +import to.bitkit.services.LspNotificationsService import to.bitkit.test.BaseUnitTest import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -209,11 +209,8 @@ class LightningRepoTest : BaseUnitTest() { val testPeer = LnPeer("nodeId", "host", "9735") val testChannelId = "testChannelId" val channelAmountSats = 100000uL - whenever(lightningService.openChannel(peer = testPeer, channelAmountSats, null)).thenReturn( - Result.success( - testChannelId - ) - ) + whenever(lightningService.openChannel(testPeer, channelAmountSats, null, null)) + .thenReturn(Result.success(testChannelId)) val result = sut.openChannel(testPeer, channelAmountSats, null) assertTrue(result.isSuccess)