From 245565c974bd1bb2da30b4a33ca4ce508bc7153b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 18 Nov 2025 14:02:02 -0300 Subject: [PATCH 01/25] chore: add geoblock env variable --- Bitkit/Constants/Env.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index 64e63e2f..0078c82e 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -16,6 +16,12 @@ enum Env { #endif static let dustLimit = 547 + #if GEO + static let isGeoblockingEnabled = true + #else + static let isGeoblockingEnabled = ProcessInfo.processInfo.environment["GEO"] == "true" + #endif + /// The current execution context of the app static var currentExecutionContext: ExecutionContext { return Bundle.main.bundleIdentifier?.lowercased().contains("notification") == true ? .pushNotificationExtension : .foregroundApp From a0e70f99244b15933011d3f560cae910e120f5a0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 18 Nov 2025 14:08:30 -0300 Subject: [PATCH 02/25] feat: add env variable check to checkGeoStatus --- Bitkit/Services/CoreService.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 4a165f2e..85764753 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -1340,6 +1340,10 @@ class CoreService { } func checkGeoStatus() async throws -> Bool? { + if !Env.isGeoblockingEnabled { + return false + } + try await ServiceQueue.background(.core) { Logger.info("Checking geo status...", context: "GeoCheck") guard let url = URL(string: Env.geoCheckUrl) else { From 2a811e904be6c5d17f6bf397c72ada116bd58f04 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 18 Nov 2025 14:13:47 -0300 Subject: [PATCH 03/25] ci: add GEO env variable --- .github/workflows/e2e-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ba6326c0..1198a9dd 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,6 +55,7 @@ jobs: CHATWOOT_API: ${{ secrets.CHATWOOT_API }} SIMULATOR_NAME: "iPhone 17" OS_VERSION: "latest" + GEO: false run: | echo "=== Building iOS app ===" echo "Using simulator: $SIMULATOR_NAME (iOS $OS_VERSION)" From 3754e0e1416743beb6e308618d857be29fd46b58 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 09:01:18 -0300 Subject: [PATCH 04/25] fix: add contextual type to nil return --- Bitkit/Services/CoreService.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 85764753..79ee7722 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -1344,11 +1344,11 @@ class CoreService { return false } - try await ServiceQueue.background(.core) { + return try await ServiceQueue.background(.core) { Logger.info("Checking geo status...", context: "GeoCheck") guard let url = URL(string: Env.geoCheckUrl) else { Logger.error("Invalid geocheck URL: \(Env.geoCheckUrl)", context: "GeoCheck") - return nil + return nil as Bool? } let (_, response) = try await URLSession.shared.data(from: url) @@ -1363,10 +1363,10 @@ class CoreService { return true default: Logger.warn("Unexpected status code: \(httpResponse.statusCode)", context: "GeoCheck") - return nil + return nil as Bool? } } - return nil + return nil as Bool? } } } From d4cd4198c1356f965228c0327e571869ed743ae5 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 09:03:56 -0300 Subject: [PATCH 05/25] chore: add swift compiler flags --- Bitkit.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 884e39e9..f413d66b 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -592,6 +592,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = GEO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -645,6 +646,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = GEO; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; From 986b6e3fc4bec05592d722ca9f1434c56bb325c7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:12:12 -0300 Subject: [PATCH 06/25] chore: add geoblock helper methods --- Bitkit/Services/LightningService.swift | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 88c9c3e0..cc4fb93d 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -546,6 +546,31 @@ extension LightningService { var peers: [PeerDetails]? { node?.listPeers() } var channels: [ChannelDetails]? { node?.listChannels() } var payments: [PaymentDetails]? { node?.listPayments() } + + /// Returns LSP (Blocktank) peer node IDs + func getLspPeerNodeIds() -> [String] { + return Env.trustedLnPeers.map(\.nodeId) + } + + /// Checks if there are connected peers other than LSP peers + /// Used for geoblocking to determine if Lightning operations can proceed + func hasExternalPeers() -> Bool { + guard let peers else { return false } + let lspNodeIds = Set(getLspPeerNodeIds()) + return peers.contains { peer in + !lspNodeIds.contains(peer.nodeId) + } + } + + /// Filters channels to exclude LSP channels + /// Used for geoblocking to only allow operations through non-Blocktank channels + func getNonLspChannels() -> [ChannelDetails] { + guard let channels else { return [] } + let lspNodeIds = Set(getLspPeerNodeIds()) + return channels.filter { channel in + !lspNodeIds.contains(channel.counterpartyNodeId) + } + } } // MARK: Events From 65c983e17dfe97e343cb476d2a1a53bd0bf515ac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:16:04 -0300 Subject: [PATCH 07/25] fix: filter only non cLSP channels and fix milisats conversion --- Bitkit/Services/LightningService.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index cc4fb93d..66b55a1f 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -298,19 +298,23 @@ class LightningService { } /// Checks if we have the correct outbound capacity to send the amount - /// - Parameter amountSats + /// - Parameter amountSats: Amount to send in satoshis + /// - Parameter isGeoblocked: If true, only count capacity from non-LSP channels /// - Returns: True if we can send the amount - func canSend(amountSats: UInt64) -> Bool { + func canSend(amountSats: UInt64, isGeoblocked: Bool = false) -> Bool { guard let channels else { Logger.warn("Channels not available") return false } + // When geoblocked, only count non-LSP channels + let channelsToUse = isGeoblocked ? getNonLspChannels() : channels + let totalNextOutboundHtlcLimitSats = - channels + channelsToUse .filter(\.isUsable) .map(\.nextOutboundHtlcLimitMsat) - .reduce(0, +) * 1000 + .reduce(0, +) / 1000 guard totalNextOutboundHtlcLimitSats > amountSats else { Logger.warn("Insufficient outbound capacity: \(totalNextOutboundHtlcLimitSats) < \(amountSats)") From 556212a524f4777bf928d2619a4c2979a5195fc8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:17:30 -0300 Subject: [PATCH 08/25] feat: add geoblock checks to lightning send --- Bitkit/Services/LightningService.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 66b55a1f..2135885b 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -369,11 +369,20 @@ class LightningService { } } - func send(bolt11: String, sats: UInt64? = nil, params: SendingParameters? = nil) async throws -> PaymentHash { + func send(bolt11: String, sats: UInt64? = nil, params: SendingParameters? = nil, isGeoblocked: Bool = false) async throws -> PaymentHash { guard let node else { throw AppError(serviceError: .nodeNotSetup) } + // When geoblocked, verify we have external (non-LSP) peers + if isGeoblocked && !hasExternalPeers() { + Logger.error("Cannot send Lightning payment when geoblocked without external peers") + throw AppError( + title: "Lightning send unavailable", + description: "You need channels with non-Blocktank nodes to send Lightning payments." + ) + } + Logger.info("Paying bolt11: \(bolt11)") do { From 72182569916008897c5ef9ea51a04cfe9b9f7945 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:18:29 -0300 Subject: [PATCH 09/25] feat: update inbound calc --- Bitkit/ViewModels/WalletViewModel.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 950aa1b6..90856389 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -497,6 +497,21 @@ class WalletViewModel: ObservableObject { return capacity } + /// Total inbound Lightning capacity excluding LSP (Blocktank) channels + /// Used when geoblocked to show only non-Blocktank receiving capacity + var totalNonLspInboundLightningSats: UInt64? { + let nonLspChannels = lightningService.getNonLspChannels() + guard !nonLspChannels.isEmpty else { + return nil + } + + var capacity: UInt64 = 0 + for channel in nonLspChannels { + capacity += channel.inboundCapacityMsat / 1000 + } + return capacity + } + func refreshBip21(forceRefreshBolt11: Bool = false) async throws { // Get old payment ID and tags before refreshing (which may change payment ID) let oldPaymentId = await paymentId() From ca8d780dd4bc9b5f41f9fc5588fab39c882d02a0 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:19:19 -0300 Subject: [PATCH 10/25] feat: display non LSP inbound --- Bitkit/Views/Wallets/Receive/ReceiveEdit.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift index 1216c170..86920355 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift @@ -174,7 +174,10 @@ struct ReceiveEdit: View { private func needsAdditionalCjit() -> Bool { let isGeoBlocked = app.isGeoBlocked ?? false let minimumAmount = blocktank.minCjitSats ?? 0 - let inboundCapacity = wallet.totalInboundLightningSats ?? 0 + // When geoblocked, only count non-LSP inbound capacity + let inboundCapacity = isGeoBlocked + ? (wallet.totalNonLspInboundLightningSats ?? 0) + : (wallet.totalInboundLightningSats ?? 0) let invoiceAmount = amountViewModel.amountSats // Calculate maxClientBalance using TransferViewModel From 3361a0b73af988c78bf39687cc3b8bd6761c2549 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:20:12 -0300 Subject: [PATCH 11/25] feat: pass geoblock status --- Bitkit/ViewModels/WalletViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 90856389..3d412fc3 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -401,7 +401,8 @@ class WalletViewModel: ObservableObject { /// or take a while to complete/fail because it's retrying different paths. /// So we need to handle all these cases here. func send(bolt11: String, sats: UInt64? = nil) async throws -> PaymentHash { - let hash = try await lightningService.send(bolt11: bolt11, sats: sats) + let isGeoblocked = appViewModel.isGeoBlocked ?? false + let hash = try await lightningService.send(bolt11: bolt11, sats: sats, isGeoblocked: isGeoblocked) let eventId = String(hash) return try await withCheckedThrowingContinuation { continuation in From e3148f818a5e667f1239ba0923cb816cb64a44ea Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:22:00 -0300 Subject: [PATCH 12/25] feat: update canSend call --- Bitkit/ViewModels/AppViewModel.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index e670c724..5969abf3 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -165,7 +165,8 @@ 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) { + let isGeoblocked = isGeoBlocked ?? false + if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis, isGeoblocked: isGeoblocked) { handleScannedLightningInvoice(lightningInvoice, bolt11: lnInvoice, onchainInvoice: invoice) return } @@ -181,7 +182,8 @@ extension AppViewModel { } Logger.debug("Lightning: \(invoice)") - if lightningService.canSend(amountSats: invoice.amountSatoshis) { + let isGeoblocked = isGeoBlocked ?? false + if lightningService.canSend(amountSats: invoice.amountSatoshis, isGeoblocked: isGeoblocked) { handleScannedLightningInvoice(invoice, bolt11: uri) } else { toast(type: .error, title: "Insufficient Funds", description: "You do not have enough funds to send this payment.") From 812751536e071b47f1411d1c4c0264b8230f4512 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:23:41 -0300 Subject: [PATCH 13/25] feat: block transfer to spending --- Bitkit/Services/TransferService.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index f7fdf80c..28785771 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -30,8 +30,19 @@ class TransferService { amountSats: UInt64, channelId: String? = nil, fundingTxId: String? = nil, - lspOrderId: String? = nil + lspOrderId: String? = nil, + isGeoblocked: Bool = false ) async throws -> String { + // When geoblocked, block transfers to spending that involve LSP (Blocktank) + // toSpending with lspOrderId means it's a Blocktank LSP channel order + if isGeoblocked && type.isToSpending() && lspOrderId != nil { + Logger.error("Cannot create LSP transfer when geoblocked", context: "TransferService") + throw AppError( + title: "Transfer unavailable", + description: "Transfer to spending via Blocktank is not available in your region." + ) + } + let id = UUID().uuidString let createdAt = UInt64(Date().timeIntervalSince1970) From fbbab2edde19dbd6abecfcdd48fc28a8d88903ce Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:39:57 -0300 Subject: [PATCH 14/25] fix: error parameter --- Bitkit/Services/LightningService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 2135885b..cc08aac7 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -378,8 +378,8 @@ class LightningService { if isGeoblocked && !hasExternalPeers() { Logger.error("Cannot send Lightning payment when geoblocked without external peers") throw AppError( - title: "Lightning send unavailable", - description: "You need channels with non-Blocktank nodes to send Lightning payments." + message: "Lightning send unavailable", + debugMessage: "You need channels with non-Blocktank nodes to send Lightning payments." ) } From 2a0576e42caac62442697118bb48c97c69a95228 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 10:45:37 -0300 Subject: [PATCH 15/25] fix: set geoblocked parameter --- Bitkit/Services/TransferService.swift | 4 ++-- Bitkit/ViewModels/WalletViewModel.swift | 3 +-- Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift | 3 ++- Bitkit/Views/Wallets/Send/SendConfirmationView.swift | 3 ++- Bitkit/Views/Wallets/Send/SendQuickpay.swift | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index 28785771..00b17e6b 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -38,8 +38,8 @@ class TransferService { if isGeoblocked && type.isToSpending() && lspOrderId != nil { Logger.error("Cannot create LSP transfer when geoblocked", context: "TransferService") throw AppError( - title: "Transfer unavailable", - description: "Transfer to spending via Blocktank is not available in your region." + message: "Transfer unavailable", + debugMessage: "Transfer to spending via Blocktank is not available in your region." ) } diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 3d412fc3..f49378d5 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -400,8 +400,7 @@ class WalletViewModel: ObservableObject { /// A LN payment can throw an error right away, be successful right away, /// or take a while to complete/fail because it's retrying different paths. /// So we need to handle all these cases here. - func send(bolt11: String, sats: UInt64? = nil) async throws -> PaymentHash { - let isGeoblocked = appViewModel.isGeoBlocked ?? false + func send(bolt11: String, sats: UInt64? = nil, isGeoblocked: Bool = false) async throws -> PaymentHash { let hash = try await lightningService.send(bolt11: bolt11, sats: sats, isGeoblocked: isGeoblocked) let eventId = String(hash) diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index 076be8d2..2a71f2c8 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -285,7 +285,8 @@ struct LnurlPayConfirm: View { do { // Perform the Lightning payment - let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats) + let isGeoblocked = app.isGeoBlocked ?? false + let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats, isGeoblocked: isGeoblocked) Logger.info("LNURL payment successful: \(paymentHash)") navigationPath.append(.success(paymentHash)) } catch { diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift index 4dce7c73..33c338da 100644 --- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift +++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift @@ -278,7 +278,8 @@ struct SendConfirmationView: View { await createPreActivityMetadata(paymentId: paymentHash, paymentHash: paymentHash) // Perform the Lightning payment - try await wallet.send(bolt11: invoice.bolt11, sats: amount) + let isGeoblocked = app.isGeoBlocked ?? false + try await wallet.send(bolt11: invoice.bolt11, sats: amount, isGeoblocked: isGeoblocked) Logger.info("Lightning payment successful: \(paymentHash)") navigationPath.append(.success(paymentHash)) diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift index 49d26464..0bc4a1d2 100644 --- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift +++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift @@ -107,7 +107,8 @@ struct SendQuickpay: View { } do { - let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats) + let isGeoblocked = app.isGeoBlocked ?? false + let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats, isGeoblocked: isGeoblocked) Logger.info("Quickpay payment successful: \(paymentHash)") navigationPath.append(.success(paymentHash)) } catch { From ded5b69853479f0909141d34ad6ea6840e0f3917 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 11:32:53 -0300 Subject: [PATCH 16/25] fix: uniffy tab visibility --- Bitkit/ViewModels/WalletViewModel.swift | 17 +++++++++++++++-- .../Views/Wallets/Receive/ReceiveEdit.swift | 3 ++- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 19 +++++++++++++++---- .../Views/Wallets/Receive/ReceiveSheet.swift | 4 +++- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index f49378d5..27718bab 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -512,7 +512,13 @@ class WalletViewModel: ObservableObject { return capacity } - func refreshBip21(forceRefreshBolt11: Bool = false) async throws { + /// Check if there are non-LSP (non-Blocktank) channels available + /// Used for geoblocking to determine if Lightning operations can proceed + func hasNonLspChannels() -> Bool { + return !lightningService.getNonLspChannels().isEmpty + } + + func refreshBip21(forceRefreshBolt11: Bool = false, isGeoblocked: Bool = false) async throws { // Get old payment ID and tags before refreshing (which may change payment ID) let oldPaymentId = await paymentId() var tagsToMigrate: [String] = [] @@ -539,7 +545,14 @@ class WalletViewModel: ObservableObject { let amountSats = invoiceAmountSats > 0 ? invoiceAmountSats : nil - if channels?.count ?? 0 > 0 { + // When geoblocked, only create Lightning invoice if we have non-LSP channels + let hasUsableChannels: Bool = if isGeoblocked { + hasNonLspChannels() + } else { + channels?.count ?? 0 > 0 + } + + if hasUsableChannels { if forceRefreshBolt11 || bolt11.isEmpty { bolt11 = try await createInvoice(amountSats: amountSats, note: invoiceNote) } else { diff --git a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift index 86920355..d59249b5 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift @@ -133,7 +133,8 @@ struct ReceiveEdit: View { do { wallet.invoiceAmountSats = amountSats wallet.invoiceNote = note - try await wallet.refreshBip21(forceRefreshBolt11: true) + let isGeoblocked = app.isGeoBlocked ?? false + try await wallet.refreshBip21(forceRefreshBolt11: true, isGeoblocked: isGeoblocked) // Check if CJIT flow should be shown if needsAdditionalCjit() { diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 995bcd66..4a5f0ec7 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -42,9 +42,19 @@ struct ReceiveQr: View { } } + /// Check if there are usable channels for Lightning receive + /// When geoblocked, only count non-LSP channels + private var hasUsableChannels: Bool { + if app.isGeoBlocked == true { + return wallet.hasNonLspChannels() + } else { + return wallet.channelCount != 0 + } + } + private var availableTabItems: [TabItem] { - // Only show unified tab if there are channels - if wallet.channelCount != 0 { + // Only show unified tab if there are usable channels + if hasUsableChannels { return [ TabItem(.savings), TabItem(.unified, activeColor: .white), @@ -75,7 +85,7 @@ struct ReceiveQr: View { TabView(selection: $selectedTab) { tabContent(for: .savings) - if wallet.channelCount != 0 { + if hasUsableChannels { tabContent(for: .unified) } @@ -305,7 +315,8 @@ struct ReceiveQr: View { func refreshBip21() async { guard wallet.nodeLifecycleState == .running else { return } do { - try await wallet.refreshBip21() + let isGeoblocked = app.isGeoBlocked ?? false + try await wallet.refreshBip21(isGeoblocked: isGeoblocked) } catch { app.toast(error) } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift index 4d63aa60..915e6538 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift @@ -33,6 +33,7 @@ struct ReceiveSheet: View { @State private var navigationPath: [ReceiveRoute] = [] @EnvironmentObject private var wallet: WalletViewModel @EnvironmentObject private var tagManager: TagManager + @EnvironmentObject private var app: AppViewModel var body: some View { Sheet(id: .receive, data: config) { @@ -52,7 +53,8 @@ struct ReceiveSheet: View { if let paymentId = await wallet.paymentId(), !paymentId.isEmpty { try? await CoreService.shared.activity.resetPreActivityMetadataTags(paymentId: paymentId) } - try? await wallet.refreshBip21(forceRefreshBolt11: true) + let isGeoblocked = app.isGeoBlocked ?? false + try? await wallet.refreshBip21(forceRefreshBolt11: true, isGeoblocked: isGeoblocked) } } } From 4f54600dee60c98f29f35f757823dd731086d60c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 13:55:54 -0300 Subject: [PATCH 17/25] feat: dont display qr spending if only has blocktank channels --- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 4a5f0ec7..9b0a6501 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -69,7 +69,12 @@ struct ReceiveQr: View { } var showingCjitOnboarding: Bool { - return wallet.channelCount == 0 && cjitInvoice == nil && selectedTab == .spending + // Show CJIT onboarding when: + // 1. No channels at all, OR + // 2. Geoblocked with only Blocktank channels (treat as no usable channels) + let hasNoUsableChannels = (wallet.channelCount == 0) || + (app.isGeoBlocked == true && !wallet.hasNonLspChannels()) + return hasNoUsableChannels && cjitInvoice == nil && selectedTab == .spending } var body: some View { @@ -106,7 +111,16 @@ struct ReceiveQr: View { .foregroundColor(.purpleAccent), isDisabled: wallet.nodeLifecycleState != .running ) { - navigationPath.append(.cjitAmount) + // Check if geoblocked with only Blocktank channels + if app.isGeoBlocked == true && !wallet.hasNonLspChannels() { + app.toast( + type: .error, + title: "Instant Payments Unavailable", + description: "Bitkit does not provide Lightning services in your country, but you can still connect to other nodes." + ) + } else { + navigationPath.append(.cjitAmount) + } } } else { CustomButton(title: showDetails ? tTodo("QR Code") : tTodo("Show Details")) { @@ -152,7 +166,7 @@ struct ReceiveQr: View { @ViewBuilder func tabContent(for tab: ReceiveTab) -> some View { VStack(spacing: 0) { - if tab == .spending && wallet.channelCount == 0 && cjitInvoice == nil { + if showingCjitOnboarding { cjitOnboarding } else if showDetails { detailsContent(for: tab) From 91e64d8ef3f19beb0f6df6071c291e7d985d867c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 19 Nov 2025 18:46:31 -0300 Subject: [PATCH 18/25] chore: create a singleton service to geoblock --- Bitkit/Services/GeoService.swift | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Bitkit/Services/GeoService.swift diff --git a/Bitkit/Services/GeoService.swift b/Bitkit/Services/GeoService.swift new file mode 100644 index 00000000..29a71dc3 --- /dev/null +++ b/Bitkit/Services/GeoService.swift @@ -0,0 +1,39 @@ +import Foundation +import SwiftUI + +/// Service responsible for managing geoblocking state +/// This is the single source of truth for geoblocking status in the app +@MainActor +@Observable +class GeoService { + static let shared = GeoService() + + /// Current geoblocking status + /// - `false`: User is not geoblocked (default/fallback if check fails) + /// - `true`: User is geoblocked + var isGeoBlocked: Bool = false + + private let coreService: CoreService + + private init(coreService: CoreService = .shared) { + self.coreService = coreService + } + + /// Checks the current geoblocking status and updates the published state + /// Uses CoreService to make the HTTP request to the geo-check endpoint + func checkGeoStatus() async { + do { + let result = try await coreService.checkGeoStatus() + + // Handle nil response from CoreService (network error, invalid response, etc.) + // Default to false (not blocked) as a safe fallback + isGeoBlocked = result ?? false + + Logger.info("Geo status check completed: isGeoBlocked=\(isGeoBlocked)", context: "GeoService") + } catch { + // On error, default to not blocked (safe fallback) + isGeoBlocked = false + Logger.error("Failed to check geo status: \(error)", context: "GeoService") + } + } +} From e04f9796a6376c45c3f90980172b3013b3f66636 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Nov 2025 06:34:14 -0300 Subject: [PATCH 19/25] refactor: replace geoblock parameters --- Bitkit.xcodeproj/project.pbxproj | 11 ++++++++++- Bitkit/Services/GeoService.swift | 12 +++++++----- Bitkit/Services/LightningService.swift | 7 ++++--- Bitkit/Services/TransferService.swift | 4 ++-- Bitkit/ViewModels/AppViewModel.swift | 15 ++++----------- Bitkit/ViewModels/WalletViewModel.swift | 7 ++++--- Bitkit/Views/Onboarding/OnboardingSlider.swift | 2 +- Bitkit/Views/Transfer/FundingOptions.swift | 6 +++--- Bitkit/Views/Wallets/Receive/ReceiveEdit.swift | 5 ++--- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 9 ++++----- Bitkit/Views/Wallets/Receive/ReceiveSheet.swift | 3 +-- Bitkit/Views/Wallets/SavingsWalletView.swift | 2 +- Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift | 3 +-- .../Views/Wallets/Send/SendConfirmationView.swift | 3 +-- Bitkit/Views/Wallets/Send/SendQuickpay.swift | 3 +-- 15 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index f413d66b..7efa29f3 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -72,6 +72,13 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 317C819C2ECF174900116EBB /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Services/GeoService.swift, + ); + target = 96FE1F7B2C2DE6AC006D0C8B /* BitkitUITests */; + }; 96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -89,6 +96,7 @@ Models/LnPeer.swift, Models/Toast.swift, Services/CoreService.swift, + Services/GeoService.swift, Services/LightningService.swift, Services/MigrationsService.swift, Services/ServiceQueue.swift, @@ -113,6 +121,7 @@ Models/ReceivedTxSheetDetails.swift, Models/Toast.swift, Services/CoreService.swift, + Services/GeoService.swift, Services/LightningService.swift, Services/ServiceQueue.swift, Services/VssStoreIdProvider.swift, @@ -135,7 +144,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 96A44E912CEF5EA700FBACFF /* Bitkit */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3E2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3F2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Bitkit; sourceTree = ""; }; + 96A44E912CEF5EA700FBACFF /* Bitkit */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3E2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 317C819C2ECF174900116EBB /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3F2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Bitkit; sourceTree = ""; }; 96A44F4A2CEF5F4B00FBACFF /* BitkitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BitkitTests; sourceTree = ""; }; 96A44F562CEF5F5400FBACFF /* BitkitUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BitkitUITests; sourceTree = ""; }; 96A44F5C2CEF5F5800FBACFF /* BitkitNotification */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (96A44F5E2CEF5F5800FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = BitkitNotification; sourceTree = ""; }; diff --git a/Bitkit/Services/GeoService.swift b/Bitkit/Services/GeoService.swift index 29a71dc3..8e4af9ac 100644 --- a/Bitkit/Services/GeoService.swift +++ b/Bitkit/Services/GeoService.swift @@ -3,15 +3,13 @@ import SwiftUI /// Service responsible for managing geoblocking state /// This is the single source of truth for geoblocking status in the app -@MainActor -@Observable -class GeoService { +class GeoService: ObservableObject { static let shared = GeoService() /// Current geoblocking status /// - `false`: User is not geoblocked (default/fallback if check fails) /// - `true`: User is geoblocked - var isGeoBlocked: Bool = false + @Published var isGeoBlocked: Bool = false private let coreService: CoreService @@ -27,7 +25,11 @@ class GeoService { // Handle nil response from CoreService (network error, invalid response, etc.) // Default to false (not blocked) as a safe fallback - isGeoBlocked = result ?? false + let newValue = result ?? false + + await MainActor.run { + self.isGeoBlocked = newValue + } Logger.info("Geo status check completed: isGeoBlocked=\(isGeoBlocked)", context: "GeoService") } catch { diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index cc08aac7..971692c4 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -299,15 +299,15 @@ class LightningService { /// Checks if we have the correct outbound capacity to send the amount /// - Parameter amountSats: Amount to send in satoshis - /// - Parameter isGeoblocked: If true, only count capacity from non-LSP channels /// - Returns: True if we can send the amount - func canSend(amountSats: UInt64, isGeoblocked: Bool = false) -> Bool { + func canSend(amountSats: UInt64) -> Bool { guard let channels else { Logger.warn("Channels not available") return false } // When geoblocked, only count non-LSP channels + let isGeoblocked = GeoService.shared.isGeoBlocked let channelsToUse = isGeoblocked ? getNonLspChannels() : channels let totalNextOutboundHtlcLimitSats = @@ -369,12 +369,13 @@ class LightningService { } } - func send(bolt11: String, sats: UInt64? = nil, params: SendingParameters? = nil, isGeoblocked: Bool = false) async throws -> PaymentHash { + func send(bolt11: String, sats: UInt64? = nil, params: SendingParameters? = nil) async throws -> PaymentHash { guard let node else { throw AppError(serviceError: .nodeNotSetup) } // When geoblocked, verify we have external (non-LSP) peers + let isGeoblocked = GeoService.shared.isGeoBlocked if isGeoblocked && !hasExternalPeers() { Logger.error("Cannot send Lightning payment when geoblocked without external peers") throw AppError( diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index 00b17e6b..16f4a72c 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -30,11 +30,11 @@ class TransferService { amountSats: UInt64, channelId: String? = nil, fundingTxId: String? = nil, - lspOrderId: String? = nil, - isGeoblocked: Bool = false + lspOrderId: String? = nil ) async throws -> String { // When geoblocked, block transfers to spending that involve LSP (Blocktank) // toSpending with lspOrderId means it's a Blocktank LSP channel order + let isGeoblocked = GeoService.shared.isGeoBlocked if isGeoblocked && type.isToSpending() && lspOrderId != nil { Logger.error("Cannot create LSP transfer when geoblocked", context: "TransferService") throw AppError( diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 5969abf3..e8dd0a1d 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -13,8 +13,6 @@ class AppViewModel: ObservableObject { @Published var lnurlPayData: LnurlPayData? @Published var lnurlWithdrawData: LnurlWithdrawData? - @Published var isGeoBlocked: Bool? = nil - // Onboarding @AppStorage("hasSeenContactsIntro") var hasSeenContactsIntro: Bool = false @AppStorage("hasSeenProfileIntro") var hasSeenProfileIntro: Bool = false @@ -91,11 +89,8 @@ class AppViewModel: ObservableObject { deinit {} func checkGeoStatus() async { - do { - isGeoBlocked = try await coreService.checkGeoStatus() - } catch { - Logger.error("Failed to check geo status: \(error)", context: "GeoCheck") - } + // Delegate to GeoService singleton for centralized geo-blocking management + await GeoService.shared.checkGeoStatus() } func wipe() async throws { @@ -165,8 +160,7 @@ extension AppViewModel { } // Lightning invoice param found, prefer lightning payment if possible if case let .lightning(lightningInvoice) = try await decode(invoice: lnInvoice) { - let isGeoblocked = isGeoBlocked ?? false - if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis, isGeoblocked: isGeoblocked) { + if lightningService.canSend(amountSats: lightningInvoice.amountSatoshis) { handleScannedLightningInvoice(lightningInvoice, bolt11: lnInvoice, onchainInvoice: invoice) return } @@ -182,8 +176,7 @@ extension AppViewModel { } Logger.debug("Lightning: \(invoice)") - let isGeoblocked = isGeoBlocked ?? false - if lightningService.canSend(amountSats: invoice.amountSatoshis, isGeoblocked: isGeoblocked) { + if lightningService.canSend(amountSats: invoice.amountSatoshis) { handleScannedLightningInvoice(invoice, bolt11: uri) } else { toast(type: .error, title: "Insufficient Funds", description: "You do not have enough funds to send this payment.") diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 27718bab..bf0e14ca 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -400,8 +400,8 @@ class WalletViewModel: ObservableObject { /// A LN payment can throw an error right away, be successful right away, /// or take a while to complete/fail because it's retrying different paths. /// So we need to handle all these cases here. - func send(bolt11: String, sats: UInt64? = nil, isGeoblocked: Bool = false) async throws -> PaymentHash { - let hash = try await lightningService.send(bolt11: bolt11, sats: sats, isGeoblocked: isGeoblocked) + func send(bolt11: String, sats: UInt64? = nil) async throws -> PaymentHash { + let hash = try await lightningService.send(bolt11: bolt11, sats: sats) let eventId = String(hash) return try await withCheckedThrowingContinuation { continuation in @@ -518,7 +518,7 @@ class WalletViewModel: ObservableObject { return !lightningService.getNonLspChannels().isEmpty } - func refreshBip21(forceRefreshBolt11: Bool = false, isGeoblocked: Bool = false) async throws { + func refreshBip21(forceRefreshBolt11: Bool = false) async throws { // Get old payment ID and tags before refreshing (which may change payment ID) let oldPaymentId = await paymentId() var tagsToMigrate: [String] = [] @@ -546,6 +546,7 @@ class WalletViewModel: ObservableObject { let amountSats = invoiceAmountSats > 0 ? invoiceAmountSats : nil // When geoblocked, only create Lightning invoice if we have non-LSP channels + let isGeoblocked = GeoService.shared.isGeoBlocked let hasUsableChannels: Bool = if isGeoblocked { hasNonLspChannels() } else { diff --git a/Bitkit/Views/Onboarding/OnboardingSlider.swift b/Bitkit/Views/Onboarding/OnboardingSlider.swift index dc780a8d..6af0bfe0 100644 --- a/Bitkit/Views/Onboarding/OnboardingSlider.swift +++ b/Bitkit/Views/Onboarding/OnboardingSlider.swift @@ -77,7 +77,7 @@ struct OnboardingSlider: View { imageName: "lightning", title: t("onboarding__slide1_header"), text: tTodo("Enjoy instant and cheap payments with friends, family, and merchants on the Lightning Network."), - disclaimerText: app.isGeoBlocked == true ? t("onboarding__slide1_note") : nil, + disclaimerText: GeoService.shared.isGeoBlocked ? t("onboarding__slide1_note") : nil, accentColor: .purpleAccent ) .tag(1) diff --git a/Bitkit/Views/Transfer/FundingOptions.swift b/Bitkit/Views/Transfer/FundingOptions.swift index bd6a7ed0..047e33e5 100644 --- a/Bitkit/Views/Transfer/FundingOptions.swift +++ b/Bitkit/Views/Transfer/FundingOptions.swift @@ -7,7 +7,7 @@ struct FundingOptions: View { @EnvironmentObject var wallet: WalletViewModel var text: String { - if app.isGeoBlocked == true { + if GeoService.shared.isGeoBlocked { return t("lightning__funding__text_blocked") } else { return t("lightning__funding__text") @@ -33,7 +33,7 @@ struct FundingOptions: View { RectangleButton( icon: "transfer", title: t("lightning__funding__button1"), - isDisabled: wallet.totalOnchainSats == 0 || app.isGeoBlocked == true, + isDisabled: wallet.totalOnchainSats == 0 || GeoService.shared.isGeoBlocked, testID: "FundTransfer" ) { if app.hasSeenTransferToSpendingIntro { @@ -46,7 +46,7 @@ struct FundingOptions: View { RectangleButton( icon: "qr", title: t("lightning__funding__button2"), - isDisabled: app.isGeoBlocked == true, + isDisabled: GeoService.shared.isGeoBlocked, testID: "FundReceive" ) { navigation.reset() diff --git a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift index d59249b5..eca166e0 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift @@ -133,8 +133,7 @@ struct ReceiveEdit: View { do { wallet.invoiceAmountSats = amountSats wallet.invoiceNote = note - let isGeoblocked = app.isGeoBlocked ?? false - try await wallet.refreshBip21(forceRefreshBolt11: true, isGeoblocked: isGeoblocked) + try await wallet.refreshBip21(forceRefreshBolt11: true) // Check if CJIT flow should be shown if needsAdditionalCjit() { @@ -173,7 +172,7 @@ struct ReceiveEdit: View { } private func needsAdditionalCjit() -> Bool { - let isGeoBlocked = app.isGeoBlocked ?? false + let isGeoBlocked = GeoService.shared.isGeoBlocked let minimumAmount = blocktank.minCjitSats ?? 0 // When geoblocked, only count non-LSP inbound capacity let inboundCapacity = isGeoBlocked diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 9b0a6501..69be99aa 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -45,7 +45,7 @@ struct ReceiveQr: View { /// Check if there are usable channels for Lightning receive /// When geoblocked, only count non-LSP channels private var hasUsableChannels: Bool { - if app.isGeoBlocked == true { + if GeoService.shared.isGeoBlocked { return wallet.hasNonLspChannels() } else { return wallet.channelCount != 0 @@ -73,7 +73,7 @@ struct ReceiveQr: View { // 1. No channels at all, OR // 2. Geoblocked with only Blocktank channels (treat as no usable channels) let hasNoUsableChannels = (wallet.channelCount == 0) || - (app.isGeoBlocked == true && !wallet.hasNonLspChannels()) + (GeoService.shared.isGeoBlocked && !wallet.hasNonLspChannels()) return hasNoUsableChannels && cjitInvoice == nil && selectedTab == .spending } @@ -112,7 +112,7 @@ struct ReceiveQr: View { isDisabled: wallet.nodeLifecycleState != .running ) { // Check if geoblocked with only Blocktank channels - if app.isGeoBlocked == true && !wallet.hasNonLspChannels() { + if GeoService.shared.isGeoBlocked && !wallet.hasNonLspChannels() { app.toast( type: .error, title: "Instant Payments Unavailable", @@ -329,8 +329,7 @@ struct ReceiveQr: View { func refreshBip21() async { guard wallet.nodeLifecycleState == .running else { return } do { - let isGeoblocked = app.isGeoBlocked ?? false - try await wallet.refreshBip21(isGeoblocked: isGeoblocked) + try await wallet.refreshBip21() } catch { app.toast(error) } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift index 915e6538..33786282 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift @@ -53,8 +53,7 @@ struct ReceiveSheet: View { if let paymentId = await wallet.paymentId(), !paymentId.isEmpty { try? await CoreService.shared.activity.resetPreActivityMetadataTags(paymentId: paymentId) } - let isGeoblocked = app.isGeoBlocked ?? false - try? await wallet.refreshBip21(forceRefreshBolt11: true, isGeoblocked: isGeoblocked) + try? await wallet.refreshBip21(forceRefreshBolt11: true) } } } diff --git a/Bitkit/Views/Wallets/SavingsWalletView.swift b/Bitkit/Views/Wallets/SavingsWalletView.swift index b5ae0386..efd0697d 100644 --- a/Bitkit/Views/Wallets/SavingsWalletView.swift +++ b/Bitkit/Views/Wallets/SavingsWalletView.swift @@ -25,7 +25,7 @@ struct SavingsWalletView: View { } if wallet.totalOnchainSats > 0 { - if !(app.isGeoBlocked ?? true) { + if !GeoService.shared.isGeoBlocked { transferButton .transition(.move(edge: .leading).combined(with: .opacity)) .padding(.top, 32) diff --git a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift index 2a71f2c8..076be8d2 100644 --- a/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift +++ b/Bitkit/Views/Wallets/Send/LnurlPayConfirm.swift @@ -285,8 +285,7 @@ struct LnurlPayConfirm: View { do { // Perform the Lightning payment - let isGeoblocked = app.isGeoBlocked ?? false - let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats, isGeoblocked: isGeoblocked) + let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats) Logger.info("LNURL payment successful: \(paymentHash)") navigationPath.append(.success(paymentHash)) } catch { diff --git a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift index 33c338da..4dce7c73 100644 --- a/Bitkit/Views/Wallets/Send/SendConfirmationView.swift +++ b/Bitkit/Views/Wallets/Send/SendConfirmationView.swift @@ -278,8 +278,7 @@ struct SendConfirmationView: View { await createPreActivityMetadata(paymentId: paymentHash, paymentHash: paymentHash) // Perform the Lightning payment - let isGeoblocked = app.isGeoBlocked ?? false - try await wallet.send(bolt11: invoice.bolt11, sats: amount, isGeoblocked: isGeoblocked) + try await wallet.send(bolt11: invoice.bolt11, sats: amount) Logger.info("Lightning payment successful: \(paymentHash)") navigationPath.append(.success(paymentHash)) diff --git a/Bitkit/Views/Wallets/Send/SendQuickpay.swift b/Bitkit/Views/Wallets/Send/SendQuickpay.swift index 0bc4a1d2..49d26464 100644 --- a/Bitkit/Views/Wallets/Send/SendQuickpay.swift +++ b/Bitkit/Views/Wallets/Send/SendQuickpay.swift @@ -107,8 +107,7 @@ struct SendQuickpay: View { } do { - let isGeoblocked = app.isGeoBlocked ?? false - let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats, isGeoblocked: isGeoblocked) + let paymentHash = try await wallet.send(bolt11: bolt11, sats: wallet.sendAmountSats) Logger.info("Quickpay payment successful: \(paymentHash)") navigationPath.append(.success(paymentHash)) } catch { From 3888486cad77ec34d5b1736712063be953119241 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Nov 2025 06:58:00 -0300 Subject: [PATCH 20/25] chore: remove unused variable --- Bitkit/Views/Wallets/Receive/ReceiveSheet.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift index 33786282..4d63aa60 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift @@ -33,7 +33,6 @@ struct ReceiveSheet: View { @State private var navigationPath: [ReceiveRoute] = [] @EnvironmentObject private var wallet: WalletViewModel @EnvironmentObject private var tagManager: TagManager - @EnvironmentObject private var app: AppViewModel var body: some View { Sheet(id: .receive, data: config) { From f4f60a55365a0bb8e8a6a1a6ab6264f21d2af1cc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Nov 2025 07:01:16 -0300 Subject: [PATCH 21/25] chore: remove comments --- Bitkit/Services/GeoService.swift | 5 ----- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 3 --- 2 files changed, 8 deletions(-) diff --git a/Bitkit/Services/GeoService.swift b/Bitkit/Services/GeoService.swift index 8e4af9ac..725643c8 100644 --- a/Bitkit/Services/GeoService.swift +++ b/Bitkit/Services/GeoService.swift @@ -2,7 +2,6 @@ import Foundation import SwiftUI /// Service responsible for managing geoblocking state -/// This is the single source of truth for geoblocking status in the app class GeoService: ObservableObject { static let shared = GeoService() @@ -18,13 +17,10 @@ class GeoService: ObservableObject { } /// Checks the current geoblocking status and updates the published state - /// Uses CoreService to make the HTTP request to the geo-check endpoint func checkGeoStatus() async { do { let result = try await coreService.checkGeoStatus() - // Handle nil response from CoreService (network error, invalid response, etc.) - // Default to false (not blocked) as a safe fallback let newValue = result ?? false await MainActor.run { @@ -33,7 +29,6 @@ class GeoService: ObservableObject { Logger.info("Geo status check completed: isGeoBlocked=\(isGeoBlocked)", context: "GeoService") } catch { - // On error, default to not blocked (safe fallback) isGeoBlocked = false Logger.error("Failed to check geo status: \(error)", context: "GeoService") } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 69be99aa..b84f9025 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -42,8 +42,6 @@ struct ReceiveQr: View { } } - /// Check if there are usable channels for Lightning receive - /// When geoblocked, only count non-LSP channels private var hasUsableChannels: Bool { if GeoService.shared.isGeoBlocked { return wallet.hasNonLspChannels() @@ -111,7 +109,6 @@ struct ReceiveQr: View { .foregroundColor(.purpleAccent), isDisabled: wallet.nodeLifecycleState != .running ) { - // Check if geoblocked with only Blocktank channels if GeoService.shared.isGeoBlocked && !wallet.hasNonLspChannels() { app.toast( type: .error, From a0b7f7e90b3a6a694b4cd671fed8238274d97f3c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Nov 2025 07:13:32 -0300 Subject: [PATCH 22/25] chore: geo build env name --- Bitkit.xcodeproj/project.pbxproj | 4 ++-- Bitkit/Constants/Env.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 7efa29f3..1b98f45d 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -601,7 +601,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = GEO; + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = CHECK_GEOBLOCK; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -655,7 +655,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = GEO; + "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = CHECK_GEOBLOCK; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index a0a3bda3..af446d36 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -16,7 +16,7 @@ enum Env { #endif static let dustLimit = 547 - #if GEO + #if CHECK_GEOBLOCK static let isGeoblockingEnabled = true #else static let isGeoblockingEnabled = ProcessInfo.processInfo.environment["GEO"] == "true" From b18e50b05715dd24ef61c9576251966f2fe0f3de Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Nov 2025 07:34:59 -0300 Subject: [PATCH 23/25] fix: remove geoservice from ui tests --- Bitkit.xcodeproj/project.pbxproj | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 1b98f45d..e2d6630a 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -72,13 +72,6 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 317C819C2ECF174900116EBB /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Services/GeoService.swift, - ); - target = 96FE1F7B2C2DE6AC006D0C8B /* BitkitUITests */; - }; 96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -144,7 +137,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 96A44E912CEF5EA700FBACFF /* Bitkit */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3E2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 317C819C2ECF174900116EBB /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3F2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Bitkit; sourceTree = ""; }; + 96A44E912CEF5EA700FBACFF /* Bitkit */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (96A44F3D2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3E2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 96A44F3F2CEF5EA700FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Bitkit; sourceTree = ""; }; 96A44F4A2CEF5F4B00FBACFF /* BitkitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BitkitTests; sourceTree = ""; }; 96A44F562CEF5F5400FBACFF /* BitkitUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = BitkitUITests; sourceTree = ""; }; 96A44F5C2CEF5F5800FBACFF /* BitkitNotification */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (96A44F5E2CEF5F5800FBACFF /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = BitkitNotification; sourceTree = ""; }; From 9bf556c8aadc16edae56f4fa3e087295e08c2abb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 20 Nov 2025 14:18:32 -0300 Subject: [PATCH 24/25] fic: change the Xcode project configuration to append CHECK_GEOBLOCK to the compilation conditions instead of overriding them --- Bitkit.xcodeproj/project.pbxproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index e2d6630a..720f1086 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -593,8 +593,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = CHECK_GEOBLOCK; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CHECK_GEOBLOCK $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -648,7 +647,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = CHECK_GEOBLOCK; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "CHECK_GEOBLOCK $(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; From ad91f6c0bb01b35bfa5964b6c607d07e0c5acc74 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 24 Nov 2025 14:29:51 -0300 Subject: [PATCH 25/25] fix: refresh geoblocking on qr sheet display --- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index b84f9025..a88f0e09 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -149,6 +149,7 @@ struct ReceiveQr: View { } catch { app.toast(error) } + try? await app.checkGeoStatus() } .onChange(of: wallet.nodeLifecycleState) { newState in // They may open this view before node has started