From 248339c6f5ead8afab9a105bc5392874cec3a684 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 09:54:30 -0300 Subject: [PATCH 1/7] fix: add expiry check for BIP 21 invoices --- Bitkit/ViewModels/AppViewModel.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index a3b878af..3bb124a2 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -177,14 +177,18 @@ extension AppViewModel { } // Lightning invoice param found, prefer lightning payment if possible if case let .lightning(lightningInvoice) = try await decode(invoice: lnInvoice) { - if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { + if lightningInvoice.isExpired { + toast(type: .info, title: t("other__scan__error__expired"), description: nil) + // Fall through to on-chain handling below + } else if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { handleScannedLightningInvoice(lightningInvoice, bolt11: lnInvoice, onchainInvoice: invoice) return } + // If expired or insufficient funds -> proceed with on-chain } } - // No LN invoice found, proceed with onchain payment + // No LN invoice found or LN not usable, proceed with onchain payment handleScannedOnchainInvoice(invoice) case let .lightning(invoice): guard lightningService.status?.isRunning == true else { From 10fb65035cee01b23e0cedbd603ba20988003395 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 09:55:32 -0300 Subject: [PATCH 2/7] fix: add expiry check for pure lightning invoices --- Bitkit/ViewModels/AppViewModel.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 3bb124a2..9a41af13 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -197,6 +197,13 @@ extension AppViewModel { } Logger.debug("Lightning: \(invoice)") + + // Check if Lightning invoice is expired + if invoice.isExpired { + toast(type: .error, title: t("other__scan__error__expired"), description: nil) + return + } + if lightningService.canSend(amountSats: invoice.amountSatoshis) { handleScannedLightningInvoice(invoice, bolt11: uri) } else { From 2c1ea1cdfbf4dddeeb1a4cc607c2aadbacc83ca6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 09:57:04 -0300 Subject: [PATCH 3/7] fix: check if invoice expired while user was on confirmation screen --- Bitkit/Views/Wallets/Send/SendConfirmationView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift index 10fcabe0..1108b4af 100644 --- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift +++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift @@ -290,6 +290,11 @@ struct SendConfirmationView: View { var createdMetadataPaymentId: String? = nil do { + if app.selectedWalletToPayFrom == .lightning, let invoice = app.scannedLightningInvoice, invoice.isExpired { + app.toast(type: .error, title: t("other__scan__error__expired"), description: nil) + return + } + if app.selectedWalletToPayFrom == .lightning, let invoice = app.scannedLightningInvoice { let amount = wallet.sendAmountSats ?? invoice.amountSatoshis // Set the amount for the success screen From 886a4659aa23f7edf679f662f298e2aa1d2088e5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 10:17:22 -0300 Subject: [PATCH 4/7] fix: replace info type with error --- Bitkit/ViewModels/AppViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 9a41af13..82ae989d 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -178,7 +178,7 @@ extension AppViewModel { // Lightning invoice param found, prefer lightning payment if possible if case let .lightning(lightningInvoice) = try await decode(invoice: lnInvoice) { if lightningInvoice.isExpired { - toast(type: .info, title: t("other__scan__error__expired"), description: nil) + toast(type: .error, title: t("other__scan__error__expired"), description: nil) // Fall through to on-chain handling below } else if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { handleScannedLightningInvoice(lightningInvoice, bolt11: lnInvoice, onchainInvoice: invoice) From 6cb9029d13281a7098b7259e1e82982245a38772 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 13:04:27 -0300 Subject: [PATCH 5/7] fix: handle route for null onchain invoice and expired ln bolt 11 --- Bitkit/Utilities/PaymentNavigationHelper.swift | 13 +++++++++---- Bitkit/ViewModels/AppViewModel.swift | 8 +++++++- .../Views/Wallets/Send/SendEnterManuallyView.swift | 7 ++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/Bitkit/Utilities/PaymentNavigationHelper.swift b/Bitkit/Utilities/PaymentNavigationHelper.swift index acc87dea..55fbf2e1 100644 --- a/Bitkit/Utilities/PaymentNavigationHelper.swift +++ b/Bitkit/Utilities/PaymentNavigationHelper.swift @@ -102,7 +102,7 @@ struct PaymentNavigationHelper { app: AppViewModel, currency: CurrencyViewModel, settings: SettingsViewModel - ) -> SendRoute { + ) -> SendRoute? { if let lnurlWithdrawData = app.lnurlWithdrawData { if lnurlWithdrawData.minWithdrawable == lnurlWithdrawData.maxWithdrawable { return .lnurlWithdrawConfirm @@ -125,8 +125,8 @@ struct PaymentNavigationHelper { } // Handle lightning invoice - if app.scannedLightningInvoice != nil { - let amount = app.scannedLightningInvoice!.amountSatoshis + if let invoice = app.scannedLightningInvoice { + let amount = invoice.amountSatoshis if amount > 0 && shouldUseQuickpay { return .quickpay @@ -140,6 +140,11 @@ struct PaymentNavigationHelper { } // Handle onchain invoice - return .amount + if app.scannedOnchainInvoice != nil { + return .amount + } + + // No valid invoice data + return nil } } diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 82ae989d..e1654ca4 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -177,7 +177,13 @@ extension AppViewModel { } // Lightning invoice param found, prefer lightning payment if possible if case let .lightning(lightningInvoice) = try await decode(invoice: lnInvoice) { + // Check if Lightning invoice is expired - if so, fallback to on-chain if available if lightningInvoice.isExpired { + // Only fallback to on-chain if address is available, otherwise return + guard !invoice.address.isEmpty else { + toast(type: .error, title: t("other__scan__error__expired"), description: nil) + return + } toast(type: .error, title: t("other__scan__error__expired"), description: nil) // Fall through to on-chain handling below } else if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { @@ -199,7 +205,7 @@ extension AppViewModel { Logger.debug("Lightning: \(invoice)") // Check if Lightning invoice is expired - if invoice.isExpired { + guard !invoice.isExpired else { toast(type: .error, title: t("other__scan__error__expired"), description: nil) return } diff --git a/Bitkit/Views/Wallets/Send/SendEnterManuallyView.swift b/Bitkit/Views/Wallets/Send/SendEnterManuallyView.swift index 58d845ee..33176f85 100644 --- a/Bitkit/Views/Wallets/Send/SendEnterManuallyView.swift +++ b/Bitkit/Views/Wallets/Send/SendEnterManuallyView.swift @@ -72,12 +72,13 @@ struct SendEnterManuallyView: View { do { try await app.handleScannedData(uri) - let route = PaymentNavigationHelper.appropriateSendRoute( + if let route = PaymentNavigationHelper.appropriateSendRoute( app: app, currency: currency, settings: settings - ) - navigationPath.append(route) + ) { + navigationPath.append(route) + } } catch { Logger.error(error, context: "Failed to read data from clipboard") app.toast(error) From 8beb29a132a9feb5a35c5213a12e68c48f1446d3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 13:26:07 -0300 Subject: [PATCH 6/7] refactor: simplify logic --- Bitkit/ViewModels/AppViewModel.swift | 29 +++++++++++----------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index e1654ca4..653d6e80 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -177,24 +177,19 @@ extension AppViewModel { } // Lightning invoice param found, prefer lightning payment if possible if case let .lightning(lightningInvoice) = try await decode(invoice: lnInvoice) { - // Check if Lightning invoice is expired - if so, fallback to on-chain if available - if lightningInvoice.isExpired { - // Only fallback to on-chain if address is available, otherwise return - guard !invoice.address.isEmpty else { - toast(type: .error, title: t("other__scan__error__expired"), description: nil) - return - } - toast(type: .error, title: t("other__scan__error__expired"), description: nil) - // Fall through to on-chain handling below - } else if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { + if !lightningInvoice.isExpired, lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { handleScannedLightningInvoice(lightningInvoice, bolt11: lnInvoice, onchainInvoice: invoice) return } - // If expired or insufficient funds -> proceed with on-chain + // Lightning not usable (expired or insufficient funds) - fallback to on-chain if available + if lightningInvoice.isExpired { + toast(type: .error, title: t("other__scan__error__expired"), description: nil) + } } } - // No LN invoice found or LN not usable, proceed with onchain payment + // Fallback to on-chain if address is available + guard !invoice.address.isEmpty else { return } handleScannedOnchainInvoice(invoice) case let .lightning(invoice): guard lightningService.status?.isRunning == true else { @@ -202,19 +197,17 @@ extension AppViewModel { return } - Logger.debug("Lightning: \(invoice)") - - // Check if Lightning invoice is expired guard !invoice.isExpired else { toast(type: .error, title: t("other__scan__error__expired"), description: nil) return } - if lightningService.canSend(amountSats: invoice.amountSatoshis) { - handleScannedLightningInvoice(invoice, bolt11: uri) - } else { + guard lightningService.canSend(amountSats: invoice.amountSatoshis) else { toast(type: .error, title: "Insufficient Funds", description: "You do not have enough funds to send this payment.") + return } + + handleScannedLightningInvoice(invoice, bolt11: uri) case let .lnurlPay(data: lnurlPayData): Logger.debug("LNURL: \(lnurlPayData)") handleLnurlPayInvoice(lnurlPayData) From b3b22a4969be81c23dd56ef6ac3a4f8a44dca130 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 12 Jan 2026 13:28:59 -0300 Subject: [PATCH 7/7] refactor: replace nil check with let --- Bitkit/Utilities/PaymentNavigationHelper.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Utilities/PaymentNavigationHelper.swift b/Bitkit/Utilities/PaymentNavigationHelper.swift index 55fbf2e1..70737565 100644 --- a/Bitkit/Utilities/PaymentNavigationHelper.swift +++ b/Bitkit/Utilities/PaymentNavigationHelper.swift @@ -140,7 +140,7 @@ struct PaymentNavigationHelper { } // Handle onchain invoice - if app.scannedOnchainInvoice != nil { + if let _ = app.scannedOnchainInvoice { return .amount }