diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj
index 35ab2455b..3ab3214ed 100644
--- a/Bitkit.xcodeproj/project.pbxproj
+++ b/Bitkit.xcodeproj/project.pbxproj
@@ -161,6 +161,7 @@
Constants/Env.swift,
Extensions/HexBytes.swift,
"Extensions/LDKNode+AddressType.swift",
+ "Extensions/PaymentFailureReason+UserMessage.swift",
Extensions/PaymentDetails.swift,
Models/BlocksWidgetOptions.swift,
Models/BlocktankNotificationType.swift,
@@ -195,6 +196,7 @@
Constants/Env.swift,
Extensions/HexBytes.swift,
"Extensions/LDKNode+AddressType.swift",
+ "Extensions/PaymentFailureReason+UserMessage.swift",
Extensions/PaymentDetails.swift,
Models/BlocktankNotificationType.swift,
Models/LnPeer.swift,
diff --git a/Bitkit/Components/SwipeButton.swift b/Bitkit/Components/SwipeButton.swift
index 2190ec9bc..0588de453 100644
--- a/Bitkit/Components/SwipeButton.swift
+++ b/Bitkit/Components/SwipeButton.swift
@@ -12,7 +12,9 @@ struct SwipeButton: View {
@State private var offset: CGFloat = 0
@State private var isSubmitting = false
- private var isBusy: Bool { isLoading || isSubmitting }
+ private var isBusy: Bool {
+ isLoading || isSubmitting
+ }
private let buttonHeight: CGFloat = 76
private let innerPadding: CGFloat = 16
@@ -94,17 +96,9 @@ struct SwipeButton: View {
Task { @MainActor in
do {
try await onComplete()
+ reset()
} catch {
- // Reset the slider back to the start on error
- withAnimation(.spring(duration: 0.3)) {
- offset = 0
- swipeProgress?.wrappedValue = 0
- }
-
- // Adjust the delay to match animation duration
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
- isSubmitting = false
- }
+ reset()
}
}
} else {
@@ -120,6 +114,17 @@ struct SwipeButton: View {
.frame(height: buttonHeight)
}
+ private func reset() {
+ withAnimation(.spring(duration: 0.3)) {
+ offset = 0
+ swipeProgress?.wrappedValue = 0
+ }
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
+ isSubmitting = false
+ }
+ }
+
private var backgroundGradient: LinearGradient {
let colors: [Color] = [Color(hex: 0x2A2A2A), Color(hex: 0x1C1C1C)]
return LinearGradient(colors: colors, startPoint: .top, endPoint: .bottom)
diff --git a/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift b/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift
index 6ebf71829..b57694ad3 100644
--- a/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift
+++ b/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift
@@ -5,17 +5,38 @@ extension PaymentFailureReason {
/// `PaymentFailureReason.toUserMessage`; reasons without a dedicated string fall back to
/// the generic payment-failed description.
static func userMessage(for reason: PaymentFailureReason?) -> String {
+ t(userMessageKey(for: reason))
+ }
+
+ static func userMessageKey(for reason: PaymentFailureReason?) -> String {
switch reason {
case .recipientRejected:
- return t("wallet__toast_payment_failed_recipient_rejected")
+ return "wallet__payment_recipient_rejected"
+ case .userAbandoned:
+ return "wallet__payment_abandoned"
case .retriesExhausted:
- return t("wallet__toast_payment_failed_retries_exhausted")
- case .routeNotFound:
- return t("wallet__toast_payment_failed_route_not_found")
+ return "wallet__payment_retries_exhausted"
case .paymentExpired:
- return t("wallet__toast_payment_failed_timeout")
+ return "wallet__payment_expired"
+ case .routeNotFound:
+ return "wallet__payment_route_not_found"
+ case .unknownRequiredFeatures:
+ return "wallet__payment_unknown_required_features"
+ case .invoiceRequestExpired:
+ return "wallet__payment_invoice_request_expired"
+ case .invoiceRequestRejected:
+ return "wallet__payment_invoice_request_rejected"
+ default:
+ return "wallet__payment_failed_description"
+ }
+ }
+
+ var shouldResetRoutingCachesOnRetry: Bool {
+ switch self {
+ case .routeNotFound, .retriesExhausted:
+ return true
default:
- return t("wallet__toast_payment_failed_description")
+ return false
}
}
}
diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift
index 215d58ace..439e3d7b6 100644
--- a/Bitkit/MainNavView.swift
+++ b/Bitkit/MainNavView.swift
@@ -561,7 +561,7 @@ struct MainNavView: View {
case .reset: ResetScreen()
// Support settings
- case .reportIssue: ReportIssue()
+ case let .reportIssue(prefill): ReportIssue(prefill: prefill)
case .appStatus: AppStatusView()
// Advanced settings
diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings
index 58349945b..03065b3f0 100644
--- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings
@@ -1,4 +1,13 @@
"common__delete_yes" = "نعم، احذف";
+"wallet__payment_recipient_rejected" = "رفض المستلم دفعة Lightning هذه. تحقق من الفاتورة وحاول مرة أخرى.";
+"wallet__payment_abandoned" = "تم إيقاف دفعة Lightning قبل اكتمالها.";
+"wallet__payment_expired" = "انتهت صلاحية دفعة Lightning هذه. اطلب فاتورة جديدة.";
+"wallet__payment_timeout" = "انتهت مهلة الدفع. حاول مرة أخرى.";
+"wallet__payment_route_not_found" = "لم يتمكن Bitkit من العثور على مسار Lightning لهذه الدفعة.";
+"wallet__payment_retries_exhausted" = "جرّب Bitkit عدة مسارات Lightning، لكن تعذر إكمال الدفعة.";
+"wallet__payment_unknown_required_features" = "تستخدم فاتورة Lightning هذه ميزات لا يدعمها Bitkit بعد.";
+"wallet__payment_invoice_request_expired" = "انتهت صلاحية طلب فاتورة Lightning هذا. اطلب فاتورة جديدة.";
+"wallet__payment_invoice_request_rejected" = "رفض المستلم طلب فاتورة Lightning هذا.";
"settings__backup__category_contacts" = "جهات الاتصال";
"slashtags__your_name" = "اسمك";
"slashtags__your_name_capital" = "اسمك";
@@ -59,3 +68,4 @@
"widgets__weather__current_fee" = "متوسط الرسوم الحالي";
"widgets__weather__next_block" = "تضمين الكتلة التالية";
"wallet__payment_request" = "طلب دفع";
+"wallet__send_error_support" = "الدعم";
diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings
index 80ea17aa7..2a33bf72d 100644
--- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings
@@ -850,7 +850,16 @@
"wallet__tags_no" = "Cap etiqueta disponible encara";
"wallet__toast_payment_success_title" = "Pagament enviat";
"wallet__toast_payment_failed_title" = "Pagament fallit";
-"wallet__toast_payment_failed_description" = "El teu pagament instantani ha fallat. Si us plau, torna-ho a provar.";
+"wallet__payment_failed_description" = "El teu pagament instantani ha fallat. Si us plau, torna-ho a provar.";
+"wallet__payment_recipient_rejected" = "El destinatari ha rebutjat aquest pagament Lightning. Comprova la factura i torna-ho a provar.";
+"wallet__payment_abandoned" = "El pagament Lightning s'ha aturat abans de completar-se.";
+"wallet__payment_expired" = "Aquest pagament Lightning ha caducat. Demana una factura nova.";
+"wallet__payment_timeout" = "El pagament ha esgotat el temps d'espera. Torna-ho a provar.";
+"wallet__payment_route_not_found" = "Bitkit no ha pogut trobar cap ruta Lightning per a aquest pagament.";
+"wallet__payment_retries_exhausted" = "Bitkit ha provat diverses rutes Lightning, però el pagament no s'ha pogut completar.";
+"wallet__payment_unknown_required_features" = "Aquesta factura Lightning utilitza funcions que Bitkit encara no admet.";
+"wallet__payment_invoice_request_expired" = "Aquesta sol·licitud de factura Lightning ha caducat. Demana una factura nova.";
+"wallet__payment_invoice_request_rejected" = "El destinatari ha rebutjat aquesta sol·licitud de factura Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Transacció rebuda substituïda";
"wallet__toast_received_transaction_replaced_description" = "La teva transacció rebuda s\'ha substituït per un augment de comissió";
"wallet__toast_transaction_replaced_title" = "Transacció substituïda";
@@ -1015,3 +1024,4 @@
"widgets__weather__condition__good__short_title" = "Favorables";
"widgets__weather__condition__average__short_title" = "Mitjanes";
"widgets__weather__condition__poor__short_title" = "Dolentes";
+"wallet__send_error_support" = "Contacta amb el suport";
diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings
index e39522383..d24013cb8 100644
--- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings
@@ -936,6 +936,7 @@
"wallet__send_error_slash_ln" = "Bohužel kontakt nemohl obdržet okamžitou platbu. Můžete zkusit běžnou platbu (dražší, pomalejší).";
"wallet__send_error_tx_failed" = "Transakce selhala";
"wallet__send_error_create_tx" = "Transakci se nepodařilo provést. Zkuste to prosím znovu.";
+"wallet__send_error_support" = "Kontaktovat podporu";
"wallet__tag_remove_error_title" = "Odstranění značky se nezdařilo";
"wallet__tag_remove_error_description" = "Bitkit se nepodařilo najít data transakce.";
"wallet__error_no_invoice" = "Nebyla nalezena žádná lightningová faktura.";
@@ -979,7 +980,16 @@
"wallet__toast_payment_success_title" = "Platba odeslána";
"wallet__toast_payment_success_description" = "Vaše okamžitá platba byla úspěšně odeslána.";
"wallet__toast_payment_failed_title" = "Platba se nezdařila";
-"wallet__toast_payment_failed_description" = "Vaše okamžitá platba se nezdařila. Zkuste to prosím znovu.";
+"wallet__payment_failed_description" = "Vaše okamžitá platba se nezdařila. Zkuste to prosím znovu.";
+"wallet__payment_recipient_rejected" = "Příjemce tuto Lightning platbu odmítl. Zkontrolujte fakturu a zkuste to znovu.";
+"wallet__payment_abandoned" = "Lightning platba byla zastavena před dokončením.";
+"wallet__payment_expired" = "Platnost této Lightning platby vypršela. Vyžádejte si novou fakturu.";
+"wallet__payment_timeout" = "Časový limit platby vypršel. Zkuste to znovu.";
+"wallet__payment_route_not_found" = "Bitkit nenašel pro tuto platbu žádnou Lightning trasu.";
+"wallet__payment_retries_exhausted" = "Bitkit vyzkoušel několik Lightning tras, ale platbu se nepodařilo dokončit.";
+"wallet__payment_unknown_required_features" = "Tato Lightning faktura používá funkce, které Bitkit zatím nepodporuje.";
+"wallet__payment_invoice_request_expired" = "Platnost této žádosti o Lightning fakturu vypršela. Vyžádejte si novou fakturu.";
+"wallet__payment_invoice_request_rejected" = "Příjemce tuto žádost o Lightning fakturu odmítl.";
"wallet__toast_received_transaction_replaced_title" = "Přijatá transakce nahrazena";
"wallet__toast_received_transaction_replaced_description" = "Vaše přijatá transakce byla nahrazena navýšením poplatku";
"wallet__toast_transaction_replaced_title" = "Transakce nahrazena";
diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings
index 12432122b..58d0dbb5b 100644
--- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings
@@ -933,6 +933,7 @@
"wallet__send_error_slash_ln" = "Dieser Kontakt konnte leider nicht sofort bezahlt werden. Du kannst eine reguläre Zahlung versuchen (teurer, langsamer).";
"wallet__send_error_tx_failed" = "Transaktion fehlgeschlagen";
"wallet__send_error_create_tx" = "Die Transaktion konnte nicht gesendet werden. Bitte versuche es erneut.";
+"wallet__send_error_support" = "Support kontaktieren";
"wallet__tag_remove_error_title" = "Tag entfernen fehlgeschlagen";
"wallet__tag_remove_error_description" = "Bitkit konnte die Transaktionsdaten nicht finden.";
"wallet__error_no_invoice" = "Keine Lightning-Rechnung gefunden.";
@@ -976,7 +977,16 @@
"wallet__toast_payment_success_title" = "Zahlung gesendet";
"wallet__toast_payment_success_description" = "Deine sofortige Zahlung wurde erfolgreich gesendet.";
"wallet__toast_payment_failed_title" = "Zahlung fehlgeschlagen";
-"wallet__toast_payment_failed_description" = "Deine sofortige Zahlung ist fehlgeschlagen. Bitte versuche es erneut.";
+"wallet__payment_failed_description" = "Deine sofortige Zahlung ist fehlgeschlagen. Bitte versuche es erneut.";
+"wallet__payment_recipient_rejected" = "Der Empfänger hat diese Lightning-Zahlung abgelehnt. Bitte prüfe die Rechnung und versuche es erneut.";
+"wallet__payment_abandoned" = "Die Lightning-Zahlung wurde gestoppt, bevor sie abgeschlossen wurde.";
+"wallet__payment_expired" = "Diese Lightning-Zahlung ist abgelaufen. Bitte fordere eine neue Rechnung an.";
+"wallet__payment_timeout" = "Zeitüberschreitung bei der Zahlung. Bitte versuche es erneut.";
+"wallet__payment_route_not_found" = "Bitkit konnte keine Lightning-Route für diese Zahlung finden.";
+"wallet__payment_retries_exhausted" = "Bitkit hat mehrere Lightning-Routen ausprobiert, aber die Zahlung konnte nicht abgeschlossen werden.";
+"wallet__payment_unknown_required_features" = "Diese Lightning-Rechnung verwendet Funktionen, die Bitkit noch nicht unterstützt.";
+"wallet__payment_invoice_request_expired" = "Diese Lightning-Rechnungsanfrage ist abgelaufen. Bitte fordere eine neue Rechnung an.";
+"wallet__payment_invoice_request_rejected" = "Der Empfänger hat diese Lightning-Rechnungsanfrage abgelehnt.";
"wallet__toast_received_transaction_replaced_title" = "Empfangene Transaktion ersetzt";
"wallet__toast_received_transaction_replaced_description" = "Deine empfangene Transaktion wurde durch eine Gebührenerhöhung ersetzt";
"wallet__toast_transaction_replaced_title" = "Transaktion ersetzt";
diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings
index e59f33aa7..81eb72f6c 100644
--- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings
@@ -723,7 +723,16 @@
"wallet__tags_filter_title" = "Επιλογή Ετικέτας";
"wallet__tags_no" = "Δεν υπάρχουν διαθέσιμες ετικέτες ακόμα";
"wallet__toast_payment_failed_title" = "Η Πληρωμή Απέτυχε";
-"wallet__toast_payment_failed_description" = "Η άμεση πληρωμή σας απέτυχε. Παρακαλώ δοκιμάστε ξανά.";
+"wallet__payment_failed_description" = "Η άμεση πληρωμή σας απέτυχε. Παρακαλώ δοκιμάστε ξανά.";
+"wallet__payment_recipient_rejected" = "Ο παραλήπτης απέρριψε αυτήν την πληρωμή Lightning. Ελέγξτε το τιμολόγιο και δοκιμάστε ξανά.";
+"wallet__payment_abandoned" = "Η πληρωμή Lightning σταμάτησε πριν ολοκληρωθεί.";
+"wallet__payment_expired" = "Αυτή η πληρωμή Lightning έληξε. Ζητήστε νέο τιμολόγιο.";
+"wallet__payment_timeout" = "Το χρονικό όριο πληρωμής έληξε. Δοκιμάστε ξανά.";
+"wallet__payment_route_not_found" = "Το Bitkit δεν μπόρεσε να βρει διαδρομή Lightning για αυτήν την πληρωμή.";
+"wallet__payment_retries_exhausted" = "Το Bitkit δοκίμασε αρκετές διαδρομές Lightning, αλλά η πληρωμή δεν μπόρεσε να ολοκληρωθεί.";
+"wallet__payment_unknown_required_features" = "Αυτό το τιμολόγιο Lightning χρησιμοποιεί λειτουργίες που το Bitkit δεν υποστηρίζει ακόμη.";
+"wallet__payment_invoice_request_expired" = "Αυτό το αίτημα τιμολογίου Lightning έληξε. Ζητήστε νέο τιμολόγιο.";
+"wallet__payment_invoice_request_rejected" = "Ο παραλήπτης απέρριψε αυτό το αίτημα τιμολογίου Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Η Ληφθείσα Συναλλαγή Αντικαταστάθηκε";
"wallet__toast_received_transaction_replaced_description" = "Η ληφθείσα συναλλαγή σας αντικαταστάθηκε από ενίσχυση τέλους";
"wallet__toast_transaction_replaced_title" = "Η Συναλλαγή Αντικαταστάθηκε";
@@ -877,3 +886,4 @@
"widgets__weather__condition__good__short_title" = "Ευνοϊκές";
"widgets__weather__condition__average__short_title" = "Μέσες";
"widgets__weather__condition__poor__short_title" = "Κακές";
+"wallet__send_error_support" = "Επικοινωνία με Υποστήριξη";
diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings
index 0992f83ab..ad53805fc 100644
--- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings
@@ -1257,11 +1257,12 @@
"wallet__send_quickpay__nav_title" = "QuickPay";
"wallet__send_quickpay__title" = "Paying\ninvoice...";
"wallet__send_pending_note" = "This payment is taking a bit longer than expected. You can continue using Bitkit.";
-"wallet__send_instant_failed" = "Instant Payment Failed";
+"wallet__send_instant_failed" = "Payment Failed";
"wallet__send_regular" = "Regular Payment";
"wallet__send_error_slash_ln" = "Unfortunately contact could not be paid instantly. You can try a regular payment (more expensive, slower).";
"wallet__send_error_tx_failed" = "Transaction Failed";
"wallet__send_error_create_tx" = "Unable to broadcast the transaction. Please try again.";
+"wallet__send_error_support" = "Contact Support";
"wallet__tag_remove_error_title" = "Removing Tag Failed";
"wallet__tag_remove_error_description" = "Bitkit was unable to find the transaction data.";
"wallet__error_no_invoice" = "No lightning invoice found.";
@@ -1305,11 +1306,16 @@
"wallet__toast_payment_success_title" = "Payment Sent";
"wallet__toast_payment_success_description" = "Your instant payment was sent successfully.";
"wallet__toast_payment_failed_title" = "Payment Failed";
-"wallet__toast_payment_failed_description" = "Your instant payment failed. Please try again.";
-"wallet__toast_payment_failed_timeout" = "Payment timed out. Please try again.";
-"wallet__toast_payment_failed_recipient_rejected" = "The recipient rejected this payment. Try a different amount.";
-"wallet__toast_payment_failed_retries_exhausted" = "Could not find a route with sufficient liquidity. Try a smaller amount or wait and try again.";
-"wallet__toast_payment_failed_route_not_found" = "Could not find a payment path to the recipient.";
+"wallet__payment_failed_description" = "Your instant payment failed. Please try again.";
+"wallet__payment_recipient_rejected" = "The recipient rejected this Lightning payment. Please check the invoice and try again.";
+"wallet__payment_abandoned" = "The Lightning payment was stopped before it completed.";
+"wallet__payment_expired" = "This Lightning payment expired. Please request a new invoice.";
+"wallet__payment_timeout" = "Payment timed out. Please try again.";
+"wallet__payment_route_not_found" = "Bitkit couldn't find a Lightning route for this payment.";
+"wallet__payment_retries_exhausted" = "Bitkit tried several Lightning routes, but the payment could not be completed.";
+"wallet__payment_unknown_required_features" = "This Lightning invoice uses features Bitkit does not support yet.";
+"wallet__payment_invoice_request_expired" = "This Lightning invoice request expired. Please request a new invoice.";
+"wallet__payment_invoice_request_rejected" = "The recipient rejected this Lightning invoice request.";
"wallet__toast_received_transaction_replaced_title" = "Received Transaction Replaced";
"wallet__toast_received_transaction_replaced_description" = "Your received transaction was replaced by a fee bump";
"wallet__toast_transaction_replaced_title" = "Transaction Replaced";
diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings
index 99068d03a..1518392f1 100644
--- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings
@@ -947,6 +947,7 @@
"wallet__send_error_slash_ln" = "Lamentablemente, el contacto no se puede pagar al instante. Puede intentar un pago regular (más caro, más lento).";
"wallet__send_error_tx_failed" = "Transacción ha fallado";
"wallet__send_error_create_tx" = "No se ha podido emitir la transacción. Por favor, inténtelo de nuevo.";
+"wallet__send_error_support" = "Contactar a soporte";
"wallet__tag_remove_error_title" = "Fallo al quitar la etiqueta";
"wallet__tag_remove_error_description" = "Bitkit no ha podido encontrar los datos de la transacción.";
"wallet__error_no_invoice" = "No se ha encontrado ninguna factura Lightning";
@@ -990,7 +991,16 @@
"wallet__toast_payment_success_title" = "Pago enviado";
"wallet__toast_payment_success_description" = "Su pago instantáneo se ha enviado.";
"wallet__toast_payment_failed_title" = "Pago fallido";
-"wallet__toast_payment_failed_description" = "Su pago instantáneo ha fallado. Por favor, inténtelo de nuevo.";
+"wallet__payment_failed_description" = "Su pago instantáneo ha fallado. Por favor, inténtelo de nuevo.";
+"wallet__payment_recipient_rejected" = "El destinatario rechazó este pago Lightning. Revise la factura e inténtelo de nuevo.";
+"wallet__payment_abandoned" = "El pago Lightning se detuvo antes de completarse.";
+"wallet__payment_expired" = "Este pago Lightning venció. Solicite una factura nueva.";
+"wallet__payment_timeout" = "El pago agotó el tiempo de espera. Inténtelo de nuevo.";
+"wallet__payment_route_not_found" = "Bitkit no pudo encontrar una ruta Lightning para este pago.";
+"wallet__payment_retries_exhausted" = "Bitkit probó varias rutas Lightning, pero el pago no se pudo completar.";
+"wallet__payment_unknown_required_features" = "Esta factura Lightning usa funciones que Bitkit aún no admite.";
+"wallet__payment_invoice_request_expired" = "Esta solicitud de factura Lightning venció. Solicite una factura nueva.";
+"wallet__payment_invoice_request_rejected" = "El destinatario rechazó esta solicitud de factura Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Transacción recibida Reemplazada";
"wallet__toast_received_transaction_replaced_description" = "Tu transacción entrante fue reemplazada al aumentar la comisión";
"wallet__toast_transaction_replaced_title" = "Transacción Sustituida";
diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings
index e7b937b0a..2e012ae75 100644
--- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings
@@ -871,7 +871,16 @@
"wallet__tags_no" = "Aún no hay etiquetas disponibles";
"wallet__toast_payment_success_title" = "Pago Enviado";
"wallet__toast_payment_failed_title" = "Pago Fallido";
-"wallet__toast_payment_failed_description" = "Tu pago instantáneo falló. Por favor, inténtalo de nuevo.";
+"wallet__payment_failed_description" = "Tu pago instantáneo falló. Por favor, inténtalo de nuevo.";
+"wallet__payment_recipient_rejected" = "El destinatario ha rechazado este pago Lightning. Comprueba la factura e inténtalo de nuevo.";
+"wallet__payment_abandoned" = "El pago Lightning se detuvo antes de completarse.";
+"wallet__payment_expired" = "Este pago Lightning ha caducado. Solicita una factura nueva.";
+"wallet__payment_timeout" = "El pago agotó el tiempo de espera. Inténtalo de nuevo.";
+"wallet__payment_route_not_found" = "Bitkit no ha podido encontrar una ruta Lightning para este pago.";
+"wallet__payment_retries_exhausted" = "Bitkit ha probado varias rutas Lightning, pero el pago no se ha podido completar.";
+"wallet__payment_unknown_required_features" = "Esta factura Lightning usa funciones que Bitkit aún no admite.";
+"wallet__payment_invoice_request_expired" = "Esta solicitud de factura Lightning ha caducado. Solicita una factura nueva.";
+"wallet__payment_invoice_request_rejected" = "El destinatario ha rechazado esta solicitud de factura Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Transacción Recibida Reemplazada";
"wallet__toast_received_transaction_replaced_description" = "Tu transacción recibida fue reemplazada por un aumento de comisión";
"wallet__toast_transaction_replaced_title" = "Transacción Reemplazada";
@@ -1044,3 +1053,4 @@
"widgets__weather__condition__good__short_title" = "Favorables";
"widgets__weather__condition__average__short_title" = "Promedio";
"widgets__weather__condition__poor__short_title" = "Desfavorables";
+"wallet__send_error_support" = "Contactar con el servicio de asistencia";
diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings
index c4505329f..c0be9761c 100644
--- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings
@@ -959,6 +959,7 @@
"wallet__send_error_slash_ln" = "Malheureusement, le contact ne peut pas être payé instantanément. Vous pouvez essayer un paiement On-Chain (plus cher, plus lent).";
"wallet__send_error_tx_failed" = "Échec de la transaction";
"wallet__send_error_create_tx" = "Impossible de diffuser la transaction. Veuillez réessayer.";
+"wallet__send_error_support" = "Contacter le support";
"wallet__tag_remove_error_title" = "Échec du retrait de l\'étiquette";
"wallet__tag_remove_error_description" = "Bitkit n\'a pas pu trouver les données de la transaction.";
"wallet__error_no_invoice" = "Aucune facture Lightning n\'a été trouvée.";
@@ -1002,7 +1003,16 @@
"wallet__toast_payment_success_title" = "Paiement envoyé";
"wallet__toast_payment_success_description" = "Votre paiement instantané a été envoyé avec succès.";
"wallet__toast_payment_failed_title" = "Échec du paiement";
-"wallet__toast_payment_failed_description" = "Votre paiement instantané a échoué. Veuillez réessayer.";
+"wallet__payment_failed_description" = "Votre paiement instantané a échoué. Veuillez réessayer.";
+"wallet__payment_recipient_rejected" = "Le destinataire a rejeté ce paiement Lightning. Vérifiez la facture et réessayez.";
+"wallet__payment_abandoned" = "Le paiement Lightning a été arrêté avant d'être terminé.";
+"wallet__payment_expired" = "Ce paiement Lightning a expiré. Demandez une nouvelle facture.";
+"wallet__payment_timeout" = "Le paiement a expiré. Veuillez réessayer.";
+"wallet__payment_route_not_found" = "Bitkit n'a pas trouvé de route Lightning pour ce paiement.";
+"wallet__payment_retries_exhausted" = "Bitkit a essayé plusieurs routes Lightning, mais le paiement n'a pas pu être terminé.";
+"wallet__payment_unknown_required_features" = "Cette facture Lightning utilise des fonctionnalités que Bitkit ne prend pas encore en charge.";
+"wallet__payment_invoice_request_expired" = "Cette demande de facture Lightning a expiré. Demandez une nouvelle facture.";
+"wallet__payment_invoice_request_rejected" = "Le destinataire a rejeté cette demande de facture Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Transaction reçue";
"wallet__toast_received_transaction_replaced_description" = "Votre transaction reçue a été remplacée par une augmentation des frais.";
"wallet__toast_transaction_replaced_title" = "Transaction remplacée";
diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings
index c8e5c4dbf..7e6b09c3f 100644
--- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings
@@ -921,6 +921,7 @@
"wallet__send_error_slash_ln" = "Purtroppo il contatto non può essere pagato istantaneamente. Puoi provare con un pagamento regolare (più costoso, più lento).";
"wallet__send_error_tx_failed" = "Transazione Fallita";
"wallet__send_error_create_tx" = "Impossibile trasmettere la transazione. Per favore riprova.";
+"wallet__send_error_support" = "Contatta l'Assistenza";
"wallet__tag_remove_error_title" = "Rimozione del tag non riuscita";
"wallet__tag_remove_error_description" = "Bitkit non è riuscito a trovare i dati della transazione.";
"wallet__error_no_invoice" = "Nessuna invoice lightning trovata.";
@@ -963,7 +964,16 @@
"wallet__toast_payment_success_title" = "Pagamento inviato";
"wallet__toast_payment_success_description" = "Il tuo pagamento istantaneo è stato inviato con successo.";
"wallet__toast_payment_failed_title" = "Pagamento fallito";
-"wallet__toast_payment_failed_description" = "Il tuo pagamento istantaneo non è riuscito. Per favore riprova.";
+"wallet__payment_failed_description" = "Il tuo pagamento istantaneo non è riuscito. Per favore riprova.";
+"wallet__payment_recipient_rejected" = "Il destinatario ha rifiutato questo pagamento Lightning. Controlla la fattura e riprova.";
+"wallet__payment_abandoned" = "Il pagamento Lightning è stato interrotto prima del completamento.";
+"wallet__payment_expired" = "Questo pagamento Lightning è scaduto. Richiedi una nuova fattura.";
+"wallet__payment_timeout" = "Il pagamento è scaduto. Riprova.";
+"wallet__payment_route_not_found" = "Bitkit non ha trovato una rotta Lightning per questo pagamento.";
+"wallet__payment_retries_exhausted" = "Bitkit ha provato diverse rotte Lightning, ma il pagamento non è stato completato.";
+"wallet__payment_unknown_required_features" = "Questa fattura Lightning usa funzionalità che Bitkit non supporta ancora.";
+"wallet__payment_invoice_request_expired" = "Questa richiesta di fattura Lightning è scaduta. Richiedi una nuova fattura.";
+"wallet__payment_invoice_request_rejected" = "Il destinatario ha rifiutato questa richiesta di fattura Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Transazione Ricevuta Sostituita";
"wallet__toast_received_transaction_replaced_description" = "La tua transazione ricevuta è stata sostituita da un aumento commissione";
"wallet__toast_transaction_replaced_title" = "Transazione Sostituita";
diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings
index f3f7f44b7..a52d6fd05 100644
--- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings
@@ -954,6 +954,7 @@
"wallet__send_error_slash_ln" = "Helaas kon het contact niet direct worden betaald. U kunt proberen een normale betaling te doen (deze is langzamer en duurder). ";
"wallet__send_error_tx_failed" = "Transactie Mislukt";
"wallet__send_error_create_tx" = "De transactie niet verzonden worden. Probeer het opnieuw.";
+"wallet__send_error_support" = "Contact Opnemen";
"wallet__tag_remove_error_title" = "Tag Verwijderen Mislukt";
"wallet__tag_remove_error_description" = "Bitkit kon de transactiegegevens niet vinden.";
"wallet__error_no_invoice" = "Geen lightning factuur gedetecteerd";
@@ -997,7 +998,16 @@
"wallet__toast_payment_success_title" = "Betaling Verzonden";
"wallet__toast_payment_success_description" = "Uw directe betaling is succesvol verzonden.";
"wallet__toast_payment_failed_title" = "Betaling Mislukt";
-"wallet__toast_payment_failed_description" = "Uw directe betaling is mislukt. Probeer het opnieuw.";
+"wallet__payment_failed_description" = "Uw directe betaling is mislukt. Probeer het opnieuw.";
+"wallet__payment_recipient_rejected" = "De ontvanger heeft deze Lightning-betaling geweigerd. Controleer de factuur en probeer het opnieuw.";
+"wallet__payment_abandoned" = "De Lightning-betaling is gestopt voordat deze was voltooid.";
+"wallet__payment_expired" = "Deze Lightning-betaling is verlopen. Vraag een nieuwe factuur aan.";
+"wallet__payment_timeout" = "Time-out voor betaling. Probeer het opnieuw.";
+"wallet__payment_route_not_found" = "Bitkit kon geen Lightning-route voor deze betaling vinden.";
+"wallet__payment_retries_exhausted" = "Bitkit heeft meerdere Lightning-routes geprobeerd, maar de betaling kon niet worden voltooid.";
+"wallet__payment_unknown_required_features" = "Deze Lightning-factuur gebruikt functies die Bitkit nog niet ondersteunt.";
+"wallet__payment_invoice_request_expired" = "Deze Lightning-factuuraanvraag is verlopen. Vraag een nieuwe factuur aan.";
+"wallet__payment_invoice_request_rejected" = "De ontvanger heeft deze Lightning-factuuraanvraag geweigerd.";
"wallet__toast_received_transaction_replaced_title" = "Ontvangen Transactie Vervangen";
"wallet__toast_received_transaction_replaced_description" = "Uw ontvangen transactie is vervangen door een vergoedingsverhoging";
"wallet__toast_transaction_replaced_title" = "Transactie Vervangen";
diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings
index 1b7ad6e50..7f1c1fbd9 100644
--- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings
@@ -960,6 +960,7 @@
"wallet__send_error_slash_ln" = "Nie udało się wykonać szybkiej płatności. Możesz spróbować regularnej płatności (droższej, wolniejszej).";
"wallet__send_error_tx_failed" = "Transakcja nie powiodła się";
"wallet__send_error_create_tx" = "Nie udało się wysłać transakcji. Proszę spróbować ponownie.";
+"wallet__send_error_support" = "Skontaktuj się z pomocą techniczną";
"wallet__tag_remove_error_title" = "Usunięcie tagu nie powiodło się";
"wallet__tag_remove_error_description" = "Bitkit nie był w stanie znaleźć danych transakcji.";
"wallet__error_no_invoice" = "Nie znaleziono faktury Lightning.";
@@ -1003,7 +1004,16 @@
"wallet__toast_payment_success_title" = "Płatność wysłana";
"wallet__toast_payment_success_description" = "Twoja płatność natychmiastowa została wysłana pomyślnie.";
"wallet__toast_payment_failed_title" = "Płatność nie powiodła się";
-"wallet__toast_payment_failed_description" = "Natychmiastowa płatność nie powiodła się. Proszę spróbować ponownie.";
+"wallet__payment_failed_description" = "Natychmiastowa płatność nie powiodła się. Proszę spróbować ponownie.";
+"wallet__payment_recipient_rejected" = "Odbiorca odrzucił tę płatność Lightning. Sprawdź fakturę i spróbuj ponownie.";
+"wallet__payment_abandoned" = "Płatność Lightning została zatrzymana przed zakończeniem.";
+"wallet__payment_expired" = "Ta płatność Lightning wygasła. Poproś o nową fakturę.";
+"wallet__payment_timeout" = "Przekroczono limit czasu płatności. Spróbuj ponownie.";
+"wallet__payment_route_not_found" = "Bitkit nie znalazł trasy Lightning dla tej płatności.";
+"wallet__payment_retries_exhausted" = "Bitkit wypróbował kilka tras Lightning, ale płatności nie udało się ukończyć.";
+"wallet__payment_unknown_required_features" = "Ta faktura Lightning używa funkcji, których Bitkit jeszcze nie obsługuje.";
+"wallet__payment_invoice_request_expired" = "To żądanie faktury Lightning wygasło. Poproś o nową fakturę.";
+"wallet__payment_invoice_request_rejected" = "Odbiorca odrzucił to żądanie faktury Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Zastąpiono otrzymaną transakcję";
"wallet__toast_received_transaction_replaced_description" = "Twoja otrzymana transakcja została zastąpiona przez zwiększenie opłaty";
"wallet__toast_transaction_replaced_title" = "Transakcja zastąpiona";
diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings
index 6cbfe9e7f..47500e956 100644
--- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings
@@ -961,6 +961,7 @@
"wallet__send_error_slash_ln" = "Infelizmente, o contato não pôde ser pago instantaneamente. Você pode tentar realizar um pagamento regular (mais caro, mais lento).";
"wallet__send_error_tx_failed" = "Falha na Transação";
"wallet__send_error_create_tx" = "Não foi possível transmitir a transação. Por favor, tente novamente.";
+"wallet__send_error_support" = "Contactar Suporte";
"wallet__tag_remove_error_title" = "Falha ao Remover a Tag";
"wallet__tag_remove_error_description" = "A Bitkit não conseguiu encontrar os dados da transação.";
"wallet__error_no_invoice" = "Não fora encontrada nenhum invoice lightning.";
@@ -1004,7 +1005,16 @@
"wallet__toast_payment_success_title" = "Pagamento Enviado";
"wallet__toast_payment_success_description" = "Seu pagamento instantâneo foi enviado com sucesso.";
"wallet__toast_payment_failed_title" = "Pagamento Falhou";
-"wallet__toast_payment_failed_description" = "Seu pagamento instantâneo falhou. Por favor, tente novamente.";
+"wallet__payment_failed_description" = "Seu pagamento instantâneo falhou. Por favor, tente novamente.";
+"wallet__payment_recipient_rejected" = "O destinatário rejeitou este pagamento Lightning. Verifique a fatura e tente novamente.";
+"wallet__payment_abandoned" = "O pagamento Lightning foi interrompido antes de ser concluído.";
+"wallet__payment_expired" = "Este pagamento Lightning expirou. Solicite uma nova fatura.";
+"wallet__payment_timeout" = "O pagamento expirou. Tente novamente.";
+"wallet__payment_route_not_found" = "O Bitkit não conseguiu encontrar uma rota Lightning para este pagamento.";
+"wallet__payment_retries_exhausted" = "O Bitkit tentou várias rotas Lightning, mas o pagamento não pôde ser concluído.";
+"wallet__payment_unknown_required_features" = "Esta fatura Lightning usa recursos que o Bitkit ainda não oferece suporte.";
+"wallet__payment_invoice_request_expired" = "Esta solicitação de fatura Lightning expirou. Solicite uma nova fatura.";
+"wallet__payment_invoice_request_rejected" = "O destinatário rejeitou esta solicitação de fatura Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Transação Recebida Substituída";
"wallet__toast_received_transaction_replaced_description" = "Sua transação recebida foi substituída por um aumento de taxa";
"wallet__toast_transaction_replaced_title" = "Transação Substituída";
diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings
index ce44c8d3b..89177e45b 100644
--- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings
@@ -1,4 +1,14 @@
"security__cp_text" = "Você pode alterar seu código PIN para uma nova\ncombinação de 4 dígitos. Primeiro, digite seu código PIN atual.";
+"wallet__payment_recipient_rejected" = "O destinatário rejeitou este pagamento Lightning. Verifique a fatura e tente novamente.";
+"wallet__payment_abandoned" = "O pagamento Lightning foi interrompido antes de ser concluído.";
+"wallet__payment_expired" = "Este pagamento Lightning expirou. Peça uma nova fatura.";
+"wallet__payment_timeout" = "O pagamento expirou. Tente novamente.";
+"wallet__send_error_support" = "Suporte";
+"wallet__payment_route_not_found" = "O Bitkit não conseguiu encontrar uma rota Lightning para este pagamento.";
+"wallet__payment_retries_exhausted" = "O Bitkit tentou várias rotas Lightning, mas o pagamento não pôde ser concluído.";
+"wallet__payment_unknown_required_features" = "Esta fatura Lightning usa funcionalidades que o Bitkit ainda não suporta.";
+"wallet__payment_invoice_request_expired" = "Este pedido de fatura Lightning expirou. Peça uma nova fatura.";
+"wallet__payment_invoice_request_rejected" = "O destinatário rejeitou este pedido de fatura Lightning.";
"settings__addr__no_addrs_with_funds" = "Não foi encontrado nenhum endereço contendo \"{searchTxt}\" com fundos ";
"settings__addr__no_addrs_str" = "Nenhum endereço foi encontrado ao pesquisar por \"{searchTxt}\"";
"slashtags__contact_your_name" = "Nome público\ndo seu perfil";
diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings
index e57ed92e4..80c6456c8 100644
--- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings
+++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings
@@ -950,6 +950,7 @@
"wallet__send_error_slash_ln" = "К сожалению, мгновенный платеж к контакту не выполнен. Можно попробовать обычный платеж (дороже, медленнее).";
"wallet__send_error_tx_failed" = "Транзакция не удалась";
"wallet__send_error_create_tx" = "Не удалось отправить транзакцию. Пожалуйста, попробуйте снова.";
+"wallet__send_error_support" = "Связаться с Поддержкой";
"wallet__tag_remove_error_title" = "Ошибка удаления тега";
"wallet__tag_remove_error_description" = "Bitkit не удалось найти данные транзакции.";
"wallet__error_no_invoice" = "Лайтнинг инвойс не найден.";
@@ -992,7 +993,16 @@
"wallet__toast_payment_success_title" = "Платеж отправлен";
"wallet__toast_payment_success_description" = "Ваш мгновенный платеж был успешно отправлен.";
"wallet__toast_payment_failed_title" = "Платеж не выполнен";
-"wallet__toast_payment_failed_description" = "Ваш мгновенный платеж не удался. Пожалуйста, попробуйте снова.";
+"wallet__payment_failed_description" = "Ваш мгновенный платеж не удался. Пожалуйста, попробуйте снова.";
+"wallet__payment_recipient_rejected" = "Получатель отклонил этот платеж Lightning. Проверьте счет и попробуйте снова.";
+"wallet__payment_abandoned" = "Платеж Lightning был остановлен до завершения.";
+"wallet__payment_expired" = "Срок действия этого платежа Lightning истек. Запросите новый счет.";
+"wallet__payment_timeout" = "Время ожидания платежа истекло. Попробуйте еще раз.";
+"wallet__payment_route_not_found" = "Bitkit не смог найти маршрут Lightning для этого платежа.";
+"wallet__payment_retries_exhausted" = "Bitkit попробовал несколько маршрутов Lightning, но платеж не удалось завершить.";
+"wallet__payment_unknown_required_features" = "Этот счет Lightning использует функции, которые Bitkit пока не поддерживает.";
+"wallet__payment_invoice_request_expired" = "Срок действия этого запроса счета Lightning истек. Запросите новый счет.";
+"wallet__payment_invoice_request_rejected" = "Получатель отклонил этот запрос счета Lightning.";
"wallet__toast_received_transaction_replaced_title" = "Полученная Транзакция Заменена";
"wallet__toast_received_transaction_replaced_description" = "Ваша полученная транзакция была заменена из-за повышения комиссии";
"wallet__toast_transaction_replaced_title" = "Транзакция Заменена";
diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift
index b0edb032a..86d0070cc 100644
--- a/Bitkit/Services/LightningService.swift
+++ b/Bitkit/Services/LightningService.swift
@@ -374,6 +374,15 @@ class LightningService {
Logger.info("Deleted network graph cache at: \(graphPath.path)")
}
+ func networkGraphCacheModificationDate() -> Date? {
+ let graphPath = Env.ldkStorage(walletIndex: currentWalletIndex).appendingPathComponent("network_graph_cache")
+ guard let attributes = try? FileManager.default.attributesOfItem(atPath: graphPath.path),
+ let modificationDate = attributes[.modificationDate] as? Date
+ else { return nil }
+
+ return modificationDate
+ }
+
func connectToTrustedPeers(remotePeers: [LnPeer]? = nil) async throws {
guard let node else {
throw AppError(serviceError: .nodeNotSetup)
diff --git a/Bitkit/Services/VssBackupClient.swift b/Bitkit/Services/VssBackupClient.swift
index 7ef20e48b..dc7b7cf60 100644
--- a/Bitkit/Services/VssBackupClient.swift
+++ b/Bitkit/Services/VssBackupClient.swift
@@ -257,6 +257,12 @@ class VssBackupClient {
}
}
+ func deleteLdkScorerCache() async throws {
+ _ = try await deleteObjectLdk(key: "scorer", namespace: .default)
+ _ = try await deleteObjectLdk(key: "external_pathfinding_scores_cache", namespace: .default)
+ Logger.info("Cleared LDK scorer cache from VSS", context: "VssBackupClient")
+ }
+
private func awaitSetup() async throws {
try await setupCoordinator.awaitSetup { [self] in
try await setup()
diff --git a/Bitkit/Utilities/Errors.swift b/Bitkit/Utilities/Errors.swift
index 91b09bdac..0e9cb5476 100644
--- a/Bitkit/Utilities/Errors.swift
+++ b/Bitkit/Utilities/Errors.swift
@@ -66,29 +66,34 @@ enum PaymentTimeoutError: Error {
case timedOut
}
-enum BlocktankError_deprecated: Error {
- case missingResponse
- case invalidResponse
- case invalidJson
- case missingDeviceToken
-}
-
/// Translates LDK and BDK error messages into translated messages that can be displayed to end users
struct AppError: LocalizedError {
+ static let genericMessage = "App Error"
+
let message: String
let debugMessage: String?
+ let paymentFailureReason: PaymentFailureReason?
/// The original error this was wrapped from, when known. Preserved so callers can unwrap and
/// inspect the underlying error (e.g. `isTrezorUserCancellation()`) after it has been boxed by
/// `ServiceQueue` into a generic `AppError`.
let underlyingError: Error?
var errorDescription: String? {
- return NSLocalizedString(message, comment: "")
+ return t(message)
+ }
+
+ var isGeneric: Bool {
+ return message == Self.genericMessage
}
/// Pass any LDK or BDK error to get a translated error message
/// - Parameter error: any error
init(error: Error) {
+ if let appError = error as? AppError {
+ self = appError
+ return
+ }
+
if let ldkBuildError = error as? BuildError {
self.init(ldkBuildError: ldkBuildError)
return
@@ -106,17 +111,26 @@ struct AppError: LocalizedError {
// EsploraError
// PersistenceError
- self.init(message: "App Error", debugMessage: error.localizedDescription, underlyingError: error)
+ self.init(message: Self.genericMessage, debugMessage: error.localizedDescription, underlyingError: error)
}
- init(message: String, debugMessage: String?, underlyingError: Error? = nil) {
+ init(message: String, debugMessage: String?, underlyingError: Error? = nil, paymentFailureReason: PaymentFailureReason? = nil) {
self.message = message
self.debugMessage = debugMessage
self.underlyingError = underlyingError
+ self.paymentFailureReason = paymentFailureReason
+ }
+
+ init(paymentFailureReason reason: PaymentFailureReason?) {
+ underlyingError = nil
+ debugMessage = reason.map { String(describing: $0) } ?? "Unknown payment failure reason"
+ message = PaymentFailureReason.userMessageKey(for: reason)
+ paymentFailureReason = reason
}
init(serviceError: CustomServiceError) {
underlyingError = serviceError
+ paymentFailureReason = nil
switch serviceError {
case .nodeNotSetup:
message = "Node is not setup"
@@ -164,6 +178,7 @@ struct AppError: LocalizedError {
private init(ldkBuildError: BuildError) {
underlyingError = ldkBuildError
+ paymentFailureReason = nil
switch ldkBuildError as BuildError {
case let .InvalidSeedBytes(message: ldkMessage):
message = "Invalid seed bytes"
@@ -221,6 +236,7 @@ struct AppError: LocalizedError {
private init(ldkError: NodeError) {
underlyingError = ldkError
+ paymentFailureReason = nil
switch ldkError as NodeError {
case let .AlreadyRunning(message: ldkMessage):
message = "Node is already running"
diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift
index a8b8c4ecd..51cde7da6 100644
--- a/Bitkit/ViewModels/AppViewModel.swift
+++ b/Bitkit/ViewModels/AppViewModel.swift
@@ -318,7 +318,7 @@ extension AppViewModel {
)
}
case .signingTimeout:
- toast(type: .error, title: t("common__error"), description: t("wallet__toast_payment_failed_timeout"))
+ toast(type: .error, title: t("common__error"), description: t("wallet__payment_timeout"))
case .broadcastUncertain:
toast(
type: .warning,
@@ -1018,7 +1018,7 @@ extension AppViewModel {
toast(
type: .error,
title: t("wallet__toast_payment_failed_title"),
- description: t("wallet__toast_payment_failed_description"),
+ description: t("wallet__payment_failed_description"),
accessibilityIdentifier: "PaymentFailedToast"
)
}
diff --git a/Bitkit/ViewModels/NavigationViewModel.swift b/Bitkit/ViewModels/NavigationViewModel.swift
index 7512e757a..75d864077 100644
--- a/Bitkit/ViewModels/NavigationViewModel.swift
+++ b/Bitkit/ViewModels/NavigationViewModel.swift
@@ -2,6 +2,10 @@ import BitkitCore
import LDKNode
import SwiftUI
+struct ReportIssuePrefill: Hashable {
+ let message: String
+}
+
enum Route: Hashable {
case savingsWallet
case spendingWallet
@@ -61,7 +65,7 @@ enum Route: Hashable {
case widgetsIntro
// Support
- case reportIssue
+ case reportIssue(ReportIssuePrefill? = nil)
case appStatus
// Settings
diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift
index de489bb24..f4739765d 100644
--- a/Bitkit/ViewModels/WalletViewModel.swift
+++ b/Bitkit/ViewModels/WalletViewModel.swift
@@ -91,6 +91,7 @@ class WalletViewModel: ObservableObject {
@Published var balanceInTransferToSpending: Int = 0
@Published var forceCloseClaimableAtHeight: UInt32?
@Published var currentBlockHeight: UInt32 = 0
+ @Published var isRetryingLightningPayment = false
init(
lightningService: LightningService = .shared,
@@ -137,6 +138,11 @@ class WalletViewModel: ObservableObject {
}
func start(walletIndex: Int = 0) async throws {
+ if !lightningService.hasNode, nodeLifecycleState == .running {
+ Logger.warn("Node lifecycle was running but service node is missing, restarting", context: "WalletViewModel")
+ nodeLifecycleState = .stopped
+ }
+
// Guard against concurrent starts - only allow start from stopped, initializing, or error states
switch nodeLifecycleState {
case .stopped, .initializing, .errorStarting:
@@ -415,7 +421,16 @@ class WalletViewModel: ObservableObject {
nodeLifecycleState = .stopping
// Stop the swap updates stream with the node; it restarts on the next wallet start.
await stopSwapUpdates()
- try await lightningService.stop(clearEventCallback: clearEventCallback)
+
+ do {
+ try await lightningService.stop(clearEventCallback: clearEventCallback)
+ } catch {
+ Logger.warn("Failed to stop Lightning node: \(error)", context: "WalletViewModel")
+ nodeLifecycleState = lightningService.hasNode ? .running : .stopped
+ syncState()
+ throw error
+ }
+
nodeLifecycleState = .stopped
probeOutcomes.removeAll()
syncState()
@@ -887,14 +902,9 @@ class WalletViewModel: ObservableObject {
continuation.resume(returning: paymentHash)
}
case .paymentFailed(paymentId: _, let paymentHash, let reason):
- // TODO: this is not working for routeNotFound
if paymentHash == hash {
self.removeOnEvent(id: eventId)
- continuation.resume(throwing: NSError(
- domain: "Lightning",
- code: -1,
- userInfo: [NSLocalizedDescriptionKey: reason.debugDescription]
- ))
+ continuation.resume(throwing: AppError(paymentFailureReason: reason))
}
default:
break
@@ -969,6 +979,59 @@ class WalletViewModel: ObservableObject {
try await clearNetworkGraph()
}
+ func resetPaymentRoutingCaches() async throws {
+ Logger.warn("Resetting payment routing caches", context: "WalletViewModel")
+ var resetErrors: [Error] = []
+
+ do {
+ try await resetNetworkGraph()
+ } catch {
+ resetErrors.append(error)
+ }
+
+ do {
+ try await VssBackupClient.shared.deleteLdkScorerCache()
+ } catch {
+ resetErrors.append(error)
+ }
+
+ if let firstError = resetErrors.first {
+ throw firstError
+ }
+ }
+
+ func waitForPaymentRoutingDataRefresh(startedAt: Date, timeoutSeconds: Double = 20.0) async throws {
+ let startedAtTimestamp = UInt64(startedAt.timeIntervalSince1970)
+ let requiresRgsRefresh = Env.network != .regtest && !rgsConfigService.getCurrentServerUrl().isEmpty
+ let requiresScorerRefresh = Env.ldkScorerUrl != nil
+
+ guard requiresRgsRefresh || requiresScorerRefresh else { return }
+
+ let startTime = Date()
+
+ while Date().timeIntervalSince(startTime) <= timeoutSeconds {
+ await lightningService.refreshCache()
+ if let status = lightningService.status {
+ let graphCacheModificationDate = lightningService.networkGraphCacheModificationDate()
+ let rgsFresh = !requiresRgsRefresh || (graphCacheModificationDate ?? .distantPast) > startedAt
+
+ let scorerFresh = !requiresScorerRefresh || (status.latestPathfindingScoresSyncTimestamp ?? 0) >= startedAtTimestamp
+
+ if rgsFresh, scorerFresh {
+ Logger.info("Payment routing data refreshed", context: "WalletViewModel")
+ return
+ }
+ }
+
+ try await Task.sleep(nanoseconds: 500_000_000)
+ }
+
+ throw AppError(
+ message: "wallet__payment_failed_description",
+ debugMessage: "Timed out waiting for RGS and scorer data before retrying payment"
+ )
+ }
+
/// Clears the cached Lightning network graph: the local cache file and the VSS backup copy.
/// Shared by the legacy one-time startup cleanup, the manual recovery reset, and the LDK debug screen.
func clearNetworkGraph() async throws {
diff --git a/Bitkit/Views/Settings/Support/ReportIssue.swift b/Bitkit/Views/Settings/Support/ReportIssue.swift
index c0ee7c67d..fac3d1728 100644
--- a/Bitkit/Views/Settings/Support/ReportIssue.swift
+++ b/Bitkit/Views/Settings/Support/ReportIssue.swift
@@ -8,6 +8,10 @@ struct ReportIssue: View {
@State private var showingSuccess: Bool = false
@State private var showingError: Bool = false
+ init(prefill: ReportIssuePrefill? = nil) {
+ _message = State(initialValue: prefill?.message ?? "")
+ }
+
private func validateEmail(_ emailText: String) -> Bool {
if emailText.contains("@") {
let parts = emailText.split(separator: "@")
diff --git a/Bitkit/Views/Settings/SupportScreen.swift b/Bitkit/Views/Settings/SupportScreen.swift
index 35eaf2ac8..2c8778608 100644
--- a/Bitkit/Views/Settings/SupportScreen.swift
+++ b/Bitkit/Views/Settings/SupportScreen.swift
@@ -84,7 +84,7 @@ struct SupportScreen: View {
.padding(.bottom, 16)
VStack(spacing: 0) {
- NavigationLink(value: Route.reportIssue) {
+ NavigationLink(value: Route.reportIssue()) {
SettingsRow(title: t("settings__support__report"), iconName: "warning")
}
diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift
index a49d41852..e38687de1 100644
--- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift
+++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift
@@ -12,6 +12,7 @@ struct LnurlPayConfirm: View {
@Binding var navigationPath: [SendRoute]
let requestPinCheck: () async -> Bool
let prepareIncomingPaymentRequest: () async throws -> Void
+ let routingCacheResetAttempted: Bool
@State private var showWarningAlert = false
@State private var alertContinuation: CheckedContinuation?
@@ -105,52 +106,7 @@ struct LnurlPayConfirm: View {
title: t("wallet__send_swipe"),
accentColor: .greenAccent
) {
- // Check if we need to show warning for amounts over $100 USD
- if settings.warnWhenSendingOver100 {
- let sats: UInt64 = if let invoice = app.scannedLightningInvoice {
- wallet.sendAmountSats ?? invoice.amountSatoshis
- } else {
- 0
- }
-
- // Convert to USD to check if over $100
- if let usdAmount = currency.convert(sats: sats, to: "USD") {
- if usdAmount.value > 100.0 {
- showWarningAlert = true
- // Wait for the alert to be dismissed
- let shouldProceed = try await waitForAlertDismissal()
- if !shouldProceed {
- // User cancelled, throw error to reset SwipeButton
- throw CancellationError()
- }
- // User confirmed, continue with authentication if needed
- }
- }
- }
-
- // Check if authentication is required for payments
- if settings.requirePinForPayments && settings.pinEnabled {
- if settings.useBiometrics && BiometricAuth.isAvailable {
- let result = await BiometricAuth.authenticate()
- switch result {
- case .success:
- break
- case .cancelled:
- throw CancellationError()
- case let .failed(message):
- biometricErrorMessage = message
- showingBiometricError = true
- throw CancellationError()
- }
- } else {
- let shouldProceed = await requestPinCheck()
- guard shouldProceed else {
- throw CancellationError()
- }
- }
- }
-
- try await performPayment()
+ try await submitPayment()
}
}
.navigationBarHidden(true)
@@ -180,6 +136,55 @@ struct LnurlPayConfirm: View {
}
}
+ private func submitPayment() async throws {
+ // Check if we need to show warning for amounts over $100 USD
+ if settings.warnWhenSendingOver100 {
+ let sats: UInt64 = if let invoice = app.scannedLightningInvoice {
+ wallet.sendAmountSats ?? invoice.amountSatoshis
+ } else {
+ 0
+ }
+
+ // Convert to USD to check if over $100
+ if let usdAmount = currency.convert(sats: sats, to: "USD") {
+ if usdAmount.value > 100.0 {
+ showWarningAlert = true
+ // Wait for the alert to be dismissed
+ let shouldProceed = try await waitForAlertDismissal()
+ if !shouldProceed {
+ // User cancelled, throw error to reset SwipeButton
+ throw CancellationError()
+ }
+ // User confirmed, continue with authentication if needed
+ }
+ }
+ }
+
+ // Check if authentication is required for payments
+ if settings.requirePinForPayments && settings.pinEnabled {
+ if settings.useBiometrics && BiometricAuth.isAvailable {
+ let result = await BiometricAuth.authenticate()
+ switch result {
+ case .success:
+ break
+ case .cancelled:
+ throw CancellationError()
+ case let .failed(message):
+ biometricErrorMessage = message
+ showingBiometricError = true
+ throw CancellationError()
+ }
+ } else {
+ let shouldProceed = await requestPinCheck()
+ guard shouldProceed else {
+ throw CancellationError()
+ }
+ }
+ }
+
+ try await performPayment()
+ }
+
private func waitForAlertDismissal() async throws -> Bool {
return try await withCheckedThrowingContinuation { continuation in
alertContinuation = continuation
@@ -194,6 +199,7 @@ struct LnurlPayConfirm: View {
let amountMsats = lnurlPayData.callbackAmountMsats(userSats: wallet.sendAmountSats)
let contactPaymentContext = app.contactPaymentContext
let contactPublicKey = contactPaymentContext?.publicKey
+ var bolt11Invoice: String?
do {
try validateIncomingPaymentRequest(contactPaymentContext, amountMsats: amountMsats)
@@ -206,6 +212,7 @@ struct LnurlPayConfirm: View {
amountMsats: amountMsats,
comment: comment.isEmpty ? nil : comment
)
+ bolt11Invoice = bolt11
let parsedInvoice = try Bolt11Invoice.fromStr(invoiceStr: bolt11)
let paymentHash = String(describing: parsedInvoice.paymentHash())
@@ -218,7 +225,7 @@ struct LnurlPayConfirm: View {
sats: nil,
onTimeout: {
app.addPendingPaymentHash(paymentHash, contactPublicKey: contactPublicKey)
- navigationPath.append(.pending(paymentHash: paymentHash))
+ navigationPath.append(.pending(paymentHash: paymentHash, retryRoute: .lnurlPayConfirm))
}
)
app.addPendingContactPaymentContext(paymentHash, contactPublicKey: contactPublicKey)
@@ -230,13 +237,12 @@ struct LnurlPayConfirm: View {
} catch {
Logger.error("LNURL payment failed: \(error)")
- // TODO: remove toast and use failure screen instead
- app.toast(error)
-
- // TODO: this is a hack to make sure the navigation binding is ready
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- navigationPath.append(.failure)
- }
+ navigationPath.append(.failure(SendFailureContext(
+ error: error,
+ retryRoute: .lnurlPayConfirm,
+ routingCacheResetAttempted: routingCacheResetAttempted,
+ paymentRequest: bolt11Invoice ?? "LNURL: \(lnurlPayData.uri)"
+ )))
}
}
diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift
index 46bf0e110..42a174069 100644
--- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift
+++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift
@@ -16,6 +16,7 @@ struct SendConfirmationView: View {
@Binding var navigationPath: [SendRoute]
let requestPinCheck: () async -> Bool
let prepareIncomingPaymentRequest: () async throws -> Void
+ let routingCacheResetAttempted: Bool
@State private var showDetails = false
@State private var showingBiometricError = false
@@ -163,38 +164,7 @@ struct SendConfirmationView: View {
}
SwipeButton(title: t("wallet__send_swipe"), accentColor: accentColor, swipeProgress: $swipeProgress) {
- // Validate payment and show warnings if needed
- let warnings = await validatePayment()
- if !warnings.isEmpty {
- let shouldProceed = try await showWarnings(warnings)
- if !shouldProceed {
- throw CancellationError()
- }
- }
-
- // Check if authentication is required for payments
- if settings.requirePinForPayments && settings.pinEnabled {
- if settings.useBiometrics && BiometricAuth.isAvailable {
- let result = await BiometricAuth.authenticate()
- switch result {
- case .success:
- break
- case .cancelled:
- throw CancellationError()
- case let .failed(message):
- biometricErrorMessage = message
- showingBiometricError = true
- throw CancellationError()
- }
- } else {
- let shouldProceed = await requestPinCheck()
- guard shouldProceed else {
- throw CancellationError()
- }
- }
- }
-
- try await performPayment()
+ try await submitPayment()
}
}
.navigationBarHidden(true)
@@ -473,6 +443,41 @@ struct SendConfirmationView: View {
}
}
+ private func submitPayment() async throws {
+ // Validate payment and show warnings if needed
+ let warnings = await validatePayment()
+ if !warnings.isEmpty {
+ let shouldProceed = try await showWarnings(warnings)
+ if !shouldProceed {
+ throw CancellationError()
+ }
+ }
+
+ // Check if authentication is required for payments
+ if settings.requirePinForPayments && settings.pinEnabled {
+ if settings.useBiometrics && BiometricAuth.isAvailable {
+ let result = await BiometricAuth.authenticate()
+ switch result {
+ case .success:
+ break
+ case .cancelled:
+ throw CancellationError()
+ case let .failed(message):
+ biometricErrorMessage = message
+ showingBiometricError = true
+ throw CancellationError()
+ }
+ } else {
+ let shouldProceed = await requestPinCheck()
+ guard shouldProceed else {
+ throw CancellationError()
+ }
+ }
+ }
+
+ try await performPayment()
+ }
+
private func contactRecipient(_ contact: PubkyContact) -> some View {
HStack(spacing: 8) {
PubkyContactAvatar(contact: contact, size: 24)
@@ -515,7 +520,7 @@ struct SendConfirmationView: View {
sats: paymentSats,
onTimeout: {
app.addPendingPaymentHash(paymentHash, contactPublicKey: contactPublicKey)
- navigationPath.append(.pending(paymentHash: paymentHash))
+ navigationPath.append(.pending(paymentHash: paymentHash, retryRoute: .confirm))
}
)
await syncContactForActivity(paymentId: paymentHash, contactPublicKey: contactPublicKey)
@@ -563,13 +568,12 @@ struct SendConfirmationView: View {
try? await CoreService.shared.activity.deletePreActivityMetadata(paymentId: paymentId)
}
- // TODO: remove toast and use failure screen instead
- app.toast(error)
-
- // TODO: this is a hack to make sure the navigation binding is ready
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- navigationPath.append(.failure)
- }
+ navigationPath.append(.failure(SendFailureContext(
+ error: error,
+ retryRoute: .confirm,
+ routingCacheResetAttempted: routingCacheResetAttempted,
+ paymentRequest: app.selectedWalletToPayFrom == .lightning ? app.scannedLightningInvoice?.bolt11 : nil
+ )))
}
}
diff --git a/Bitkit/Views/Wallets/Send/SendFailure.swift b/Bitkit/Views/Wallets/Send/SendFailure.swift
index fa4580f77..829dd5b17 100644
--- a/Bitkit/Views/Wallets/Send/SendFailure.swift
+++ b/Bitkit/Views/Wallets/Send/SendFailure.swift
@@ -1,15 +1,90 @@
+import LDKNode
import SwiftUI
-// TODO: add error message and retry button
+func sendFailureMessage(for error: Error) -> String {
+ let fallbackMessage = t("wallet__payment_failed_description")
+
+ if let reason = (error as? AppError)?.paymentFailureReason {
+ return PaymentFailureReason.userMessage(for: reason)
+ }
+
+ if let requestError = error as? PaykitPaymentRequestError {
+ return requestError.localizedDescription
+ }
+
+ return fallbackMessage
+}
+
+func shouldResetRoutingCachesOnRetry(for error: Error) -> Bool {
+ guard let reason = (error as? AppError)?.paymentFailureReason else {
+ return false
+ }
+
+ return reason.shouldResetRoutingCachesOnRetry
+}
+
+func sendFailureType(for error: Error) -> String {
+ if let reason = (error as? AppError)?.paymentFailureReason {
+ return compactFailureType(String(describing: reason))
+ }
+
+ if let requestError = error as? PaykitPaymentRequestError {
+ return compactFailureType(String(describing: requestError))
+ }
+
+ if let appError = error as? AppError, let underlyingError = appError.underlyingError {
+ return compactFailureType(String(describing: underlyingError))
+ }
+
+ return compactFailureType(String(describing: error))
+}
+
+private func compactFailureType(_ value: String) -> String {
+ var result = value
+
+ if result.hasPrefix("Optional("), result.hasSuffix(")") {
+ result = String(result.dropFirst("Optional(".count).dropLast())
+ }
+
+ if let parenthesisIndex = result.firstIndex(of: "(") {
+ result = String(result[.. Void
+
+ private var title: String {
+ switch context.retryRoute {
+ case .confirm:
+ return app.selectedWalletToPayFrom == .lightning ? t("wallet__send_instant_failed") : t("wallet__send_error_tx_failed")
+ case .quickpay, .lnurlPayConfirm:
+ return t("wallet__send_instant_failed")
+ }
+ }
var body: some View {
VStack(alignment: .leading, spacing: 0) {
ZStack {
VStack(alignment: .leading, spacing: 0) {
- SheetHeader(title: t("wallet__send_error_tx_failed"), showBackButton: false)
+ SheetHeader(title: title, showBackButton: false)
+ .accessibilityIdentifier("SendFailure")
+
+ BodyMText(context.message ?? t("wallet__payment_failed_description"))
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .accessibilityIdentifier("SendFailureMessage")
Spacer()
@@ -22,10 +97,23 @@ struct SendFailure: View {
Spacer()
- HStack(spacing: 16) {
- CustomButton(title: t("common__close")) {
- sheets.hideSheet()
+ VStack(spacing: 16) {
+ CustomButton(
+ title: t("wallet__send_error_support"),
+ variant: .secondary,
+ isDisabled: wallet.isRetryingLightningPayment
+ ) {
+ contactSupport()
+ }
+ .accessibilityIdentifier("Support")
+
+ CustomButton(
+ title: t("common__try_again"),
+ isLoading: wallet.isRetryingLightningPayment
+ ) {
+ retryPayment()
}
+ .accessibilityIdentifier("Retry")
}
}
.padding(.horizontal, 16)
@@ -35,4 +123,73 @@ struct SendFailure: View {
.sheetBackground()
}
}
+
+ private func retryPayment() {
+ guard context.resetRoutingCachesOnRetry else {
+ onRetryReady(false)
+ return
+ }
+
+ guard !wallet.isRetryingLightningPayment else { return }
+ wallet.isRetryingLightningPayment = true
+
+ Task { @MainActor in
+ defer {
+ wallet.isRetryingLightningPayment = false
+ }
+
+ do {
+ var cacheResetError: Error?
+ do {
+ try await wallet.resetPaymentRoutingCaches()
+ } catch {
+ cacheResetError = error
+ }
+
+ try await wallet.start()
+ let refreshStartedAt = Date()
+
+ if let cacheResetError {
+ throw cacheResetError
+ }
+
+ try await wallet.waitForPaymentRoutingDataRefresh(startedAt: refreshStartedAt)
+ onRetryReady(true)
+ } catch {
+ Logger.error("Failed to reset routing caches before payment retry: \(error)", context: "SendFailure")
+ app.toast(error)
+ }
+ }
+ }
+
+ private func contactSupport() {
+ sheets.hideSheet()
+ navigation.navigate(.reportIssue(ReportIssuePrefill(message: supportMessage())))
+ }
+
+ private func supportMessage() -> String {
+ return """
+ I need help with a failed send payment.
+
+ Failure type: \(context.failureType)
+ Payment method: \(app.selectedWalletToPayFrom)
+ Routing cache reset attempted: \(context.routingCacheResetAttempted ? "Yes" : "No")
+
+ Payment request: \(context.paymentRequest ?? supportPaymentRequest())
+
+ Please investigate this payment failure.
+ """
+ }
+
+ private func supportPaymentRequest() -> String {
+ if let invoice = app.scannedLightningInvoice {
+ return invoice.bolt11
+ }
+
+ if let lnurlPayData = app.lnurlPayData {
+ return "LNURL: \(lnurlPayData.uri)"
+ }
+
+ return "Unavailable"
+ }
}
diff --git a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift
index 481b7191b..697780722 100644
--- a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift
+++ b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift
@@ -25,6 +25,7 @@ struct HourglassLoadingView: View {
struct SendPendingScreen: View {
let paymentHash: String
+ let retryRoute: SendRetryRoute
@Binding var navigationPath: [SendRoute]
@EnvironmentObject private var activityList: ActivityListViewModel
@@ -87,7 +88,11 @@ struct SendPendingScreen: View {
}
} else {
app.consumeContactPaymentContext(forPendingPaymentHash: paymentHash)
- navigationPath.append(.failure)
+ navigationPath.append(.failure(SendFailureContext(
+ message: t("wallet__payment_failed_description"),
+ retryRoute: retryRoute,
+ resetRoutingCachesOnRetry: false
+ )))
}
}
}
diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift
index e23c1580a..5c39f51a7 100644
--- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift
+++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift
@@ -7,6 +7,7 @@ struct SendQuickpay: View {
@EnvironmentObject var wallet: WalletViewModel
@Binding var navigationPath: [SendRoute]
+ let routingCacheResetAttempted: Bool
var body: some View {
VStack {
@@ -40,9 +41,9 @@ struct SendQuickpay: View {
}
private func performPayment() async {
- do {
- var bolt11Invoice: String?
+ var bolt11Invoice: String?
+ do {
// Handle LNURL Pay
if let lnurlPayData = app.lnurlPayData {
// Set the amount in sats for the success screen
@@ -73,7 +74,7 @@ struct SendQuickpay: View {
sats: nil,
onTimeout: {
app.addPendingPaymentHash(paymentHash)
- navigationPath.append(.pending(paymentHash: paymentHash))
+ navigationPath.append(.pending(paymentHash: paymentHash, retryRoute: .quickpay))
}
)
Logger.info("Quickpay payment successful: \(paymentHash)")
@@ -82,19 +83,18 @@ struct SendQuickpay: View {
// onTimeout callback already navigated to .pending; suppress throw
return
} catch {
- handlePaymentError(error)
+ handlePaymentError(error, paymentRequest: bolt11Invoice)
}
}
- private func handlePaymentError(_ error: Error) {
+ private func handlePaymentError(_ error: Error, paymentRequest: String?) {
Logger.error("Quickpay payment failed: \(error)")
- // TODO: remove toast and use failure screen instead
- app.toast(error)
-
- // TODO: this is a hack to make sure the navigation binding is ready
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- navigationPath.append(.failure)
- }
+ navigationPath.append(.failure(SendFailureContext(
+ error: error,
+ retryRoute: .quickpay,
+ routingCacheResetAttempted: routingCacheResetAttempted,
+ paymentRequest: paymentRequest
+ )))
}
}
diff --git a/Bitkit/Views/Wallets/Send/SendSheet.swift b/Bitkit/Views/Wallets/Send/SendSheet.swift
index a6ae0c0c9..17984993c 100644
--- a/Bitkit/Views/Wallets/Send/SendSheet.swift
+++ b/Bitkit/Views/Wallets/Send/SendSheet.swift
@@ -1,5 +1,55 @@
import SwiftUI
+enum SendRetryRoute: Hashable {
+ case confirm
+ case quickpay
+ case lnurlPayConfirm
+
+ var sendRoute: SendRoute {
+ switch self {
+ case .confirm: .confirm
+ case .quickpay: .quickpay
+ case .lnurlPayConfirm: .lnurlPayConfirm
+ }
+ }
+}
+
+struct SendFailureContext: Hashable {
+ let message: String?
+ let retryRoute: SendRetryRoute
+ let resetRoutingCachesOnRetry: Bool
+ let failureType: String
+ let paymentRequest: String?
+ let routingCacheResetAttempted: Bool
+
+ init(
+ message: String?,
+ retryRoute: SendRetryRoute,
+ resetRoutingCachesOnRetry: Bool,
+ failureType: String = "Unknown",
+ paymentRequest: String? = nil,
+ routingCacheResetAttempted: Bool = false
+ ) {
+ self.message = message
+ self.retryRoute = retryRoute
+ self.resetRoutingCachesOnRetry = resetRoutingCachesOnRetry
+ self.failureType = failureType
+ self.paymentRequest = paymentRequest
+ self.routingCacheResetAttempted = routingCacheResetAttempted
+ }
+
+ init(error: Error, retryRoute: SendRetryRoute, routingCacheResetAttempted: Bool = false, paymentRequest: String? = nil) {
+ let shouldResetRoutingCaches = shouldResetRoutingCachesOnRetry(for: error)
+
+ message = sendFailureMessage(for: error)
+ self.retryRoute = retryRoute
+ resetRoutingCachesOnRetry = shouldResetRoutingCaches && !routingCacheResetAttempted
+ failureType = sendFailureType(for: error)
+ self.paymentRequest = paymentRequest
+ self.routingCacheResetAttempted = routingCacheResetAttempted
+ }
+}
+
enum SendRoute: Hashable {
case options
case contact
@@ -13,9 +63,9 @@ enum SendRoute: Hashable {
case tag
case quickpay
case pin
- case pending(paymentHash: String)
+ case pending(paymentHash: String, retryRoute: SendRetryRoute)
case success(paymentId: String)
- case failure
+ case failure(SendFailureContext)
case lnurlPayAmount
case lnurlPayConfirm
case lnurlWithdrawAmount
@@ -54,6 +104,7 @@ struct SendSheet: View {
@State private var navigationPath: [SendRoute] = []
@State private var hasValidatedAfterSync = false
+ @State private var routingCacheResetAttempted = false
@State private var syncTimedOut = false
@State private var pinCheckContinuations: [CheckedContinuation] = []
@@ -421,7 +472,8 @@ struct SendSheet: View {
SendConfirmationView(
navigationPath: $navigationPath,
requestPinCheck: requestPinCheck,
- prepareIncomingPaymentRequest: prepareIncomingPaymentRequest
+ prepareIncomingPaymentRequest: prepareIncomingPaymentRequest,
+ routingCacheResetAttempted: routingCacheResetAttempted
)
case .feeRate:
SendFeeRate(navigationPath: $navigationPath)
@@ -430,22 +482,31 @@ struct SendSheet: View {
case .tag:
SendTagScreen(navigationPath: $navigationPath)
case .quickpay:
- SendQuickpay(navigationPath: $navigationPath)
+ SendQuickpay(navigationPath: $navigationPath, routingCacheResetAttempted: routingCacheResetAttempted)
case .pin:
SendPinScreen(onCancel: { resolvePinCheck(false) }, onPinVerified: { resolvePinCheck(true) })
- case let .pending(paymentHash):
- SendPendingScreen(paymentHash: paymentHash, navigationPath: $navigationPath)
+ case let .pending(paymentHash, retryRoute):
+ SendPendingScreen(paymentHash: paymentHash, retryRoute: retryRoute, navigationPath: $navigationPath)
case let .success(paymentId):
SendSuccess(paymentId: paymentId)
- case .failure:
- SendFailure()
+ case let .failure(context):
+ SendFailure(
+ context: context,
+ onRetryReady: { didResetRoutingCaches in
+ if didResetRoutingCaches {
+ routingCacheResetAttempted = true
+ }
+ resetNavigationForRetry(context.retryRoute)
+ }
+ )
case .lnurlPayAmount:
LnurlPayAmount(navigationPath: $navigationPath)
case .lnurlPayConfirm:
LnurlPayConfirm(
navigationPath: $navigationPath,
requestPinCheck: requestPinCheck,
- prepareIncomingPaymentRequest: prepareIncomingPaymentRequest
+ prepareIncomingPaymentRequest: prepareIncomingPaymentRequest,
+ routingCacheResetAttempted: routingCacheResetAttempted
)
case .lnurlWithdrawAmount:
LnurlWithdrawAmount {
@@ -473,6 +534,11 @@ struct SendSheet: View {
)
}
}
+
+ private func resetNavigationForRetry(_ retryRoute: SendRetryRoute) {
+ let route = retryRoute.sendRoute
+ navigationPath = route == config.initialRoute ? [] : [route]
+ }
}
private struct SendComingSoonView: View {
diff --git a/BitkitTests/SavingsSwapTests.swift b/BitkitTests/SavingsSwapTests.swift
index 0f7aed361..0720817a2 100644
--- a/BitkitTests/SavingsSwapTests.swift
+++ b/BitkitTests/SavingsSwapTests.swift
@@ -1,9 +1,8 @@
+@testable import Bitkit
import BitkitCore
import LDKNode
import XCTest
-@testable import Bitkit
-
final class SavingsSwapTests: XCTestCase {
// MARK: - Quote math
@@ -91,33 +90,33 @@ final class SavingsSwapTests: XCTestCase {
// MARK: - Payment failure messages
- func testPaymentFailureReasonsMapToTheirUserMessages() {
+ func testPaymentFailureReasonsMapToSendFailureMessages() {
XCTAssertEqual(
- PaymentFailureReason.userMessage(for: .recipientRejected),
- t("wallet__toast_payment_failed_recipient_rejected")
+ AppError(paymentFailureReason: .recipientRejected).localizedDescription,
+ t("wallet__payment_recipient_rejected")
)
XCTAssertEqual(
- PaymentFailureReason.userMessage(for: .retriesExhausted),
- t("wallet__toast_payment_failed_retries_exhausted")
+ AppError(paymentFailureReason: .retriesExhausted).localizedDescription,
+ t("wallet__payment_retries_exhausted")
)
XCTAssertEqual(
- PaymentFailureReason.userMessage(for: .routeNotFound),
- t("wallet__toast_payment_failed_route_not_found")
+ AppError(paymentFailureReason: .routeNotFound).localizedDescription,
+ t("wallet__payment_route_not_found")
)
XCTAssertEqual(
- PaymentFailureReason.userMessage(for: .paymentExpired),
- t("wallet__toast_payment_failed_timeout")
+ AppError(paymentFailureReason: .paymentExpired).localizedDescription,
+ t("wallet__payment_expired")
)
}
func testUnmappedAndAbsentPaymentFailureReasonsFallBackToTheGenericMessage() {
XCTAssertEqual(
- PaymentFailureReason.userMessage(for: .unexpectedError),
- t("wallet__toast_payment_failed_description")
+ AppError(paymentFailureReason: .unexpectedError).localizedDescription,
+ t("wallet__payment_failed_description")
)
XCTAssertEqual(
- PaymentFailureReason.userMessage(for: nil),
- t("wallet__toast_payment_failed_description")
+ AppError(paymentFailureReason: nil).localizedDescription,
+ t("wallet__payment_failed_description")
)
}
diff --git a/changelog.d/next/652.added.md b/changelog.d/next/652.added.md
new file mode 100644
index 000000000..704b7a84d
--- /dev/null
+++ b/changelog.d/next/652.added.md
@@ -0,0 +1 @@
+Improved Lightning send failures with clearer localized messages and a retry action that refreshes payment routing before trying again.