From 960481756bd7b61fa1ac26e493d41f129a02eed5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 31 Mar 2026 10:44:32 -0300 Subject: [PATCH 1/9] feat: ConnectionIssuesView --- .../ui/components/ConnectionIssuesView.kt | 144 ++++++++++++++++++ app/src/main/res/values/strings.xml | 2 + 2 files changed, 146 insertions(+) create mode 100644 app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt new file mode 100644 index 000000000..612dd591d --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -0,0 +1,144 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +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.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent + +@Composable +fun ConnectionIssuesView( + titleText: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .padding(horizontal = 16.dp) + .testTag("ConnectionIssueView"), + ) { + SheetTopBar(titleText = titleText) + + Box( + contentAlignment = Alignment.BottomCenter, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + DashedRingsLayer(outerOnly = true) + + Image( + painter = painterResource(R.drawable.phone), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(311.dp), + ) + + DashedRingsLayer(outerOnly = false) + } + + Display( + text = stringResource(R.string.other__connection_issues_title) + .withAccent(accentColor = Colors.Yellow), + modifier = Modifier.fillMaxWidth(), + ) + + VerticalSpacer(8.dp) + + BodyM( + text = stringResource(R.string.other__connection_issues_explain), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + + VerticalSpacer(24.dp) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(), + ) { + GradientCircularProgressIndicator( + strokeWidth = 1.dp, + modifier = Modifier.size(32.dp), + ) + } + + VerticalSpacer(16.dp) + } +} + + +private val outerRing = DashedRingSpec( + radiusFraction = 0.60f, + color = Colors.White.copy(alpha = 0.08f), +) + +private val innerRings = listOf( + DashedRingSpec(radiusFraction = 0.15f, color = Colors.Yellow.copy(alpha = 0.4f)), + DashedRingSpec(radiusFraction = 0.30f, color = Colors.Yellow.copy(alpha = 0.25f)), + DashedRingSpec(radiusFraction = 0.45f, color = Colors.Brand.copy(alpha = 0.15f)), +) + +@Composable +private fun DashedRingsLayer(outerOnly: Boolean, modifier: Modifier = Modifier) { + val rings = if (outerOnly) listOf(outerRing) else innerRings + Canvas(modifier = modifier.fillMaxSize()) { + val center = Offset(size.width * 0.35f, size.height * 0.55f) + rings.forEach { ring -> drawDashedRing(ring, center) } + } +} + +private fun DrawScope.drawDashedRing(ring: DashedRingSpec, center: Offset) { + drawCircle( + color = ring.color, + radius = size.minDimension * ring.radiusFraction, + center = center, + style = Stroke( + width = 1.dp.toPx(), + pathEffect = PathEffect.dashPathEffect( + floatArrayOf(8.dp.toPx(), 6.dp.toPx()), + ), + ), + ) +} + +private data class DashedRingSpec( + val radiusFraction: Float, + val color: Color, +) + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { + BottomSheetPreview { + ConnectionIssuesView(titleText = "Send Bitcoin") + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24f968538..a2b3a3acb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -350,6 +350,8 @@ Internet Connection Restored Internet Connectivity Issues It appears you’re disconnected, trying to reconnect... + It appears you\’re disconnected. Please check your connection. Bitkit will try to reconnect every few seconds. + Connection\nIssues]]> Claiming your Bitkit gift code... Claiming Gift Bitkit couldn\'t claim the funds. Please try again later or contact us. From b93d72926d2605421e587863d5dab80709e659aa Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 31 Mar 2026 11:08:07 -0300 Subject: [PATCH 2/9] feat: display connection issues view --- app/src/main/java/to/bitkit/ui/ContentView.kt | 5 +- .../screens/wallets/receive/ReceiveSheet.kt | 254 +++++---- .../to/bitkit/ui/sheets/ForceTransferSheet.kt | 37 +- .../java/to/bitkit/ui/sheets/SendSheet.kt | 512 +++++++++--------- 4 files changed, 440 insertions(+), 368 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index f4e30ee01..a653e929c 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -48,6 +48,7 @@ import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast import to.bitkit.models.WidgetType +import to.bitkit.repositories.ConnectivityState import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen import to.bitkit.ui.components.DrawerMenu @@ -380,12 +381,14 @@ fun ContentView( is Sheet.Receive -> { val walletState by walletViewModel.walletState.collectAsStateWithLifecycle() + val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle() ReceiveSheet( walletState = walletState, + isOffline = connectivityState != ConnectivityState.CONNECTED, navigateToExternalConnection = { navController.navigateTo(ExternalConnection()) appViewModel.hideSheet() - } + }, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index dd19fc938..b9e1add89 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -1,6 +1,11 @@ package to.bitkit.ui.screens.wallets.receive +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.runtime.Composable @@ -11,13 +16,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import kotlinx.serialization.Serializable +import to.bitkit.R import to.bitkit.repositories.LightningState import to.bitkit.repositories.WalletState +import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.navigateTo import to.bitkit.ui.openNotificationSettings import to.bitkit.ui.screens.wallets.send.AddTagScreen @@ -31,6 +39,7 @@ import to.bitkit.viewmodels.SettingsViewModel fun ReceiveSheet( navigateToExternalConnection: () -> Unit, walletState: WalletState, + isOffline: Boolean, editInvoiceAmountViewModel: AmountInputViewModel = hiltViewModel(), settingsViewModel: SettingsViewModel = hiltViewModel(), ) { @@ -49,138 +58,155 @@ fun ReceiveSheet( wallet.refreshReceiveState() } - Column( + Box( modifier = Modifier .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("ReceiveScreen") + .sheetHeight(), ) { - NavHost( - navController = navController, - startDestination = ReceiveRoute.QR, + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .testTag("ReceiveScreen"), ) { - composableWithDefaultTransitions { - LaunchedEffect(cjitInvoice.value) { - showCreateCjit.value = !cjitInvoice.value.isNullOrBlank() - } + NavHost( + navController = navController, + startDestination = ReceiveRoute.QR, + ) { + composableWithDefaultTransitions { + LaunchedEffect(cjitInvoice.value) { + showCreateCjit.value = !cjitInvoice.value.isNullOrBlank() + } - ReceiveQrScreen( - cjitInvoice = cjitInvoice.value, - walletState = walletState, - lightningState = lightningState, - onClickReceiveCjit = { - if (lightningState.isGeoBlocked) { - navController.navigateTo(ReceiveRoute.GeoBlock) - } else { - showCreateCjit.value = true - navController.navigateTo(ReceiveRoute.Amount) - } - }, - onClickEditInvoice = { navController.navigateTo(ReceiveRoute.EditInvoice) }, - ) - } - composableWithDefaultTransitions { - ReceiveAmountScreen( - onCjitCreated = { entry -> - cjitEntryDetails.value = entry - navController.navigateTo(ReceiveRoute.Confirm) - }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - LocationBlockScreen( - onBackPressed = { navController.popBackStack() }, - navigateAdvancedSetup = navigateToExternalConnection, - ) - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - ReceiveConfirmScreen( - entry = entryDetails, - onLearnMore = { navController.navigateTo(ReceiveRoute.Liquidity) }, - onContinue = { invoice -> - cjitInvoice.value = invoice - navController.navigateTo(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } + ReceiveQrScreen( + cjitInvoice = cjitInvoice.value, + walletState = walletState, + lightningState = lightningState, + onClickReceiveCjit = { + if (lightningState.isGeoBlocked) { + navController.navigateTo(ReceiveRoute.GeoBlock) + } else { + showCreateCjit.value = true + navController.navigateTo(ReceiveRoute.Amount) + } }, - onBack = { navController.popBackStack() }, + onClickEditInvoice = { navController.navigateTo(ReceiveRoute.EditInvoice) }, ) } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - ReceiveConfirmScreen( - entry = entryDetails, - onLearnMore = { navController.navigateTo(ReceiveRoute.LiquidityAdditional) }, - onContinue = { invoice -> - cjitInvoice.value = invoice - navController.navigateTo(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } + composableWithDefaultTransitions { + ReceiveAmountScreen( + onCjitCreated = { entry -> + cjitEntryDetails.value = entry + navController.navigateTo(ReceiveRoute.Confirm) }, - isAdditional = true, onBack = { navController.popBackStack() }, ) } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - val context = LocalContext.current - val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - - ReceiveLiquidityScreen( - entry = entryDetails, - onContinue = { navController.popBackStack() }, - onBack = { navController.popBackStack() }, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { context.openNotificationSettings() }, + composableWithDefaultTransitions { + LocationBlockScreen( + onBackPressed = { navController.popBackStack() }, + navigateAdvancedSetup = navigateToExternalConnection, ) } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - val context = LocalContext.current - val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - - ReceiveLiquidityScreen( - entry = entryDetails, - onContinue = { navController.popBackStack() }, - isAdditional = true, + composableWithDefaultTransitions { + cjitEntryDetails.value?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + onLearnMore = { navController.navigateTo(ReceiveRoute.Liquidity) }, + onContinue = { invoice -> + cjitInvoice.value = invoice + navController.navigateTo( + ReceiveRoute.QR + ) { popUpTo(ReceiveRoute.QR) { inclusive = true } } + }, + onBack = { navController.popBackStack() }, + ) + } + } + composableWithDefaultTransitions { + cjitEntryDetails.value?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + onLearnMore = { navController.navigateTo(ReceiveRoute.LiquidityAdditional) }, + onContinue = { invoice -> + cjitInvoice.value = invoice + navController.navigateTo( + ReceiveRoute.QR + ) { popUpTo(ReceiveRoute.QR) { inclusive = true } } + }, + isAdditional = true, + onBack = { navController.popBackStack() }, + ) + } + } + composableWithDefaultTransitions { + cjitEntryDetails.value?.let { entryDetails -> + val context = LocalContext.current + val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + + ReceiveLiquidityScreen( + entry = entryDetails, + onContinue = { navController.popBackStack() }, + onBack = { navController.popBackStack() }, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { context.openNotificationSettings() }, + ) + } + } + composableWithDefaultTransitions { + cjitEntryDetails.value?.let { entryDetails -> + val context = LocalContext.current + val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + + ReceiveLiquidityScreen( + entry = entryDetails, + onContinue = { navController.popBackStack() }, + isAdditional = true, + onBack = { navController.popBackStack() }, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { context.openNotificationSettings() }, + ) + } + } + composableWithDefaultTransitions { + val walletUiState by wallet.walletState.collectAsStateWithLifecycle() + @Suppress("ViewModelForwarding") + EditInvoiceScreen( + amountInputViewModel = editInvoiceAmountViewModel, + walletUiState = walletUiState, onBack = { navController.popBackStack() }, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { context.openNotificationSettings() }, + updateInvoice = wallet::updateBip21Invoice, + onClickAddTag = { navController.navigateTo(ReceiveRoute.AddTag) }, + onClickTag = wallet::removeTag, + onDescriptionUpdate = wallet::updateBip21Description, + navigateReceiveConfirm = { entry -> + cjitEntryDetails.value = entry + navController.navigateTo(ReceiveRoute.ConfirmIncreaseInbound) + }, + ) + } + composableWithDefaultTransitions { + AddTagScreen( + onBack = { + navController.popBackStack() + }, + onTagSelected = { tag -> + wallet.addTagToSelected(tag) + navController.popBackStack() + }, + tqgInputTestTag = "TagInputReceive", + addButtonTestTag = "ReceiveTagsSubmit", ) } } - composableWithDefaultTransitions { - val walletUiState by wallet.walletState.collectAsStateWithLifecycle() - @Suppress("ViewModelForwarding") - EditInvoiceScreen( - amountInputViewModel = editInvoiceAmountViewModel, - walletUiState = walletUiState, - onBack = { navController.popBackStack() }, - updateInvoice = wallet::updateBip21Invoice, - onClickAddTag = { navController.navigateTo(ReceiveRoute.AddTag) }, - onClickTag = wallet::removeTag, - onDescriptionUpdate = wallet::updateBip21Description, - navigateReceiveConfirm = { entry -> - cjitEntryDetails.value = entry - navController.navigateTo(ReceiveRoute.ConfirmIncreaseInbound) - } - ) - } - composableWithDefaultTransitions { - AddTagScreen( - onBack = { - navController.popBackStack() - }, - onTagSelected = { tag -> - wallet.addTagToSelected(tag) - navController.popBackStack() - }, - tqgInputTestTag = "TagInputReceive", - addButtonTestTag = "ReceiveTagsSubmit", - ) - } + } + + AnimatedVisibility( + visible = isOffline, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConnectionIssuesView(titleText = stringResource(R.string.wallet__receive_bitcoin)) } } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt index 0800b4de2..d8f3a0466 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt @@ -1,7 +1,11 @@ package to.bitkit.ui.sheets +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut 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.aspectRatio @@ -18,8 +22,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R +import to.bitkit.repositories.ConnectivityState import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton @@ -39,15 +45,28 @@ fun ForceTransferSheet( transferViewModel: TransferViewModel, ) { val isLoading by transferViewModel.isForceTransferLoading.collectAsStateWithLifecycle() - Content( - isLoading = isLoading, - onForceTransfer = { - transferViewModel.forceTransfer { - appViewModel.hideSheet() - } - }, - onCancel = { appViewModel.hideSheet() }, - ) + val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle() + val isOffline = connectivityState != ConnectivityState.CONNECTED + + Box { + Content( + isLoading = isLoading, + onForceTransfer = { + transferViewModel.forceTransfer { + appViewModel.hideSheet() + } + }, + onCancel = { appViewModel.hideSheet() }, + ) + + AnimatedVisibility( + visible = isOffline, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConnectionIssuesView(titleText = stringResource(R.string.lightning__transfer__nav_title)) + } + } } @Composable 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 9b9431d65..6c751db8b 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -1,5 +1,9 @@ package to.bitkit.ui.sheets +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -11,15 +15,19 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import kotlinx.serialization.Serializable +import to.bitkit.R import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType +import to.bitkit.repositories.ConnectivityState +import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.navigateTo import to.bitkit.ui.screens.scanner.QrScanningScreen import to.bitkit.ui.screens.wallets.send.AddTagScreen @@ -56,6 +64,9 @@ fun SendSheet( walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, ) { + val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle() + val isOffline = connectivityState != ConnectivityState.CONNECTED + LaunchedEffect(startDestination) { // always reset state on new user-initiated send if (startDestination == SendRoute.Recipient) { @@ -63,274 +74,287 @@ fun SendSheet( appViewModel.resetQuickPay() } } - Column( + Box( modifier = Modifier .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("SendSheet") + .sheetHeight(), ) { - val navController = rememberNavController() - LaunchedEffect(appViewModel, navController) { - appViewModel.sendEffect.collect { - when (it) { - is SendEffect.NavigateToAmount -> navController.navigateTo(SendRoute.Amount) - is SendEffect.NavigateToAddress -> navController.navigateTo(SendRoute.Address) - is SendEffect.NavigateToScan -> navController.navigateTo(SendRoute.QrScanner) - is SendEffect.NavigateToCoinSelection -> navController.navigateTo(SendRoute.CoinSelection) - is SendEffect.NavigateToConfirm -> navController.navigateTo(SendRoute.Confirm) - is SendEffect.PopBack -> navController.popBackStack(it.route, inclusive = false) - is SendEffect.PaymentSuccess -> { - appViewModel.clearClipboardForAutoRead() - navController.navigateTo(SendRoute.Success) { - popUpTo(navController.graph.id) { inclusive = true } + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .testTag("SendSheet"), + ) { + val navController = rememberNavController() + LaunchedEffect(appViewModel, navController) { + appViewModel.sendEffect.collect { + when (it) { + is SendEffect.NavigateToAmount -> navController.navigateTo(SendRoute.Amount) + is SendEffect.NavigateToAddress -> navController.navigateTo(SendRoute.Address) + is SendEffect.NavigateToScan -> navController.navigateTo(SendRoute.QrScanner) + is SendEffect.NavigateToCoinSelection -> navController.navigateTo(SendRoute.CoinSelection) + is SendEffect.NavigateToConfirm -> navController.navigateTo(SendRoute.Confirm) + is SendEffect.PopBack -> navController.popBackStack(it.route, inclusive = false) + is SendEffect.PaymentSuccess -> { + appViewModel.clearClipboardForAutoRead() + navController.navigateTo(SendRoute.Success) { + popUpTo(navController.graph.id) { inclusive = true } + } } - } - is SendEffect.NavigateToQuickPay -> navController.navigateTo(SendRoute.QuickPay) - is SendEffect.NavigateToWithdrawConfirm -> navController.navigateTo( - SendRoute.WithdrawConfirm - ) - is SendEffect.NavigateToWithdrawError -> navController.navigateTo(SendRoute.WithdrawError) - is SendEffect.NavigateToFee -> navController.navigateTo(SendRoute.FeeRate) - is SendEffect.NavigateToFeeCustom -> navController.navigateTo(SendRoute.FeeCustom) - is SendEffect.NavigateToComingSoon -> navController.navigateTo(SendRoute.ComingSoon) - is SendEffect.NavigateToPending -> navController.navigateTo( - SendRoute.Pending(it.paymentHash, it.amount) - ) { popUpTo(startDestination) { inclusive = true } } + is SendEffect.NavigateToQuickPay -> navController.navigateTo(SendRoute.QuickPay) + is SendEffect.NavigateToWithdrawConfirm -> navController.navigateTo( + SendRoute.WithdrawConfirm + ) + is SendEffect.NavigateToWithdrawError -> navController.navigateTo(SendRoute.WithdrawError) + is SendEffect.NavigateToFee -> navController.navigateTo(SendRoute.FeeRate) + is SendEffect.NavigateToFeeCustom -> navController.navigateTo(SendRoute.FeeCustom) + is SendEffect.NavigateToComingSoon -> navController.navigateTo(SendRoute.ComingSoon) + is SendEffect.NavigateToPending -> navController.navigateTo( + SendRoute.Pending(it.paymentHash, it.amount) + ) { popUpTo(startDestination) { inclusive = true } } + } } } - } - NavHost( - navController = navController, - startDestination = startDestination, - ) { - composableWithDefaultTransitions { - SendRecipientScreen( - onEvent = { appViewModel.setSendEvent(it) } - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - SendAddressScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onEvent = { appViewModel.setSendEvent(it) }, - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() - SendAmountScreen( - uiState = uiState, - nodeLifecycleState = lightningState.nodeLifecycleState, - canGoBack = startDestination != SendRoute.Amount, - onBack = { - if (!navController.popBackStack()) { - appViewModel.hideSheet() - } - }, - onEvent = { appViewModel.setSendEvent(it) } - ) - } - composableWithDefaultTransitions { - QrScanningScreen( - onBack = { navController.popBackStack() }, - onScanSuccess = { - navController.popBackStack() - appViewModel.onScanResult(data = it) - }, - ) - } - composableWithDefaultTransitions { - val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - SendCoinSelectionScreen( - requiredAmount = sendUiState.amount, - address = sendUiState.address, - onBack = { navController.popBackStack() }, - onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, - ) - } - navigationWithDefaultTransitions( - startDestination = SendRoute.FeeRate, + NavHost( + navController = navController, + startDestination = startDestination, ) { - composableWithDefaultTransitions { - val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } - SendFeeRateScreen( - sendUiState = sendUiState, - viewModel = hiltViewModel(parentEntry), + composableWithDefaultTransitions { + SendRecipientScreen( + onEvent = { appViewModel.setSendEvent(it) } + ) + } + composableWithDefaultTransitions { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendAddressScreen( + uiState = uiState, onBack = { navController.popBackStack() }, - onContinue = { navController.popBackStack() }, - onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, - onSelectInstant = { - appViewModel.switchToLightning() + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + composableWithDefaultTransitions { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() + SendAmountScreen( + uiState = uiState, + nodeLifecycleState = lightningState.nodeLifecycleState, + canGoBack = startDestination != SendRoute.Amount, + onBack = { + if (!navController.popBackStack()) { + appViewModel.hideSheet() + } + }, + onEvent = { appViewModel.setSendEvent(it) } + ) + } + composableWithDefaultTransitions { + QrScanningScreen( + onBack = { navController.popBackStack() }, + onScanSuccess = { navController.popBackStack() + appViewModel.onScanResult(data = it) }, ) } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } - SendFeeCustomScreen( - viewModel = hiltViewModel(parentEntry), + composableWithDefaultTransitions { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendCoinSelectionScreen( + requiredAmount = sendUiState.amount, + address = sendUiState.address, onBack = { navController.popBackStack() }, - onContinue = { speed -> appViewModel.setTransactionSpeed(speed) }, + onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, ) } - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() - - SendConfirmScreen( - savedStateHandle = it.savedStateHandle, - uiState = uiState, - isNodeRunning = lightningState.nodeLifecycleState.isRunning(), - canGoBack = startDestination != SendRoute.Confirm, - onBack = { - if (!navController.popBackStack()) { - appViewModel.hideSheet() - } - }, - onEvent = { e -> appViewModel.setSendEvent(e) }, - onClickAddTag = { navController.navigateTo(SendRoute.AddTag) }, - onClickTag = { tag -> appViewModel.removeTag(tag) }, - onNavigateToPin = { navController.navigateTo(SendRoute.PinCheck) }, - ) - } - composableWithDefaultTransitions { - val sendDetail by appViewModel.successSendUiState.collectAsStateWithLifecycle() - NewTransactionSheetView( - details = sendDetail, - onCloseClick = { appViewModel.hideSheet() }, - onDetailClick = { appViewModel.onClickSendDetail() }, - modifier = Modifier - .fillMaxSize() - .gradientBackground() - .navigationBarsPadding() - .testTag("SendSuccess") - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithdrawConfirmScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onConfirm = { appViewModel.onConfirmWithdraw() }, - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithdrawErrorScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onClickScan = { navController.navigateTo(SendRoute.QrScanner) }, - onClickSupport = { navController.navigateTo(SendRoute.Support) }, - ) - } - // TODO navigate to main support screen, not inside SEND sheet - composableWithDefaultTransitions { - SupportScreen(navController) - } - composableWithDefaultTransitions { - AddTagScreen( - onBack = { navController.popBackStack() }, - onTagSelected = { tag -> - appViewModel.addTagToSelected(tag) - navController.popBackStack() - }, - tqgInputTestTag = "TagInputSend", - addButtonTestTag = "SendTagsSubmit", - ) - } - composableWithDefaultTransitions { - SendPinCheckScreen( - onBack = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(PIN_CHECK_RESULT_KEY, false) - navController.popBackStack() - }, - onSuccess = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(PIN_CHECK_RESULT_KEY, true) - navController.popBackStack() - appViewModel.setSendEvent(SendEvent.PayConfirmed) - }, - ) - } - composableWithDefaultTransitions { - val quickPayData by appViewModel.quickPayData.collectAsStateWithLifecycle() - SendQuickPayScreen( - quickPayData = requireNotNull(quickPayData), - onPaymentComplete = { paymentHash, amountWithFee -> - appViewModel.onSendSuccess( - NewTransactionSheetDetails( - type = NewTransactionSheetType.LIGHTNING, - direction = NewTransactionSheetDirection.SENT, - paymentHashOrTxId = paymentHash, - sats = amountWithFee, - ), + navigationWithDefaultTransitions( + startDestination = SendRoute.FeeRate, + ) { + composableWithDefaultTransitions { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } + SendFeeRateScreen( + sendUiState = sendUiState, + viewModel = hiltViewModel(parentEntry), + onBack = { navController.popBackStack() }, + onContinue = { navController.popBackStack() }, + onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, + onSelectInstant = { + appViewModel.switchToLightning() + navController.popBackStack() + }, ) - }, - onPaymentPending = { paymentHash, amount -> - navController.navigateTo(SendRoute.Pending(paymentHash, amount)) { - popUpTo(startDestination) { inclusive = true } - } - }, - onShowError = { errorMessage -> - navController.navigateTo(SendRoute.Error(errorMessage)) } - ) - } - composableWithDefaultTransitions { - val route = it.toRoute() - SendPendingScreen( - paymentHash = route.paymentHash, - amount = route.amount, - onPaymentSuccess = { paymentHash -> - appViewModel.onSendSuccess( - NewTransactionSheetDetails( - type = NewTransactionSheetType.LIGHTNING, - direction = NewTransactionSheetDirection.SENT, - paymentHashOrTxId = paymentHash, - sats = route.amount, - ), + composableWithDefaultTransitions { + val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } + SendFeeCustomScreen( + viewModel = hiltViewModel(parentEntry), + onBack = { navController.popBackStack() }, + onContinue = { speed -> appViewModel.setTransactionSpeed(speed) }, ) - }, - onPaymentError = { - navController.navigateTo(SendRoute.Error()) { - popUpTo { inclusive = true } + } + } + composableWithDefaultTransitions { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() + + SendConfirmScreen( + savedStateHandle = it.savedStateHandle, + uiState = uiState, + isNodeRunning = lightningState.nodeLifecycleState.isRunning(), + canGoBack = startDestination != SendRoute.Confirm, + onBack = { + if (!navController.popBackStack()) { + appViewModel.hideSheet() + } + }, + onEvent = { e -> appViewModel.setSendEvent(e) }, + onClickAddTag = { navController.navigateTo(SendRoute.AddTag) }, + onClickTag = { tag -> appViewModel.removeTag(tag) }, + onNavigateToPin = { navController.navigateTo(SendRoute.PinCheck) }, + ) + } + composableWithDefaultTransitions { + val sendDetail by appViewModel.successSendUiState.collectAsStateWithLifecycle() + NewTransactionSheetView( + details = sendDetail, + onCloseClick = { appViewModel.hideSheet() }, + onDetailClick = { appViewModel.onClickSendDetail() }, + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .testTag("SendSuccess") + ) + } + composableWithDefaultTransitions { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + WithdrawConfirmScreen( + uiState = uiState, + onBack = { navController.popBackStack() }, + onConfirm = { appViewModel.onConfirmWithdraw() }, + ) + } + composableWithDefaultTransitions { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + WithdrawErrorScreen( + uiState = uiState, + onBack = { navController.popBackStack() }, + onClickScan = { navController.navigateTo(SendRoute.QrScanner) }, + onClickSupport = { navController.navigateTo(SendRoute.Support) }, + ) + } + // TODO navigate to main support screen, not inside SEND sheet + composableWithDefaultTransitions { + SupportScreen(navController) + } + composableWithDefaultTransitions { + AddTagScreen( + onBack = { navController.popBackStack() }, + onTagSelected = { tag -> + appViewModel.addTagToSelected(tag) + navController.popBackStack() + }, + tqgInputTestTag = "TagInputSend", + addButtonTestTag = "SendTagsSubmit", + ) + } + composableWithDefaultTransitions { + SendPinCheckScreen( + onBack = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(PIN_CHECK_RESULT_KEY, false) + navController.popBackStack() + }, + onSuccess = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(PIN_CHECK_RESULT_KEY, true) + navController.popBackStack() + appViewModel.setSendEvent(SendEvent.PayConfirmed) + }, + ) + } + composableWithDefaultTransitions { + val quickPayData by appViewModel.quickPayData.collectAsStateWithLifecycle() + SendQuickPayScreen( + quickPayData = requireNotNull(quickPayData), + onPaymentComplete = { paymentHash, amountWithFee -> + appViewModel.onSendSuccess( + NewTransactionSheetDetails( + type = NewTransactionSheetType.LIGHTNING, + direction = NewTransactionSheetDirection.SENT, + paymentHashOrTxId = paymentHash, + sats = amountWithFee, + ), + ) + }, + onPaymentPending = { paymentHash, amount -> + navController.navigateTo(SendRoute.Pending(paymentHash, amount)) { + popUpTo(startDestination) { inclusive = true } + } + }, + onShowError = { errorMessage -> + navController.navigateTo(SendRoute.Error(errorMessage)) } - }, - onClose = { appViewModel.hideSheet() }, - onViewDetails = { rawId -> appViewModel.navigateToActivity(rawId) }, - viewModel = hiltViewModel(), - ) - } - composableWithDefaultTransitions { - ComingSoonSheetContent( - onWalletOverviewClick = { appViewModel.hideSheet() }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - val route = it.toRoute() - SendErrorScreen( - message = route.message, - onRetry = { - navController.navigateTo(SendRoute.Recipient) { - popUpTo(navController.graph.id) { inclusive = true } + ) + } + composableWithDefaultTransitions { + val route = it.toRoute() + SendPendingScreen( + paymentHash = route.paymentHash, + amount = route.amount, + onPaymentSuccess = { paymentHash -> + appViewModel.onSendSuccess( + NewTransactionSheetDetails( + type = NewTransactionSheetType.LIGHTNING, + direction = NewTransactionSheetDirection.SENT, + paymentHashOrTxId = paymentHash, + sats = route.amount, + ), + ) + }, + onPaymentError = { + navController.navigateTo(SendRoute.Error()) { + popUpTo { inclusive = true } + } + }, + onClose = { appViewModel.hideSheet() }, + onViewDetails = { rawId -> appViewModel.navigateToActivity(rawId) }, + viewModel = hiltViewModel(), + ) + } + composableWithDefaultTransitions { + ComingSoonSheetContent( + onWalletOverviewClick = { appViewModel.hideSheet() }, + onBack = { navController.popBackStack() }, + ) + } + composableWithDefaultTransitions { + val route = it.toRoute() + SendErrorScreen( + message = route.message, + onRetry = { + navController.navigateTo(SendRoute.Recipient) { + popUpTo(navController.graph.id) { inclusive = true } + } + }, + onClose = { + appViewModel.hideSheet() } - }, - onClose = { - appViewModel.hideSheet() - } - ) + ) + } } } + + AnimatedVisibility( + visible = isOffline, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConnectionIssuesView(titleText = stringResource(R.string.wallet__send_bitcoin)) + } } } From 1671051f77d7467bf5b5a30b04b2164be2e724b9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 31 Mar 2026 11:18:09 -0300 Subject: [PATCH 3/9] fix: circle alignment --- .../java/to/bitkit/ui/components/ConnectionIssuesView.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt index 612dd591d..ef3009532 100644 --- a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -46,7 +46,6 @@ fun ConnectionIssuesView( SheetTopBar(titleText = titleText) Box( - contentAlignment = Alignment.BottomCenter, modifier = Modifier .fillMaxWidth() .weight(1f), @@ -57,7 +56,9 @@ fun ConnectionIssuesView( painter = painterResource(R.drawable.phone), contentDescription = null, contentScale = ContentScale.Fit, - modifier = Modifier.size(311.dp), + modifier = Modifier + .size(311.dp) + .align(Alignment.CenterStart), ) DashedRingsLayer(outerOnly = false) @@ -109,7 +110,7 @@ private val innerRings = listOf( private fun DashedRingsLayer(outerOnly: Boolean, modifier: Modifier = Modifier) { val rings = if (outerOnly) listOf(outerRing) else innerRings Canvas(modifier = modifier.fillMaxSize()) { - val center = Offset(size.width * 0.35f, size.height * 0.55f) + val center = Offset(size.width * 0.25f, size.height * 0.40f) rings.forEach { ring -> drawDashedRing(ring, center) } } } From 61e03710681d018630d4b36224d37943d02b996d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 31 Mar 2026 11:40:57 -0300 Subject: [PATCH 4/9] fix: circle color and fading --- .../ui/components/ConnectionIssuesView.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt index ef3009532..8262c0341 100644 --- a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.drawscope.DrawScope @@ -97,13 +98,13 @@ fun ConnectionIssuesView( private val outerRing = DashedRingSpec( radiusFraction = 0.60f, - color = Colors.White.copy(alpha = 0.08f), + color = Colors.Yellow.copy(alpha = 0.08f), ) private val innerRings = listOf( DashedRingSpec(radiusFraction = 0.15f, color = Colors.Yellow.copy(alpha = 0.4f)), DashedRingSpec(radiusFraction = 0.30f, color = Colors.Yellow.copy(alpha = 0.25f)), - DashedRingSpec(radiusFraction = 0.45f, color = Colors.Brand.copy(alpha = 0.15f)), + DashedRingSpec(radiusFraction = 0.45f, color = Colors.Yellow.copy(alpha = 0.15f)), ) @Composable @@ -111,6 +112,20 @@ private fun DashedRingsLayer(outerOnly: Boolean, modifier: Modifier = Modifier) val rings = if (outerOnly) listOf(outerRing) else innerRings Canvas(modifier = modifier.fillMaxSize()) { val center = Offset(size.width * 0.25f, size.height * 0.40f) + + if (outerOnly) { + val fadeRadius = size.minDimension * 0.45f + drawCircle( + brush = Brush.radialGradient( + colors = listOf(Colors.White.copy(alpha = 0.06f), Color.Transparent), + center = center, + radius = fadeRadius, + ), + radius = fadeRadius, + center = center, + ) + } + rings.forEach { ring -> drawDashedRing(ring, center) } } } From e60c9fb9ccfa9f9d1dd04c59c55c3360c0f2cb8b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 31 Mar 2026 13:35:28 -0300 Subject: [PATCH 5/9] chore: lint --- .../main/java/to/bitkit/ui/components/ConnectionIssuesView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt index 8262c0341..d4f1a2962 100644 --- a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -95,7 +95,6 @@ fun ConnectionIssuesView( } } - private val outerRing = DashedRingSpec( radiusFraction = 0.60f, color = Colors.Yellow.copy(alpha = 0.08f), From aaa3d8d3a262c4330b42493453d45f1de31f00ac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 31 Mar 2026 13:53:06 -0300 Subject: [PATCH 6/9] fix: gradient color --- .../ui/components/ConnectionIssuesView.kt | 50 ++++++------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt index d4f1a2962..7d91930b5 100644 --- a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -47,6 +47,7 @@ fun ConnectionIssuesView( SheetTopBar(titleText = titleText) Box( + contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() .weight(1f), @@ -59,7 +60,7 @@ fun ConnectionIssuesView( contentScale = ContentScale.Fit, modifier = Modifier .size(311.dp) - .align(Alignment.CenterStart), + .align(Alignment.Center), ) DashedRingsLayer(outerOnly = false) @@ -95,44 +96,28 @@ fun ConnectionIssuesView( } } -private val outerRing = DashedRingSpec( - radiusFraction = 0.60f, - color = Colors.Yellow.copy(alpha = 0.08f), -) - -private val innerRings = listOf( - DashedRingSpec(radiusFraction = 0.15f, color = Colors.Yellow.copy(alpha = 0.4f)), - DashedRingSpec(radiusFraction = 0.30f, color = Colors.Yellow.copy(alpha = 0.25f)), - DashedRingSpec(radiusFraction = 0.45f, color = Colors.Yellow.copy(alpha = 0.15f)), -) +private val outerRingRadii = listOf(200f) +private val innerRingRadii = listOf(150f, 100f, 50f) @Composable private fun DashedRingsLayer(outerOnly: Boolean, modifier: Modifier = Modifier) { - val rings = if (outerOnly) listOf(outerRing) else innerRings + val radii = if (outerOnly) outerRingRadii else innerRingRadii Canvas(modifier = modifier.fillMaxSize()) { val center = Offset(size.width * 0.25f, size.height * 0.40f) - - if (outerOnly) { - val fadeRadius = size.minDimension * 0.45f - drawCircle( - brush = Brush.radialGradient( - colors = listOf(Colors.White.copy(alpha = 0.06f), Color.Transparent), - center = center, - radius = fadeRadius, - ), - radius = fadeRadius, - center = center, - ) - } - - rings.forEach { ring -> drawDashedRing(ring, center) } + radii.forEach { radiusDp -> drawDashedGradientRing(radiusDp, center) } } } -private fun DrawScope.drawDashedRing(ring: DashedRingSpec, center: Offset) { +private fun DrawScope.drawDashedGradientRing(radiusDp: Float, center: Offset) { + val radius = radiusDp.dp.toPx() + val brush = Brush.linearGradient( + colors = listOf(Color.Black, Colors.Yellow), + start = Offset(center.x - radius, center.y - radius), + end = Offset(center.x + radius, center.y + radius), + ) drawCircle( - color = ring.color, - radius = size.minDimension * ring.radiusFraction, + brush = brush, + radius = radius, center = center, style = Stroke( width = 1.dp.toPx(), @@ -143,11 +128,6 @@ private fun DrawScope.drawDashedRing(ring: DashedRingSpec, center: Offset) { ) } -private data class DashedRingSpec( - val radiusFraction: Float, - val color: Color, -) - @Preview(showSystemUi = true) @Composable private fun Preview() { From 9f3c1f93e896973a42f870ed2fea0b85f390b0bc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 06:33:40 -0300 Subject: [PATCH 7/9] feat: display connection issues screen in transfer flows --- app/src/main/java/to/bitkit/ui/ContentView.kt | 8 ++- .../ui/components/ConnectionIssuesView.kt | 3 + .../screens/transfer/SavingsConfirmScreen.kt | 44 ++++++++---- .../screens/transfer/SpendingAmountScreen.kt | 72 ++++++++++++------- .../screens/transfer/SpendingConfirmScreen.kt | 43 +++++++---- 5 files changed, 116 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index a653e929c..073fb1767 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -577,7 +577,9 @@ private fun RootNavHost( ) } composableWithDefaultTransitions { + val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle() SavingsConfirmScreen( + isOffline = connectivityState != ConnectivityState.CONNECTED, onConfirm = { navController.navigateTo(Routes.SavingsProgress) }, onAdvancedClick = { navController.navigateTo(Routes.SavingsAdvanced) }, onBackClick = { navController.popBackStack() }, @@ -608,8 +610,10 @@ private fun RootNavHost( ) } composableWithDefaultTransitions { + val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle() SpendingAmountScreen( viewModel = transferViewModel, + isOffline = connectivityState != ConnectivityState.CONNECTED, onBackClick = { navController.popBackStack() }, onOrderCreated = { navController.navigateTo(Routes.SpendingConfirm) }, toastException = { appViewModel.toast(it) }, @@ -617,14 +621,16 @@ private fun RootNavHost( appViewModel.toast( type = Toast.ToastType.ERROR, title = title, - description = description + description = description, ) }, ) } composableWithDefaultTransitions { + val connectivityState by appViewModel.isOnline.collectAsStateWithLifecycle() SpendingConfirmScreen( viewModel = transferViewModel, + isOffline = connectivityState != ConnectivityState.CONNECTED, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, onLearnMoreClick = { navController.navigateTo(Routes.TransferLiquidity) }, diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt index 7d91930b5..2baf85883 100644 --- a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,11 +36,13 @@ import to.bitkit.ui.utils.withAccent fun ConnectionIssuesView( titleText: String, modifier: Modifier = Modifier, + includeStatusBarPadding: Boolean = false, ) { Column( modifier = modifier .fillMaxSize() .gradientBackground() + .then(if (includeStatusBarPadding) Modifier.statusBarsPadding() else Modifier) .navigationBarsPadding() .padding(horizontal = 16.dp) .testTag("ConnectionIssueView"), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt index da45ad90f..c15803a5d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt @@ -1,6 +1,10 @@ package to.bitkit.ui.screens.transfer +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -30,6 +34,7 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.filterOpen import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.components.Display import to.bitkit.ui.components.MoneyDisplay import to.bitkit.ui.components.PrimaryButton @@ -46,6 +51,7 @@ import to.bitkit.ui.walletViewModel @Composable fun SavingsConfirmScreen( + isOffline: Boolean, onConfirm: () -> Unit, onAdvancedClick: () -> Unit, onBackClick: () -> Unit, @@ -70,19 +76,31 @@ fun SavingsConfirmScreen( val amount = channels.sumOf { it.amountOnClose } - SavingsConfirmContent( - amount = amount, - hasMultiple = hasMultiple, - hasSelected = hasSelected, - onBackClick = onBackClick, - onAmountClick = { currency.switchUnit() }, - onAdvancedClick = onAdvancedClick, - onSelectAllClick = { transfer.setSelectedChannelIds(emptySet()) }, - onConfirm = { - transfer.onTransferToSavingsConfirm(channels) - onConfirm() - }, - ) + Box { + SavingsConfirmContent( + amount = amount, + hasMultiple = hasMultiple, + hasSelected = hasSelected, + onBackClick = onBackClick, + onAmountClick = { currency.switchUnit() }, + onAdvancedClick = onAdvancedClick, + onSelectAllClick = { transfer.setSelectedChannelIds(emptySet()) }, + onConfirm = { + transfer.onTransferToSavingsConfirm(channels) + onConfirm() + }, + ) + AnimatedVisibility( + visible = isOffline, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConnectionIssuesView( + titleText = stringResource(R.string.lightning__transfer__nav_title), + includeStatusBarPadding = true, + ) + } + } } @Suppress("MagicNumber") 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 d313421c2..b8b94bd88 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 @@ -1,6 +1,10 @@ package to.bitkit.ui.screens.transfer +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut 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.fillMaxSize @@ -23,6 +27,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -52,6 +57,7 @@ import kotlin.math.min @Composable fun SpendingAmountScreen( viewModel: TransferViewModel, + isOffline: Boolean, onBackClick: () -> Unit = {}, onOrderCreated: () -> Unit = {}, toastException: (Throwable) -> Unit, @@ -78,33 +84,45 @@ fun SpendingAmountScreen( } } - Content( - isNodeRunning = isNodeRunning, - uiState = uiState, - amountInputViewModel = amountInputViewModel, - currencies = currencies, - onBackClick = onBackClick, - onClickQuarter = { - val quarter = uiState.balanceAfterFeeQuarter() - val max = uiState.maxAllowedToSend - if (quarter > max) { - toast( - context.getString(R.string.lightning__spending_amount__error_max__title), - context.getString(R.string.lightning__spending_amount__error_max__description) - .replace("{amount}", "$max"), - ) - } - val cappedQuarter = min(quarter, max) - viewModel.updateLimits(cappedQuarter) - amountInputViewModel.setSats(cappedQuarter, currencies) - }, - onClickMaxAmount = { - val newAmountSats = uiState.maxAllowedToSend - viewModel.updateLimits(newAmountSats) - amountInputViewModel.setSats(newAmountSats, currencies) - }, - onConfirmAmount = { viewModel.onConfirmAmount(amountUiState.sats) }, - ) + Box { + Content( + isNodeRunning = isNodeRunning, + uiState = uiState, + amountInputViewModel = amountInputViewModel, + currencies = currencies, + onBackClick = onBackClick, + onClickQuarter = { + val quarter = uiState.balanceAfterFeeQuarter() + val max = uiState.maxAllowedToSend + if (quarter > max) { + toast( + context.getString(R.string.lightning__spending_amount__error_max__title), + context.getString(R.string.lightning__spending_amount__error_max__description) + .replace("{amount}", "$max"), + ) + } + val cappedQuarter = min(quarter, max) + viewModel.updateLimits(cappedQuarter) + amountInputViewModel.setSats(cappedQuarter, currencies) + }, + onClickMaxAmount = { + val newAmountSats = uiState.maxAllowedToSend + viewModel.updateLimits(newAmountSats) + amountInputViewModel.setSats(newAmountSats, currencies) + }, + onConfirmAmount = { viewModel.onConfirmAmount(amountUiState.sats) }, + ) + AnimatedVisibility( + visible = isOffline, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConnectionIssuesView( + titleText = stringResource(R.string.lightning__transfer__nav_title), + includeStatusBarPadding = true, + ) + } + } } @Suppress("ViewModelForwarding") diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt index ebdf4766f..629455c0e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt @@ -1,5 +1,8 @@ package to.bitkit.ui.screens.transfer +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -45,6 +48,7 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.ChannelStatusUi +import to.bitkit.ui.components.ConnectionIssuesView import to.bitkit.ui.components.Display import to.bitkit.ui.components.FeeInfo import to.bitkit.ui.components.FillHeight @@ -68,6 +72,7 @@ import to.bitkit.viewmodels.TransferViewModel @Composable fun SpendingConfirmScreen( viewModel: TransferViewModel, + isOffline: Boolean, onBackClick: () -> Unit = {}, onCloseClick: () -> Unit = {}, onLearnMoreClick: () -> Unit = {}, @@ -91,21 +96,33 @@ fun SpendingConfirmScreen( onPermissionChange = { granted -> settingsViewModel.setNotificationPreference(granted) }, - showPermissionDialog = false + showPermissionDialog = false, ) - Content( - onBackClick = onBackClick, - onLearnMoreClick = onLearnMoreClick, - onAdvancedClick = onAdvancedClick, - onConfirm = onConfirm, - onUseDefaultLspBalanceClick = viewModel::onUseDefaultLspBalanceClick, - onTransferToSpendingConfirm = viewModel::onTransferToSpendingConfirm, - order = order, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { context.openNotificationSettings() }, - isAdvanced = isAdvanced, - ) + Box { + Content( + onBackClick = onBackClick, + onLearnMoreClick = onLearnMoreClick, + onAdvancedClick = onAdvancedClick, + onConfirm = onConfirm, + onUseDefaultLspBalanceClick = viewModel::onUseDefaultLspBalanceClick, + onTransferToSpendingConfirm = viewModel::onTransferToSpendingConfirm, + order = order, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { context.openNotificationSettings() }, + isAdvanced = isAdvanced, + ) + AnimatedVisibility( + visible = isOffline, + enter = fadeIn(), + exit = fadeOut(), + ) { + ConnectionIssuesView( + titleText = stringResource(R.string.lightning__transfer__nav_title), + includeStatusBarPadding = true, + ) + } + } } @Suppress("MagicNumber") From 46f955f84b631b2e200b8d615d71f89a1e9a97d4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 06:40:08 -0300 Subject: [PATCH 8/9] refactor: remove unnecessary parameter --- .../main/java/to/bitkit/ui/components/ConnectionIssuesView.kt | 3 --- .../java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt | 3 ++- .../java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt | 3 ++- .../to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt | 3 ++- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt index 2baf85883..7d91930b5 100644 --- a/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ConnectionIssuesView.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,13 +35,11 @@ import to.bitkit.ui.utils.withAccent fun ConnectionIssuesView( titleText: String, modifier: Modifier = Modifier, - includeStatusBarPadding: Boolean = false, ) { Column( modifier = modifier .fillMaxSize() .gradientBackground() - .then(if (includeStatusBarPadding) Modifier.statusBarsPadding() else Modifier) .navigationBarsPadding() .padding(horizontal = 16.dp) .testTag("ConnectionIssueView"), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt index c15803a5d..a756a320a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsConfirmScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -97,7 +98,7 @@ fun SavingsConfirmScreen( ) { ConnectionIssuesView( titleText = stringResource(R.string.lightning__transfer__nav_title), - includeStatusBarPadding = true, + modifier = Modifier.statusBarsPadding(), ) } } 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 b8b94bd88..28d828080 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 @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -119,7 +120,7 @@ fun SpendingAmountScreen( ) { ConnectionIssuesView( titleText = stringResource(R.string.lightning__transfer__nav_title), - includeStatusBarPadding = true, + modifier = Modifier.statusBarsPadding(), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt index 629455c0e..7bc62dca1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -119,7 +120,7 @@ fun SpendingConfirmScreen( ) { ConnectionIssuesView( titleText = stringResource(R.string.lightning__transfer__nav_title), - includeStatusBarPadding = true, + modifier = Modifier.statusBarsPadding(), ) } } From a5fde6daf4fe730f3476da3a358061571227bb06 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 1 Apr 2026 14:49:50 -0300 Subject: [PATCH 9/9] fix: re-trigger updateLimits when switch to online --- .../java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 28d828080..d2ed7e081 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 @@ -71,7 +71,7 @@ fun SpendingAmountScreen( val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current - LaunchedEffect(Unit) { + LaunchedEffect(isOffline) { viewModel.updateLimits() }