diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 66f8d29d7..7b5b701e7 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -1,6 +1,5 @@ package to.bitkit.repositories -import com.google.firebase.messaging.FirebaseMessaging import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -8,7 +7,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update -import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.BalanceDetails @@ -26,14 +24,12 @@ import to.bitkit.env.Env import to.bitkit.ext.toHex import to.bitkit.models.BalanceState import to.bitkit.models.NodeLifecycleState -import to.bitkit.services.BlocktankNotificationsService import to.bitkit.services.CoreService import to.bitkit.utils.AddressChecker import to.bitkit.utils.Bip21Utils import to.bitkit.utils.Logger import uniffi.bitkitcore.Activity import uniffi.bitkitcore.ActivityFilter -import uniffi.bitkitcore.IBtInfo import uniffi.bitkitcore.PaymentType import uniffi.bitkitcore.Scanner import uniffi.bitkitcore.decode @@ -345,6 +341,22 @@ class WalletRepo @Inject constructor( } } + suspend fun shouldRequestAdditionalLiquidity() : Result = withContext(bgDispatcher) { + return@withContext try { + if (_walletState.value.receiveOnSpendingBalance == false) return@withContext Result.success(false) + + if (coreService.checkGeoStatus() == true) return@withContext Result.success(false) + + val channels = lightningRepo.lightningState.value.channels + val inboundBalanceSats = channels.sumOf { it.inboundCapacityMsat / 1000u }.toULong() + + Result.success((_walletState.value.bip21AmountSats ?: 0uL) >= inboundBalanceSats) + } catch (e: Exception) { + Logger.error("shouldRequestAdditionalLiquidity error", e, context = TAG) + Result.failure(e) + } + } + // Debug methods suspend fun debugKeychain(key: String, value: String): Result = withContext(bgDispatcher) { try { 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 522607630..f426904c1 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 @@ -25,6 +25,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,11 +40,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import to.bitkit.R import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.PrimaryDisplay import to.bitkit.repositories.WalletState import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.AmountInputHandler import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.ButtonSize @@ -61,11 +64,13 @@ import to.bitkit.ui.theme.AppTextFieldDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.keyboardAsState +import to.bitkit.utils.Logger import to.bitkit.viewmodels.CurrencyUiState @Composable fun EditInvoiceScreen( currencyUiState: CurrencyUiState = LocalCurrencies.current, + editInvoiceVM: EditInvoiceVM = hiltViewModel(), walletUiState: WalletState, updateInvoice: (ULong?) -> Unit, onClickAddTag: () -> Unit, @@ -73,12 +78,53 @@ fun EditInvoiceScreen( onInputUpdated: (String) -> Unit, onDescriptionUpdate: (String) -> Unit, onBack: () -> Unit, + navigateReceiveConfirm: (CjitEntryDetails) -> Unit, ) { val currencyVM = currencyViewModel ?: return + val blocktankVM = blocktankViewModel ?: return var satsString by rememberSaveable { mutableStateOf("") } var keyboardVisible by remember { mutableStateOf(false) } var isSoftKeyboardVisible by keyboardAsState() + LaunchedEffect(Unit) { + editInvoiceVM.editInvoiceEffect.collect { effect -> + when(effect) { + is EditInvoiceVM.EditInvoiceScreenEffects.NavigateAddLiquidity -> { + val receiveSats = satsString.toULongOrNull() + updateInvoice(receiveSats) + + + if (receiveSats == null) { + onBack() + return@collect + } + + satsString.toULongOrNull()?.let { sats -> + runCatching { blocktankVM.createCjit(sats) }.onSuccess { entry -> + navigateReceiveConfirm( + CjitEntryDetails( + networkFeeSat = entry.networkFeeSat.toLong(), + serviceFeeSat = entry.serviceFeeSat.toLong(), + channelSizeSat = entry.channelSizeSat.toLong(), + feeSat = entry.feeSat.toLong(), + receiveAmountSats = receiveSats.toLong(), + invoice = entry.invoice.request, + ) + ) + }.onFailure { e -> + Logger.error(e = e, msg = "error creating cjit invoice" ,context = "EditInvoiceScreen") + onBack() + } + } + } + EditInvoiceVM.EditInvoiceScreenEffects.UpdateInvoice -> { + updateInvoice(satsString.toULongOrNull()) + onBack() + } + } + } + } + AmountInputHandler( input = walletUiState.balanceInput, primaryDisplay = currencyUiState.primaryDisplay, @@ -100,7 +146,10 @@ fun EditInvoiceScreen( onClickBalance = { keyboardVisible = true }, onInputChanged = onInputUpdated, onContinueKeyboard = { keyboardVisible = false }, - onContinueGeneral = { updateInvoice(satsString.toULongOrNull()) }, + onContinueGeneral = { + updateInvoice(satsString.toULongOrNull()) + editInvoiceVM.onClickContinue() + }, onClickAddTag = onClickAddTag, onClickTag = onClickTag, isSoftKeyboardVisible = isSoftKeyboardVisible diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceVM.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceVM.kt new file mode 100644 index 000000000..d059b9e4b --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceVM.kt @@ -0,0 +1,45 @@ +package to.bitkit.ui.screens.wallets.receive + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import to.bitkit.repositories.WalletRepo +import to.bitkit.utils.Logger +import javax.inject.Inject + +@HiltViewModel +class EditInvoiceVM @Inject constructor( + val walletRepo: WalletRepo +): ViewModel() { + + private val _editInvoiceEffect = MutableSharedFlow(extraBufferCapacity = 1) + val editInvoiceEffect = _editInvoiceEffect.asSharedFlow() + private fun editInvoiceEffect(effect: EditInvoiceScreenEffects) = viewModelScope.launch { _editInvoiceEffect.emit(effect) } + + fun onClickContinue() { + viewModelScope.launch { + walletRepo.shouldRequestAdditionalLiquidity().onSuccess { shouldRequest -> + if (shouldRequest) { + editInvoiceEffect(EditInvoiceScreenEffects.NavigateAddLiquidity) + } else { + editInvoiceEffect(EditInvoiceScreenEffects.UpdateInvoice) + } + }.onFailure { + Logger.warn("Error checking for liquidity, navigating back to QR Screen", context = TAG) + editInvoiceEffect(EditInvoiceScreenEffects.UpdateInvoice) + } + } + } + + sealed interface EditInvoiceScreenEffects { + data object UpdateInvoice : EditInvoiceScreenEffects + data object NavigateAddLiquidity : EditInvoiceScreenEffects + } + + companion object { + const val TAG = "EditInvoiceVM" + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt index 5267a9108..985d8fe4a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt @@ -2,21 +2,23 @@ package to.bitkit.ui.screens.wallets.receive import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.serialization.Serializable @@ -50,6 +52,7 @@ data class CjitEntryDetails( @Composable fun ReceiveConfirmScreen( entry: CjitEntryDetails, + isAdditional: Boolean = false, onLearnMore: () -> Unit, onContinue: (String) -> Unit, onBack: () -> Unit, @@ -90,6 +93,7 @@ fun ReceiveConfirmScreen( serviceFeeFormatted = serviceFeeFormatted, receiveAmountFormatted = receiveAmountFormatted, onLearnMoreClick = onLearnMore, + isAdditional = isAdditional, onContinueClick = { onContinue(entry.invoice) }, onBackClick = onBack, ) @@ -98,6 +102,7 @@ fun ReceiveConfirmScreen( @Composable private fun ReceiveConfirmContent( receiveSats: Long, + isAdditional: Boolean, networkFeeFormatted: String, serviceFeeFormatted: String, receiveAmountFormatted: String, @@ -113,57 +118,63 @@ private fun ReceiveConfirmContent( SheetTopBar(stringResource(R.string.wallet__receive_bitcoin), onBack = onBackClick) Spacer(Modifier.height(24.dp)) - Column( - modifier = Modifier.padding(horizontal = 16.dp) + Box( + modifier = Modifier.fillMaxWidth() ) { - BalanceHeaderView( - sats = receiveSats, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(24.dp)) - BodyM( - text = stringResource(R.string.wallet__receive_connect_initial) - .replace("{networkFee}", networkFeeFormatted) - .replace("{serviceFee}", serviceFeeFormatted) - .withAccent( - defaultColor = Colors.White64, - accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold) - ) - ) - Spacer(modifier = Modifier.height(32.dp)) - Column { - Caption13Up(text = stringResource(R.string.wallet__receive_will), color = Colors.White64) - Spacer(Modifier.height(4.dp)) - Title(text = receiveAmountFormatted) - } - Spacer(modifier = Modifier.weight(1f)) + Image( painter = painterResource(R.drawable.lightning), contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier - .heightIn(max = 256.dp) + .padding(bottom = 150.dp) .fillMaxWidth() + .align(Alignment.BottomCenter) ) - Spacer(modifier = Modifier.weight(1f)) - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - SecondaryButton( - text = stringResource(R.string.common__learn_more), - onClick = onLearnMoreClick, - modifier = Modifier.weight(1f) + + Column( + modifier = Modifier.padding(horizontal = 16.dp) + ) { + BalanceHeaderView( + sats = receiveSats, + modifier = Modifier.fillMaxWidth() ) - PrimaryButton( - text = stringResource(R.string.common__continue), - onClick = onContinueClick, - modifier = Modifier.weight(1f) + Spacer(modifier = Modifier.height(24.dp)) + BodyM( + text = stringResource(if (isAdditional) R.string.wallet__receive_connect_additional else R.string.wallet__receive_connect_initial) + .replace("{networkFee}", networkFeeFormatted) + .replace("{serviceFee}", serviceFeeFormatted) + .withAccent( + defaultColor = Colors.White64, + accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold) + ) ) + Spacer(modifier = Modifier.height(32.dp)) + Column { + Caption13Up(text = stringResource(R.string.wallet__receive_will), color = Colors.White64) + Spacer(Modifier.height(4.dp)) + Title(text = receiveAmountFormatted) + } + Spacer(modifier = Modifier.weight(1f)) + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + SecondaryButton( + text = stringResource(R.string.common__learn_more), + onClick = onLearnMoreClick, + modifier = Modifier.weight(1f) + ) + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinueClick, + modifier = Modifier.weight(1f) + ) + } + Spacer(modifier = Modifier.height(32.dp)) } - Spacer(modifier = Modifier.height(32.dp)) } } } -@Preview(showBackground = true) +@Preview(showBackground = true, name = "Initial flow") @Composable private fun Preview() { AppThemeSurface { @@ -175,6 +186,58 @@ private fun Preview() { onLearnMoreClick = {}, onContinueClick = {}, onBackClick = {}, + isAdditional = false + ) + } +} + +@Preview(showBackground = true, name = "Aditional flow") +@Composable +private fun Preview2() { + AppThemeSurface { + ReceiveConfirmContent( + receiveSats = 12500L, + isAdditional = true, + networkFeeFormatted = "$0.50", + serviceFeeFormatted = "$1.00", + receiveAmountFormatted = "$100.00", + onLearnMoreClick = {}, + onContinueClick = {}, + onBackClick = {}, + ) + } +} + +@Preview(showBackground = true, name = "Small device", widthDp = 400, heightDp = 620) +@Composable +private fun Preview3() { + AppThemeSurface { + ReceiveConfirmContent( + receiveSats = 12500L, + isAdditional = true, + networkFeeFormatted = "$0.50", + serviceFeeFormatted = "$1.00", + receiveAmountFormatted = "$100.00", + onLearnMoreClick = {}, + onContinueClick = {}, + onBackClick = {}, + ) + } +} + +@Preview(showBackground = true, name = "Tablet", device = PIXEL_TABLET) +@Composable +private fun Preview4() { + AppThemeSurface { + ReceiveConfirmContent( + receiveSats = 12500L, + isAdditional = true, + networkFeeFormatted = "$0.50", + serviceFeeFormatted = "$1.00", + receiveAmountFormatted = "$100.00", + onLearnMoreClick = {}, + onContinueClick = {}, + onBackClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt index 5cb1072df..77f48425f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt @@ -26,6 +26,7 @@ import kotlin.math.round @Composable fun ReceiveLiquidityScreen( entry: CjitEntryDetails, + isAdditional: Boolean = false, onContinue: () -> Unit, onBack: () -> Unit, ) { @@ -41,17 +42,23 @@ fun ReceiveLiquidityScreen( .fillMaxWidth() .gradientBackground() ) { - SheetTopBar(stringResource(R.string.wallet__receive_liquidity__nav_title), onBack = onBack) + SheetTopBar( + stringResource(if (isAdditional) R.string.wallet__receive_liquidity__nav_title_additional else R.string.wallet__receive_liquidity__nav_title), + onBack = onBack + ) Spacer(Modifier.height(24.dp)) Column( modifier = Modifier.padding(horizontal = 16.dp) ) { - BodyM(text = stringResource(R.string.wallet__receive_liquidity__text), color = Colors.White64) + BodyM( + text = stringResource(if (isAdditional) R.string.wallet__receive_liquidity__text_additional else R.string.wallet__receive_liquidity__text), + color = Colors.White64 + ) Spacer(modifier = Modifier.weight(1f)) - BodyMB(text = stringResource(R.string.wallet__receive_liquidity__label)) + BodyMB(text = stringResource(if (isAdditional) R.string.wallet__receive_liquidity__label_additional else R.string.wallet__receive_liquidity__label)) Spacer(modifier = Modifier.height(16.dp)) LightningChannel( @@ -72,7 +79,7 @@ fun ReceiveLiquidityScreen( } } -@Preview(showBackground = true) +@Preview(showBackground = true, name = "Initial flow") @Composable private fun Preview() { AppThemeSurface { @@ -85,6 +92,27 @@ private fun Preview() { serviceFeeSat = 150_000L, invoice = "", ), + isAdditional = false, + onContinue = {}, + onBack = {}, + ) + } +} + +@Preview(showBackground = true, name = "Additional flow") +@Composable +private fun Preview2() { + AppThemeSurface { + ReceiveLiquidityScreen( + entry = CjitEntryDetails( + channelSizeSat = 200_000L, + receiveAmountSats = 50_000L, + feeSat = 10_000L, + networkFeeSat = 5_000L, + serviceFeeSat = 150_000L, + invoice = "", + ), + isAdditional = true, onContinue = {}, onBack = {}, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 4adb320c7..aa39c5a3f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -75,7 +75,9 @@ private object ReceiveRoutes { const val QR = "qr" const val AMOUNT = "amount" const val CONFIRM = "confirm" + const val CONFIRM_INCREASE_INBOUND = "confirm_increase_inbound" const val LIQUIDITY = "liquidity" + const val LIQUIDITY_ADDITIONAL = "liquidity_additional" const val EDIT_INVOICE = "edit_invoice" const val ADD_TAG = "add_tag" } @@ -158,6 +160,20 @@ fun ReceiveQrSheet( ) } } + composable(ReceiveRoutes.CONFIRM_INCREASE_INBOUND) { + cjitEntryDetails.value?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + onLearnMore = { navController.navigate(ReceiveRoutes.LIQUIDITY_ADDITIONAL) }, + onContinue = { invoice -> + cjitInvoice.value = invoice + navController.navigate(ReceiveRoutes.QR) { popUpTo(ReceiveRoutes.QR) { inclusive = true } } + }, + isAdditional = true, + onBack = { navController.popBackStack() }, + ) + } + } composable(ReceiveRoutes.LIQUIDITY) { cjitEntryDetails.value?.let { entryDetails -> ReceiveLiquidityScreen( @@ -167,6 +183,16 @@ fun ReceiveQrSheet( ) } } + composable(ReceiveRoutes.LIQUIDITY_ADDITIONAL) { + cjitEntryDetails.value?.let { entryDetails -> + ReceiveLiquidityScreen( + entry = entryDetails, + onContinue = { navController.popBackStack() }, + isAdditional = true, + onBack = { navController.popBackStack() }, + ) + } + } composable(ReceiveRoutes.EDIT_INVOICE) { val walletUiState by wallet.walletState.collectAsStateWithLifecycle() EditInvoiceScreen( @@ -174,7 +200,6 @@ fun ReceiveQrSheet( onBack = { navController.popBackStack() }, updateInvoice = { sats -> wallet.updateBip21Invoice(amountSats = sats) - navController.popBackStack() }, onClickAddTag = { navController.navigate(ReceiveRoutes.ADD_TAG) @@ -187,6 +212,10 @@ fun ReceiveQrSheet( }, onInputUpdated = { newText -> wallet.updateBalanceInput(newText) + }, + navigateReceiveConfirm = { entry -> + cjitEntryDetails.value = entry + navController.navigate(ReceiveRoutes.CONFIRM_INCREASE_INBOUND) } ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/BlocktankViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/BlocktankViewModel.kt index e9d00561c..8a6e3a714 100644 --- a/app/src/main/java/to/bitkit/viewmodels/BlocktankViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/BlocktankViewModel.kt @@ -114,7 +114,7 @@ class BlocktankViewModel @Inject constructor( } } - suspend fun createCjit(amountSats: ULong, description: String): IcJitEntry { + suspend fun createCjit(amountSats: ULong, description: String = Env.DEFAULT_INVOICE_MESSAGE): IcJitEntry { val nodeId = lightningService.nodeId ?: throw ServiceError.NodeNotStarted val lspBalance = getDefaultLspBalance(clientBalance = amountSats) diff --git a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt index bcfc315dc..f96e97e9f 100644 --- a/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/WalletRepoTest.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.flowOf import org.junit.Before import org.junit.Test import org.lightningdevkit.ldknode.BalanceDetails +import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.Network import org.mockito.kotlin.any @@ -416,4 +417,89 @@ class WalletRepoTest : BaseUnitTest() { assertTrue(sut.walletState.value.selectedTags.isEmpty()) } + + @Test + fun `shouldRequestAdditionalLiquidity should return false when receiveOnSpendingBalance is false`() = test { + // Given + whenever(coreService.checkGeoStatus()).thenReturn(false) + sut.toggleReceiveOnSpendingBalance() // Set to false (initial is true) + + // When + val result = sut.shouldRequestAdditionalLiquidity() + + // Then + assertTrue(result.isSuccess) + assertFalse(result.getOrThrow()) + } + + @Test + fun `shouldRequestAdditionalLiquidity should return false when geo status is true`() = test { + // Given + whenever(coreService.checkGeoStatus()).thenReturn(true) + + // When + val result = sut.shouldRequestAdditionalLiquidity() + + // Then + assertTrue(result.isSuccess) + assertFalse(result.getOrThrow()) + } + + @Test + fun `shouldRequestAdditionalLiquidity should return true when amount exceeds inbound capacity`() = test { + // Given + whenever(coreService.checkGeoStatus()).thenReturn(false) + val testChannels = listOf( + mock { + on { inboundCapacityMsat } doReturn 500_000u // 500 sats + }, + mock { + on { inboundCapacityMsat } doReturn 300_000u // 300 sats + } + ) + whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(LightningState(channels = testChannels))) + sut.updateBip21Invoice(amountSats = 1000uL) // 1000 sats + + // When + val result = sut.shouldRequestAdditionalLiquidity() + + // Then + assertTrue(result.isSuccess) + assertTrue(result.getOrThrow()) + } + + @Test + fun `shouldRequestAdditionalLiquidity should return false when amount is less than inbound capacity`() = test { + // Given + whenever(coreService.checkGeoStatus()).thenReturn(false) + val testChannels = listOf( + mock { + on { inboundCapacityMsat } doReturn 500_000u // 500 sats + }, + mock { + on { inboundCapacityMsat } doReturn 500_000u // 500 sats + } + ) + whenever(lightningRepo.lightningState).thenReturn(MutableStateFlow(LightningState(channels = testChannels))) + sut.updateBip21Invoice(amountSats = 900uL) // 900 sats + + // When + val result = sut.shouldRequestAdditionalLiquidity() + + // Then + assertTrue(result.isSuccess) + assertFalse(result.getOrThrow()) + } + + @Test + fun `shouldRequestAdditionalLiquidity should return failure when exception occurs`() = test { + // Given + whenever(coreService.checkGeoStatus()).thenThrow(RuntimeException("Test error")) + + // When + val result = sut.shouldRequestAdditionalLiquidity() + + // Then + assertTrue(result.isFailure) + } } diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceVMTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceVMTest.kt new file mode 100644 index 000000000..33448947d --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/EditInvoiceVMTest.kt @@ -0,0 +1,70 @@ +package to.bitkit.ui.screens.wallets.receive + +import app.cash.turbine.test +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import to.bitkit.repositories.WalletRepo +import to.bitkit.test.BaseUnitTest +import to.bitkit.ui.screens.wallets.receive.EditInvoiceVM.EditInvoiceScreenEffects +import kotlin.test.assertEquals + +class EditInvoiceVMTest : BaseUnitTest() { + + private lateinit var sut: EditInvoiceVM + private val walletRepo: WalletRepo = mock() + + @Before + fun setUp() { + sut = EditInvoiceVM(walletRepo) + } + + @Test + fun `onClickContinue should emit NavigateAddLiquidity when shouldRequestAdditionalLiquidity returns true`() = test { + // Given + whenever(walletRepo.shouldRequestAdditionalLiquidity()).thenReturn(Result.success(true)) + + // When & Then + sut.editInvoiceEffect.test { + sut.onClickContinue() + + assertEquals(EditInvoiceScreenEffects.NavigateAddLiquidity, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + + verify(walletRepo).shouldRequestAdditionalLiquidity() + } + + @Test + fun `onClickContinue should emit UpdateInvoice when shouldRequestAdditionalLiquidity returns false`() = test { + // Given + whenever(walletRepo.shouldRequestAdditionalLiquidity()).thenReturn(Result.success(false)) + + // When & Then + sut.editInvoiceEffect.test { + sut.onClickContinue() + + assertEquals(EditInvoiceScreenEffects.UpdateInvoice, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + + verify(walletRepo).shouldRequestAdditionalLiquidity() + } + @Test + fun `onClickContinue should emit UpdateInvoice when shouldRequestAdditionalLiquidity fails`() = test { + // Given + whenever(walletRepo.shouldRequestAdditionalLiquidity()).thenReturn(Result.failure(Exception("Error"))) + + // When & Then + sut.editInvoiceEffect.test { + sut.onClickContinue() + + assertEquals(EditInvoiceScreenEffects.UpdateInvoice, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + + verify(walletRepo).shouldRequestAdditionalLiquidity() + } +}