Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/src/main/java/to/bitkit/repositories/LightningRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import to.bitkit.services.LdkNodeEventBus
import to.bitkit.services.LightningService
import to.bitkit.services.NodeEventHandler
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import uniffi.bitkitcore.IBtInfo
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -272,6 +273,11 @@ class LightningRepo @Inject constructor(
description: String,
expirySeconds: UInt = 86_400u
): Result<Bolt11Invoice> = executeWhenNodeRunning("Create invoice") {

if (coreService.shouldBlockLightning()) {
return@executeWhenNodeRunning Result.failure(ServiceError.GeoBlocked)
}

val invoice = lightningService.receive(amountSats, description, expirySeconds)
Result.success(invoice)
}
Expand Down Expand Up @@ -336,6 +342,7 @@ class LightningRepo @Inject constructor(
nodeStatus = getStatus(),
peers = getPeers().orEmpty(),
channels = getChannels().orEmpty(),
shouldBlockLightning = coreService.shouldBlockLightning()
)
}
}
Expand Down Expand Up @@ -420,5 +427,6 @@ data class LightningState(
val nodeLifecycleState: NodeLifecycleState = NodeLifecycleState.Stopped,
val peers: List<LnPeer> = emptyList(),
val channels: List<ChannelDetails> = emptyList(),
val isSyncingWallet: Boolean = false
val isSyncingWallet: Boolean = false,
val shouldBlockLightning: Boolean = false
)
18 changes: 15 additions & 3 deletions app/src/main/java/to/bitkit/repositories/WalletRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import to.bitkit.services.CoreService
import to.bitkit.utils.AddressChecker
import to.bitkit.utils.Bip21Utils
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import uniffi.bitkitcore.Activity
import uniffi.bitkitcore.ActivityFilter
import uniffi.bitkitcore.PaymentType
Expand Down Expand Up @@ -84,6 +85,12 @@ class WalletRepo @Inject constructor(
suspend fun refreshBip21(): Result<Unit> = withContext(bgDispatcher) {
Logger.debug("Refreshing bip21", context = TAG)

if (coreService.shouldBlockLightning()) {
_walletState.update {
it.copy(receiveOnSpendingBalance = false)
}
}

//Reset invoice state
_walletState.update {
it.copy(
Expand Down Expand Up @@ -283,8 +290,14 @@ class WalletRepo @Inject constructor(
_walletState.update { it.copy(balanceInput = newText) }
}

fun toggleReceiveOnSpendingBalance() {
suspend fun toggleReceiveOnSpendingBalance(): Result<Unit> = withContext(bgDispatcher) {
if (_walletState.value.receiveOnSpendingBalance == false && coreService.shouldBlockLightning()) {
return@withContext Result.failure(ServiceError.GeoBlocked)
}

_walletState.update { it.copy(receiveOnSpendingBalance = !it.receiveOnSpendingBalance) }

return@withContext Result.success(Unit)
}

fun addTagToSelected(newTag: String) {
Expand All @@ -307,15 +320,14 @@ class WalletRepo @Inject constructor(
suspend fun updateBip21Invoice(
amountSats: ULong? = null,
description: String = "",
generateBolt11IfAvailable: Boolean = true,
): Result<Unit> = withContext(bgDispatcher) {
try {
updateBip21AmountSats(amountSats)
updateBip21Description(description)

val hasChannels = lightningRepo.hasChannels()

if (hasChannels && generateBolt11IfAvailable) {
if (hasChannels && _walletState.value.receiveOnSpendingBalance) {
lightningRepo.createInvoice(
amountSats = _walletState.value.bip21AmountSats,
description = _walletState.value.bip21Description
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/to/bitkit/services/CoreService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.lightningdevkit.ldknode.PaymentStatus
import to.bitkit.async.ServiceQueue
import to.bitkit.env.Env
import to.bitkit.ext.amountSats
import to.bitkit.models.LnPeer
import to.bitkit.utils.AppError
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
Expand Down Expand Up @@ -122,6 +123,21 @@ class CoreService @Inject constructor(
}
}
}

suspend fun getLspPeers(): List<LnPeer> {
val blocktankPeers = Env.trustedLnPeers
// TODO get from blocktank info when lightningService.setup sets trustedPeers0conf using BT API
// pseudocode idea:
// val blocktankPeers = getInfo(refresh = true)?.nodes?.map { LnPeer(nodeId = it.pubkey, address = "TO_DO") }.orEmpty()
return blocktankPeers
}

suspend fun getConnectedPeers(): List<LnPeer> = lightningService.peers.orEmpty()

suspend fun hasExternalNode() = getConnectedPeers().any { connectedPeer -> connectedPeer !in getLspPeers() }

//TODO this is business logic, should be moved to the domain layer in the future
suspend fun shouldBlockLightning() = checkGeoStatus() == true && !hasExternalNode()
}

// endregion
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import kotlinx.serialization.Serializable
import to.bitkit.R
import to.bitkit.ext.requiresPermission
import to.bitkit.ui.LocalBalances
import to.bitkit.ui.Routes
import to.bitkit.ui.activityListViewModel
import to.bitkit.ui.appViewModel
import to.bitkit.ui.components.BalanceHeaderView
Expand Down Expand Up @@ -105,7 +106,12 @@ fun HomeScreen(
}

is BottomSheetType.Receive -> {
ReceiveQrSheet(uiState)
ReceiveQrSheet(
walletState = uiState,
navigateToExternalConnection = {
rootNavController.navigate(Routes.ExternalConnection)
}
)
}

is BottomSheetType.ActivityDateRangeSelector -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package to.bitkit.ui.screens.wallets.receive

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import to.bitkit.R
import to.bitkit.ui.components.BodyM
import to.bitkit.ui.components.PrimaryButton
import to.bitkit.ui.scaffold.SheetTopBar
import to.bitkit.ui.shared.util.gradientBackground
import to.bitkit.ui.theme.Colors

@Composable
fun LocationBlockScreen(
onBackPressed: () -> Unit,
navigateAdvancedSetup: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.gradientBackground()
) {
SheetTopBar(stringResource(R.string.wallet__receive_bitcoin), onBack = onBackPressed)

Spacer(modifier = Modifier.height(16.dp))

Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.weight(1f),
) {
BodyM(text = stringResource(R.string.lightning__funding__text_blocked_cjit), color = Colors.White64)

Spacer(modifier = Modifier.weight(1f))

Image(
painter = painterResource(R.drawable.globe),
contentScale = ContentScale.FillWidth,
contentDescription = null,
modifier = Modifier.fillMaxWidth().padding(horizontal = 60.dp)
)

Spacer(modifier = Modifier.weight(1f))

PrimaryButton(text = stringResource(R.string.onboarding__advanced_setup), onClick = navigateAdvancedSetup)
Spacer(modifier = Modifier.height(16.dp))
}
}
}

@Preview(showBackground = true)
@Composable
private fun Preview() {
LocationBlockScreen(
onBackPressed = {},
navigateAdvancedSetup = {}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import to.bitkit.R
import to.bitkit.ext.truncate
import to.bitkit.models.NodeLifecycleState
import to.bitkit.models.NodeLifecycleState.Running
import to.bitkit.repositories.LightningState
import to.bitkit.ui.appViewModel
import to.bitkit.ui.blocktankViewModel
import to.bitkit.ui.components.BodyM
Expand All @@ -70,6 +71,7 @@ import to.bitkit.ui.theme.Colors
import to.bitkit.ui.utils.withAccent
import to.bitkit.ui.walletViewModel
import to.bitkit.viewmodels.MainUiState
import to.bitkit.viewmodels.WalletViewModelEffects

private object ReceiveRoutes {
const val QR = "qr"
Expand All @@ -80,10 +82,12 @@ private object ReceiveRoutes {
const val LIQUIDITY_ADDITIONAL = "liquidity_additional"
const val EDIT_INVOICE = "edit_invoice"
const val ADD_TAG = "add_tag"
const val LOCATION_BLOCK = "location_block"
}

@Composable
fun ReceiveQrSheet(
navigateToExternalConnection: () -> Unit,
walletState: MainUiState,
modifier: Modifier = Modifier,
) {
Expand All @@ -96,6 +100,7 @@ fun ReceiveQrSheet(
val cjitInvoice = remember { mutableStateOf<String?>(null) }
val showCreateCjit = remember { mutableStateOf(false) }
val cjitEntryDetails = remember { mutableStateOf<CjitEntryDetails?>(null) }
val lightningState : LightningState by wallet.lightningState.collectAsStateWithLifecycle()

LaunchedEffect(Unit) {
try {
Expand All @@ -122,20 +127,37 @@ fun ReceiveQrSheet(
LaunchedEffect(cjitInvoice.value) {
showCreateCjit.value = !cjitInvoice.value.isNullOrBlank()
}

LaunchedEffect(Unit) {
wallet.walletEffect.collect { effect ->
when(effect) {
WalletViewModelEffects.NavigateGeoBlockScreen -> {
navController.navigate(ReceiveRoutes.LOCATION_BLOCK)
}
}
}
}

ReceiveQrScreen(
cjitInvoice = cjitInvoice,
cjitActive = showCreateCjit,
walletState = walletState,
onCjitToggle = { active ->
showCreateCjit.value = active
if (!active) {
cjitInvoice.value = null
} else if (cjitInvoice.value == null) {
navController.navigate(ReceiveRoutes.AMOUNT)
when {
active && lightningState.shouldBlockLightning -> navController.navigate(ReceiveRoutes.LOCATION_BLOCK)

!active -> {
showCreateCjit.value = false
cjitInvoice.value = null
}
active && cjitInvoice.value == null -> {
showCreateCjit.value = true
navController.navigate(ReceiveRoutes.AMOUNT)
}
}
},
onClickEditInvoice = { navController.navigate(ReceiveRoutes.EDIT_INVOICE) },
onClickReceiveOnSpending = { wallet.updateReceiveOnSpending() }
onClickReceiveOnSpending = { wallet.toggleReceiveOnSpending() }
)
}
composable(ReceiveRoutes.AMOUNT) {
Expand All @@ -147,6 +169,12 @@ fun ReceiveQrSheet(
onBack = { navController.popBackStack() },
)
}
composable(ReceiveRoutes.LOCATION_BLOCK) {
LocationBlockScreen(
onBackPressed = { navController.popBackStack() },
navigateAdvancedSetup = navigateToExternalConnection
)
}
composable(ReceiveRoutes.CONFIRM) {
cjitEntryDetails.value?.let { entryDetails ->
ReceiveConfirmScreen(
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/utils/Errors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ sealed class ServiceError(message: String) : AppError(message) {
data object NodeStillRunning : ServiceError("Node is still running")
data object InvalidNodeSigningMessage : ServiceError("Invalid node signing message")
data object CurrencyRateUnavailable : ServiceError("Currency rate unavailable")
data object GeoBlocked : ServiceError("Geo blocked user")
}

sealed class KeychainError(message: String) : AppError(message) {
Expand Down
36 changes: 28 additions & 8 deletions app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
Expand All @@ -28,6 +30,7 @@ import to.bitkit.repositories.LightningRepo
import to.bitkit.repositories.WalletRepo
import to.bitkit.ui.shared.toast.ToastEventBus
import to.bitkit.utils.Logger
import to.bitkit.utils.ServiceError
import javax.inject.Inject

@HiltViewModel
Expand All @@ -53,6 +56,10 @@ class WalletViewModel @Inject constructor(
@Deprecated("Prioritize get the wallet and lightning states from LightningRepo or WalletRepo")
val uiState = _uiState.asStateFlow()

private val _walletEffect = MutableSharedFlow<WalletViewModelEffects>(extraBufferCapacity = 1)
val walletEffect = _walletEffect.asSharedFlow()
private fun walletEffect(effect: WalletViewModelEffects) = viewModelScope.launch { _walletEffect.emit(effect) }

init {
collectStates()
}
Expand Down Expand Up @@ -181,13 +188,11 @@ class WalletViewModel @Inject constructor(

fun updateBip21Invoice(
amountSats: ULong? = null,
generateBolt11IfAvailable: Boolean = true
) {
viewModelScope.launch {
walletRepo.updateBip21Invoice(
amountSats = amountSats,
description = walletState.value.bip21Description,
generateBolt11IfAvailable = generateBolt11IfAvailable,
).onFailure { error ->
ToastEventBus.send(
type = Toast.ToastType.ERROR,
Expand All @@ -198,12 +203,23 @@ class WalletViewModel @Inject constructor(
}
}

fun updateReceiveOnSpending() {
walletRepo.toggleReceiveOnSpendingBalance()
updateBip21Invoice(
amountSats = walletState.value.bip21AmountSats,
generateBolt11IfAvailable = walletState.value.receiveOnSpendingBalance
)
fun toggleReceiveOnSpending() {
viewModelScope.launch {
walletRepo.toggleReceiveOnSpendingBalance().onSuccess {
updateBip21Invoice(
amountSats = walletState.value.bip21AmountSats,
)
}.onFailure { e ->
if (e is ServiceError.GeoBlocked) {
walletEffect(WalletViewModelEffects.NavigateGeoBlockScreen)
return@launch
}

updateBip21Invoice(
amountSats = walletState.value.bip21AmountSats,
)
}
}
}

fun refreshBip21() {
Expand Down Expand Up @@ -424,3 +440,7 @@ data class MainUiState(
val bip21Description: String = "",
val selectedTags: List<String> = listOf(),
)

sealed interface WalletViewModelEffects {
data object NavigateGeoBlockScreen: WalletViewModelEffects
}
Loading