From 0b31dc87b07f0804a8453269bf72b75301d5c0f1 Mon Sep 17 00:00:00 2001 From: Philipp Walter Date: Thu, 6 Aug 2026 16:15:39 +0200 Subject: [PATCH] feat(send): improve Lightning send failure recovery --- .../ext/PaymentFailureMessageContext.kt | 6 ++ .../to/bitkit/ext/PaymentFailureReasonExt.kt | 40 ++++++-- .../to/bitkit/repositories/LightningRepo.kt | 97 ++++++++++++++++++- .../to/bitkit/services/LightningService.kt | 16 ++- .../screens/wallets/send/SendErrorScreen.kt | 21 +++- .../wallets/send/SendQuickPayScreen.kt | 17 ++-- .../java/to/bitkit/ui/sheets/SendSheet.kt | 55 +++++++++-- .../java/to/bitkit/viewmodels/AppViewModel.kt | 19 +++- .../to/bitkit/viewmodels/QuickPayViewModel.kt | 10 +- .../to/bitkit/viewmodels/TransferViewModel.kt | 2 +- .../to/bitkit/viewmodels/WalletViewModel.kt | 20 ++++ app/src/main/res/values-ar/strings.xml | 12 +++ app/src/main/res/values-b+es+419/strings.xml | 12 +++ app/src/main/res/values-ca/strings.xml | 12 +++ app/src/main/res/values-cs/strings.xml | 12 +++ app/src/main/res/values-de/strings.xml | 12 +++ app/src/main/res/values-el/strings.xml | 12 +++ app/src/main/res/values-es-rES/strings.xml | 12 +++ app/src/main/res/values-es/strings.xml | 12 +++ app/src/main/res/values-fr/strings.xml | 12 +++ app/src/main/res/values-it/strings.xml | 12 +++ app/src/main/res/values-nl/strings.xml | 12 +++ app/src/main/res/values-pl/strings.xml | 12 +++ app/src/main/res/values-pt-rBR/strings.xml | 12 +++ app/src/main/res/values-pt/strings.xml | 12 +++ app/src/main/res/values-ru/strings.xml | 12 +++ app/src/main/res/values/strings.xml | 16 ++- .../bitkit/ext/PaymentFailureReasonExtTest.kt | 44 +++++++++ .../viewmodels/AppViewModelSendFlowTest.kt | 25 +++-- .../viewmodels/TransferViewModelTest.kt | 2 +- changelog.d/next/1137.feat.md | 1 + 31 files changed, 513 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ext/PaymentFailureMessageContext.kt create mode 100644 app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt create mode 100644 changelog.d/next/1137.feat.md diff --git a/app/src/main/java/to/bitkit/ext/PaymentFailureMessageContext.kt b/app/src/main/java/to/bitkit/ext/PaymentFailureMessageContext.kt new file mode 100644 index 0000000000..2bd0d828ce --- /dev/null +++ b/app/src/main/java/to/bitkit/ext/PaymentFailureMessageContext.kt @@ -0,0 +1,6 @@ +package to.bitkit.ext + +enum class PaymentFailureMessageContext { + GENERIC, + SEND, +} diff --git a/app/src/main/java/to/bitkit/ext/PaymentFailureReasonExt.kt b/app/src/main/java/to/bitkit/ext/PaymentFailureReasonExt.kt index 49f64498e8..1904c1c13c 100644 --- a/app/src/main/java/to/bitkit/ext/PaymentFailureReasonExt.kt +++ b/app/src/main/java/to/bitkit/ext/PaymentFailureReasonExt.kt @@ -3,15 +3,43 @@ package to.bitkit.ext import android.content.Context import org.lightningdevkit.ldknode.PaymentFailureReason import to.bitkit.R +import to.bitkit.utils.LdkError -fun PaymentFailureReason?.toUserMessage(context: Context): String = when (this) { +fun PaymentFailureReason?.toUserMessage( + context: Context, + messageContext: PaymentFailureMessageContext = PaymentFailureMessageContext.GENERIC, +): String = when (this) { PaymentFailureReason.RECIPIENT_REJECTED -> - context.getString(R.string.wallet__toast_payment_failed_recipient_rejected) + context.getString(R.string.wallet__payment_recipient_rejected) + PaymentFailureReason.USER_ABANDONED -> + context.getString(R.string.wallet__payment_abandoned) PaymentFailureReason.RETRIES_EXHAUSTED -> - context.getString(R.string.wallet__toast_payment_failed_retries_exhausted) + when (messageContext) { + PaymentFailureMessageContext.GENERIC -> context.getString(R.string.wallet__payment_retries_exhausted) + PaymentFailureMessageContext.SEND -> context.getString(R.string.wallet__send_payment_retries_exhausted) + } PaymentFailureReason.ROUTE_NOT_FOUND -> - context.getString(R.string.wallet__toast_payment_failed_route_not_found) + when (messageContext) { + PaymentFailureMessageContext.GENERIC -> context.getString(R.string.wallet__payment_route_not_found) + PaymentFailureMessageContext.SEND -> context.getString(R.string.wallet__send_payment_route_not_found) + } PaymentFailureReason.PAYMENT_EXPIRED -> - context.getString(R.string.wallet__toast_payment_failed_timeout) - else -> context.getString(R.string.wallet__toast_payment_failed_description) + context.getString(R.string.wallet__payment_expired) + PaymentFailureReason.UNKNOWN_REQUIRED_FEATURES -> + context.getString(R.string.wallet__payment_unknown_required_features) + PaymentFailureReason.INVOICE_REQUEST_EXPIRED -> + context.getString(R.string.wallet__payment_invoice_request_expired) + PaymentFailureReason.INVOICE_REQUEST_REJECTED -> + context.getString(R.string.wallet__payment_invoice_request_rejected) + else -> context.getString(R.string.wallet__payment_failed_description) +} + +fun Throwable.toSendFailureMessage(context: Context): String { + val fallbackMessage = context.getString(R.string.wallet__payment_failed_description) + if (this is LdkError) return fallbackMessage + + return message + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?: fallbackMessage } diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 728e878f55..c855e6f399 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -48,6 +48,7 @@ import org.lightningdevkit.ldknode.ChannelDetails import org.lightningdevkit.ldknode.ClosureReason import org.lightningdevkit.ldknode.CoinSelectionAlgorithm import org.lightningdevkit.ldknode.Event +import org.lightningdevkit.ldknode.Network import org.lightningdevkit.ldknode.NodeStatus import org.lightningdevkit.ldknode.PaymentDetails import org.lightningdevkit.ldknode.PaymentHash @@ -637,8 +638,14 @@ class LightningRepo @Inject constructor( } private suspend fun clearNetworkGraph(walletIndex: Int): Result { - lightningService.resetNetworkGraph(walletIndex) - return runCatching { + runSuspendCatching { + lightningService.resetNetworkGraph(walletIndex) + }.onFailure { + Logger.warn("Failed to clear local network graph", it, context = TAG) + return Result.failure(it) + } + + return runSuspendCatching { vssBackupClientLdk.setup(walletIndex).getOrThrow() vssBackupClientLdk.deleteObject("network_graph").getOrThrow() Logger.info("Cleared network graph from VSS", context = TAG) @@ -1859,7 +1866,7 @@ class LightningRepo @Inject constructor( vssBackupClientLdk.deleteObject(VSS_KEY_EXTERNAL_SCORES_CACHE).getOrThrow() }.onFailure { Logger.error("Failed to delete pathfinding scores from VSS", it, context = TAG) - start(walletIndex = walletIndex, shouldRetry = false).onFailure { startError -> + start(walletIndex = walletIndex, shouldRetry = false, shouldValidateGraph = false).onFailure { startError -> Logger.error("Failed to restart node after pathfinding scores reset failure", startError, context = TAG) } return@withContext Result.failure(it) @@ -1867,12 +1874,72 @@ class LightningRepo @Inject constructor( val resetAtSecs = nowMillis() / 1000 - start(walletIndex = walletIndex, shouldRetry = false) + start(walletIndex = walletIndex, shouldRetry = false, shouldValidateGraph = false) .map { resetAtSecs } .onSuccess { Logger.info("Pathfinding scores reset at '$resetAtSecs'", context = TAG) } } + + suspend fun resetPaymentRoutingCachesAndWait(walletIndex: Int = 0): Result = withContext(bgDispatcher) { + val refreshStartedAtSecs = (nowMillis() / 1000).toULong() + val requiresRgsRefresh = Env.network != Network.REGTEST && + !settingsStore.data.first().rgsServerUrl.isNullOrEmpty() + val requiresScorerRefresh = Env.ldkScorerUrl != null + val resetErrors = mutableListOf() + + stop().onFailure { + return@withContext Result.failure(it) + } + + clearNetworkGraph(walletIndex).onFailure { + resetErrors.add(it) + } + + resetPathfindingScores(walletIndex).onFailure { + resetErrors.add(it) + } + + resetErrors.firstOrNull()?.let { + return@withContext Result.failure(it) + } + + waitForPaymentRoutingDataRefresh( + walletIndex = walletIndex, + refreshStartedAtSecs = refreshStartedAtSecs, + requiresRgsRefresh = requiresRgsRefresh, + requiresScorerRefresh = requiresScorerRefresh, + ) + } + + private suspend fun waitForPaymentRoutingDataRefresh( + walletIndex: Int, + refreshStartedAtSecs: ULong, + requiresRgsRefresh: Boolean, + requiresScorerRefresh: Boolean, + ): Result = withContext(bgDispatcher) { + if (!requiresRgsRefresh && !requiresScorerRefresh) return@withContext Result.success(Unit) + + val refreshed = withTimeoutOrNull(PAYMENT_ROUTING_REFRESH_TIMEOUT) { + while (isActive) { + syncState() + if ( + _lightningState.value.hasFreshPaymentRoutingData( + graphCacheModificationDate = lightningService.networkGraphCacheModificationDate(walletIndex), + refreshStartedAtSecs = refreshStartedAtSecs, + requiresRgsRefresh = requiresRgsRefresh, + requiresScorerRefresh = requiresScorerRefresh, + ) + ) { + return@withTimeoutOrNull true + } + delay(PAYMENT_ROUTING_REFRESH_POLL_DELAY) + } + false + } == true + + if (refreshed) Result.success(Unit) else Result.failure(PaymentRoutingRefreshTimeoutError()) + } // endregion suspend fun restartNode(): Result = withContext(bgDispatcher) { @@ -1901,9 +1968,30 @@ class LightningRepo @Inject constructor( private val NO_USABLE_CHANNELS_FEEDBACK_DELAY = 2_500.milliseconds val SEND_LN_TIMEOUT = 10.seconds private val PROBE_TIMEOUT = 60.seconds + private val PAYMENT_ROUTING_REFRESH_TIMEOUT = 20.seconds + private val PAYMENT_ROUTING_REFRESH_POLL_DELAY = 500.milliseconds } } +private fun LightningState.hasFreshPaymentRoutingData( + graphCacheModificationDate: Long?, + refreshStartedAtSecs: ULong, + requiresRgsRefresh: Boolean, + requiresScorerRefresh: Boolean, +): Boolean { + val status = nodeStatus + if (!nodeLifecycleState.isRunning()) return false + + val hasFreshRgs = !requiresRgsRefresh || + graphCacheModificationDate != null && (graphCacheModificationDate / 1000).toULong() >= refreshStartedAtSecs + + val latestScoresTimestamp = status?.latestPathfindingScoresSyncTimestamp + val hasFreshScores = !requiresScorerRefresh || + latestScoresTimestamp != null && latestScoresTimestamp >= refreshStartedAtSecs + + return hasFreshRgs && hasFreshScores +} + class RecoveryModeError : AppError("App in recovery mode, skipping node start") class NodeSetupError : AppError("Unknown node setup error") class NodeStopTimeoutError : AppError("Timeout waiting for node to stop") @@ -1912,6 +2000,7 @@ class NodeRunTimeoutError(opName: String) : AppError("Timeout waiting for node t class GetPaymentsError : AppError("It wasn't possible get the payments") class SyncUnhealthyError : AppError("Wallet sync failed before send") class LnurlPayInvoiceMismatchError : AppError("The invoice did not match the requested payment. Payment cancelled.") +class PaymentRoutingRefreshTimeoutError : AppError("Timeout waiting for payment routing data refresh") data class NodeEventUpdate( val event: Event, diff --git a/app/src/main/java/to/bitkit/services/LightningService.kt b/app/src/main/java/to/bitkit/services/LightningService.kt index d82fea6521..634ca03452 100644 --- a/app/src/main/java/to/bitkit/services/LightningService.kt +++ b/app/src/main/java/to/bitkit/services/LightningService.kt @@ -526,16 +526,24 @@ class LightningService @Inject constructor( if (node != null) throw ServiceError.NodeStillRunning() awaitNodeRelease() Logger.warn("Resetting network graph cache…", context = TAG) - val ldkPath = Path(Env.ldkStoragePath(walletIndex)).toFile() - val graphFile = ldkPath.resolve("network_graph_cache") + val graphFile = networkGraphCacheFile(walletIndex) if (graphFile.exists()) { - graphFile.delete() + if (!graphFile.delete()) throw NetworkGraphCacheDeleteError() Logger.info("Network graph cache deleted", context = TAG) } else { Logger.info("No network graph cache found", context = TAG) } } + fun networkGraphCacheModificationDate(walletIndex: Int): Long? { + val graphFile = networkGraphCacheFile(walletIndex) + return graphFile.takeIf { it.exists() }?.lastModified() + } + + private fun networkGraphCacheFile(walletIndex: Int): File { + return Path(Env.ldkStoragePath(walletIndex)).toFile().resolve("network_graph_cache") + } + @Suppress("ReturnCount") fun aresRequiredPeersInNetworkGraph(): Boolean { val node = this.node ?: return true @@ -1385,3 +1393,5 @@ data class NetworkGraphInfo( class TrustedPeerForceCloseException : AppError( "Cannot force close channel with trusted peer. Force close is disabled for Blocktank LSP channels." ) + +class NetworkGraphCacheDeleteError : AppError("Failed to delete network graph cache") diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt index c65190b5ca..eb0c926e61 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendErrorScreen.kt @@ -31,12 +31,16 @@ import to.bitkit.ui.theme.Colors @Composable fun SendErrorScreen( + title: String, message: String?, + isRetrying: Boolean, onRetry: () -> Unit, onClose: () -> Unit, ) { Content( + title = title, message, + isRetrying = isRetrying, onRetry = onRetry, onClose = onClose, ) @@ -44,8 +48,10 @@ fun SendErrorScreen( @Composable private fun Content( + title: String, message: String?, modifier: Modifier = Modifier, + isRetrying: Boolean = false, onRetry: () -> Unit = {}, onClose: () -> Unit = {}, ) { @@ -55,7 +61,7 @@ private fun Content( .gradientBackground() .navigationBarsPadding() ) { - SheetTopBar(stringResource(R.string.wallet__send_error_tx_failed)) + SheetTopBar(title) Column( modifier = Modifier @@ -64,9 +70,10 @@ private fun Content( ) { VerticalSpacer(16.dp) - message?.let { - BodyM(it, color = Colors.White64) - } + BodyM( + text = message ?: stringResource(R.string.wallet__payment_failed_description), + color = Colors.White64, + ) FillHeight() Image( @@ -85,13 +92,15 @@ private fun Content( SecondaryButton( text = stringResource(R.string.common__cancel), onClick = onClose, + enabled = !isRetrying, modifier = Modifier .weight(1f) .testTag("Close") ) PrimaryButton( - text = stringResource(R.string.common__try_again), + text = stringResource(R.string.common__retry), onClick = onRetry, + isLoading = isRetrying, modifier = Modifier .weight(1f) .testTag("Retry") @@ -109,6 +118,7 @@ private fun Preview() { AppThemeSurface { BottomSheetPreview { Content( + title = stringResource(R.string.wallet__send_error_tx_failed), message = stringResource(R.string.wallet__send_error_create_tx), modifier = Modifier.sheetHeight(), ) @@ -122,6 +132,7 @@ private fun PreviewUnknown() { AppThemeSurface { BottomSheetPreview { Content( + title = stringResource(R.string.wallet__toast_payment_failed_title), message = null, modifier = Modifier.sheetHeight(), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt index 201896a2c4..89abfa7bcf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendQuickPayScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -53,16 +52,16 @@ fun SendQuickPayScreen( } } - DisposableEffect(Unit) { - onDispose { - app.resetQuickPay() - } - } - LaunchedEffect(uiState.result) { when (val result = uiState.result) { - is QuickPayResult.Success -> onPaymentComplete(result.paymentHash, result.amountWithFee) - is QuickPayResult.Pending -> onPaymentPending(result.paymentHash, result.amount) + is QuickPayResult.Success -> { + app.resetQuickPay() + onPaymentComplete(result.paymentHash, result.amountWithFee) + } + is QuickPayResult.Pending -> { + app.resetQuickPay() + onPaymentPending(result.paymentHash, result.amount) + } is QuickPayResult.Error -> onShowError(result.message) null -> Unit // continue showing loading state } diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 5b801ee8aa..7d153b3d47 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -22,6 +23,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute +import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.R import to.bitkit.models.NewTransactionSheetDetails @@ -60,6 +62,7 @@ import to.bitkit.ui.utils.navigationWithDefaultTransitions import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.SendEffect import to.bitkit.viewmodels.SendEvent +import to.bitkit.viewmodels.SendMethod import to.bitkit.viewmodels.WalletViewModel @Suppress("CyclomaticComplexMethod") @@ -128,6 +131,7 @@ fun SendSheet( is SendEffect.NavigateToPending -> navController.navigateTo( SendRoute.Pending(it.paymentHash, it.amount) ) { popUpTo(startDestination) { inclusive = true } } + is SendEffect.NavigateToError -> navController.navigateTo(SendRoute.Error(it.message)) } } } @@ -320,13 +324,15 @@ fun SendSheet( }, onPaymentPending = { paymentHash, amount -> appViewModel.preserveContactPaymentContext(paymentHash) - navController.navigateTo(SendRoute.Pending(paymentHash, amount)) { + navController.navigateTo( + SendRoute.Pending(paymentHash, amount, SendRetryRoute.QuickPay) + ) { popUpTo(startDestination) { inclusive = true } } }, onShowError = { errorMessage -> appViewModel.clearActiveContactPaymentContext() - navController.navigateTo(SendRoute.Error(errorMessage)) + navController.navigateTo(SendRoute.Error(errorMessage, SendRetryRoute.QuickPay)) } ) } @@ -346,7 +352,7 @@ fun SendSheet( ) }, onPaymentError = { - navController.navigateTo(SendRoute.Error()) { + navController.navigateTo(SendRoute.Error(retryRoute = route.retryRoute)) { popUpTo { inclusive = true } } }, @@ -363,11 +369,23 @@ fun SendSheet( } composableWithDefaultTransitions { val route = it.toRoute() + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val isRetrying by walletViewModel.isRetryingLightningPayment.collectAsStateWithLifecycle() + val scope = rememberCoroutineScope() SendErrorScreen( + title = stringResource(route.failureTitle(sendUiState.payMethod)), message = route.message, + isRetrying = isRetrying, onRetry = { - navController.navigateTo(SendRoute.Recipient) { - popUpTo(navController.graph.id) { inclusive = true } + if (isRetrying) return@SendErrorScreen + scope.launch { + walletViewModel.resetPaymentRoutingCachesAndWait() + .onSuccess { + navController.navigateTo(route.retryRoute.sendRoute) { + popUpTo(navController.graph.id) { inclusive = true } + } + } + .onFailure { appViewModel.toast(it) } } }, onClose = { @@ -461,10 +479,17 @@ sealed interface SendRoute { data object ComingSoon : DeepLinkStart @Serializable - data class Pending(val paymentHash: String, val amount: Long) : InternalOnly + data class Pending( + val paymentHash: String, + val amount: Long, + val retryRoute: SendRetryRoute = SendRetryRoute.Confirm, + ) : InternalOnly @Serializable - data class Error(val message: String? = null) : InternalOnly + data class Error( + val message: String? = null, + val retryRoute: SendRetryRoute = SendRetryRoute.Confirm, + ) : InternalOnly companion object { private val DEEP_LINK_STARTS: List = listOf( @@ -483,3 +508,19 @@ sealed interface SendRoute { ScreenDeepLinks.matchStart(path, Recipient, DEEP_LINK_STARTS) } } + +@Serializable +enum class SendRetryRoute(val sendRoute: SendRoute) { + Confirm(SendRoute.Confirm), + QuickPay(SendRoute.QuickPay), +} + +private fun SendRoute.Error.failureTitle(payMethod: SendMethod): Int { + return when (retryRoute) { + SendRetryRoute.QuickPay -> R.string.wallet__toast_payment_failed_title + SendRetryRoute.Confirm -> when (payMethod) { + SendMethod.LIGHTNING -> R.string.wallet__toast_payment_failed_title + SendMethod.ONCHAIN -> R.string.wallet__send_error_tx_failed + } + } +} diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 6ce64be2ae..a1cad1bb99 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -81,6 +81,7 @@ import to.bitkit.domain.commands.NotifyPaymentReceived import to.bitkit.domain.commands.NotifyPaymentReceivedHandler import to.bitkit.env.Defaults import to.bitkit.env.Env +import to.bitkit.ext.PaymentFailureMessageContext import to.bitkit.ext.WatchResult import to.bitkit.ext.amountSats import to.bitkit.ext.callbackAmountMsats @@ -98,6 +99,7 @@ import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.setClipboardText import to.bitkit.ext.toHex +import to.bitkit.ext.toSendFailureMessage import to.bitkit.ext.toUserMessage import to.bitkit.ext.totalValue import to.bitkit.ext.walletId @@ -1129,8 +1131,11 @@ class AppViewModel @Inject constructor( val activePaymentHash = _sendUiState.value.decodedInvoice?.paymentHash?.toHex() if (_currentSheet.value !is Sheet.Send || activePaymentHash != paymentHash) return false - notifyPaymentFailed(reason) - hideSheet() + setSendEffect( + SendEffect.NavigateToError( + reason.toUserMessage(context, PaymentFailureMessageContext.SEND) + ) + ) return true } @@ -2705,8 +2710,9 @@ class AppViewModel @Inject constructor( preActivityMetadataRepo.deletePreActivityMetadata(createdMetadataPaymentId) } Logger.error("Error sending lightning payment", it, context = TAG) - toast(it) - hideSheet() + setSendEffect( + SendEffect.NavigateToError(it.toSendFailureMessage(context)) + ) } } } @@ -2900,7 +2906,9 @@ class AppViewModel @Inject constructor( when (it) { is Event.PaymentSuccessful if it.paymentHash == hash -> WatchResult.Complete(Result.success(hash)) is Event.PaymentFailed if it.paymentHash == hash -> WatchResult.Complete( - Result.failure(AppError(it.reason.toUserMessage(context))) + Result.failure( + AppError(it.reason.toUserMessage(context, PaymentFailureMessageContext.SEND)) + ) ) else -> WatchResult.Continue() @@ -3809,6 +3817,7 @@ sealed class SendEffect { data object NavigateToContacts : SendEffect() data object NavigateToComingSoon : SendEffect() data object PaymentSuccess : SendEffect() + data class NavigateToError(val message: String) : SendEffect() data class NavigateToPending(val paymentHash: String, val amount: Long) : SendEffect() } diff --git a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt index 0599d4ae42..c5750fbba1 100644 --- a/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/QuickPayViewModel.kt @@ -11,8 +11,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId +import to.bitkit.ext.PaymentFailureMessageContext import to.bitkit.ext.WatchResult import to.bitkit.ext.callbackAmountMsats +import to.bitkit.ext.toSendFailureMessage import to.bitkit.ext.toUserMessage import to.bitkit.ext.watchUntil import to.bitkit.repositories.LightningRepo @@ -54,7 +56,7 @@ class QuickPayViewModel @Inject constructor( ) .getOrElse { error -> _uiState.update { - it.copy(result = QuickPayResult.Error(error.message.orEmpty())) + it.copy(result = QuickPayResult.Error(error.toSendFailureMessage(context))) } return@launch } @@ -90,7 +92,7 @@ class QuickPayViewModel @Inject constructor( Logger.error("QuickPay lightning payment failed", error, context = TAG) _uiState.update { - it.copy(result = QuickPayResult.Error(error.message.orEmpty())) + it.copy(result = QuickPayResult.Error(error.toSendFailureMessage(context))) } } } @@ -111,7 +113,9 @@ class QuickPayViewModel @Inject constructor( when (it) { is Event.PaymentSuccessful if it.paymentHash == hash -> WatchResult.Complete(Result.success(hash)) is Event.PaymentFailed if it.paymentHash == hash -> WatchResult.Complete( - Result.failure(AppError(it.reason.toUserMessage(context))) + Result.failure( + AppError(it.reason.toUserMessage(context, PaymentFailureMessageContext.SEND)) + ) ) else -> WatchResult.Continue() diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index e08c0d3e9d..5c1495aa1e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -1065,7 +1065,7 @@ class TransferViewModel @Inject constructor( ToastEventBus.send( type = Toast.ToastType.ERROR, title = context.getString(R.string.common__error), - description = context.getString(R.string.wallet__toast_payment_failed_timeout), + description = context.getString(R.string.wallet__payment_timeout), ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 9c99ef1d77..56a8c8feac 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.ChannelDataMigration import org.lightningdevkit.ldknode.PeerDetails @@ -44,6 +45,7 @@ import to.bitkit.services.BoltzService import to.bitkit.services.MigrationService import to.bitkit.ui.onboarding.LOADING_MS import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.utils.AppError import to.bitkit.utils.Logger import to.bitkit.utils.isTxSyncTimeout import javax.inject.Inject @@ -104,6 +106,10 @@ class WalletViewModel @Inject constructor( private val _isRefreshing = MutableStateFlow(false) val isRefreshing = _isRefreshing.asStateFlow() + private val retryLightningPaymentMutex = Mutex() + private val _isRetryingLightningPayment = MutableStateFlow(false) + val isRetryingLightningPayment = _isRetryingLightningPayment.asStateFlow() + private var syncJob: Job? = null private var pendingWalletStart = false @@ -455,6 +461,18 @@ class WalletViewModel @Inject constructor( lightningRepo.syncState() } + suspend fun resetPaymentRoutingCachesAndWait(): Result { + if (!retryLightningPaymentMutex.tryLock()) return Result.failure(LightningPaymentRetryInProgressError()) + + _isRetryingLightningPayment.update { true } + return try { + lightningRepo.resetPaymentRoutingCachesAndWait() + } finally { + _isRetryingLightningPayment.update { false } + retryLightningPaymentMutex.unlock() + } + } + fun onPullToRefresh() { // Cancel any existing sync, manual or event triggered syncJob?.cancel() @@ -587,3 +605,5 @@ sealed interface RestoreState { fun isOngoing() = this is InProgress fun isIdle() = this is Initial || this is Settled } + +class LightningPaymentRetryInProgressError : AppError("Lightning payment retry already in progress") diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 02ad62e9f4..f06964c1f4 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -960,6 +960,18 @@ علامة جديدة أدخل علامة جديدة العلامات المستخدمة سابقًا + تم إيقاف دفعة Lightning قبل اكتمالها. + انتهت صلاحية دفعة Lightning هذه. اطلب فاتورة جديدة. + فشل دفعك الفوري. يرجى المحاولة مرة أخرى. + انتهت صلاحية طلب فاتورة Lightning هذا. اطلب فاتورة جديدة. + رفض المستلم طلب فاتورة Lightning هذا. + رفض المستلم دفعة Lightning هذه. تحقق من الفاتورة وحاول مرة أخرى. + جرّب Bitkit عدة مسارات Lightning، لكن تعذر إكمال الدفعة. + لم يتمكن Bitkit من العثور على مسار Lightning لهذه الدفعة. + انتهت مهلة الدفع. حاول مرة أخرى. + تستخدم فاتورة Lightning هذه ميزات لا يدعمها Bitkit بعد. + جرّب Bitkit عدة مسارات Lightning، لكن تعذر إكمال الدفعة. اضغط على إعادة المحاولة لتحديث توجيه المدفوعات وتجربة مسار جديد. + لم يتمكن Bitkit من العثور على مسار Lightning لهذه الدفعة. اضغط على إعادة المحاولة لتحديث توجيه المدفوعات والبحث عن مسار جديد. فشل دفعك الفوري. يرجى المحاولة مرة أخرى. فشل الدفع تم استبدال معاملتك المستلمة برفع الرسوم 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 7664e9a943..56c83713eb 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -960,6 +960,18 @@ Nueva etiqueta Introduzca una nueva etiqueta Etiquetas utilizadas anteriormente + El pago Lightning se detuvo antes de completarse. + Este pago Lightning venció. Solicite una factura nueva. + Su pago instantáneo ha fallado. Por favor, inténtelo de nuevo. + Esta solicitud de factura Lightning venció. Solicite una factura nueva. + El destinatario rechazó esta solicitud de factura Lightning. + El destinatario rechazó este pago Lightning. Revise la factura e inténtelo de nuevo. + Bitkit probó varias rutas Lightning, pero el pago no se pudo completar. + Bitkit no pudo encontrar una ruta Lightning para este pago. + El pago agotó el tiempo de espera. Inténtelo de nuevo. + Esta factura Lightning usa funciones que Bitkit aún no admite. + Bitkit probó varias rutas Lightning, pero el pago no se pudo completar. Toque Reintentar para actualizar el enrutamiento de pagos y probar una ruta nueva. + Bitkit no pudo encontrar una ruta Lightning para este pago. Toque Reintentar para actualizar el enrutamiento de pagos y buscar una ruta nueva. Su pago instantáneo ha fallado. Por favor, inténtelo de nuevo. Pago fallido Tu transacción entrante fue reemplazada al aumentar la comisión diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 61a2571fed..0aaf6fb561 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -960,6 +960,18 @@ Nova etiqueta Introdueix una nova etiqueta Etiquetes prèviament utilitzades + El pagament Lightning s\'ha aturat abans de completar-se. + Aquest pagament Lightning ha caducat. Demana una factura nova. + El teu pagament instantani ha fallat. Si us plau, torna-ho a provar. + Aquesta sol·licitud de factura Lightning ha caducat. Demana una factura nova. + El destinatari ha rebutjat aquesta sol·licitud de factura Lightning. + El destinatari ha rebutjat aquest pagament Lightning. Comprova la factura i torna-ho a provar. + Bitkit ha provat diverses rutes Lightning, però el pagament no s\'ha pogut completar. + Bitkit no ha pogut trobar cap ruta Lightning per a aquest pagament. + El pagament ha esgotat el temps d\'espera. Torna-ho a provar. + Aquesta factura Lightning utilitza funcions que Bitkit encara no admet. + Bitkit ha provat diverses rutes Lightning, però el pagament no s\'ha pogut completar. Toca Torna-ho a provar per actualitzar l\'encaminament de pagaments i provar una ruta nova. + Bitkit no ha pogut trobar cap ruta Lightning per a aquest pagament. Toca Torna-ho a provar per actualitzar l\'encaminament de pagaments i buscar una ruta nova. El teu pagament instantani ha fallat. Si us plau, torna-ho a provar. Pagament fallit La teva transacció rebuda ha estat substituïda per un augment de tarifa diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index d55526311d..a70bd206fc 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -960,6 +960,18 @@ Nový tag Vložte nový tag Dříve použité tagy + Lightning platba byla zastavena před dokončením. + Platnost této Lightning platby vypršela. Vyžádejte si novou fakturu. + Vaše okamžitá platba se nezdařila. Zkuste to prosím znovu. + Platnost této žádosti o Lightning fakturu vypršela. Vyžádejte si novou fakturu. + Příjemce tuto žádost o Lightning fakturu odmítl. + Příjemce tuto Lightning platbu odmítl. Zkontrolujte fakturu a zkuste to znovu. + Bitkit vyzkoušel několik Lightning tras, ale platbu se nepodařilo dokončit. + Bitkit nenašel pro tuto platbu žádnou Lightning trasu. + Časový limit platby vypršel. Zkuste to znovu. + Tato Lightning faktura používá funkce, které Bitkit zatím nepodporuje. + Bitkit vyzkoušel několik Lightning tras, ale platbu se nepodařilo dokončit. Klepněte na Opakovat, aby se aktualizovalo směrování plateb a zkusila se nová trasa. + Bitkit nenašel pro tuto platbu žádnou Lightning trasu. Klepněte na Opakovat, aby se aktualizovalo směrování plateb a zkusila se najít nová trasa. Vaše okamžitá platba se nezdařila. Zkuste to prosím znovu. Platba se nezdařila Vaše přijatá transakce byla nahrazena navýšením poplatku diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 778412f0b5..5be596fc6f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -838,6 +838,18 @@ Aktivität nach Tags filtern Tag auswählen Zahlung fehlgeschlagen + Die Lightning-Zahlung wurde gestoppt, bevor sie abgeschlossen wurde. + Diese Lightning-Zahlung ist abgelaufen. Bitte fordere eine neue Rechnung an. + Deine sofortige Zahlung ist fehlgeschlagen. Bitte versuche es erneut. + Diese Lightning-Rechnungsanfrage ist abgelaufen. Bitte fordere eine neue Rechnung an. + Der Empfänger hat diese Lightning-Rechnungsanfrage abgelehnt. + Der Empfänger hat diese Lightning-Zahlung abgelehnt. Bitte prüfe die Rechnung und versuche es erneut. + Bitkit hat mehrere Lightning-Routen ausprobiert, aber die Zahlung konnte nicht abgeschlossen werden. + Bitkit konnte keine Lightning-Route für diese Zahlung finden. + Zeitüberschreitung bei der Zahlung. Bitte versuche es erneut. + Diese Lightning-Rechnung verwendet Funktionen, die Bitkit noch nicht unterstützt. + Bitkit hat mehrere Lightning-Routen ausprobiert, aber die Zahlung konnte nicht abgeschlossen werden. Tippe auf Wiederholen, um das Zahlungsrouting zu aktualisieren und einen neuen Pfad zu probieren. + Bitkit konnte keine Lightning-Route für diese Zahlung finden. Tippe auf Wiederholen, um das Zahlungsrouting zu aktualisieren und nach einem neuen Pfad zu suchen. Deine sofortige Zahlung ist fehlgeschlagen. Bitte versuche es erneut. Empfangene Transaktion ersetzt Deine empfangene Transaktion wurde durch eine Gebührenerhöhung ersetzt diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 70fe46fce4..36571abe4a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -960,6 +960,18 @@ Νέα ετικέτα Εισάγαγε νέα ετικέτα Προηγούμενες ετικέτες + Η πληρωμή Lightning σταμάτησε πριν ολοκληρωθεί. + Αυτή η πληρωμή Lightning έληξε. Ζητήστε νέο τιμολόγιο. + Η άμεση πληρωμή σας απέτυχε. Παρακαλώ δοκιμάστε ξανά. + Αυτό το αίτημα τιμολογίου Lightning έληξε. Ζητήστε νέο τιμολόγιο. + Ο παραλήπτης απέρριψε αυτό το αίτημα τιμολογίου Lightning. + Ο παραλήπτης απέρριψε αυτήν την πληρωμή Lightning. Ελέγξτε το τιμολόγιο και δοκιμάστε ξανά. + Το Bitkit δοκίμασε αρκετές διαδρομές Lightning, αλλά η πληρωμή δεν μπόρεσε να ολοκληρωθεί. + Το Bitkit δεν μπόρεσε να βρει διαδρομή Lightning για αυτήν την πληρωμή. + Το χρονικό όριο πληρωμής έληξε. Δοκιμάστε ξανά. + Αυτό το τιμολόγιο Lightning χρησιμοποιεί λειτουργίες που το Bitkit δεν υποστηρίζει ακόμη. + Το Bitkit δοκίμασε αρκετές διαδρομές Lightning, αλλά η πληρωμή δεν μπόρεσε να ολοκληρωθεί. Πατήστε Επανάληψη για να ενημερωθεί η δρομολόγηση πληρωμών και να δοκιμαστεί νέα διαδρομή. + Το Bitkit δεν μπόρεσε να βρει διαδρομή Lightning για αυτήν την πληρωμή. Πατήστε Επανάληψη για να ενημερωθεί η δρομολόγηση πληρωμών και να αναζητηθεί νέα διαδρομή. Η άμεση πληρωμή σου απέτυχε. Δοκίμασε ξανά. Αποτυχία πληρωμής Η εισερχόμενη συναλλαγή σου αντικαταστάθηκε από αύξηση τέλους diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 67310bbbf6..ca980ba63e 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -960,6 +960,18 @@ Nueva etiqueta Introduzca una nueva etiqueta Etiquetas utilizadas anteriormente + El pago Lightning se detuvo antes de completarse. + Este pago Lightning ha caducado. Solicita una factura nueva. + Tu pago instantáneo falló. Por favor, inténtalo de nuevo. + Esta solicitud de factura Lightning ha caducado. Solicita una factura nueva. + El destinatario ha rechazado esta solicitud de factura Lightning. + El destinatario ha rechazado este pago Lightning. Comprueba la factura e inténtalo de nuevo. + Bitkit ha probado varias rutas Lightning, pero el pago no se ha podido completar. + Bitkit no ha podido encontrar una ruta Lightning para este pago. + El pago agotó el tiempo de espera. Inténtalo de nuevo. + Esta factura Lightning usa funciones que Bitkit aún no admite. + Bitkit ha probado varias rutas Lightning, pero el pago no se ha podido completar. Toca Reintentar para actualizar el enrutamiento de pagos y probar una ruta nueva. + Bitkit no ha podido encontrar una ruta Lightning para este pago. Toca Reintentar para actualizar el enrutamiento de pagos y buscar una ruta nueva. Tu pago instantáneo falló. Por favor, inténtalo de nuevo. Pago fallido Tu transacción recibida fue reemplazada por un aumento de comisión diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2622d91547..7fe23529f9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -832,6 +832,18 @@ Introduzca una nueva etiqueta Etiquetas utilizadas anteriormente Filtrar la actividad mediante etiquetas + El pago Lightning se detuvo antes de completarse. + Este pago Lightning ha caducado. Solicita una factura nueva. + Tu pago instantáneo falló. Por favor, inténtalo de nuevo. + Esta solicitud de factura Lightning ha caducado. Solicita una factura nueva. + El destinatario ha rechazado esta solicitud de factura Lightning. + El destinatario ha rechazado este pago Lightning. Comprueba la factura e inténtalo de nuevo. + Bitkit ha probado varias rutas Lightning, pero el pago no se ha podido completar. + Bitkit no ha podido encontrar una ruta Lightning para este pago. + El pago agotó el tiempo de espera. Inténtalo de nuevo. + Esta factura Lightning usa funciones que Bitkit aún no admite. + Bitkit ha probado varias rutas Lightning, pero el pago no se ha podido completar. Toca Reintentar para actualizar el enrutamiento de pagos y probar una ruta nueva. + Bitkit no ha podido encontrar una ruta Lightning para este pago. Toca Reintentar para actualizar el enrutamiento de pagos y buscar una ruta nueva. Tu pago instantáneo ha fallado. Por favor, inténtalo de nuevo. Pago Fallido Tu transacción recibida fue reemplazada por un aumento de comisión diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0b663cbae7..b7afc8d2e2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -834,6 +834,18 @@ Filtrer l\'activité à l\'aide de tags Sélectionnez un Tag Échec du paiement + Le paiement Lightning a été arrêté avant d\'être terminé. + Ce paiement Lightning a expiré. Demandez une nouvelle facture. + Votre paiement instantané a échoué. Veuillez réessayer. + Cette demande de facture Lightning a expiré. Demandez une nouvelle facture. + Le destinataire a rejeté cette demande de facture Lightning. + Le destinataire a rejeté ce paiement Lightning. Vérifiez la facture et réessayez. + Bitkit a essayé plusieurs routes Lightning, mais le paiement n\'a pas pu être terminé. + Bitkit n\'a pas trouvé de route Lightning pour ce paiement. + Le paiement a expiré. Veuillez réessayer. + Cette facture Lightning utilise des fonctionnalités que Bitkit ne prend pas encore en charge. + Bitkit a essayé plusieurs routes Lightning, mais le paiement n\'a pas pu être terminé. Touchez Réessayer pour mettre à jour le routage des paiements et essayer un nouveau chemin. + Bitkit n\'a pas trouvé de route Lightning pour ce paiement. Touchez Réessayer pour mettre à jour le routage des paiements et rechercher un nouveau chemin. Votre paiement instantané a échoué. Veuillez réessayer. Votre transaction reçue a été remplacée par une augmentation de frais Transaction reçue remplacée diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f56f3f570c..c68c48ba19 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -960,6 +960,18 @@ Nuovo tag Inserisci un nuovo tag Tag usati in precedenza + Il pagamento Lightning è stato interrotto prima del completamento. + Questo pagamento Lightning è scaduto. Richiedi una nuova fattura. + Il tuo pagamento istantaneo non è riuscito. Per favore riprova. + Questa richiesta di fattura Lightning è scaduta. Richiedi una nuova fattura. + Il destinatario ha rifiutato questa richiesta di fattura Lightning. + Il destinatario ha rifiutato questo pagamento Lightning. Controlla la fattura e riprova. + Bitkit ha provato diverse rotte Lightning, ma il pagamento non è stato completato. + Bitkit non ha trovato una rotta Lightning per questo pagamento. + Il pagamento è scaduto. Riprova. + Questa fattura Lightning usa funzionalità che Bitkit non supporta ancora. + Bitkit ha provato diverse rotte Lightning, ma il pagamento non è stato completato. Tocca Riprova per aggiornare l\'instradamento dei pagamenti e provare un nuovo percorso. + Bitkit non ha trovato una rotta Lightning per questo pagamento. Tocca Riprova per aggiornare l\'instradamento dei pagamenti e cercare un nuovo percorso. Il tuo pagamento istantaneo non è riuscito. Per favore riprova. Pagamento fallito La tua transazione ricevuta è stata sostituita da un aumento di commissione diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5fcd184f99..96a29e038a 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -960,6 +960,18 @@ Nieuwe tag Voer een nieuwe tag in Eerder gebruikte tags + De Lightning-betaling is gestopt voordat deze was voltooid. + Deze Lightning-betaling is verlopen. Vraag een nieuwe factuur aan. + Uw directe betaling is mislukt. Probeer het opnieuw. + Deze Lightning-factuuraanvraag is verlopen. Vraag een nieuwe factuur aan. + De ontvanger heeft deze Lightning-factuuraanvraag geweigerd. + De ontvanger heeft deze Lightning-betaling geweigerd. Controleer de factuur en probeer het opnieuw. + Bitkit heeft meerdere Lightning-routes geprobeerd, maar de betaling kon niet worden voltooid. + Bitkit kon geen Lightning-route voor deze betaling vinden. + Time-out voor betaling. Probeer het opnieuw. + Deze Lightning-factuur gebruikt functies die Bitkit nog niet ondersteunt. + Bitkit heeft meerdere Lightning-routes geprobeerd, maar de betaling kon niet worden voltooid. Tik op Opnieuw proberen om betalingsroutering bij te werken en een nieuw pad te proberen. + Bitkit kon geen Lightning-route voor deze betaling vinden. Tik op Opnieuw proberen om betalingsroutering bij te werken en naar een nieuw pad te zoeken. Je directe betaling is mislukt. Probeer het opnieuw. Betaling mislukt Je ontvangen transactie is vervangen door een vergoedingsverhoging diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 6f84a53c0e..087f92ced2 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -834,6 +834,18 @@ Filtruj aktywność za pomocą tagów Wybierz tag Płatność nie powiodła się + Płatność Lightning została zatrzymana przed zakończeniem. + Ta płatność Lightning wygasła. Poproś o nową fakturę. + Natychmiastowa płatność nie powiodła się. Proszę spróbować ponownie. + To żądanie faktury Lightning wygasło. Poproś o nową fakturę. + Odbiorca odrzucił to żądanie faktury Lightning. + Odbiorca odrzucił tę płatność Lightning. Sprawdź fakturę i spróbuj ponownie. + Bitkit wypróbował kilka tras Lightning, ale płatności nie udało się ukończyć. + Bitkit nie znalazł trasy Lightning dla tej płatności. + Przekroczono limit czasu płatności. Spróbuj ponownie. + Ta faktura Lightning używa funkcji, których Bitkit jeszcze nie obsługuje. + Bitkit wypróbował kilka tras Lightning, ale płatności nie udało się ukończyć. Stuknij Ponów, aby zaktualizować routing płatności i spróbować nowej ścieżki. + Bitkit nie znalazł trasy Lightning dla tej płatności. Stuknij Ponów, aby zaktualizować routing płatności i wyszukać nową ścieżkę. Natychmiastowa płatność nie powiodła się. Proszę spróbować ponownie. Twoja otrzymana transakcja została zastąpiona przez przyspieszenie opłaty Otrzymana transakcja zastąpiona diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index ed9a98bc6b..255a0239e6 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -960,6 +960,18 @@ Nova Tag Inserir uma nova tag Tags usadas anteriormente + O pagamento Lightning foi interrompido antes de ser concluído. + Este pagamento Lightning expirou. Solicite uma nova fatura. + Seu pagamento instantâneo falhou. Por favor, tente novamente. + Esta solicitação de fatura Lightning expirou. Solicite uma nova fatura. + O destinatário rejeitou esta solicitação de fatura Lightning. + O destinatário rejeitou este pagamento Lightning. Verifique a fatura e tente novamente. + O Bitkit tentou várias rotas Lightning, mas o pagamento não pôde ser concluído. + O Bitkit não conseguiu encontrar uma rota Lightning para este pagamento. + O pagamento expirou. Tente novamente. + Esta fatura Lightning usa recursos que o Bitkit ainda não oferece suporte. + O Bitkit tentou várias rotas Lightning, mas o pagamento não pôde ser concluído. Toque em Tentar novamente para atualizar o roteamento de pagamentos e tentar um novo caminho. + O Bitkit não conseguiu encontrar uma rota Lightning para este pagamento. Toque em Tentar novamente para atualizar o roteamento de pagamentos e procurar um novo caminho. Seu pagamento instantâneo falhou. Por favor, tente novamente. Pagamento Falhou Sua transação recebida foi substituída por um aumento de taxa diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 3e2f0e0a9b..36d213aae6 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -800,6 +800,18 @@ Filtrar atividades usando tags Selecionar Tag Pagamento Falhou + O pagamento Lightning foi interrompido antes de ser concluído. + Este pagamento Lightning expirou. Peça uma nova fatura. + Seu pagamento instantâneo falhou. Por favor, tente novamente. + Este pedido de fatura Lightning expirou. Peça uma nova fatura. + O destinatário rejeitou este pedido de fatura Lightning. + O destinatário rejeitou este pagamento Lightning. Verifique a fatura e tente novamente. + O Bitkit tentou várias rotas Lightning, mas o pagamento não pôde ser concluído. + O Bitkit não conseguiu encontrar uma rota Lightning para este pagamento. + O pagamento expirou. Tente novamente. + Esta fatura Lightning usa funcionalidades que o Bitkit ainda não suporta. + O Bitkit tentou várias rotas Lightning, mas o pagamento não pôde ser concluído. Toque em Tentar novamente para atualizar o encaminhamento de pagamentos e tentar um novo caminho. + O Bitkit não conseguiu encontrar uma rota Lightning para este pagamento. Toque em Tentar novamente para atualizar o encaminhamento de pagamentos e procurar um novo caminho. Seu pagamento instantâneo falhou. Por favor, tente novamente. Seleção de Moedas Auto diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ac7ef8c831..7f57370085 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -988,6 +988,18 @@ Bitkit должен увеличить приёмную ёмкость ваше Новый тег Введите тег Ранее использовавшиеся теги + Платеж Lightning был остановлен до завершения. + Срок действия этого платежа Lightning истек. Запросите новый счет. + Ваш мгновенный платеж не удался. Пожалуйста, попробуйте снова. + Срок действия этого запроса счета Lightning истек. Запросите новый счет. + Получатель отклонил этот запрос счета Lightning. + Получатель отклонил этот платеж Lightning. Проверьте счет и попробуйте снова. + Bitkit попробовал несколько маршрутов Lightning, но платеж не удалось завершить. + Bitkit не смог найти маршрут Lightning для этого платежа. + Время ожидания платежа истекло. Попробуйте еще раз. + Этот счет Lightning использует функции, которые Bitkit пока не поддерживает. + Bitkit попробовал несколько маршрутов Lightning, но платеж не удалось завершить. Нажмите Повторить, чтобы обновить маршрутизацию платежей и попробовать новый путь. + Bitkit не смог найти маршрут Lightning для этого платежа. Нажмите Повторить, чтобы обновить маршрутизацию платежей и найти новый путь. Ваш мгновенный платеж не удался. Пожалуйста, попробуйте снова. Платеж не выполнен Ваша полученная транзакция была заменена повышением комиссии diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1af6e9845f..f8e13f61b1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1149,8 +1149,18 @@ MINIMUM Note Received Bitcoin + The Lightning payment was stopped before it completed. + This Lightning payment expired. Please request a new invoice. + Your instant payment failed. Please try again. + This Lightning invoice request expired. Please request a new invoice. + The recipient rejected this Lightning invoice request. + The recipient rejected this Lightning payment. Please check the invoice and try again. Payment Request The payment details did not match the request. Payment cancelled. + Bitkit tried several Lightning routes, but the payment could not be completed. + Bitkit couldn\'t find a Lightning route for this payment. + Payment timed out. Please try again. + This Lightning invoice uses features Bitkit does not support yet. Peer disconnected. Receive Receive Lightning funds @@ -1225,6 +1235,8 @@ Reserve Balance This payment is taking a bit longer than expected. You can continue using Bitkit. Payment Pending + Bitkit tried several Lightning routes, but the payment could not be completed. Tap Retry to update payment routing and try a new path. + Bitkit couldn\'t find a Lightning route for this payment. Tap Retry to update payment routing and check for a new path. QuickPay Paying\n<accent>invoice...</accent> Confirm @@ -1243,10 +1255,6 @@ Enter a new tag Previously used tags Your instant payment failed. Please try again. - The recipient rejected this payment. Try a different amount. - Could not find a route with sufficient liquidity. Try a smaller amount or wait and try again. - Could not find a payment path to the recipient. - Payment timed out. Please try again. Payment Failed Your instant payment was sent successfully. Payment Sent diff --git a/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt b/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt new file mode 100644 index 0000000000..ccef490748 --- /dev/null +++ b/app/src/test/java/to/bitkit/ext/PaymentFailureReasonExtTest.kt @@ -0,0 +1,44 @@ +package to.bitkit.ext + +import android.content.Context +import org.junit.Test +import org.lightningdevkit.ldknode.PaymentFailureReason +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import to.bitkit.R +import kotlin.test.assertEquals + +class PaymentFailureReasonExtTest { + private val context = mock() + + @Test + fun `route failures use generic or send copy based on context`() { + val generic = "Generic route message" + val send = "Send route message" + whenever(context.getString(R.string.wallet__payment_route_not_found)).thenReturn(generic) + whenever(context.getString(R.string.wallet__send_payment_route_not_found)).thenReturn(send) + + assertEquals(generic, PaymentFailureReason.ROUTE_NOT_FOUND.toUserMessage(context)) + assertEquals( + send, + PaymentFailureReason.ROUTE_NOT_FOUND.toUserMessage(context, PaymentFailureMessageContext.SEND), + ) + } + + @Test + fun `unmapped reasons fall back to generic payment failure copy`() { + val message = "Generic payment failed" + whenever(context.getString(R.string.wallet__payment_failed_description)).thenReturn(message) + + assertEquals(message, PaymentFailureReason.UNEXPECTED_ERROR.toUserMessage(context)) + assertEquals(message, (null as PaymentFailureReason?).toUserMessage(context, PaymentFailureMessageContext.SEND)) + } + + @Test + fun `send failure messages fall back when exception message is blank`() { + val message = "Generic payment failed" + whenever(context.getString(R.string.wallet__payment_failed_description)).thenReturn(message) + + assertEquals(message, Exception(" ").toSendFailureMessage(context)) + } +} diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 181f882999..426f55cffe 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -29,6 +29,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.lightningdevkit.ldknode.Event +import org.lightningdevkit.ldknode.PaymentFailureReason import org.lightningdevkit.ldknode.TransactionDetails import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -1345,9 +1346,11 @@ class AppViewModelSendFlowTest : BaseUnitTest() { } @Test - fun `active lightning send failure hides send sheet`() = test { + fun `active lightning send failure navigates to failure screen`() = test { val bolt11 = "lnbcrt1activefailure" val paymentHash = "010203" + val errorMessage = "Bitkit could not find a route" + whenever(context.getString(R.string.wallet__send_payment_route_not_found)).thenReturn(errorMessage) whenever(pendingPaymentRepo.isPending(paymentHash)).thenReturn(false) setSendState( SendUiState( @@ -1360,16 +1363,18 @@ class AppViewModelSendFlowTest : BaseUnitTest() { sut.showSheet(Sheet.Send()) advanceUntilIdle() - emitNodeEvent( - Event.PaymentFailed( - paymentId = "payment_id", - paymentHash = paymentHash, - reason = null, - ), - ) - advanceUntilIdle() + sut.sendEffect.test { + emitNodeEvent( + Event.PaymentFailed( + paymentId = "payment_id", + paymentHash = paymentHash, + reason = PaymentFailureReason.ROUTE_NOT_FOUND, + ), + ) + advanceUntilIdle() - assertNull(sut.currentSheet.value) + assertEquals(SendEffect.NavigateToError(errorMessage), awaitItem()) + } } @Test diff --git a/app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt index 7b6c796b14..67d640f8b9 100644 --- a/app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/TransferViewModelTest.kt @@ -1427,7 +1427,7 @@ class TransferViewModelTest : BaseUnitTest() { @Test fun `startSavingsSwap fails when the paid invoice reports a lightning routing failure`() = test { stubSavingsSwapHappyPath() - whenever(context.getString(R.string.wallet__toast_payment_failed_route_not_found)) + whenever(context.getString(R.string.wallet__payment_route_not_found)) .thenReturn(ROUTE_NOT_FOUND_MSG) sut.loadSavingsSwapQuote(REQUESTED_SAT) advanceUntilIdle() diff --git a/changelog.d/next/1137.feat.md b/changelog.d/next/1137.feat.md new file mode 100644 index 0000000000..08e36e9883 --- /dev/null +++ b/changelog.d/next/1137.feat.md @@ -0,0 +1 @@ +Improved Lightning send failure recovery with clearer messages and a retry action that refreshes payment routing.