From 1b861be812639c797a6e76c5eb3ec0c5eede7965 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 17:52:25 +0100 Subject: [PATCH 1/6] feat: channel balances move to spark at low fees --- .../main/java/to/bitkit/data/SettingsStore.kt | 1 + .../to/bitkit/repositories/LightningRepo.kt | 13 + .../to/bitkit/repositories/MoveToSparkRepo.kt | 198 ++++++++++++ app/src/main/java/to/bitkit/ui/ContentView.kt | 28 ++ .../java/to/bitkit/ui/components/SheetHost.kt | 1 + .../screens/wallets/SpendingWalletScreen.kt | 21 ++ .../lightning/LightningConnectionsScreen.kt | 11 + .../LightningConnectionsViewModel.kt | 4 + .../settings/lightning/MoveToSparkScreen.kt | 297 ++++++++++++++++++ .../lightning/MoveToSparkViewModel.kt | 95 ++++++ .../to/bitkit/ui/sheets/MoveToSparkSheet.kt | 86 +++++ .../sheets/MoveToSparkTimedSheet.kt | 28 ++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 18 ++ app/src/main/res/values-ar/strings.xml | 24 ++ app/src/main/res/values-b+es+419/strings.xml | 24 ++ app/src/main/res/values-ca/strings.xml | 24 ++ app/src/main/res/values-cs/strings.xml | 24 ++ app/src/main/res/values-de/strings.xml | 24 ++ app/src/main/res/values-el/strings.xml | 24 ++ app/src/main/res/values-es-rES/strings.xml | 24 ++ app/src/main/res/values-es/strings.xml | 24 ++ app/src/main/res/values-fr/strings.xml | 24 ++ app/src/main/res/values-it/strings.xml | 24 ++ app/src/main/res/values-nl/strings.xml | 24 ++ app/src/main/res/values-pl/strings.xml | 24 ++ app/src/main/res/values-pt-rBR/strings.xml | 24 ++ app/src/main/res/values-pt/strings.xml | 24 ++ app/src/main/res/values-ru/strings.xml | 24 ++ app/src/main/res/values/strings.xml | 24 ++ .../repositories/MoveToSparkRepoTest.kt | 125 ++++++++ .../viewmodels/AppViewModelSendFlowTest.kt | 6 + changelog.d/next/1768.added.md | 1 + docs/screens-map.md | 1 + journeys/transfer/move-to-spark.xml | 29 ++ 34 files changed, 1347 insertions(+) create mode 100644 app/src/main/java/to/bitkit/repositories/MoveToSparkRepo.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkViewModel.kt create mode 100644 app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt create mode 100644 app/src/main/java/to/bitkit/utils/timedsheets/sheets/MoveToSparkTimedSheet.kt create mode 100644 app/src/test/java/to/bitkit/repositories/MoveToSparkRepoTest.kt create mode 100644 changelog.d/next/1768.added.md create mode 100644 journeys/transfer/move-to-spark.xml diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index e5085ff311..6373c4c520 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -230,6 +230,7 @@ data class SettingsData( val publicPaykitUsdtEnabled: Boolean = true, val usdtPreference: UsdtPreference = UsdtPreference.UNDECIDED, val hasSeenUsdtIntro: Boolean = false, + val hasSeenMoveToSparkIntro: Boolean = false, val publicPaykitBolt11: String = "", val publicPaykitBolt11PaymentHash: String = "", val publicPaykitBolt11ExpiresAtMillis: Long = 0, diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index ad995b68cc..cb71bc768a 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -1472,6 +1472,19 @@ class LightningRepo @Inject constructor( return payInvoiceWithLdk(bolt11, sats, paymentDeadlineAt, maxTotalRoutingFeeMsat, onBeforeSend) } + /** Pays [bolt11] from the channels only, never falling back to Spark, which moving a channel balance needs. */ + suspend fun payInvoiceOverLdk( + bolt11: String, + sats: ULong?, + maxTotalRoutingFeeMsat: ULong?, + ): Result = payInvoiceWithLdk(bolt11, sats, null, maxTotalRoutingFeeMsat, onBeforeSend = { true }) + + /** The routing fee for paying [amountSats] to [bolt11] from the channels, never quoted on Spark. */ + suspend fun estimateLdkRoutingFeesForAmount(bolt11: String, amountSats: ULong): Result = + executeWhenNodeRunning("estimateLdkRoutingFeesForAmount") { + lightningService.estimateRoutingFeesForAmount(bolt11, amountSats) + } + private suspend fun payInvoiceWithLdk( bolt11: String, sats: ULong?, diff --git a/app/src/main/java/to/bitkit/repositories/MoveToSparkRepo.kt b/app/src/main/java/to/bitkit/repositories/MoveToSparkRepo.kt new file mode 100644 index 0000000000..f560b09fa1 --- /dev/null +++ b/app/src/main/java/to/bitkit/repositories/MoveToSparkRepo.kt @@ -0,0 +1,198 @@ +package to.bitkit.repositories + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import org.lightningdevkit.ldknode.Bolt11Invoice +import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.Event +import org.lightningdevkit.ldknode.PaymentFailureReason +import to.bitkit.di.BgDispatcher +import to.bitkit.ext.runSuspendCatching +import to.bitkit.ext.totalNextOutboundHtlcLimitSats +import to.bitkit.models.safe +import to.bitkit.utils.AppError +import to.bitkit.utils.Logger +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration.Companion.minutes + +/** + * Moves the balance of the user's Lightning channels to their own Spark wallet, paying a Spark invoice over the + * channels only. Blocktank is closing its channels, and this is the cheapest way to keep the balance in Spending. + */ +@Singleton +class MoveToSparkRepo @Inject constructor( + @BgDispatcher private val bgDispatcher: CoroutineDispatcher, + private val lightningRepo: LightningRepo, + private val sparkRepo: SparkRepo, +) { + companion object { + private const val TAG = "MoveToSparkRepo" + private const val INVOICE_DESCRIPTION = "Move to Spark" + private const val INVOICE_EXPIRY_SECS = 3600u + private const val MAX_QUOTE_ROUNDS = 4 + + /** How long the move waits for the payment to settle before it reports it as still in flight. */ + private val PAYMENT_TIMEOUT = 2.minutes + } + + private val ownPaymentHashes = ConcurrentHashMap.newKeySet() + + /** Whether [paymentHash] is a move to Spark, whose send and receive the app does not announce as payments. */ + fun isMovePayment(paymentHash: String): Boolean = paymentHash in ownPaymentHashes + + /** Whether the channels hold a balance worth moving to a usable Spark wallet. */ + val canMove: Flow = combine(lightningRepo.lightningState, sparkRepo.state) { lightning, spark -> + MoveToSparkMath.canMove(lightning.channels, spark) + }.distinctUntilChanged() + + /** The most the channels can send now, before routing fees. */ + fun movableSats(): ULong = MoveToSparkMath.movableSats(lightningRepo.lightningState.value.channels) + + /** + * Quotes moving [targetSats], or everything the channels can send, to a new Spark invoice. + * The amount leaves room for the routing fee, which the quote carries and the move caps the payment at. + */ + suspend fun quote(targetSats: ULong? = null): Result = withContext(bgDispatcher) { + runSuspendCatching { + val limitSats = movableSats() + val wantedSats = targetSats?.coerceAtMost(limitSats) ?: limitSats + if (wantedSats < MoveToSparkMath.MIN_MOVE_SATS) throw MoveToSparkError.NothingToMove() + if (!sparkRepo.state.value.isReady) throw MoveToSparkError.SparkUnavailable() + + val invoice = sparkRepo.createInvoice(null, INVOICE_DESCRIPTION, INVOICE_EXPIRY_SECS).getOrThrow() + var amountSats = MoveToSparkMath.firstCandidate(wantedSats, limitSats) + var fitting: MoveToSparkQuote? = null + // The first amount leaves room for a guessed fee; once the real fee is known, the amount grows to fit it. + repeat(MAX_QUOTE_ROUNDS) { + if (amountSats < MoveToSparkMath.MIN_MOVE_SATS) return@repeat + val feeSats = lightningRepo.estimateLdkRoutingFeesForAmount(invoice, amountSats).getOrElse { + fitting?.let { quote -> return@runSuspendCatching quote } + throw MoveToSparkError.TooLarge(amountSats, it) + } + val nextSats = minOf(wantedSats, MoveToSparkMath.amountAfterFee(limitSats, feeSats)) + if (amountSats.safe() + feeSats.safe() <= limitSats) { + fitting = MoveToSparkQuote(invoice, amountSats, feeSats) + if (nextSats <= amountSats) return@runSuspendCatching checkNotNull(fitting) + } + amountSats = nextSats + } + fitting ?: throw MoveToSparkError.TooLarge(amountSats) + }.onFailure { + Logger.warn("Failed to quote move to Spark of '$targetSats'", it, context = TAG) + } + } + + /** Pays [quote] over the channels and waits for it to settle in Spark. */ + suspend fun move(quote: MoveToSparkQuote): Result = withContext(bgDispatcher) { + runSuspendCatching { + val paymentHash = Bolt11Invoice.fromStr(quote.invoice).paymentHash() + ownPaymentHashes += paymentHash + coroutineScope { + val settled = async(start = CoroutineStart.UNDISPATCHED) { + lightningRepo.nodeEvents.first { it.settles(paymentHash) } + } + lightningRepo.payInvoiceOverLdk( + bolt11 = quote.invoice, + sats = quote.amountSats, + maxTotalRoutingFeeMsat = MoveToSparkMath.feeCapMsat(quote.feeSats), + ).getOrElse { + settled.cancel() + throw MoveToSparkError.TooLarge(quote.amountSats, it) + } + when (val event = withTimeoutOrNull(PAYMENT_TIMEOUT) { settled.await() }) { + is Event.PaymentSuccessful -> MoveToSparkOutcome.Moved( + amountSats = quote.amountSats, + feeSats = (event.feePaidMsat ?: 0uL) / 1000uL, + ) + is Event.PaymentFailed -> throw MoveToSparkError.TooLarge(quote.amountSats, reason = event.reason) + else -> { + settled.cancel() + MoveToSparkOutcome.InFlight(quote.amountSats) + } + } + } + }.onFailure { + Logger.error("Failed to move '${quote.amountSats}' sats to Spark", it, context = TAG) + } + } + + private fun Event.settles(paymentHash: String): Boolean = when (this) { + is Event.PaymentSuccessful -> this.paymentHash == paymentHash + is Event.PaymentFailed -> this.paymentHash == paymentHash + else -> false + } +} + +/** The amounts behind moving the channels' balance, kept apart so they can be tested without a node. */ +object MoveToSparkMath { + /** Below this a move is not offered: what is left is about the routing fee. */ + const val MIN_MOVE_SATS = 1_000uL + + /** The fee guessed for the first quote of a full move, as a share of the amount in parts per million. */ + private const val FIRST_FEE_GUESS_PPM = 5_000uL + + /** The least fee guessed for the first quote of a full move. */ + private const val MIN_FEE_GUESS_SATS = 10uL + + /** Room left above the quoted fee, in sats, for the fee moving between the quote and the payment. */ + private const val FEE_MARGIN_SATS = 2uL + + fun movableSats(channels: List?): ULong = channels.totalNextOutboundHtlcLimitSats() + + /** Whether the channels hold a balance worth moving to a usable Spark wallet. */ + fun canMove(channels: List?, spark: SparkState): Boolean = + spark.isReady && movableSats(channels) >= MIN_MOVE_SATS + + /** The first amount to quote: [wantedSats] when it leaves room for fees, else the limit less a guessed fee. */ + fun firstCandidate(wantedSats: ULong, limitSats: ULong): ULong { + val guessSats = maxOf(limitSats.safe() * FIRST_FEE_GUESS_PPM.safe() / 1_000_000uL, MIN_FEE_GUESS_SATS) + return minOf(wantedSats, amountAfterFee(limitSats, guessSats)) + } + + /** The amount that fits under [limitSats] together with [feeSats] and its margin. */ + fun amountAfterFee(limitSats: ULong, feeSats: ULong): ULong = + limitSats.safe() - (feeSats.safe() + FEE_MARGIN_SATS.safe()).safe() + + /** The routing fee cap for the payment: the quoted fee and its margin, so it never pays more than shown. */ + fun feeCapMsat(feeSats: ULong): ULong = (feeSats.safe() + FEE_MARGIN_SATS.safe()).safe() * 1000uL.safe() + + /** The smaller amount to offer after one did not route: half of it, or nothing below the minimum. */ + fun smallerAmount(sats: ULong): ULong? = (sats / 2uL).takeIf { it >= MIN_MOVE_SATS } +} + +data class MoveToSparkQuote( + val invoice: String, + val amountSats: ULong, + val feeSats: ULong, +) + +sealed interface MoveToSparkOutcome { + val amountSats: ULong + + data class Moved(override val amountSats: ULong, val feeSats: ULong) : MoveToSparkOutcome + + /** The payment left but had not settled when the move stopped waiting; Spark shows it once it does. */ + data class InFlight(override val amountSats: ULong) : MoveToSparkOutcome +} + +sealed class MoveToSparkError(message: String, cause: Throwable? = null) : AppError(message, cause) { + class NothingToMove : MoveToSparkError("The channels have nothing to move") + class SparkUnavailable : MoveToSparkError("Spark is not ready to receive") + + /** [amountSats] did not route from the channels to Spark in one payment. */ + class TooLarge( + val amountSats: ULong, + cause: Throwable? = null, + val reason: PaymentFailureReason? = null, + ) : MoveToSparkError("Could not route '$amountSats' sats to Spark, reason '$reason'", cause) +} diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index c0789dd495..09e0863e08 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -69,6 +69,7 @@ import to.bitkit.ext.walletId import to.bitkit.models.NodeLifecycleState import to.bitkit.models.Toast import to.bitkit.repositories.ConnectivityState +import to.bitkit.repositories.MoveToSparkMath import to.bitkit.repositories.PaykitPaymentRequestId import to.bitkit.repositories.PaykitSubscriptionId import to.bitkit.ui.Routes.ExternalConnection @@ -204,6 +205,7 @@ import to.bitkit.ui.settings.lightning.ChannelDetailScreen import to.bitkit.ui.settings.lightning.CloseConnectionScreen import to.bitkit.ui.settings.lightning.LightningConnectionsScreen import to.bitkit.ui.settings.lightning.LightningConnectionsViewModel +import to.bitkit.ui.settings.lightning.MoveToSparkScreen import to.bitkit.ui.settings.lightning.SparkDetailScreen import to.bitkit.ui.settings.pin.PinManagementScreen import to.bitkit.ui.settings.quickPay.QuickPayIntroScreen @@ -223,6 +225,7 @@ import to.bitkit.ui.sheets.DisablePinSheet import to.bitkit.ui.sheets.ForceTransferSheet import to.bitkit.ui.sheets.HighBalanceWarningSheet import to.bitkit.ui.sheets.LnurlAuthSheet +import to.bitkit.ui.sheets.MoveToSparkSheet import to.bitkit.ui.sheets.PinSheet import to.bitkit.ui.sheets.QrScanningSheet import to.bitkit.ui.sheets.QuickPayIntroSheet @@ -764,6 +767,14 @@ fun ContentView( ) } + TimedSheetType.MOVE_TO_SPARK -> MoveToSparkSheet( + onLater = { appViewModel.dismissTimedSheet() }, + onMove = { + appViewModel.dismissTimedSheet() + navController.navigateTo(Routes.MoveToSpark) + }, + ) + TimedSheetType.HIGH_BALANCE -> { HighBalanceWarningSheet( understoodClick = { appViewModel.dismissTimedSheet() }, @@ -1366,6 +1377,8 @@ private fun NavGraphBuilder.home( SpendingWalletScreen( channels = lightningState.channels, hasSparkBalance = sparkState.balanceSats > 0uL, + canMoveToSpark = MoveToSparkMath.canMove(lightningState.channels, sparkState), + onMoveToSparkClick = { navController.navigateTo(Routes.MoveToSpark) }, lightningActivities = lightningActivities ?: persistentListOf(), onAllActivityButtonClick = { navController.navigateToAllActivity(activityListViewModel::clearFilters) }, onActivityItemClick = { navController.navToActivityDetail(it) }, @@ -2003,6 +2016,18 @@ private fun NavGraphBuilder.lightningConnections( }, ) } + composableWithDefaultTransitions { + MoveToSparkScreen( + viewModel = hiltViewModel(), + onBack = { navController.popBackStack() }, + onCloseChannel = { channelId -> + // Closing returns to Lightning Connections, so the close screen opens on top of it. + navController.popBackStack() + navController.navigateTo(Routes.LightningConnections) + channelId?.let { navController.navigateTo(Routes.CloseConnection(it)) } + }, + ) + } deepLinkableComposable { val route = it.toRoute() CloseConnectionScreen( @@ -2474,6 +2499,9 @@ sealed interface Routes { @Serializable data class CloseConnection(val channelId: String) : Routes.DeepLinkable + @Serializable + data object MoveToSpark : Routes.InternalOnly + @Serializable data object DevSettings : Routes.DeepLinkable diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 66640d1037..12d42a7539 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -114,6 +114,7 @@ sealed interface Sheet { /**@param priority Priority levels for timed sheets (higher number = higher priority)*/ enum class TimedSheetType(val priority: Int) { + MOVE_TO_SPARK(priority = 7), APP_UPDATE(priority = 6), USDT(priority = 5), BACKUP(priority = 4), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt index 169ae8b03a..ff7bd6ce69 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt @@ -43,6 +43,7 @@ import to.bitkit.ui.LocalBalances import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.EmptyStateView import to.bitkit.ui.components.IncomingTransfer +import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.StatusBarSpacer import to.bitkit.ui.components.TabBar @@ -70,6 +71,8 @@ fun SpendingWalletScreen( onBackClick: () -> Unit, balances: BalanceState = LocalBalances.current, hasSparkBalance: Boolean = false, + canMoveToSpark: Boolean = false, + onMoveToSparkClick: () -> Unit = {}, ) { val showEmptyState by remember(balances.totalLightningSats, lightningActivities.size) { val hasLnFunds = balances.totalLightningSats == 0uL @@ -176,6 +179,24 @@ fun SpendingWalletScreen( if (!showEmptyState) { item { VerticalSpacer(32.dp) } + if (canMoveToSpark) { + item { + PrimaryButton( + onClick = onMoveToSparkClick, + text = stringResource(R.string.lightning__move_to_spark__nav_title), + icon = { + Icon( + painter = painterResource(R.drawable.ic_lightning), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + }, + modifier = Modifier.testTag("MoveToSpark") + ) + } + item { VerticalSpacer(16.dp) } + } + if (canTransfer) { item { SecondaryButton( diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index e1f37f26b3..295b98a04f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -108,6 +108,7 @@ fun LightningConnectionsScreen( navController.navigateTo(Routes.ChannelDetail(channelUi.details.channelId)) }, onClickSpark = { navController.navigateTo(Routes.SparkDetail) }, + onClickMoveToSpark = { navController.navigateTo(Routes.MoveToSpark) }, onOpenTx = { txId -> context.startActivity(Intent(Intent.ACTION_VIEW, getBlockExplorerUrl(txId).toUri())) }, onRefresh = { viewModel.onPullToRefresh() @@ -124,6 +125,7 @@ private fun Content( onClickExportLogs: () -> Unit = {}, onClickChannel: (ChannelUi) -> Unit = {}, onClickSpark: () -> Unit = {}, + onClickMoveToSpark: () -> Unit = {}, onOpenTx: (String) -> Unit = {}, onRefresh: () -> Unit = {}, ) { @@ -209,6 +211,15 @@ private fun Content( SyncingRow() } + if (uiState.canMoveToSpark) { + VerticalSpacer(16.dp) + PrimaryButton( + text = stringResource(R.string.lightning__move_to_spark__nav_title), + onClick = onClickMoveToSpark, + modifier = Modifier.testTag("MoveToSpark") + ) + } + uiState.spark?.let { spark -> VerticalSpacer(16.dp) Caption13Up(stringResource(R.string.lightning__spark), color = Colors.White64) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 2687b39880..8638c3ef8b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -37,6 +37,7 @@ import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo +import to.bitkit.repositories.MoveToSparkMath import to.bitkit.repositories.SparkDeposit import to.bitkit.repositories.SparkRepo import to.bitkit.repositories.SparkState @@ -117,6 +118,7 @@ class LightningConnectionsViewModel @Inject constructor( .reversed().toImmutableList(), localBalance = calculateLocalBalance(channels).safe() + sparkState.balanceSats.safe(), remoteBalance = channels.calculateRemoteBalance(), + canMoveToSpark = MoveToSparkMath.canMove(channels, sparkState), spark = sparkState.toUi(lightningRepo), isSparkReceiving = sparkState.isReady, ) @@ -231,6 +233,8 @@ data class LightningConnectionsUiState( val remoteBalance: ULong = 0u, val spark: SparkUi? = null, val isSparkReceiving: Boolean = false, + /** The channels hold a balance that can move to Spark, which Blocktank's channel closes make the cheaper path. */ + val canMoveToSpark: Boolean = false, ) /** The Spark wallet shown as a connection, present once it holds a balance or a deposit. */ diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt new file mode 100644 index 0000000000..21677808ac --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt @@ -0,0 +1,297 @@ +package to.bitkit.ui.settings.lightning + +import androidx.annotation.DrawableRes +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 androidx.lifecycle.compose.collectAsStateWithLifecycle +import to.bitkit.R +import to.bitkit.repositories.MoveToSparkOutcome +import to.bitkit.repositories.MoveToSparkQuote +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.GradientCircularProgressIndicator +import to.bitkit.ui.components.MoneyCaptionB +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.SwipeToConfirm +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent + +/** Moves the channels' balance to the user's Spark wallet over Lightning, then offers to close the emptied channel. */ +@Composable +fun MoveToSparkScreen( + viewModel: MoveToSparkViewModel, + onBack: () -> Unit, + onCloseChannel: (channelId: String?) -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + Content( + uiState = uiState, + onBack = onBack, + onMove = viewModel::move, + onRetry = viewModel::quote, + onCloseChannel = onCloseChannel, + ) +} + +@Composable +private fun Content( + uiState: MoveToSparkUiState, + onBack: () -> Unit = {}, + onMove: () -> Unit = {}, + onRetry: (ULong?) -> Unit = {}, + onCloseChannel: (String?) -> Unit = {}, +) { + ScreenColumn { + AppTopBar( + titleText = stringResource(R.string.lightning__move_to_spark__nav_title), + onBackClick = onBack, + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()) + ) { + when (uiState) { + MoveToSparkUiState.Quoting -> Quoting() + is MoveToSparkUiState.Confirm -> Confirm(uiState.quote, uiState.isMoving, onMove) + is MoveToSparkUiState.Done -> Done(uiState, onBack, onRetry, onCloseChannel) + is MoveToSparkUiState.TooLarge -> TooLarge(uiState, onBack, onRetry) + is MoveToSparkUiState.Failed -> Failed(uiState.failure, onBack, onRetry) + } + VerticalSpacer(16.dp) + } + } +} + +@Composable +private fun Header(title: String, text: String) { + VerticalSpacer(32.dp) + Display(text = title.withAccent()) + VerticalSpacer(8.dp) + BodyM(text = text, color = Colors.White64) +} + +@Composable +private fun Illustration(@DrawableRes drawable: Int) { + Image( + painter = painterResource(drawable), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 32.dp) + .size(256.dp) + ) +} + +@Composable +private fun Quoting() { + Header( + title = stringResource(R.string.lightning__move_to_spark__confirm_title), + text = stringResource(R.string.lightning__move_to_spark__quoting), + ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 64.dp) + ) { + GradientCircularProgressIndicator(modifier = Modifier.size(64.dp)) + } +} + +@Composable +private fun Confirm(quote: MoveToSparkQuote, isMoving: Boolean, onMove: () -> Unit) { + Header( + title = stringResource(R.string.lightning__move_to_spark__confirm_title), + text = stringResource(R.string.lightning__move_to_spark__confirm_text), + ) + VerticalSpacer(32.dp) + AmountRow(stringResource(R.string.lightning__move_to_spark__amount), quote.amountSats, "MoveToSparkAmount") + AmountRow(stringResource(R.string.lightning__move_to_spark__fee), quote.feeSats, "MoveToSparkFee") + Illustration(R.drawable.lightning) + SwipeToConfirm( + text = stringResource(R.string.lightning__move_to_spark__swipe), + color = Colors.Purple, + loading = isMoving, + enabled = !isMoving, + onConfirm = onMove, + modifier = Modifier.testTag("MoveToSparkSwipe") + ) +} + +@Composable +private fun AmountRow(label: String, sats: ULong, testTag: String) { + SectionRow( + name = label, + valueContent = { MoneyCaptionB(sats = sats.toLong(), symbol = true, modifier = Modifier.testTag(testTag)) }, + ) +} + +@Composable +private fun Done( + uiState: MoveToSparkUiState.Done, + onDone: () -> Unit, + onMoveRest: (ULong?) -> Unit, + onCloseChannel: (String?) -> Unit, +) { + val outcome = uiState.outcome + Header( + title = stringResource(R.string.lightning__move_to_spark__done_title), + text = stringResource( + when { + outcome is MoveToSparkOutcome.InFlight -> R.string.lightning__move_to_spark__in_flight_text + uiState.hasRest -> R.string.lightning__move_to_spark__done_rest_text + else -> R.string.lightning__move_to_spark__done_text + } + ), + ) + VerticalSpacer(32.dp) + AmountRow(stringResource(R.string.lightning__move_to_spark__amount), outcome.amountSats, "MoveToSparkMoved") + if (outcome is MoveToSparkOutcome.Moved) { + AmountRow(stringResource(R.string.lightning__move_to_spark__fee), outcome.feeSats, "MoveToSparkFeePaid") + } + Illustration(R.drawable.check) + if (uiState.hasRest) { + Buttons( + secondaryText = stringResource(R.string.common__ok), + onSecondary = onDone, + primaryText = stringResource(R.string.lightning__move_to_spark__move_rest), + onPrimary = { onMoveRest(null) }, + primaryTestTag = "MoveToSparkMoveRest", + ) + return + } + Buttons( + secondaryText = stringResource(R.string.common__ok), + onSecondary = onDone, + primaryText = stringResource(R.string.lightning__move_to_spark__close), + onPrimary = { onCloseChannel(uiState.closeChannelId) }, + primaryTestTag = "MoveToSparkClose", + ) +} + +@Composable +private fun TooLarge(uiState: MoveToSparkUiState.TooLarge, onBack: () -> Unit, onRetry: (ULong?) -> Unit) { + val amount = sparkExitAmountText(uiState.amountSats) + Header( + title = stringResource(R.string.lightning__move_to_spark__too_large_title), + text = stringResource(R.string.lightning__move_to_spark__too_large_text, amount) + " " + stringResource( + when (uiState.smallerSats) { + null -> R.string.lightning__move_to_spark__too_large_none + else -> R.string.lightning__move_to_spark__too_large_smaller + } + ), + ) + Illustration(R.drawable.exclamation_mark) + val smallerSats = uiState.smallerSats + if (smallerSats == null) { + PrimaryButton(text = stringResource(R.string.common__back), onClick = onBack) + return + } + Buttons( + secondaryText = stringResource(R.string.common__cancel), + onSecondary = onBack, + primaryText = stringResource(R.string.lightning__move_to_spark__try, sparkExitAmountText(smallerSats)), + onPrimary = { onRetry(smallerSats) }, + primaryTestTag = "MoveToSparkTrySmaller", + ) +} + +@Composable +private fun Failed(failure: MoveToSparkFailure, onBack: () -> Unit, onRetry: (ULong?) -> Unit) { + Header( + title = stringResource(R.string.lightning__move_to_spark__failed_title), + text = stringResource( + when (failure) { + MoveToSparkFailure.NOTHING_TO_MOVE -> R.string.lightning__move_to_spark__failed_nothing + MoveToSparkFailure.SPARK_UNAVAILABLE -> R.string.lightning__move_to_spark__failed_spark + MoveToSparkFailure.UNKNOWN -> R.string.lightning__move_to_spark__failed_unknown + } + ), + ) + Illustration(R.drawable.cross) + if (failure == MoveToSparkFailure.NOTHING_TO_MOVE) { + PrimaryButton(text = stringResource(R.string.common__back), onClick = onBack) + return + } + Buttons( + secondaryText = stringResource(R.string.common__cancel), + onSecondary = onBack, + primaryText = stringResource(R.string.common__try_again), + onPrimary = { onRetry(null) }, + primaryTestTag = "MoveToSparkRetry", + ) +} + +@Composable +private fun Buttons( + secondaryText: String, + onSecondary: () -> Unit, + primaryText: String, + onPrimary: () -> Unit, + primaryTestTag: String, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + SecondaryButton(text = secondaryText, onClick = onSecondary, modifier = Modifier.weight(1f)) + PrimaryButton( + text = primaryText, + onClick = onPrimary, + modifier = Modifier + .weight(1f) + .testTag(primaryTestTag) + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewConfirm() { + AppThemeSurface { + Content(uiState = MoveToSparkUiState.Confirm(MoveToSparkQuote("lnbc", 98_500uL, 12uL))) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewDone() { + AppThemeSurface { + Content(uiState = MoveToSparkUiState.Done(MoveToSparkOutcome.Moved(98_500uL, 9uL), "channel")) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun PreviewTooLarge() { + AppThemeSurface { + Content(uiState = MoveToSparkUiState.TooLarge(98_500uL, 49_250uL)) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkViewModel.kt new file mode 100644 index 0000000000..d0e7f935ee --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkViewModel.kt @@ -0,0 +1,95 @@ +package to.bitkit.ui.settings.lightning + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import to.bitkit.ext.filterOpen +import to.bitkit.repositories.LightningRepo +import to.bitkit.repositories.MoveToSparkError +import to.bitkit.repositories.MoveToSparkMath +import to.bitkit.repositories.MoveToSparkOutcome +import to.bitkit.repositories.MoveToSparkQuote +import to.bitkit.repositories.MoveToSparkRepo +import to.bitkit.repositories.WalletRepo +import javax.inject.Inject + +@HiltViewModel +class MoveToSparkViewModel @Inject constructor( + private val moveToSparkRepo: MoveToSparkRepo, + private val lightningRepo: LightningRepo, + private val walletRepo: WalletRepo, +) : ViewModel() { + + private val _uiState = MutableStateFlow(MoveToSparkUiState.Quoting) + val uiState = _uiState.asStateFlow() + + init { + quote() + } + + /** Quotes moving [targetSats], or everything the channels can send. */ + fun quote(targetSats: ULong? = null) { + _uiState.update { MoveToSparkUiState.Quoting } + viewModelScope.launch { + moveToSparkRepo.quote(targetSats).fold( + onSuccess = { quote -> _uiState.update { MoveToSparkUiState.Confirm(quote) } }, + onFailure = { error -> _uiState.update { error.toUiState() } }, + ) + } + } + + fun move() { + val quote = (_uiState.value as? MoveToSparkUiState.Confirm)?.quote ?: return + _uiState.update { MoveToSparkUiState.Confirm(quote, isMoving = true) } + viewModelScope.launch { + moveToSparkRepo.move(quote).fold( + onSuccess = { outcome -> + walletRepo.syncNodeAndWallet() + val hasRest = moveToSparkRepo.movableSats() >= MoveToSparkMath.MIN_MOVE_SATS + _uiState.update { MoveToSparkUiState.Done(outcome, emptiedChannelId(), hasRest) } + }, + onFailure = { error -> _uiState.update { error.toUiState() } }, + ) + } + } + + /** The channel to close after the move, when exactly one is open; with several, the user picks in Connections. */ + private fun emptiedChannelId(): String? = lightningRepo.lightningState.value.channels.filterOpen() + .singleOrNull()?.channelId + + private fun Throwable.toUiState(): MoveToSparkUiState = when (this) { + is MoveToSparkError.TooLarge -> MoveToSparkUiState.TooLarge( + amountSats = amountSats, + smallerSats = MoveToSparkMath.smallerAmount(amountSats), + ) + is MoveToSparkError.NothingToMove -> MoveToSparkUiState.Failed(MoveToSparkFailure.NOTHING_TO_MOVE) + is MoveToSparkError.SparkUnavailable -> MoveToSparkUiState.Failed(MoveToSparkFailure.SPARK_UNAVAILABLE) + else -> MoveToSparkUiState.Failed(MoveToSparkFailure.UNKNOWN) + } +} + +@Immutable +sealed interface MoveToSparkUiState { + data object Quoting : MoveToSparkUiState + + data class Confirm(val quote: MoveToSparkQuote, val isMoving: Boolean = false) : MoveToSparkUiState + + /** [hasRest]: the channels can still send enough to move again, as after a smaller amount. */ + data class Done( + val outcome: MoveToSparkOutcome, + val closeChannelId: String?, + val hasRest: Boolean = false, + ) : MoveToSparkUiState + + /** [amountSats] did not route in one payment; [smallerSats] is the next amount to try, if any is left. */ + data class TooLarge(val amountSats: ULong, val smallerSats: ULong?) : MoveToSparkUiState + + data class Failed(val failure: MoveToSparkFailure) : MoveToSparkUiState +} + +enum class MoveToSparkFailure { NOTHING_TO_MOVE, SPARK_UNAVAILABLE, UNKNOWN } diff --git a/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt new file mode 100644 index 0000000000..3447a81cfb --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt @@ -0,0 +1,86 @@ +package to.bitkit.ui.sheets + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +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.components.BodyM +import to.bitkit.ui.components.BottomSheetPreview +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent + +/** Tells channel holders that Blocktank closes its channels and that moving the balance to Spark costs least. */ +@Composable +fun MoveToSparkSheet(onLater: () -> Unit, onMove: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier.sheetHeight().navigationBarsPadding().testTag("MoveToSparkIntro") + ) { + SheetTopBar(stringResource(R.string.lightning__move_to_spark__nav_title)) + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(horizontal = 16.dp) + ) { + Image( + painter = painterResource(R.drawable.lightning), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(256.dp).align(Alignment.CenterHorizontally) + ) + VerticalSpacer(32.dp) + Display( + stringResource(R.string.lightning__move_to_spark__intro_title).withAccent(accentColor = Colors.Purple) + ) + VerticalSpacer(8.dp) + BodyM(stringResource(R.string.lightning__move_to_spark__intro_text), color = Colors.White64) + } + VerticalSpacer(32.dp) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = if (LocalDensity.current.fontScale > 1.3f) 1 else 2, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + SecondaryButton( + stringResource(R.string.common__later), + onClick = onLater, + testTag = "MoveToSparkIntroLater", + modifier = Modifier.weight(1f) + ) + PrimaryButton( + stringResource(R.string.lightning__move_to_spark__nav_title), + onClick = onMove, + modifier = Modifier.weight(1f).testTag("MoveToSparkIntroMove") + ) + } + VerticalSpacer(16.dp) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun Preview() { + AppThemeSurface { BottomSheetPreview { MoveToSparkSheet({}, {}) } } +} diff --git a/app/src/main/java/to/bitkit/utils/timedsheets/sheets/MoveToSparkTimedSheet.kt b/app/src/main/java/to/bitkit/utils/timedsheets/sheets/MoveToSparkTimedSheet.kt new file mode 100644 index 0000000000..d886770099 --- /dev/null +++ b/app/src/main/java/to/bitkit/utils/timedsheets/sheets/MoveToSparkTimedSheet.kt @@ -0,0 +1,28 @@ +package to.bitkit.utils.timedsheets.sheets + +import kotlinx.coroutines.flow.first +import to.bitkit.data.SettingsStore +import to.bitkit.repositories.LightningRepo +import to.bitkit.repositories.MoveToSparkMath +import to.bitkit.repositories.SparkRepo +import to.bitkit.ui.components.TimedSheetType +import to.bitkit.utils.timedsheets.TimedSheetItem +import javax.inject.Inject + +/** Shown once after the update to users whose channels still hold a balance worth moving to Spark. */ +class MoveToSparkTimedSheet @Inject constructor( + private val settingsStore: SettingsStore, + private val lightningRepo: LightningRepo, + private val sparkRepo: SparkRepo, +) : TimedSheetItem { + override val type = TimedSheetType.MOVE_TO_SPARK + override val priority = type.priority + + override suspend fun shouldShow(): Boolean { + if (settingsStore.data.first().hasSeenMoveToSparkIntro) return false + return MoveToSparkMath.canMove(lightningRepo.lightningState.value.channels, sparkRepo.state.value) + } + + override suspend fun onShown() = settingsStore.update { it.copy(hasSeenMoveToSparkIntro = true) } + override suspend fun onDismissed() = Unit +} diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 6fabcf9943..17ef144808 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -176,6 +176,7 @@ import to.bitkit.repositories.IncomingPaykitPaymentRequestFailureReason import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LnurlPayInvoiceMismatchError import to.bitkit.repositories.MethodId +import to.bitkit.repositories.MoveToSparkRepo import to.bitkit.repositories.NodeEventUpdate import to.bitkit.repositories.OnchainPreparedReceipt import to.bitkit.repositories.OnchainSendAttempt @@ -261,6 +262,7 @@ import to.bitkit.utils.timedsheets.TimedSheetManager import to.bitkit.utils.timedsheets.sheets.AppUpdateTimedSheet import to.bitkit.utils.timedsheets.sheets.BackupTimedSheet import to.bitkit.utils.timedsheets.sheets.HighBalanceTimedSheet +import to.bitkit.utils.timedsheets.sheets.MoveToSparkTimedSheet import to.bitkit.utils.timedsheets.sheets.NotificationsTimedSheet import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet import to.bitkit.utils.timedsheets.sheets.UsdtTimedSheet @@ -328,6 +330,8 @@ class AppViewModel @Inject constructor( private val usdtSheet: UsdtTimedSheet, private val usdtPreferences: UsdtPreferencesRepo, private val highBalanceSheet: HighBalanceTimedSheet, + private val moveToSparkSheet: MoveToSparkTimedSheet, + private val moveToSparkRepo: MoveToSparkRepo, private val formatMoneyValue: FormatMoneyValue, private val widgetsRepo: WidgetsRepo, private val timeSource: TimeSource, @@ -565,6 +569,7 @@ class AppViewModel @Inject constructor( private val paymentRequestPresentationRetryJobs = mutableMapOf() private val paymentRequestPresentationCompletionJobs = mutableMapOf() private val timedSheetManager = timedSheetManagerProvider(viewModelScope).apply { + registerSheet(moveToSparkSheet) registerSheet(appUpdateSheet) registerSheet(usdtSheet) registerSheet(backupSheet) @@ -733,8 +738,13 @@ class AppViewModel @Inject constructor( sheet == null && authenticated && !forgotPin && receipt == NewTransactionSheetDetails.EMPTY }.collect { if (it) retryUsdtIntro() + if (it) retryMoveToSparkIntro() } } + viewModelScope.launch { + // Channels and Spark load after Home first checks its sheets, so the intro is checked again once they can. + moveToSparkRepo.canMove.collect { if (it) retryMoveToSparkIntro() } + } viewModelScope.launch { pubkyRepo.sessionRestorationFailed.collect { failed -> if (failed) { @@ -2856,6 +2866,7 @@ class AppViewModel @Inject constructor( refreshPaymentActivity(paymentHash) // A failed automatic Allowance payment falls back to the request's manual flow without a send error. if (isAutomaticAllowancePayment(paymentHash)) return + if (moveToSparkRepo.isMovePayment(paymentHash)) return if (pendingPaymentRepo.isPending(paymentHash)) { clearPendingContactPaymentContext(paymentHash) pendingPaymentRepo.resolve(PendingPaymentResolution.Failure(paymentHash, event.reason)) @@ -2903,6 +2914,7 @@ class AppViewModel @Inject constructor( event.paymentHash.let { paymentHash -> closeSettledReceiveSheet(receiveSheetToClose) activityRepo.notifyPaymentActivityChanged() + if (moveToSparkRepo.isMovePayment(paymentHash)) return privatePaykitRepo.contactPublicKeyForPrivateInvoicePaymentHash(paymentHash)?.let { publicKey -> activityRepo.setContact( contactPublicKey = publicKey, @@ -2953,6 +2965,8 @@ class AppViewModel @Inject constructor( refreshPaymentActivity(paymentHash) // An automatic Allowance payment settles headlessly; the allowance repo posts its notice and contact tag. if (isAutomaticAllowancePayment(paymentHash)) return + // A move to Spark reports its outcome on its own screen. + if (moveToSparkRepo.isMovePayment(paymentHash)) return if (!pendingPaymentRepo.isPending(paymentHash)) { notifyPaymentSentOnLightning(event) return @@ -8803,6 +8817,10 @@ class AppViewModel @Inject constructor( if (!isUsdtIntroPresented && usdtSheet.shouldShow()) timedSheetManager.reevaluate() } + private suspend fun retryMoveToSparkIntro() { + if (moveToSparkSheet.shouldShow()) timedSheetManager.reevaluate() + } + private fun canShowTimedSheet() = _isAuthenticated.value && !_showForgotPinSheet.value && _currentSheet.value == null && sheetTransitionJob?.isActive != true && diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 39227d7601..fb00f29d3e 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -167,6 +167,30 @@ قابل للاستخدام أرصدة Lightning ارتفاع أرشفة مراقب القناة + المبلغ + إغلاق الاتصال + ينتقل رصيدك إلى Spark عبر Lightning، وهي أقل طريقة رسومًا للإبقاء عليه في الإنفاق. رسوم التوجيه أدناه هي أقصى ما تدفعه. + الانتقال إلى <accent>Spark</accent> + جزء من رصيدك الآن على Spark. انقل الباقي بالطريقة نفسها، ثم أغلق الاتصال الذي أصبح فارغًا. + رصيدك الآن على Spark. أغلق الاتصال الذي أصبح فارغًا لإعادة احتياطيه إلى المدخرات. + تم النقل إلى <accent>Spark</accent> + لم يتبق في اتصالات Lightning لديك رصيد للنقل. + Spark غير جاهز للاستلام بعد. حاول مرة أخرى بعد قليل. + تعذّر <accent>النقل</accent> + حدث خطأ أثناء تحضير النقل. حاول مرة أخرى. + رسوم التوجيه + الدفعة في طريقها وستظهر في Spark بمجرد إتمامها. ثم أغلق الاتصال الفارغ لإعادة احتياطيه إلى المدخرات. + يقوم Blocktank بإغلاق اتصالات Lightning الخاصة به. انقل رصيدك إلى Spark الآن: فهو الخيار الأقل رسومًا، ويبقى ضمن الإنفاق. + اتصالك <accent>قيد الإغلاق</accent> + نقل الباقي + النقل إلى Spark + جارٍ البحث عن مسار ورسومه… + اسحب للنقل + أغلق الاتصال بدلاً من ذلك لإعادة الرصيد إلى المدخرات. + يمكنك نقله على دفعات أصغر. + %1$s أكبر من أن يُوجَّه إلى Spark في دفعة واحدة. + المبلغ <accent>كبير جدًا</accent> + نقل %1$s حد HTLC الصادر التالي الحد الأدنى لـ HTLC الصادر التالي قبل أن تتمكن من تحويل الأموال من رصيد مدخراتك، تحتاج إلى إرسال Bitcoin إلى محفظة Bitkit الخاصة بك. diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index dac1da7456..0166211735 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -174,6 +174,30 @@ Es utilizable Saldos Lightning Altura de archivo del monitor de canal + Monto + Terminar conexión + Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga. + Mover a <accent>Spark</accent> + Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía. + Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro. + Movido a <accent>Spark</accent> + Sus conexiones Lightning ya no tienen saldo para mover. + Spark aún no está listo para recibir. Inténtelo de nuevo en un momento. + No se pudo <accent>mover</accent> + Algo salió mal al preparar el movimiento. Inténtelo de nuevo. + Comisión de enrutamiento + El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro. + Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto. + Su conexión <accent>se está cerrando</accent> + Mover el resto + Mover a Spark + Buscando una ruta y su comisión… + Deslizar para mover + Termine la conexión para devolver el saldo a Ahorro. + Puede moverlo en partes más pequeñas. + %1$s es demasiado grande para enrutarse a Spark en un solo pago. + Monto <accent>demasiado grande</accent> + Mover %1$s Límite del próximo HTLC saliente Mínimo del próximo HTLC saliente Para transferir fondos a su saldo de ahorros, primero tiene que enviar bitcoin a su billetera Bitkit. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 334505505c..429461e485 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -171,6 +171,30 @@ És utilitzable Saldos Lightning Alçada d\'arxiu del monitor de canal + Import + Tanca la connexió + El teu saldo passa a Spark per Lightning, la manera amb menys tarifes de mantenir-lo a Despesa. La tarifa d\'enrutament de sota és el màxim que pagues. + Moure a <accent>Spark</accent> + Una part del teu saldo ja és a Spark. Mou la resta de la mateixa manera i després tanca la connexió buida. + El teu saldo ja és a Spark. Tanca la connexió buida per tornar-ne la reserva als Estalvis. + Mogut a <accent>Spark</accent> + Les teves connexions Lightning ja no tenen saldo per moure. + Spark encara no està a punt per rebre. Torna-ho a provar d\'aquí a un moment. + No s\'ha pogut <accent>moure</accent> + Alguna cosa ha anat malament en preparar el moviment. Torna-ho a provar. + Tarifa d\'enrutament + El pagament està en camí i apareixerà a Spark quan s\'hagi liquidat. Després tanca la connexió buida per tornar-ne la reserva als Estalvis. + Blocktank està tancant les seves connexions Lightning. Mou ara el teu saldo a Spark: és l\'opció amb menys tarifes i es manté a Despesa. + La teva connexió <accent>es tanca</accent> + Moure la resta + Moure a Spark + Cercant una ruta i la seva tarifa… + Desplaça per moure + Tanca la connexió per tornar el saldo als Estalvis. + El pots moure en parts més petites. + %1$s és massa gran per enrutar-se a Spark en un sol pagament. + Import <accent>massa gran</accent> + Moure %1$s Límit del pròxim HTLC de sortida Mínim del pròxim HTLC de sortida Abans de poder transferir fons del teu saldo d\'estalvis, has d\'enviar bitcoin a la teva cartera Bitkit. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 91f4805c90..72dfaba94d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -174,6 +174,30 @@ Je použitelný? Lightning zůstatky Archivní výška monitoru kanálu + Částka + Uzavřít spojení + Váš zůstatek se přesune do Spark přes Lightning, což je způsob s nejnižšími poplatky, jak jej udržet v Útratách. Poplatek za směrování níže je maximum, které zaplatíte. + Přesun do <accent>Spark</accent> + Část vašeho zůstatku je nyní ve Spark. Zbytek přesuňte stejným způsobem a poté uzavřete vyprázdněné spojení. + Váš zůstatek je nyní ve Spark. Uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor. + Přesunuto do <accent>Spark</accent> + Vaše spojení Lightning už nemají žádný zůstatek k přesunu. + Spark zatím není připraven přijímat. Zkuste to za chvíli znovu. + <accent>Přesun</accent> se nezdařil + Při přípravě přesunu se něco pokazilo. Zkuste to znovu. + Poplatek za směrování + Platba je na cestě a ve Spark se zobrazí po vypořádání. Poté uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor. + Blocktank uzavírá svá spojení Lightning. Přesuňte svůj zůstatek do Spark hned: je to možnost s nejnižšími poplatky a zůstane v Útratách. + Vaše spojení <accent>se uzavírá</accent> + Přesunout zbytek + Přesun do Spark + Hledání trasy a jejího poplatku… + Přejetím přesuňte + Místo toho uzavřete spojení, aby se zůstatek vrátil do Úspor. + Můžete jej přesunout po menších částech. + %1$s je příliš velká částka na směrování do Spark v jedné platbě. + Částka <accent>je příliš velká</accent> + Přesunout %1$s Limit dalšího odchozího HTLC Min dalšího odchozího HTLC Než budete moci převést prostředky ze svého zůstatku na spořicím účtu, musíte poslat bitcoiny do peněženky Bitkit. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5cea87a889..28ba9dc55c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -226,6 +226,30 @@ Eingehender HTLC Min Lightning-Guthaben Channel Monitor Archivhöhe + Betrag + Verbindung schließen + Ihr Guthaben wechselt über Lightning zu Spark, dem günstigsten Weg, es im Ausgabenkonto zu behalten. Die Routing-Gebühr unten ist das Maximum, das Sie zahlen. + Zu <accent>Spark</accent> verschieben + Ein Teil Ihres Guthabens ist jetzt bei Spark. Verschieben Sie den Rest auf dieselbe Weise und schließen Sie dann die geleerte Verbindung. + Ihr Guthaben ist jetzt bei Spark. Schließen Sie die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben. + Zu <accent>Spark</accent> verschoben + Ihre Lightning-Verbindungen haben kein Guthaben mehr zum Verschieben. + Spark ist noch nicht bereit zum Empfangen. Versuchen Sie es gleich noch einmal. + <accent>Verschieben</accent> fehlgeschlagen + Beim Vorbereiten der Verschiebung ist etwas schiefgelaufen. Versuchen Sie es erneut. + Routing-Gebühr + Die Zahlung ist unterwegs und erscheint in Spark, sobald sie abgeschlossen ist. Schließen Sie dann die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben. + Blocktank schließt seine Lightning-Verbindungen. Verschieben Sie Ihr Guthaben jetzt zu Spark: Das ist die günstigste Option, und es bleibt im Ausgabenkonto. + Ihre Verbindung <accent>wird geschlossen</accent> + Rest verschieben + Zu Spark verschieben + Route und Gebühr werden gesucht… + Swipe zum Verschieben + Schließen Sie stattdessen die Verbindung, um das Guthaben an das Sparkonto zurückzugeben. + Sie können es in kleineren Teilen verschieben. + %1$s ist zu groß, um in einer Zahlung zu Spark geroutet zu werden. + Betrag <accent>zu groß</accent> + %1$s verschieben Nächstes ausgehendes HTLC-Limit Nächster ausgehender HTLC Min Bevor du Geld von deinem Sparkonto übertragen kannst, musst du Bitcoin zu deiner Bitkit Wallet senden. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 13bc6c27e3..c0d400692d 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -167,6 +167,30 @@ Είναι χρησιμοποιήσιμο Υπόλοιπα Lightning Ύψος αρχειοθέτησης παρακολούθησης καναλιού + Ποσό + Κλείσιμο σύνδεσης + Το υπόλοιπό σας μεταφέρεται στο Spark μέσω Lightning, τον τρόπο με τα χαμηλότερα τέλη για να παραμείνει στις Δαπάνες. Το τέλος δρομολόγησης παρακάτω είναι το μέγιστο που πληρώνετε. + Μεταφορά στο <accent>Spark</accent> + Ένα μέρος του υπολοίπου σας βρίσκεται πλέον στο Spark. Μεταφέρετε τα υπόλοιπα με τον ίδιο τρόπο και έπειτα κλείστε την άδεια σύνδεση. + Το υπόλοιπό σας βρίσκεται πλέον στο Spark. Κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις. + Μεταφέρθηκε στο <accent>Spark</accent> + Οι συνδέσεις Lightning σας δεν έχουν πλέον υπόλοιπο για μεταφορά. + Το Spark δεν είναι ακόμη έτοιμο να λάβει. Δοκιμάστε ξανά σε λίγο. + Αποτυχία <accent>μεταφοράς</accent> + Κάτι πήγε στραβά κατά την προετοιμασία της μεταφοράς. Δοκιμάστε ξανά. + Τέλος δρομολόγησης + Η πληρωμή βρίσκεται σε εξέλιξη και θα εμφανιστεί στο Spark μόλις διακανονιστεί. Έπειτα κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις. + Το Blocktank κλείνει τις συνδέσεις Lightning του. Μεταφέρετε το υπόλοιπό σας στο Spark τώρα: είναι η επιλογή με τα χαμηλότερα τέλη και παραμένει στις Δαπάνες. + Η σύνδεσή σας <accent>κλείνει</accent> + Μεταφορά των υπολοίπων + Μεταφορά στο Spark + Εύρεση διαδρομής και του τέλους της… + Σύρε για μεταφορά + Κλείστε αντί αυτού τη σύνδεση για να επιστρέψει το υπόλοιπο στις Αποταμιεύσεις. + Μπορείτε να το μεταφέρετε σε μικρότερα μέρη. + Το %1$s είναι πολύ μεγάλο για δρομολόγηση στο Spark με μία πληρωμή. + Το ποσό <accent>είναι πολύ μεγάλο</accent> + Μεταφορά %1$s Όριο επόμενου εξερχόμενου HTLC Ελάχιστο επόμενο εξερχόμενο HTLC Πριν μπορέσεις να μεταφέρεις κεφάλαια από το υπόλοιπο αποταμιεύσεων, πρέπει να στείλεις bitcoin στο πορτοφόλι Bitkit. diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index e50e8de4c2..7c823d9ef3 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -173,6 +173,30 @@ ¿Es utilizable? Saldos Lightning Altura de archivo del monitor de canales + Cantidad + Terminar conexión + Tu saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que pagas. + Mover a <accent>Spark</accent> + Una parte de tu saldo ya está en Spark. Mueve el resto de la misma forma y luego termina la conexión vacía. + Tu saldo ya está en Spark. Termina la conexión vacía para devolver su reserva a Ahorro. + Movido a <accent>Spark</accent> + Tus conexiones Lightning ya no tienen saldo para mover. + Spark aún no está listo para recibir. Inténtalo de nuevo en un momento. + No se pudo <accent>mover</accent> + Algo salió mal al preparar el movimiento. Inténtalo de nuevo. + Comisión de enrutamiento + El pago está en camino y aparecerá en Spark cuando se complete. Luego termina la conexión vacía para devolver su reserva a Ahorro. + Blocktank está cerrando sus conexiones Lightning. Mueve ahora tu saldo a Spark: es la opción de menor comisión y se mantiene en Gasto. + Tu conexión <accent>se está cerrando</accent> + Mover el resto + Mover a Spark + Buscando una ruta y su comisión… + Deslizar para mover + Termina la conexión para devolver el saldo a Ahorro. + Puedes moverlo en partes más pequeñas. + %1$s es demasiado grande para enrutarse a Spark en un solo pago. + Cantidad <accent>demasiado grande</accent> + Mover %1$s Límite del siguiente HTLC saliente Mínimo del siguiente HTLC saliente Antes de que pueda transferir fondos a su saldo de ahorros, tiene que enviar bitcoin a su monedero Bitkit. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index fa41c6b382..dbb1acf3cf 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -140,6 +140,30 @@ HTLC de entrada mínimo Saldos Lightning Altura de archivo del monitor de canal + Cantidad + Terminar conexión + Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga. + Mover a <accent>Spark</accent> + Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía. + Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro. + Movido a <accent>Spark</accent> + Sus conexiones Lightning ya no tienen saldo para mover. + Spark aún no está listo para recibir. Inténtelo de nuevo en un momento. + No se pudo <accent>mover</accent> + Algo salió mal al preparar el movimiento. Inténtelo de nuevo. + Comisión de enrutamiento + El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro. + Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto. + Su conexión <accent>se está cerrando</accent> + Mover el resto + Mover a Spark + Buscando una ruta y su comisión… + Deslizar para mover + Termine la conexión para devolver el saldo a Ahorro. + Puede moverlo en partes más pequeñas. + %1$s es demasiado grande para enrutarse a Spark en un solo pago. + Cantidad <accent>demasiado grande</accent> + Mover %1$s Límite del siguiente HTLC saliente Mínimo del siguiente HTLC saliente Antes de que pueda transferir fondos a su saldo de ahorros, tiene que enviar bitcoin a su monedero Bitkit. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 205a538624..a0359f06ca 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -254,6 +254,30 @@ Point du canal Motif de la fermeture Hauteur d\'archivage du moniteur de canal + Montant + Fermer la connexion + Votre solde passe à Spark via Lightning, le moyen le moins coûteux de le garder dans Dépenses. Les frais de routage ci-dessous sont le maximum que vous payez. + Déplacer vers <accent>Spark</accent> + Une partie de votre solde est maintenant sur Spark. Déplacez le reste de la même façon, puis fermez la connexion vidée. + Votre solde est maintenant sur Spark. Fermez la connexion vidée pour rendre sa réserve à Épargnes. + Déplacé vers <accent>Spark</accent> + Vos connexions Lightning n\'ont plus de solde à déplacer. + Spark n\'est pas encore prêt à recevoir. Réessayez dans un instant. + Échec du <accent>déplacement</accent> + Un problème est survenu pendant la préparation du déplacement. Réessayez. + Frais de routage + Le paiement est en route et apparaîtra sur Spark une fois réglé. Fermez ensuite la connexion vidée pour rendre sa réserve à Épargnes. + Blocktank ferme ses connexions Lightning. Déplacez votre solde vers Spark maintenant : c\'est l\'option la moins coûteuse, et il reste dans Dépenses. + Votre connexion <accent>se ferme</accent> + Déplacer le reste + Déplacer vers Spark + Recherche d\'un itinéraire et de ses frais… + Glissez pour déplacer + Fermez plutôt la connexion pour rendre le solde à Épargnes. + Vous pouvez le déplacer en plus petites parties. + %1$s est trop élevé pour être acheminé vers Spark en un seul paiement. + Montant <accent>trop élevé</accent> + Déplacer %1$s Limite HTLC sortant suivant HTLC sortant min suivant Avant de pouvoir transférer des fonds depuis votre solde d\'épargne, vous devez envoyer des bitcoins à votre portefeuille Bitkit. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f46eaf89c8..25f4680e1d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -172,6 +172,30 @@ È utilizzabile? Saldi Lightning Altezza Archivio Monitor Canali + Importo + Chiudi Connessione + Il tuo saldo passa a Spark tramite Lightning, il modo a commissioni più basse per mantenerlo in Spesa. La commissione di instradamento qui sotto è il massimo che paghi. + Sposta su <accent>Spark</accent> + Una parte del tuo saldo è ora su Spark. Sposta il resto allo stesso modo, poi chiudi la connessione svuotata. + Il tuo saldo è ora su Spark. Chiudi la connessione svuotata per restituire la sua riserva ai Risparmi. + Spostato su <accent>Spark</accent> + Le tue connessioni Lightning non hanno più saldo da spostare. + Spark non è ancora pronto a ricevere. Riprova tra un momento. + Impossibile <accent>spostare</accent> + Qualcosa è andato storto durante la preparazione dello spostamento. Riprova. + Commissione di instradamento + Il pagamento è in arrivo e compare su Spark una volta completato. Poi chiudi la connessione svuotata per restituire la sua riserva ai Risparmi. + Blocktank sta chiudendo le sue connessioni Lightning. Sposta ora il tuo saldo su Spark: è l\'opzione con le commissioni più basse e resta in Spesa. + La tua connessione <accent>si sta chiudendo</accent> + Sposta Il Resto + Sposta Su Spark + Ricerca di un percorso e della sua commissione… + Swipe Per Spostare + Chiudi invece la connessione per restituire il saldo ai Risparmi. + Puoi spostarlo in parti più piccole. + %1$s è troppo grande per essere instradato su Spark in un unico pagamento. + Importo <accent>troppo grande</accent> + Sposta %1$s Limite HTLC in Uscita Successivo HTLC Min in Uscita Successivo Prima di poter trasferire fondi dal tuo conto di risparmio, devi inviare bitcoin al tuo portafoglio Bitkit. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index fc7c27129e..57067ca1b9 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -171,6 +171,30 @@ Is bruikbaar Lightning-saldi Kanaalmonitor archiveringshoogte + Bedrag + Verbinding sluiten + Uw saldo gaat via Lightning naar Spark, de goedkoopste manier om het op uw bestedingssaldo te houden. De routeringsvergoeding hieronder is het maximum dat u betaalt. + Verplaatsen naar <accent>Spark</accent> + Een deel van uw saldo staat nu op Spark. Verplaats de rest op dezelfde manier en sluit daarna de lege verbinding. + Uw saldo staat nu op Spark. Sluit de lege verbinding om de reserve terug te geven aan uw spaargeld. + Verplaatst naar <accent>Spark</accent> + Uw Lightning-verbindingen hebben geen saldo meer om te verplaatsen. + Spark is nog niet klaar om te ontvangen. Probeer het zo meteen opnieuw. + <accent>Verplaatsen</accent> mislukt + Er is iets misgegaan bij het voorbereiden van de verplaatsing. Probeer het opnieuw. + Routeringsvergoeding + De betaling is onderweg en verschijnt in Spark zodra ze is afgerond. Sluit daarna de lege verbinding om de reserve terug te geven aan uw spaargeld. + Blocktank sluit zijn Lightning-verbindingen. Verplaats uw saldo nu naar Spark: dit is de goedkoopste optie en het blijft op uw bestedingssaldo. + Uw verbinding <accent>wordt gesloten</accent> + De rest verplaatsen + Verplaatsen naar Spark + Route en vergoeding zoeken… + Veeg om te verplaatsen + Sluit in plaats daarvan de verbinding om het saldo terug te geven aan uw spaargeld. + U kunt het in kleinere delen verplaatsen. + %1$s is te groot om in één betaling naar Spark te routeren. + Bedrag <accent>te groot</accent> + %1$s verplaatsen Volgende uitgaande HTLC-limiet Volgende uitgaande HTLC min Voordat je geld van je spaargeld kunt overboeken, moet je bitcoin naar je Bitkit-wallet sturen. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 61316f843c..ef2f387f62 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -228,6 +228,30 @@ Min. HTLC przychodząca Saldo Lightning Wysokość archiwalnego monitora kanału + Kwota + Zamknij połączenie + Twoje saldo trafia do Spark przez Lightning, najtańszym sposobem na pozostawienie go w Wydatkach. Opłata za routing poniżej to maksimum, jakie zapłacisz. + Przenieś do <accent>Spark</accent> + Część Twojego salda jest teraz w Spark. Przenieś resztę w ten sam sposób, a następnie zamknij opróżnione połączenie. + Twoje saldo jest teraz w Spark. Zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności. + Przeniesiono do <accent>Spark</accent> + Twoje połączenia Lightning nie mają już salda do przeniesienia. + Spark nie jest jeszcze gotowy do odbioru. Spróbuj ponownie za chwilę. + Nie udało się <accent>przenieść</accent> + Coś poszło nie tak podczas przygotowania przeniesienia. Spróbuj ponownie. + Opłata za routing + Płatność jest w drodze i pojawi się w Spark po rozliczeniu. Następnie zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności. + Blocktank zamyka swoje połączenia Lightning. Przenieś teraz saldo do Spark: to opcja z najniższymi opłatami, a środki zostają w Wydatkach. + Twoje połączenie <accent>się zamyka</accent> + Przenieś resztę + Przenieś do Spark + Szukanie trasy i jej opłaty… + Przesuń, aby przenieść + Zamiast tego zamknij połączenie, aby zwrócić saldo do Oszczędności. + Możesz przenieść je w mniejszych częściach. + Kwota %1$s jest zbyt duża, aby przekazać ją do Spark jedną płatnością. + Kwota <accent>zbyt duża</accent> + Przenieś %1$s Limit następnej wychodzącej HTLC Min. następnej wychodzącej HTLC Zanim będzie można przelać środki z salda oszczędności, należy wysłać bitcoiny do portfela Bitkit. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3e7b7deb6b..36517a7cf7 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -175,6 +175,30 @@ É utilizável Saldos Lightning Altura de Arquivamento do Monitor de Canal + Valor + Fechar Conexão + Seu saldo vai para o Spark pela Lightning, a forma de menor taxa de mantê-lo em Gastos. A taxa de roteamento abaixo é o máximo que você paga. + Mover para o <accent>Spark</accent> + Parte do seu saldo agora está no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada. + Seu saldo agora está no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança. + Movido para o <accent>Spark</accent> + Suas conexões Lightning não têm mais saldo para mover. + O Spark ainda não está pronto para receber. Tente novamente em instantes. + Não foi possível <accent>mover</accent> + Algo deu errado ao preparar a movimentação. Tente novamente. + Taxa de roteamento + O pagamento está a caminho e aparece no Spark assim que for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança. + O Blocktank está fechando suas conexões Lightning. Mova seu saldo para o Spark agora: é a opção de menor taxa e ele continua em Gastos. + Sua conexão <accent>está fechando</accent> + Mover o Resto + Mover Para o Spark + Buscando uma rota e sua taxa… + Deslizar Para Mover + Feche a conexão para devolver o saldo à Poupança. + Você pode movê-lo em partes menores. + %1$s é grande demais para ser roteado ao Spark em um único pagamento. + Valor <accent>grande demais</accent> + Mover %1$s Limite do Próximo HTLC de Saída HTLC de Saída Mín Próximo Antes de transferir fundos de seu saldo de poupança, receba bitcoin na sua carteira Bitkit. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 81321c2947..18c65c130e 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -272,6 +272,30 @@ Continuar Usando o Bitkit Tempo de Atualização do Cache de Taxa Capacidade de Entrada + Montante + Fechar Conexão + O seu saldo passa para o Spark através do Lightning, a forma com as taxas mais baixas de o manter em Gastos. A taxa de encaminhamento abaixo é o máximo que paga. + Mover para o <accent>Spark</accent> + Parte do seu saldo está agora no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada. + O seu saldo está agora no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança. + Movido para o <accent>Spark</accent> + As suas conexões Lightning já não têm saldo para mover. + O Spark ainda não está pronto para receber. Tente novamente dentro de instantes. + Não foi possível <accent>mover</accent> + Algo correu mal ao preparar a movimentação. Tente novamente. + Taxa de encaminhamento + O pagamento está a caminho e aparece no Spark quando for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança. + O Blocktank está a fechar as suas conexões Lightning. Mova já o seu saldo para o Spark: é a opção com as taxas mais baixas e fica em Gastos. + A sua conexão <accent>está a fechar</accent> + Mover o Resto + Mover Para o Spark + A procurar uma rota e a respetiva taxa… + Deslizar Para Mover + Feche antes a conexão para devolver o saldo à Poupança. + Pode movê-lo em partes mais pequenas. + %1$s é demasiado grande para ser encaminhado para o Spark num só pagamento. + Montante <accent>demasiado grande</accent> + Mover %1$s Limite do Próximo HTLC de Saída Mínimo do Próximo HTLC de Saída Antes de transferir fundos de seu saldo de poupança, receba bitcoin na sua carteira Bitkit. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3f8d38a426..d0fab6e4b5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -183,6 +183,30 @@ Можно Использовать Балансы Lightning Архивная высота мониторинга канала + Сумма + Закрыть Соединение + Ваш баланс переходит в Spark через Lightning — самым дешёвым по комиссиям способом сохранить его в Расходах. Комиссия за маршрутизацию ниже — максимум, который вы платите. + Перенос в <accent>Spark</accent> + Часть вашего баланса теперь в Spark. Перенесите остаток таким же способом, затем закройте опустевшее соединение. + Ваш баланс теперь в Spark. Закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения. + Перенесено в <accent>Spark</accent> + На ваших соединениях Lightning больше нет баланса для переноса. + Spark пока не готов к получению. Попробуйте ещё раз чуть позже. + Не удалось <accent>перенести</accent> + Что-то пошло не так при подготовке переноса. Попробуйте ещё раз. + Комиссия за маршрутизацию + Платёж в пути и появится в Spark после завершения. Затем закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения. + Blocktank закрывает свои соединения Lightning. Перенесите баланс в Spark сейчас: это вариант с самой низкой комиссией, и средства остаются в Расходах. + Ваше соединение <accent>закрывается</accent> + Перенести Остаток + Перенос в Spark + Поиск маршрута и его комиссии… + Проведите для Переноса + Вместо этого закройте соединение, чтобы вернуть баланс в Сбережения. + Вы можете перенести его меньшими частями. + %1$s — слишком большая сумма для маршрутизации в Spark одним платежом. + Сумма <accent>слишком велика</accent> + Перенести %1$s Лимит след. исходящего HTLC Мин. след. исходящего HTLC Прежде чем вы сможете перевести средства со своего баланса сбережений, вам нужно отправить биткойны в ваш кошелёк Bitkit. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2fb23120cf..1ff8c8d52d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -320,6 +320,30 @@ Is Usable Lightning Balances Channel Monitor Archival Height + Amount + Close Connection + Your balance moves to Spark over Lightning, the lowest-fee way to keep it in Spending. The routing fee below is the most you pay. + Move to <accent>Spark</accent> + Part of your balance is on Spark now. Move the rest the same way, then close the emptied connection. + Your balance is on Spark now. Close the emptied connection to return its reserve to Savings. + Moved to <accent>Spark</accent> + Your Lightning connections have no balance left to move. + Spark is not ready to receive yet. Try again in a moment. + Could not <accent>move</accent> + Something went wrong while preparing the move. Try again. + Routing fee + The payment is on its way and shows in Spark once it settles. Then close the emptied connection to return its reserve to Savings. + Blocktank is closing its Lightning connections. Move your balance to Spark now: it is the lowest-fee option, and it stays in Spending. + Your connection <accent>is closing</accent> + Move The Rest + Move To Spark + Finding a route and its fee… + Swipe To Move + Close the connection instead to return the balance to Savings. + You can move it in smaller parts. + %1$s is too large to route to Spark in one payment. + Amount <accent>too large</accent> + Move %1$s Next Outbound HTLC Limit Next Outbound HTLC Min Before you can transfer funds from your savings balance, you need to send bitcoin to your Bitkit wallet. diff --git a/app/src/test/java/to/bitkit/repositories/MoveToSparkRepoTest.kt b/app/src/test/java/to/bitkit/repositories/MoveToSparkRepoTest.kt new file mode 100644 index 0000000000..ea69bad78e --- /dev/null +++ b/app/src/test/java/to/bitkit/repositories/MoveToSparkRepoTest.kt @@ -0,0 +1,125 @@ +package to.bitkit.repositories + +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import to.bitkit.ext.createChannelDetails +import to.bitkit.test.BaseUnitTest +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class MoveToSparkRepoTest : BaseUnitTest() { + companion object { + private const val INVOICE = "lnbcrt1spark" + } + + private val lightningRepo = mock() + private val sparkRepo = mock() + private val sparkState = MutableStateFlow(SparkState(isReady = true)) + private val lightningState = MutableStateFlow(LightningState()) + private lateinit var sut: MoveToSparkRepo + + @Before + fun setUp() { + whenever(lightningRepo.lightningState).thenReturn(lightningState) + whenever(sparkRepo.state).thenReturn(sparkState) + sut = MoveToSparkRepo(testDispatcher, lightningRepo, sparkRepo) + } + + private fun sendable(sats: ULong) { + val channel = createChannelDetails().copy(isUsable = true, nextOutboundHtlcLimitMsat = sats * 1000uL) + lightningState.value = LightningState(channels = persistentListOf(channel)) + } + + @Test + fun `quote leaves room under the outbound limit for the routing fee`() = test { + sendable(100_000uL) + whenever(sparkRepo.createInvoice(anyOrNull(), any(), any())).thenReturn(Result.success(INVOICE)) + whenever(lightningRepo.estimateLdkRoutingFeesForAmount(eq(INVOICE), any())).thenReturn(Result.success(30uL)) + + val quote = sut.quote().getOrThrow() + + assertEquals(INVOICE, quote.invoice) + assertEquals(30uL, quote.feeSats) + assertEquals(MoveToSparkMath.amountAfterFee(100_000uL, 30uL), quote.amountSats) + verify(sparkRepo).createInvoice(eq(null), any(), any()) + } + + @Test + fun `quote shrinks the amount when the fee does not fit the first guess`() = test { + sendable(1_000_000uL) + whenever(sparkRepo.createInvoice(anyOrNull(), any(), any())).thenReturn(Result.success(INVOICE)) + whenever(lightningRepo.estimateLdkRoutingFeesForAmount(eq(INVOICE), any())).thenReturn(Result.success(9_000uL)) + + val quote = sut.quote().getOrThrow() + + assertEquals(MoveToSparkMath.amountAfterFee(1_000_000uL, 9_000uL), quote.amountSats) + assertTrue(quote.amountSats + quote.feeSats <= 1_000_000uL) + } + + @Test + fun `quote of a smaller target keeps the target`() = test { + sendable(100_000uL) + whenever(sparkRepo.createInvoice(anyOrNull(), any(), any())).thenReturn(Result.success(INVOICE)) + whenever(lightningRepo.estimateLdkRoutingFeesForAmount(eq(INVOICE), any())).thenReturn(Result.success(5uL)) + + assertEquals(50_000uL, sut.quote(50_000uL).getOrThrow().amountSats) + } + + @Test + fun `quote fails as too large when no route is found`() = test { + sendable(100_000uL) + whenever(sparkRepo.createInvoice(anyOrNull(), any(), any())).thenReturn(Result.success(INVOICE)) + whenever(lightningRepo.estimateLdkRoutingFeesForAmount(eq(INVOICE), any())) + .thenReturn(Result.failure(IllegalStateException("route not found"))) + + val error = sut.quote().exceptionOrNull() + + assertIs(error) + } + + @Test + fun `quote fails with nothing to move below the minimum`() = test { + sendable(MoveToSparkMath.MIN_MOVE_SATS - 1uL) + + assertIs(sut.quote().exceptionOrNull()) + verify(sparkRepo, never()).createInvoice(anyOrNull(), any(), any()) + } + + @Test + fun `quote fails while Spark is not usable`() = test { + sendable(100_000uL) + sparkState.value = SparkState(isReady = false) + + assertIs(sut.quote().exceptionOrNull()) + } + + @Test + fun `a move is offered only with a usable Spark and a balance above the minimum`() { + val channels = listOf(createChannelDetails().copy(isUsable = true, nextOutboundHtlcLimitMsat = 5_000_000uL)) + assertTrue(MoveToSparkMath.canMove(channels, SparkState(isReady = true))) + assertTrue(!MoveToSparkMath.canMove(channels, SparkState(isReady = false))) + assertTrue(!MoveToSparkMath.canMove(emptyList(), SparkState(isReady = true))) + } + + @Test + fun `smaller amount halves until the minimum`() { + assertEquals(50_000uL, MoveToSparkMath.smallerAmount(100_000uL)) + assertNull(MoveToSparkMath.smallerAmount(MoveToSparkMath.MIN_MOVE_SATS)) + } + + @Test + fun `fee cap covers the quoted fee and its margin`() { + assertEquals(32_000uL, MoveToSparkMath.feeCapMsat(30uL)) + } +} diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index ac1d2b580f..1b54560d75 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf @@ -967,6 +968,11 @@ class AppViewModelSendFlowTest : BaseUnitTest() { usdtPreferences = usdtPreferences, highBalanceSheet = mock { on { type } doReturn TimedSheetType.HIGH_BALANCE }, + moveToSparkSheet = mock { + on { type } doReturn TimedSheetType.MOVE_TO_SPARK + onBlocking { shouldShow() } doReturn false + }, + moveToSparkRepo = mock { on { canMove } doReturn emptyFlow() }, formatMoneyValue = formatMoneyValue, widgetsRepo = widgetsRepo, pubkyRepo = pubkyRepo, diff --git a/changelog.d/next/1768.added.md b/changelog.d/next/1768.added.md new file mode 100644 index 0000000000..4562661bb3 --- /dev/null +++ b/changelog.d/next/1768.added.md @@ -0,0 +1 @@ +Move to Spark: move a Lightning connection's balance to Spark at the lowest fee, then close the emptied connection to return its reserve to Savings. diff --git a/docs/screens-map.md b/docs/screens-map.md index b46984af02..837f3f1cf8 100644 --- a/docs/screens-map.md +++ b/docs/screens-map.md @@ -316,6 +316,7 @@ Frame names are stable across handoff iterations; node ids are not, so the map l | ChannelDetailScreen.kt | Settings > Advanced › Connection details | | CloseConnectionScreen.kt | Settings > Advanced › Close connection | | LightningConnectionsScreen.kt | Settings > Advanced › Lightning Connections overview | +| MoveToSparkScreen.kt | `todo` | | SparkDetailScreen.kt | `todo` | ## ui/settings/pin diff --git a/journeys/transfer/move-to-spark.xml b/journeys/transfer/move-to-spark.xml new file mode 100644 index 0000000000..83e2502e03 --- /dev/null +++ b/journeys/transfer/move-to-spark.xml @@ -0,0 +1,29 @@ + + + Verifies the post-update "Move to Spark" sheet and the move itself. With an LDK channel that can send at least + 1,000 sats and Spark connected, Home shows the "Move To Spark" sheet once (testTag "MoveToSparkIntro"); Spending + and Lightning Connections show a "Move To Spark" button (testTag "MoveToSpark") while the channel can still send. + The move quotes the routing fee over the channel only, pays the amount to a new invoice of the user's own Spark + wallet, and then offers to close the emptied connection. A wallet without such a channel never gets the sheet or + the button. + + The "too large to route" state (testTag "MoveToSparkTrySmaller") needs a route that cannot carry the balance; on a + direct channel to the Spark service provider every amount routes, so it is covered by MoveToSparkRepoTest and the + MoveToSparkScreen previews rather than here. + + Precondition: dev build pointed at a local Spark regtest (spark-sdk regtest/local), and a wallet with an open + channel to that stack's SSP Lightning node that holds most of its capacity on the wallet's side (open it from + Lightning Connections, Manual Setup, with the node URI the stack prints). Start on the wallet home screen with the + app freshly launched. + + + Wait on Home and verify the "Move To Spark" sheet (testTag "MoveToSparkIntro") shows, saying Blocktank is closing its Lightning connections + Tap "Later" (testTag "MoveToSparkIntroLater") and verify the sheet closes + Relaunch the app, wait on Home and verify the sheet does not show again + Open the Spending wallet and verify the "Move To Spark" button (testTag "MoveToSpark") shows + Tap "Move To Spark" and verify the screen shows an amount (testTag "MoveToSparkAmount") and a routing fee (testTag "MoveToSparkFee") + Swipe to move (testTag "MoveToSparkSwipe") and verify "Moved to Spark" shows the moved amount (testTag "MoveToSparkMoved") and "Close Connection" (testTag "MoveToSparkClose") + Tap "Close Connection", then "Close" on the close confirmation, and verify Lightning Connections shows no open connection and the Spark balance grew by the moved amount + Go back to Home, open Spending and verify the "Move To Spark" button is gone + + From b414f5fe7f75ab2853f7e4cf92a17ef8ad916d54 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 19:20:37 +0100 Subject: [PATCH 2/6] test: keep the move to spark journey platform neutral --- journeys/transfer/move-to-spark.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/journeys/transfer/move-to-spark.xml b/journeys/transfer/move-to-spark.xml index 83e2502e03..4a47e1307e 100644 --- a/journeys/transfer/move-to-spark.xml +++ b/journeys/transfer/move-to-spark.xml @@ -8,8 +8,8 @@ the button. The "too large to route" state (testTag "MoveToSparkTrySmaller") needs a route that cannot carry the balance; on a - direct channel to the Spark service provider every amount routes, so it is covered by MoveToSparkRepoTest and the - MoveToSparkScreen previews rather than here. + direct channel to the Spark service provider every amount routes, so it is covered by the quote's unit tests and the + PR's manual test rather than here. Precondition: dev build pointed at a local Spark regtest (spark-sdk regtest/local), and a wallet with an open channel to that stack's SSP Lightning node that holds most of its capacity on the wallet's side (open it from From e289d5a79eda2684518a318baf9aea1b9957a6ab Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 19:29:19 +0100 Subject: [PATCH 3/6] fix: list move to spark after the channels in connections --- .../lightning/LightningConnectionsScreen.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index 295b98a04f..628c5cda9e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -211,15 +211,6 @@ private fun Content( SyncingRow() } - if (uiState.canMoveToSpark) { - VerticalSpacer(16.dp) - PrimaryButton( - text = stringResource(R.string.lightning__move_to_spark__nav_title), - onClick = onClickMoveToSpark, - modifier = Modifier.testTag("MoveToSpark") - ) - } - uiState.spark?.let { spark -> VerticalSpacer(16.dp) Caption13Up(stringResource(R.string.lightning__spark), color = Colors.White64) @@ -248,6 +239,15 @@ private fun Content( ) } + if (uiState.canMoveToSpark) { + VerticalSpacer(16.dp) + PrimaryButton( + text = stringResource(R.string.lightning__move_to_spark__nav_title), + onClick = onClickMoveToSpark, + modifier = Modifier.testTag("MoveToSpark") + ) + } + // Closed Channels Section AnimatedVisibility(visible = showClosed && uiState.closedChannels.isNotEmpty()) { Column { From dbff2f7ee960c652bf48545be3586006683d27d0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 20:23:51 +0100 Subject: [PATCH 4/6] fix: fit move to spark labels and headlines to small screens --- .../settings/lightning/MoveToSparkScreen.kt | 51 +++++++++++-------- .../to/bitkit/ui/sheets/MoveToSparkSheet.kt | 2 +- app/src/main/res/values-ar/strings.xml | 14 ++--- app/src/main/res/values-b+es+419/strings.xml | 16 +++--- app/src/main/res/values-ca/strings.xml | 16 +++--- app/src/main/res/values-cs/strings.xml | 16 +++--- app/src/main/res/values-de/strings.xml | 16 +++--- app/src/main/res/values-el/strings.xml | 16 +++--- app/src/main/res/values-es-rES/strings.xml | 16 +++--- app/src/main/res/values-es/strings.xml | 16 +++--- app/src/main/res/values-fr/strings.xml | 16 +++--- app/src/main/res/values-it/strings.xml | 16 +++--- app/src/main/res/values-nl/strings.xml | 16 +++--- app/src/main/res/values-pl/strings.xml | 16 +++--- app/src/main/res/values-pt-rBR/strings.xml | 16 +++--- app/src/main/res/values-pt/strings.xml | 16 +++--- app/src/main/res/values-ru/strings.xml | 16 +++--- app/src/main/res/values/strings.xml | 16 +++--- 18 files changed, 158 insertions(+), 149 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt index 21677808ac..03dbfc13f3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt @@ -5,13 +5,13 @@ 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.ColumnScope 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.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.sizeIn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -74,7 +74,6 @@ private fun Content( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()) ) { when (uiState) { MoveToSparkUiState.Quoting -> Quoting() @@ -96,21 +95,27 @@ private fun Header(title: String, text: String) { BodyM(text = text, color = Colors.White64) } +/** Takes the height between the content and the bottom buttons, so the buttons stay at the screen's bottom. */ @Composable -private fun Illustration(@DrawableRes drawable: Int) { - Image( - painter = painterResource(drawable), - contentDescription = null, - contentScale = ContentScale.Fit, +private fun ColumnScope.Illustration(@DrawableRes drawable: Int) { + Box( + contentAlignment = Alignment.Center, modifier = Modifier + .weight(1f) .fillMaxWidth() - .padding(vertical = 32.dp) - .size(256.dp) - ) + .padding(vertical = 16.dp) + ) { + Image( + painter = painterResource(drawable), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.sizeIn(maxWidth = 256.dp, maxHeight = 256.dp) + ) + } } @Composable -private fun Quoting() { +private fun ColumnScope.Quoting() { Header( title = stringResource(R.string.lightning__move_to_spark__confirm_title), text = stringResource(R.string.lightning__move_to_spark__quoting), @@ -118,15 +123,15 @@ private fun Quoting() { Box( contentAlignment = Alignment.Center, modifier = Modifier + .weight(1f) .fillMaxWidth() - .padding(vertical = 64.dp) ) { GradientCircularProgressIndicator(modifier = Modifier.size(64.dp)) } } @Composable -private fun Confirm(quote: MoveToSparkQuote, isMoving: Boolean, onMove: () -> Unit) { +private fun ColumnScope.Confirm(quote: MoveToSparkQuote, isMoving: Boolean, onMove: () -> Unit) { Header( title = stringResource(R.string.lightning__move_to_spark__confirm_title), text = stringResource(R.string.lightning__move_to_spark__confirm_text), @@ -154,7 +159,7 @@ private fun AmountRow(label: String, sats: ULong, testTag: String) { } @Composable -private fun Done( +private fun ColumnScope.Done( uiState: MoveToSparkUiState.Done, onDone: () -> Unit, onMoveRest: (ULong?) -> Unit, @@ -179,7 +184,7 @@ private fun Done( Illustration(R.drawable.check) if (uiState.hasRest) { Buttons( - secondaryText = stringResource(R.string.common__ok), + secondaryText = stringResource(R.string.common__later), onSecondary = onDone, primaryText = stringResource(R.string.lightning__move_to_spark__move_rest), onPrimary = { onMoveRest(null) }, @@ -188,16 +193,20 @@ private fun Done( return } Buttons( - secondaryText = stringResource(R.string.common__ok), + secondaryText = stringResource(R.string.common__later), onSecondary = onDone, - primaryText = stringResource(R.string.lightning__move_to_spark__close), + primaryText = stringResource(R.string.common__close), onPrimary = { onCloseChannel(uiState.closeChannelId) }, primaryTestTag = "MoveToSparkClose", ) } @Composable -private fun TooLarge(uiState: MoveToSparkUiState.TooLarge, onBack: () -> Unit, onRetry: (ULong?) -> Unit) { +private fun ColumnScope.TooLarge( + uiState: MoveToSparkUiState.TooLarge, + onBack: () -> Unit, + onRetry: (ULong?) -> Unit, +) { val amount = sparkExitAmountText(uiState.amountSats) Header( title = stringResource(R.string.lightning__move_to_spark__too_large_title), @@ -217,14 +226,14 @@ private fun TooLarge(uiState: MoveToSparkUiState.TooLarge, onBack: () -> Unit, o Buttons( secondaryText = stringResource(R.string.common__cancel), onSecondary = onBack, - primaryText = stringResource(R.string.lightning__move_to_spark__try, sparkExitAmountText(smallerSats)), + primaryText = stringResource(R.string.lightning__move_to_spark__move_half), onPrimary = { onRetry(smallerSats) }, primaryTestTag = "MoveToSparkTrySmaller", ) } @Composable -private fun Failed(failure: MoveToSparkFailure, onBack: () -> Unit, onRetry: (ULong?) -> Unit) { +private fun ColumnScope.Failed(failure: MoveToSparkFailure, onBack: () -> Unit, onRetry: (ULong?) -> Unit) { Header( title = stringResource(R.string.lightning__move_to_spark__failed_title), text = stringResource( diff --git a/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt index 3447a81cfb..243f84cf4e 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt @@ -70,7 +70,7 @@ fun MoveToSparkSheet(onLater: () -> Unit, onMove: () -> Unit, modifier: Modifier modifier = Modifier.weight(1f) ) PrimaryButton( - stringResource(R.string.lightning__move_to_spark__nav_title), + stringResource(R.string.lightning__move_to_spark__move_now), onClick = onMove, modifier = Modifier.weight(1f).testTag("MoveToSparkIntroMove") ) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index fb00f29d3e..b9d8ad59d6 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -168,20 +168,21 @@ أرصدة Lightning ارتفاع أرشفة مراقب القناة المبلغ - إغلاق الاتصال ينتقل رصيدك إلى Spark عبر Lightning، وهي أقل طريقة رسومًا للإبقاء عليه في الإنفاق. رسوم التوجيه أدناه هي أقصى ما تدفعه. - الانتقال إلى <accent>Spark</accent> + الانتقال إلى\n<accent>Spark</accent> جزء من رصيدك الآن على Spark. انقل الباقي بالطريقة نفسها، ثم أغلق الاتصال الذي أصبح فارغًا. رصيدك الآن على Spark. أغلق الاتصال الذي أصبح فارغًا لإعادة احتياطيه إلى المدخرات. - تم النقل إلى <accent>Spark</accent> + تم النقل إلى\n<accent>Spark</accent> لم يتبق في اتصالات Lightning لديك رصيد للنقل. Spark غير جاهز للاستلام بعد. حاول مرة أخرى بعد قليل. - تعذّر <accent>النقل</accent> + تعذّر إتمام\n<accent>النقل</accent> حدث خطأ أثناء تحضير النقل. حاول مرة أخرى. رسوم التوجيه الدفعة في طريقها وستظهر في Spark بمجرد إتمامها. ثم أغلق الاتصال الفارغ لإعادة احتياطيه إلى المدخرات. يقوم Blocktank بإغلاق اتصالات Lightning الخاصة به. انقل رصيدك إلى Spark الآن: فهو الخيار الأقل رسومًا، ويبقى ضمن الإنفاق. - اتصالك <accent>قيد الإغلاق</accent> + اتصالك على وشك\n<accent>الإغلاق</accent> + نقل النصف + نقل الآن نقل الباقي النقل إلى Spark جارٍ البحث عن مسار ورسومه… @@ -189,8 +190,7 @@ أغلق الاتصال بدلاً من ذلك لإعادة الرصيد إلى المدخرات. يمكنك نقله على دفعات أصغر. %1$s أكبر من أن يُوجَّه إلى Spark في دفعة واحدة. - المبلغ <accent>كبير جدًا</accent> - نقل %1$s + المبلغ كبير\n<accent>جدًا</accent> حد HTLC الصادر التالي الحد الأدنى لـ HTLC الصادر التالي قبل أن تتمكن من تحويل الأموال من رصيد مدخراتك، تحتاج إلى إرسال Bitcoin إلى محفظة Bitkit الخاصة بك. diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 0166211735..bcd63749f8 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -175,29 +175,29 @@ Saldos Lightning Altura de archivo del monitor de canal Monto - Terminar conexión Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga. - Mover a <accent>Spark</accent> + Mover a\n<accent>Spark</accent> Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía. Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro. - Movido a <accent>Spark</accent> + Movido a\n<accent>Spark</accent> Sus conexiones Lightning ya no tienen saldo para mover. Spark aún no está listo para recibir. Inténtelo de nuevo en un momento. - No se pudo <accent>mover</accent> + No se pudo\n<accent>mover</accent> Algo salió mal al preparar el movimiento. Inténtelo de nuevo. Comisión de enrutamiento El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro. Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto. - Su conexión <accent>se está cerrando</accent> - Mover el resto + Su conexión\n<accent>se cierra</accent> + Mover mitad + Mover ahora + Mover resto Mover a Spark Buscando una ruta y su comisión… Deslizar para mover Termine la conexión para devolver el saldo a Ahorro. Puede moverlo en partes más pequeñas. %1$s es demasiado grande para enrutarse a Spark en un solo pago. - Monto <accent>demasiado grande</accent> - Mover %1$s + El monto es\n<accent>excesivo</accent> Límite del próximo HTLC saliente Mínimo del próximo HTLC saliente Para transferir fondos a su saldo de ahorros, primero tiene que enviar bitcoin a su billetera Bitkit. diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 429461e485..5824650ede 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -172,29 +172,29 @@ Saldos Lightning Alçada d\'arxiu del monitor de canal Import - Tanca la connexió El teu saldo passa a Spark per Lightning, la manera amb menys tarifes de mantenir-lo a Despesa. La tarifa d\'enrutament de sota és el màxim que pagues. - Moure a <accent>Spark</accent> + Moure a\n<accent>Spark</accent> Una part del teu saldo ja és a Spark. Mou la resta de la mateixa manera i després tanca la connexió buida. El teu saldo ja és a Spark. Tanca la connexió buida per tornar-ne la reserva als Estalvis. - Mogut a <accent>Spark</accent> + Mogut a\n<accent>Spark</accent> Les teves connexions Lightning ja no tenen saldo per moure. Spark encara no està a punt per rebre. Torna-ho a provar d\'aquí a un moment. - No s\'ha pogut <accent>moure</accent> + No s\'ha pogut\n<accent>moure</accent> Alguna cosa ha anat malament en preparar el moviment. Torna-ho a provar. Tarifa d\'enrutament El pagament està en camí i apareixerà a Spark quan s\'hagi liquidat. Després tanca la connexió buida per tornar-ne la reserva als Estalvis. Blocktank està tancant les seves connexions Lightning. Mou ara el teu saldo a Spark: és l\'opció amb menys tarifes i es manté a Despesa. - La teva connexió <accent>es tanca</accent> - Moure la resta + La teva connexió\n<accent>es tanca</accent> + Moure meitat + Moure ara + Moure resta Moure a Spark Cercant una ruta i la seva tarifa… Desplaça per moure Tanca la connexió per tornar el saldo als Estalvis. El pots moure en parts més petites. %1$s és massa gran per enrutar-se a Spark en un sol pagament. - Import <accent>massa gran</accent> - Moure %1$s + Import massa\n<accent>gran</accent> Límit del pròxim HTLC de sortida Mínim del pròxim HTLC de sortida Abans de poder transferir fons del teu saldo d\'estalvis, has d\'enviar bitcoin a la teva cartera Bitkit. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 72dfaba94d..5cbe916972 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -175,29 +175,29 @@ Lightning zůstatky Archivní výška monitoru kanálu Částka - Uzavřít spojení Váš zůstatek se přesune do Spark přes Lightning, což je způsob s nejnižšími poplatky, jak jej udržet v Útratách. Poplatek za směrování níže je maximum, které zaplatíte. - Přesun do <accent>Spark</accent> + Přesun do\n<accent>Spark</accent> Část vašeho zůstatku je nyní ve Spark. Zbytek přesuňte stejným způsobem a poté uzavřete vyprázdněné spojení. Váš zůstatek je nyní ve Spark. Uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor. - Přesunuto do <accent>Spark</accent> + Přesunuto do\n<accent>Spark</accent> Vaše spojení Lightning už nemají žádný zůstatek k přesunu. Spark zatím není připraven přijímat. Zkuste to za chvíli znovu. - <accent>Přesun</accent> se nezdařil + Přesun se\n<accent>nezdařil</accent> Při přípravě přesunu se něco pokazilo. Zkuste to znovu. Poplatek za směrování Platba je na cestě a ve Spark se zobrazí po vypořádání. Poté uzavřete vyprázdněné spojení, aby se jeho rezerva vrátila do Úspor. Blocktank uzavírá svá spojení Lightning. Přesuňte svůj zůstatek do Spark hned: je to možnost s nejnižšími poplatky a zůstane v Útratách. - Vaše spojení <accent>se uzavírá</accent> - Přesunout zbytek + Vaše spojení\n<accent>se uzavírá</accent> + Přesun půlku + Přesun hned + Zbytek Přesun do Spark Hledání trasy a jejího poplatku… Přejetím přesuňte Místo toho uzavřete spojení, aby se zůstatek vrátil do Úspor. Můžete jej přesunout po menších částech. %1$s je příliš velká částka na směrování do Spark v jedné platbě. - Částka <accent>je příliš velká</accent> - Přesunout %1$s + Částka je příliš\n<accent>velká</accent> Limit dalšího odchozího HTLC Min dalšího odchozího HTLC Než budete moci převést prostředky ze svého zůstatku na spořicím účtu, musíte poslat bitcoiny do peněženky Bitkit. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 28ba9dc55c..1271bfceaa 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -227,29 +227,29 @@ Lightning-Guthaben Channel Monitor Archivhöhe Betrag - Verbindung schließen Ihr Guthaben wechselt über Lightning zu Spark, dem günstigsten Weg, es im Ausgabenkonto zu behalten. Die Routing-Gebühr unten ist das Maximum, das Sie zahlen. - Zu <accent>Spark</accent> verschieben + Verschieben zu\n<accent>Spark</accent> Ein Teil Ihres Guthabens ist jetzt bei Spark. Verschieben Sie den Rest auf dieselbe Weise und schließen Sie dann die geleerte Verbindung. Ihr Guthaben ist jetzt bei Spark. Schließen Sie die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben. - Zu <accent>Spark</accent> verschoben + Verschoben zu\n<accent>Spark</accent> Ihre Lightning-Verbindungen haben kein Guthaben mehr zum Verschieben. Spark ist noch nicht bereit zum Empfangen. Versuchen Sie es gleich noch einmal. - <accent>Verschieben</accent> fehlgeschlagen + Das Verschieben\n<accent>schlug fehl</accent> Beim Vorbereiten der Verschiebung ist etwas schiefgelaufen. Versuchen Sie es erneut. Routing-Gebühr Die Zahlung ist unterwegs und erscheint in Spark, sobald sie abgeschlossen ist. Schließen Sie dann die geleerte Verbindung, um deren Reserve an das Sparkonto zurückzugeben. Blocktank schließt seine Lightning-Verbindungen. Verschieben Sie Ihr Guthaben jetzt zu Spark: Das ist die günstigste Option, und es bleibt im Ausgabenkonto. - Ihre Verbindung <accent>wird geschlossen</accent> - Rest verschieben + Ihre Verbindung wird\n<accent>geschlossen</accent> + Halb senden + Jetzt senden + Rest senden Zu Spark verschieben Route und Gebühr werden gesucht… Swipe zum Verschieben Schließen Sie stattdessen die Verbindung, um das Guthaben an das Sparkonto zurückzugeben. Sie können es in kleineren Teilen verschieben. %1$s ist zu groß, um in einer Zahlung zu Spark geroutet zu werden. - Betrag <accent>zu groß</accent> - %1$s verschieben + Betrag ist\n<accent>zu groß</accent> Nächstes ausgehendes HTLC-Limit Nächster ausgehender HTLC Min Bevor du Geld von deinem Sparkonto übertragen kannst, musst du Bitcoin zu deiner Bitkit Wallet senden. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index c0d400692d..a8850314c7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -168,29 +168,29 @@ Υπόλοιπα Lightning Ύψος αρχειοθέτησης παρακολούθησης καναλιού Ποσό - Κλείσιμο σύνδεσης Το υπόλοιπό σας μεταφέρεται στο Spark μέσω Lightning, τον τρόπο με τα χαμηλότερα τέλη για να παραμείνει στις Δαπάνες. Το τέλος δρομολόγησης παρακάτω είναι το μέγιστο που πληρώνετε. - Μεταφορά στο <accent>Spark</accent> + Μεταφορά στο\n<accent>Spark</accent> Ένα μέρος του υπολοίπου σας βρίσκεται πλέον στο Spark. Μεταφέρετε τα υπόλοιπα με τον ίδιο τρόπο και έπειτα κλείστε την άδεια σύνδεση. Το υπόλοιπό σας βρίσκεται πλέον στο Spark. Κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις. - Μεταφέρθηκε στο <accent>Spark</accent> + Μεταφέρθηκε στο\n<accent>Spark</accent> Οι συνδέσεις Lightning σας δεν έχουν πλέον υπόλοιπο για μεταφορά. Το Spark δεν είναι ακόμη έτοιμο να λάβει. Δοκιμάστε ξανά σε λίγο. - Αποτυχία <accent>μεταφοράς</accent> + Η μεταφορά\n<accent>απέτυχε</accent> Κάτι πήγε στραβά κατά την προετοιμασία της μεταφοράς. Δοκιμάστε ξανά. Τέλος δρομολόγησης Η πληρωμή βρίσκεται σε εξέλιξη και θα εμφανιστεί στο Spark μόλις διακανονιστεί. Έπειτα κλείστε την άδεια σύνδεση για να επιστρέψει το απόθεμά της στις Αποταμιεύσεις. Το Blocktank κλείνει τις συνδέσεις Lightning του. Μεταφέρετε το υπόλοιπό σας στο Spark τώρα: είναι η επιλογή με τα χαμηλότερα τέλη και παραμένει στις Δαπάνες. - Η σύνδεσή σας <accent>κλείνει</accent> - Μεταφορά των υπολοίπων + Η σύνδεσή σας\n<accent>κλείνει</accent> + Το μισό + Μεταφορά + Τα υπόλοιπα Μεταφορά στο Spark Εύρεση διαδρομής και του τέλους της… Σύρε για μεταφορά Κλείστε αντί αυτού τη σύνδεση για να επιστρέψει το υπόλοιπο στις Αποταμιεύσεις. Μπορείτε να το μεταφέρετε σε μικρότερα μέρη. Το %1$s είναι πολύ μεγάλο για δρομολόγηση στο Spark με μία πληρωμή. - Το ποσό <accent>είναι πολύ μεγάλο</accent> - Μεταφορά %1$s + Το ποσό είναι\n<accent>πολύ μεγάλο</accent> Όριο επόμενου εξερχόμενου HTLC Ελάχιστο επόμενο εξερχόμενο HTLC Πριν μπορέσεις να μεταφέρεις κεφάλαια από το υπόλοιπο αποταμιεύσεων, πρέπει να στείλεις bitcoin στο πορτοφόλι Bitkit. diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 7c823d9ef3..05819ef552 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -174,29 +174,29 @@ Saldos Lightning Altura de archivo del monitor de canales Cantidad - Terminar conexión Tu saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que pagas. - Mover a <accent>Spark</accent> + Mover a\n<accent>Spark</accent> Una parte de tu saldo ya está en Spark. Mueve el resto de la misma forma y luego termina la conexión vacía. Tu saldo ya está en Spark. Termina la conexión vacía para devolver su reserva a Ahorro. - Movido a <accent>Spark</accent> + Movido a\n<accent>Spark</accent> Tus conexiones Lightning ya no tienen saldo para mover. Spark aún no está listo para recibir. Inténtalo de nuevo en un momento. - No se pudo <accent>mover</accent> + No se pudo\n<accent>mover</accent> Algo salió mal al preparar el movimiento. Inténtalo de nuevo. Comisión de enrutamiento El pago está en camino y aparecerá en Spark cuando se complete. Luego termina la conexión vacía para devolver su reserva a Ahorro. Blocktank está cerrando sus conexiones Lightning. Mueve ahora tu saldo a Spark: es la opción de menor comisión y se mantiene en Gasto. - Tu conexión <accent>se está cerrando</accent> - Mover el resto + Tu conexión\n<accent>se cierra</accent> + Mover mitad + Mover ahora + Mover resto Mover a Spark Buscando una ruta y su comisión… Deslizar para mover Termina la conexión para devolver el saldo a Ahorro. Puedes moverlo en partes más pequeñas. %1$s es demasiado grande para enrutarse a Spark en un solo pago. - Cantidad <accent>demasiado grande</accent> - Mover %1$s + La cantidad es\n<accent>excesiva</accent> Límite del siguiente HTLC saliente Mínimo del siguiente HTLC saliente Antes de que pueda transferir fondos a su saldo de ahorros, tiene que enviar bitcoin a su monedero Bitkit. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index dbb1acf3cf..7d92361c93 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -141,29 +141,29 @@ Saldos Lightning Altura de archivo del monitor de canal Cantidad - Terminar conexión Su saldo pasa a Spark por Lightning, la forma de menor comisión para mantenerlo en Gasto. La comisión de enrutamiento de abajo es lo máximo que paga. - Mover a <accent>Spark</accent> + Mover a\n<accent>Spark</accent> Una parte de su saldo ya está en Spark. Mueva el resto de la misma forma y luego termine la conexión vacía. Su saldo ya está en Spark. Termine la conexión vacía para devolver su reserva a Ahorro. - Movido a <accent>Spark</accent> + Movido a\n<accent>Spark</accent> Sus conexiones Lightning ya no tienen saldo para mover. Spark aún no está listo para recibir. Inténtelo de nuevo en un momento. - No se pudo <accent>mover</accent> + No se pudo\n<accent>mover</accent> Algo salió mal al preparar el movimiento. Inténtelo de nuevo. Comisión de enrutamiento El pago está en camino y aparecerá en Spark cuando se complete. Luego termine la conexión vacía para devolver su reserva a Ahorro. Blocktank está cerrando sus conexiones Lightning. Mueva su saldo a Spark ahora: es la opción de menor comisión y se mantiene en Gasto. - Su conexión <accent>se está cerrando</accent> - Mover el resto + Su conexión\n<accent>se cierra</accent> + Mover mitad + Mover ahora + Mover resto Mover a Spark Buscando una ruta y su comisión… Deslizar para mover Termine la conexión para devolver el saldo a Ahorro. Puede moverlo en partes más pequeñas. %1$s es demasiado grande para enrutarse a Spark en un solo pago. - Cantidad <accent>demasiado grande</accent> - Mover %1$s + La cantidad es\n<accent>excesiva</accent> Límite del siguiente HTLC saliente Mínimo del siguiente HTLC saliente Antes de que pueda transferir fondos a su saldo de ahorros, tiene que enviar bitcoin a su monedero Bitkit. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a0359f06ca..c6a6ca6ac4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -255,29 +255,29 @@ Motif de la fermeture Hauteur d\'archivage du moniteur de canal Montant - Fermer la connexion Votre solde passe à Spark via Lightning, le moyen le moins coûteux de le garder dans Dépenses. Les frais de routage ci-dessous sont le maximum que vous payez. - Déplacer vers <accent>Spark</accent> + Déplacer vers\n<accent>Spark</accent> Une partie de votre solde est maintenant sur Spark. Déplacez le reste de la même façon, puis fermez la connexion vidée. Votre solde est maintenant sur Spark. Fermez la connexion vidée pour rendre sa réserve à Épargnes. - Déplacé vers <accent>Spark</accent> + Déplacé vers\n<accent>Spark</accent> Vos connexions Lightning n\'ont plus de solde à déplacer. Spark n\'est pas encore prêt à recevoir. Réessayez dans un instant. - Échec du <accent>déplacement</accent> + Le déplacement\n<accent>a échoué</accent> Un problème est survenu pendant la préparation du déplacement. Réessayez. Frais de routage Le paiement est en route et apparaîtra sur Spark une fois réglé. Fermez ensuite la connexion vidée pour rendre sa réserve à Épargnes. Blocktank ferme ses connexions Lightning. Déplacez votre solde vers Spark maintenant : c\'est l\'option la moins coûteuse, et il reste dans Dépenses. - Votre connexion <accent>se ferme</accent> - Déplacer le reste + Votre connexion\n<accent>se ferme</accent> + Moitié + Déplacer + Le reste Déplacer vers Spark Recherche d\'un itinéraire et de ses frais… Glissez pour déplacer Fermez plutôt la connexion pour rendre le solde à Épargnes. Vous pouvez le déplacer en plus petites parties. %1$s est trop élevé pour être acheminé vers Spark en un seul paiement. - Montant <accent>trop élevé</accent> - Déplacer %1$s + Le montant est\n<accent>trop élevé</accent> Limite HTLC sortant suivant HTLC sortant min suivant Avant de pouvoir transférer des fonds depuis votre solde d\'épargne, vous devez envoyer des bitcoins à votre portefeuille Bitkit. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 25f4680e1d..d5d3f28e33 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -173,29 +173,29 @@ Saldi Lightning Altezza Archivio Monitor Canali Importo - Chiudi Connessione Il tuo saldo passa a Spark tramite Lightning, il modo a commissioni più basse per mantenerlo in Spesa. La commissione di instradamento qui sotto è il massimo che paghi. - Sposta su <accent>Spark</accent> + Sposta su\n<accent>Spark</accent> Una parte del tuo saldo è ora su Spark. Sposta il resto allo stesso modo, poi chiudi la connessione svuotata. Il tuo saldo è ora su Spark. Chiudi la connessione svuotata per restituire la sua riserva ai Risparmi. - Spostato su <accent>Spark</accent> + Spostato su\n<accent>Spark</accent> Le tue connessioni Lightning non hanno più saldo da spostare. Spark non è ancora pronto a ricevere. Riprova tra un momento. - Impossibile <accent>spostare</accent> + Impossibile\n<accent>spostare</accent> Qualcosa è andato storto durante la preparazione dello spostamento. Riprova. Commissione di instradamento Il pagamento è in arrivo e compare su Spark una volta completato. Poi chiudi la connessione svuotata per restituire la sua riserva ai Risparmi. Blocktank sta chiudendo le sue connessioni Lightning. Sposta ora il tuo saldo su Spark: è l\'opzione con le commissioni più basse e resta in Spesa. - La tua connessione <accent>si sta chiudendo</accent> - Sposta Il Resto + La tua connessione\n<accent>si sta chiudendo</accent> + Sposta Metà + Sposta Ora + Sposta Resto Sposta Su Spark Ricerca di un percorso e della sua commissione… Swipe Per Spostare Chiudi invece la connessione per restituire il saldo ai Risparmi. Puoi spostarlo in parti più piccole. %1$s è troppo grande per essere instradato su Spark in un unico pagamento. - Importo <accent>troppo grande</accent> - Sposta %1$s + Importo troppo\n<accent>grande</accent> Limite HTLC in Uscita Successivo HTLC Min in Uscita Successivo Prima di poter trasferire fondi dal tuo conto di risparmio, devi inviare bitcoin al tuo portafoglio Bitkit. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 57067ca1b9..a2451762c5 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -172,29 +172,29 @@ Lightning-saldi Kanaalmonitor archiveringshoogte Bedrag - Verbinding sluiten Uw saldo gaat via Lightning naar Spark, de goedkoopste manier om het op uw bestedingssaldo te houden. De routeringsvergoeding hieronder is het maximum dat u betaalt. - Verplaatsen naar <accent>Spark</accent> + Verplaatsen naar\n<accent>Spark</accent> Een deel van uw saldo staat nu op Spark. Verplaats de rest op dezelfde manier en sluit daarna de lege verbinding. Uw saldo staat nu op Spark. Sluit de lege verbinding om de reserve terug te geven aan uw spaargeld. - Verplaatst naar <accent>Spark</accent> + Verplaatst naar\n<accent>Spark</accent> Uw Lightning-verbindingen hebben geen saldo meer om te verplaatsen. Spark is nog niet klaar om te ontvangen. Probeer het zo meteen opnieuw. - <accent>Verplaatsen</accent> mislukt + Verplaatsen\n<accent>mislukt</accent> Er is iets misgegaan bij het voorbereiden van de verplaatsing. Probeer het opnieuw. Routeringsvergoeding De betaling is onderweg en verschijnt in Spark zodra ze is afgerond. Sluit daarna de lege verbinding om de reserve terug te geven aan uw spaargeld. Blocktank sluit zijn Lightning-verbindingen. Verplaats uw saldo nu naar Spark: dit is de goedkoopste optie en het blijft op uw bestedingssaldo. - Uw verbinding <accent>wordt gesloten</accent> - De rest verplaatsen + Uw verbinding wordt\n<accent>gesloten</accent> + Stuur helft + Stuur nu + Stuur rest Verplaatsen naar Spark Route en vergoeding zoeken… Veeg om te verplaatsen Sluit in plaats daarvan de verbinding om het saldo terug te geven aan uw spaargeld. U kunt het in kleinere delen verplaatsen. %1$s is te groot om in één betaling naar Spark te routeren. - Bedrag <accent>te groot</accent> - %1$s verplaatsen + Bedrag is\n<accent>te groot</accent> Volgende uitgaande HTLC-limiet Volgende uitgaande HTLC min Voordat je geld van je spaargeld kunt overboeken, moet je bitcoin naar je Bitkit-wallet sturen. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index ef2f387f62..c817b19b7a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -229,29 +229,29 @@ Saldo Lightning Wysokość archiwalnego monitora kanału Kwota - Zamknij połączenie Twoje saldo trafia do Spark przez Lightning, najtańszym sposobem na pozostawienie go w Wydatkach. Opłata za routing poniżej to maksimum, jakie zapłacisz. - Przenieś do <accent>Spark</accent> + Przenieś do\n<accent>Spark</accent> Część Twojego salda jest teraz w Spark. Przenieś resztę w ten sam sposób, a następnie zamknij opróżnione połączenie. Twoje saldo jest teraz w Spark. Zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności. - Przeniesiono do <accent>Spark</accent> + Przeniesiono do\n<accent>Spark</accent> Twoje połączenia Lightning nie mają już salda do przeniesienia. Spark nie jest jeszcze gotowy do odbioru. Spróbuj ponownie za chwilę. - Nie udało się <accent>przenieść</accent> + Nie udało się\n<accent>przenieść</accent> Coś poszło nie tak podczas przygotowania przeniesienia. Spróbuj ponownie. Opłata za routing Płatność jest w drodze i pojawi się w Spark po rozliczeniu. Następnie zamknij opróżnione połączenie, aby zwrócić jego rezerwę do Oszczędności. Blocktank zamyka swoje połączenia Lightning. Przenieś teraz saldo do Spark: to opcja z najniższymi opłatami, a środki zostają w Wydatkach. - Twoje połączenie <accent>się zamyka</accent> - Przenieś resztę + Twoje połączenie\n<accent>się zamyka</accent> + Połowę + Przenieś + Resztę Przenieś do Spark Szukanie trasy i jej opłaty… Przesuń, aby przenieść Zamiast tego zamknij połączenie, aby zwrócić saldo do Oszczędności. Możesz przenieść je w mniejszych częściach. Kwota %1$s jest zbyt duża, aby przekazać ją do Spark jedną płatnością. - Kwota <accent>zbyt duża</accent> - Przenieś %1$s + Kwota jest\n<accent>zbyt duża</accent> Limit następnej wychodzącej HTLC Min. następnej wychodzącej HTLC Zanim będzie można przelać środki z salda oszczędności, należy wysłać bitcoiny do portfela Bitkit. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 36517a7cf7..7b9cb9a6ab 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -176,29 +176,29 @@ Saldos Lightning Altura de Arquivamento do Monitor de Canal Valor - Fechar Conexão Seu saldo vai para o Spark pela Lightning, a forma de menor taxa de mantê-lo em Gastos. A taxa de roteamento abaixo é o máximo que você paga. - Mover para o <accent>Spark</accent> + Mover para o\n<accent>Spark</accent> Parte do seu saldo agora está no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada. Seu saldo agora está no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança. - Movido para o <accent>Spark</accent> + Movido para o\n<accent>Spark</accent> Suas conexões Lightning não têm mais saldo para mover. O Spark ainda não está pronto para receber. Tente novamente em instantes. - Não foi possível <accent>mover</accent> + Não foi possível\n<accent>mover</accent> Algo deu errado ao preparar a movimentação. Tente novamente. Taxa de roteamento O pagamento está a caminho e aparece no Spark assim que for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança. O Blocktank está fechando suas conexões Lightning. Mova seu saldo para o Spark agora: é a opção de menor taxa e ele continua em Gastos. - Sua conexão <accent>está fechando</accent> - Mover o Resto + Sua conexão está\n<accent>fechando</accent> + Mover Metade + Mover Agora + Mover Resto Mover Para o Spark Buscando uma rota e sua taxa… Deslizar Para Mover Feche a conexão para devolver o saldo à Poupança. Você pode movê-lo em partes menores. %1$s é grande demais para ser roteado ao Spark em um único pagamento. - Valor <accent>grande demais</accent> - Mover %1$s + Valor grande\n<accent>demais</accent> Limite do Próximo HTLC de Saída HTLC de Saída Mín Próximo Antes de transferir fundos de seu saldo de poupança, receba bitcoin na sua carteira Bitkit. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 18c65c130e..9ec8c382df 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -273,29 +273,29 @@ Tempo de Atualização do Cache de Taxa Capacidade de Entrada Montante - Fechar Conexão O seu saldo passa para o Spark através do Lightning, a forma com as taxas mais baixas de o manter em Gastos. A taxa de encaminhamento abaixo é o máximo que paga. - Mover para o <accent>Spark</accent> + Mover para o\n<accent>Spark</accent> Parte do seu saldo está agora no Spark. Mova o resto da mesma forma e depois feche a conexão esvaziada. O seu saldo está agora no Spark. Feche a conexão esvaziada para devolver a reserva à Poupança. - Movido para o <accent>Spark</accent> + Movido para o\n<accent>Spark</accent> As suas conexões Lightning já não têm saldo para mover. O Spark ainda não está pronto para receber. Tente novamente dentro de instantes. - Não foi possível <accent>mover</accent> + Não foi possível\n<accent>mover</accent> Algo correu mal ao preparar a movimentação. Tente novamente. Taxa de encaminhamento O pagamento está a caminho e aparece no Spark quando for liquidado. Depois, feche a conexão esvaziada para devolver a reserva à Poupança. O Blocktank está a fechar as suas conexões Lightning. Mova já o seu saldo para o Spark: é a opção com as taxas mais baixas e fica em Gastos. - A sua conexão <accent>está a fechar</accent> - Mover o Resto + A sua conexão\n<accent>vai fechar</accent> + Mover Metade + Mover Agora + Mover Resto Mover Para o Spark A procurar uma rota e a respetiva taxa… Deslizar Para Mover Feche antes a conexão para devolver o saldo à Poupança. Pode movê-lo em partes mais pequenas. %1$s é demasiado grande para ser encaminhado para o Spark num só pagamento. - Montante <accent>demasiado grande</accent> - Mover %1$s + Montante demasiado\n<accent>grande</accent> Limite do Próximo HTLC de Saída Mínimo do Próximo HTLC de Saída Antes de transferir fundos de seu saldo de poupança, receba bitcoin na sua carteira Bitkit. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d0fab6e4b5..d4eae42c6d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -184,29 +184,29 @@ Балансы Lightning Архивная высота мониторинга канала Сумма - Закрыть Соединение Ваш баланс переходит в Spark через Lightning — самым дешёвым по комиссиям способом сохранить его в Расходах. Комиссия за маршрутизацию ниже — максимум, который вы платите. - Перенос в <accent>Spark</accent> + Перенос в\n<accent>Spark</accent> Часть вашего баланса теперь в Spark. Перенесите остаток таким же способом, затем закройте опустевшее соединение. Ваш баланс теперь в Spark. Закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения. - Перенесено в <accent>Spark</accent> + Перенесено в\n<accent>Spark</accent> На ваших соединениях Lightning больше нет баланса для переноса. Spark пока не готов к получению. Попробуйте ещё раз чуть позже. - Не удалось <accent>перенести</accent> + Не удалось\n<accent>перенести</accent> Что-то пошло не так при подготовке переноса. Попробуйте ещё раз. Комиссия за маршрутизацию Платёж в пути и появится в Spark после завершения. Затем закройте опустевшее соединение, чтобы вернуть его резерв в Сбережения. Blocktank закрывает свои соединения Lightning. Перенесите баланс в Spark сейчас: это вариант с самой низкой комиссией, и средства остаются в Расходах. - Ваше соединение <accent>закрывается</accent> - Перенести Остаток + Ваше соединение\n<accent>закрывается</accent> + Половину + Перенести + Остаток Перенос в Spark Поиск маршрута и его комиссии… Проведите для Переноса Вместо этого закройте соединение, чтобы вернуть баланс в Сбережения. Вы можете перенести его меньшими частями. %1$s — слишком большая сумма для маршрутизации в Spark одним платежом. - Сумма <accent>слишком велика</accent> - Перенести %1$s + Сумма слишком\n<accent>велика</accent> Лимит след. исходящего HTLC Мин. след. исходящего HTLC Прежде чем вы сможете перевести средства со своего баланса сбережений, вам нужно отправить биткойны в ваш кошелёк Bitkit. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ff8c8d52d..7ed4f07771 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -321,29 +321,29 @@ Lightning Balances Channel Monitor Archival Height Amount - Close Connection Your balance moves to Spark over Lightning, the lowest-fee way to keep it in Spending. The routing fee below is the most you pay. - Move to <accent>Spark</accent> + Move to\n<accent>Spark</accent> Part of your balance is on Spark now. Move the rest the same way, then close the emptied connection. Your balance is on Spark now. Close the emptied connection to return its reserve to Savings. - Moved to <accent>Spark</accent> + Moved to\n<accent>Spark</accent> Your Lightning connections have no balance left to move. Spark is not ready to receive yet. Try again in a moment. - Could not <accent>move</accent> + Could not\n<accent>move</accent> Something went wrong while preparing the move. Try again. Routing fee The payment is on its way and shows in Spark once it settles. Then close the emptied connection to return its reserve to Savings. Blocktank is closing its Lightning connections. Move your balance to Spark now: it is the lowest-fee option, and it stays in Spending. - Your connection <accent>is closing</accent> - Move The Rest + Your connection\n<accent>is closing</accent> + Move Half + Move Now + Move Rest Move To Spark Finding a route and its fee… Swipe To Move Close the connection instead to return the balance to Savings. You can move it in smaller parts. %1$s is too large to route to Spark in one payment. - Amount <accent>too large</accent> - Move %1$s + Amount\n<accent>too large</accent> Next Outbound HTLC Limit Next Outbound HTLC Min Before you can transfer funds from your savings balance, you need to send bitcoin to your Bitkit wallet. From 74dbe6ad72ccef2daf7077da2317e55bd8efc2a1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Nov 2026 20:50:17 +0100 Subject: [PATCH 5/6] fix: keep the move to spark sheet copy whole on small screens --- .../settings/lightning/MoveToSparkScreen.kt | 2 +- .../to/bitkit/ui/sheets/MoveToSparkSheet.kt | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt index 03dbfc13f3..4a6042826c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt @@ -90,7 +90,7 @@ private fun Content( @Composable private fun Header(title: String, text: String) { VerticalSpacer(32.dp) - Display(text = title.withAccent()) + Display(text = title.withAccent(accentColor = Colors.Purple)) VerticalSpacer(8.dp) BodyM(text = text, color = Colors.White64) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt index 243f84cf4e..63f1706fb9 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/MoveToSparkSheet.kt @@ -2,13 +2,13 @@ package to.bitkit.ui.sheets 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.FlowRow +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.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.sizeIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,16 +39,19 @@ fun MoveToSparkSheet(onLater: () -> Unit, onMove: () -> Unit, modifier: Modifier modifier = modifier.sheetHeight().navigationBarsPadding().testTag("MoveToSparkIntro") ) { SheetTopBar(stringResource(R.string.lightning__move_to_spark__nav_title)) - Column( - verticalArrangement = Arrangement.Bottom, - modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(horizontal = 16.dp) - ) { - Image( - painter = painterResource(R.drawable.lightning), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier.size(256.dp).align(Alignment.CenterHorizontally) - ) + Column(modifier = Modifier.weight(1f).padding(horizontal = 16.dp)) { + // The illustration shrinks on small screens so the copy below it stays whole. + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.weight(1f).fillMaxWidth() + ) { + Image( + painter = painterResource(R.drawable.lightning), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.sizeIn(maxWidth = 256.dp, maxHeight = 256.dp) + ) + } VerticalSpacer(32.dp) Display( stringResource(R.string.lightning__move_to_spark__intro_title).withAccent(accentColor = Colors.Purple) From 967292c097eac4954025617efce9ff257cc481af Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Nov 2026 03:06:04 +0100 Subject: [PATCH 6/6] fix: build move to spark without the spark exit --- .../to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt index 4a6042826c..4a8741816a 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/MoveToSparkScreen.kt @@ -34,6 +34,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.SwipeToConfirm import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.components.rememberMoneyText import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface @@ -207,7 +208,7 @@ private fun ColumnScope.TooLarge( onBack: () -> Unit, onRetry: (ULong?) -> Unit, ) { - val amount = sparkExitAmountText(uiState.amountSats) + val amount = amountText(uiState.amountSats) Header( title = stringResource(R.string.lightning__move_to_spark__too_large_title), text = stringResource(R.string.lightning__move_to_spark__too_large_text, amount) + " " + stringResource( @@ -258,6 +259,10 @@ private fun ColumnScope.Failed(failure: MoveToSparkFailure, onBack: () -> Unit, ) } +@Composable +private fun amountText(sats: ULong): String = + rememberMoneyText(sats.toLong(), showSymbol = true)?.replace(Regex(""), "") ?: sats.toString() + @Composable private fun Buttons( secondaryText: String,