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/Extensions/PaymentFailureReason+UserMessage.swift b/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift index 6ebf71829..37a176241 100644 --- a/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift +++ b/Bitkit/Extensions/PaymentFailureReason+UserMessage.swift @@ -1,21 +1,51 @@ import LDKNode extension PaymentFailureReason { + enum UserMessageContext { + case generic + case send + } + /// User-facing message for a failed payment, mirroring Android's /// `PaymentFailureReason.toUserMessage`; reasons without a dedicated string fall back to /// the generic payment-failed description. - static func userMessage(for reason: PaymentFailureReason?) -> String { + static func userMessage(for reason: PaymentFailureReason?, context: UserMessageContext = .generic) -> String { + t(userMessageKey(for: reason, context: context)) + } + + static func userMessageKey(for reason: PaymentFailureReason?, context: UserMessageContext = .generic) -> 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") + switch context { + case .generic: + return "wallet__payment_retries_exhausted" + case .send: + return "wallet__send_payment_retries_exhausted" + } case .paymentExpired: - return t("wallet__toast_payment_failed_timeout") + return "wallet__payment_expired" + case .routeNotFound: + switch context { + case .generic: + return "wallet__payment_route_not_found" + case .send: + return "wallet__send_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 t("wallet__toast_payment_failed_description") + switch context { + case .generic, .send: + return "wallet__payment_failed_description" + } } } } diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 58349945b..dd87eddcb 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -1,4 +1,15 @@ "common__delete_yes" = "نعم، احذف"; +"wallet__payment_recipient_rejected" = "رفض المستلم دفعة Lightning هذه. تحقق من الفاتورة وحاول مرة أخرى."; +"wallet__payment_abandoned" = "تم إيقاف دفعة Lightning قبل اكتمالها."; +"wallet__payment_expired" = "انتهت صلاحية دفعة Lightning هذه. اطلب فاتورة جديدة."; +"wallet__payment_timeout" = "انتهت مهلة الدفع. حاول مرة أخرى."; +"wallet__send_payment_route_not_found" = "لم يتمكن Bitkit من العثور على مسار Lightning لهذه الدفعة. اضغط على إعادة المحاولة لتحديث توجيه المدفوعات والبحث عن مسار جديد."; +"wallet__send_payment_retries_exhausted" = "جرّب Bitkit عدة مسارات Lightning، لكن تعذر إكمال الدفعة. اضغط على إعادة المحاولة لتحديث توجيه المدفوعات وتجربة مسار جديد."; +"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" = "اسمك"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 80ea17aa7..9d4d35d31 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -850,7 +850,18 @@ "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__send_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index e39522383..0364499cf 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -979,7 +979,18 @@ "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__send_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..0b42448d4 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -976,7 +976,18 @@ "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__send_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..0f1e37c6a 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -723,7 +723,18 @@ "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__send_payment_route_not_found" = "Το Bitkit δεν μπόρεσε να βρει διαδρομή Lightning για αυτήν την πληρωμή. Πατήστε Επανάληψη για να ενημερωθεί η δρομολόγηση πληρωμών και να αναζητηθεί νέα διαδρομή."; +"wallet__send_payment_retries_exhausted" = "Το Bitkit δοκίμασε αρκετές διαδρομές Lightning, αλλά η πληρωμή δεν μπόρεσε να ολοκληρωθεί. Πατήστε Επανάληψη για να ενημερωθεί η δρομολόγηση πληρωμών και να δοκιμαστεί νέα διαδρομή."; +"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/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 0992f83ab..851abcafb 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1305,11 +1305,18 @@ "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__send_payment_route_not_found" = "Bitkit couldn't find a Lightning route for this payment. Tap Retry to update payment routing and check for a new path."; +"wallet__send_payment_retries_exhausted" = "Bitkit tried several Lightning routes, but the payment could not be completed. Tap Retry to update payment routing and try a new path."; +"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..51059cdb9 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -990,7 +990,18 @@ "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__send_payment_route_not_found" = "Bitkit no pudo encontrar una ruta Lightning para este pago. Toque Reintentar para actualizar el enrutamiento de pagos y buscar una ruta nueva."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..327cca4b0 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -871,7 +871,18 @@ "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__send_payment_route_not_found" = "Bitkit no ha podido encontrar una ruta Lightning para este pago. Toca Reintentar para actualizar el enrutamiento de pagos y buscar una ruta nueva."; +"wallet__send_payment_retries_exhausted" = "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."; +"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"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index c4505329f..8cd1b8fcd 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -1002,7 +1002,18 @@ "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__send_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..ae24e2ba8 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -963,7 +963,18 @@ "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__send_payment_route_not_found" = "Bitkit non ha trovato una rotta Lightning per questo pagamento. Tocca Riprova per aggiornare l'instradamento dei pagamenti e cercare un nuovo percorso."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..8a3e54d33 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -997,7 +997,18 @@ "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__send_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..035fc13c2 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -1003,7 +1003,18 @@ "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__send_payment_route_not_found" = "Bitkit nie znalazł trasy Lightning dla tej płatności. Stuknij Ponów, aby zaktualizować routing płatności i wyszukać nową ścieżkę."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..0e75fde75 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -1004,7 +1004,18 @@ "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__send_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..bf2e8ed9f 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -1,4 +1,15 @@ "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_payment_route_not_found" = "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."; +"wallet__send_payment_retries_exhausted" = "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."; +"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..913fd40d5 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -992,7 +992,18 @@ "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__send_payment_route_not_found" = "Bitkit не смог найти маршрут Lightning для этого платежа. Нажмите Повторить, чтобы обновить маршрутизацию платежей и найти новый путь."; +"wallet__send_payment_retries_exhausted" = "Bitkit попробовал несколько маршрутов Lightning, но платеж не удалось завершить. Нажмите Повторить, чтобы обновить маршрутизацию платежей и попробовать новый путь."; +"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..7f7c1bc07 100644 --- a/Bitkit/Utilities/Errors.swift +++ b/Bitkit/Utilities/Errors.swift @@ -66,15 +66,10 @@ 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? /// The original error this was wrapped from, when known. Preserved so callers can unwrap and @@ -83,7 +78,11 @@ struct AppError: LocalizedError { 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 @@ -106,7 +105,7 @@ 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) { @@ -115,6 +114,12 @@ struct AppError: LocalizedError { self.underlyingError = underlyingError } + init(paymentFailureReason reason: PaymentFailureReason?) { + underlyingError = nil + debugMessage = reason.map { String(describing: $0) } ?? "Unknown payment failure reason" + message = PaymentFailureReason.userMessageKey(for: reason, context: .send) + } + init(serviceError: CustomServiceError) { underlyingError = serviceError switch serviceError { 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/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/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index a49d41852..62a2c435d 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -105,52 +105,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 +135,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 @@ -218,7 +222,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 +234,7 @@ 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(message: sendFailureMessage(for: error), retryRoute: .lnurlPayConfirm)) } } diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift index 46bf0e110..cf86033b6 100644 --- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift +++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift @@ -163,38 +163,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 +442,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 +519,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 +567,7 @@ 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(message: sendFailureMessage(for: error), retryRoute: .confirm)) } } diff --git a/Bitkit/Views/Wallets/Send/SendFailure.swift b/Bitkit/Views/Wallets/Send/SendFailure.swift index fa4580f77..a2e8c6c57 100644 --- a/Bitkit/Views/Wallets/Send/SendFailure.swift +++ b/Bitkit/Views/Wallets/Send/SendFailure.swift @@ -1,15 +1,43 @@ import SwiftUI -// TODO: add error message and retry button +func sendFailureMessage(for error: Error) -> String { + let fallbackMessage = t("wallet__payment_failed_description") + if let appError = error as? AppError, appError.isGeneric { + return fallbackMessage + } + + let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) + return message.isEmpty ? fallbackMessage : message +} struct SendFailure: View { + @EnvironmentObject var app: AppViewModel @EnvironmentObject var sheets: SheetViewModel + @EnvironmentObject var wallet: WalletViewModel + + let message: String? + let retryRoute: SendRetryRoute + let onRetryReady: () -> Void + + private var title: String { + switch 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(message ?? t("wallet__payment_failed_description")) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityIdentifier("SendFailureMessage") Spacer() @@ -23,9 +51,15 @@ struct SendFailure: View { Spacer() HStack(spacing: 16) { - CustomButton(title: t("common__close")) { + CustomButton(title: t("common__close"), variant: .secondary, isDisabled: wallet.isRetryingLightningPayment) { sheets.hideSheet() } + .accessibilityIdentifier("Close") + + CustomButton(title: t("common__retry"), isLoading: wallet.isRetryingLightningPayment) { + retryPayment() + } + .accessibilityIdentifier("Retry") } } .padding(.horizontal, 16) @@ -35,4 +69,37 @@ struct SendFailure: View { .sheetBackground() } } + + private func retryPayment() { + 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() + } catch { + Logger.error("Failed to reset routing caches before payment retry: \(error)", context: "SendFailure") + app.toast(error) + } + } + } } diff --git a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift index 481b7191b..c16eb6782 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,7 @@ struct SendPendingScreen: View { } } else { app.consumeContactPaymentContext(forPendingPaymentHash: paymentHash) - navigationPath.append(.failure) + navigationPath.append(.failure(message: t("wallet__payment_failed_description"), retryRoute: retryRoute)) } } } diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift index e23c1580a..5a90c5d9c 100644 --- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift +++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift @@ -73,7 +73,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)") @@ -89,12 +89,6 @@ struct SendQuickpay: View { private func handlePaymentError(_ error: Error) { 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(message: sendFailureMessage(for: error), retryRoute: .quickpay)) } } diff --git a/Bitkit/Views/Wallets/Send/SendSheet.swift b/Bitkit/Views/Wallets/Send/SendSheet.swift index a6ae0c0c9..a8efa071e 100644 --- a/Bitkit/Views/Wallets/Send/SendSheet.swift +++ b/Bitkit/Views/Wallets/Send/SendSheet.swift @@ -1,5 +1,19 @@ 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 + } + } +} + enum SendRoute: Hashable { case options case contact @@ -13,9 +27,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(message: String?, retryRoute: SendRetryRoute) case lnurlPayAmount case lnurlPayConfirm case lnurlWithdrawAmount @@ -433,12 +447,16 @@ struct SendSheet: View { SendQuickpay(navigationPath: $navigationPath) 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(message, retryRoute): + SendFailure( + message: message, + retryRoute: retryRoute, + onRetryReady: { resetNavigationForRetry(retryRoute) } + ) case .lnurlPayAmount: LnurlPayAmount(navigationPath: $navigationPath) case .lnurlPayConfirm: @@ -473,6 +491,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..d6722913f 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__send_payment_retries_exhausted") ) XCTAssertEqual( - PaymentFailureReason.userMessage(for: .routeNotFound), - t("wallet__toast_payment_failed_route_not_found") + AppError(paymentFailureReason: .routeNotFound).localizedDescription, + t("wallet__send_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.