From b5b05977fb2f85352e3f71c469be1a1dd9871ba0 Mon Sep 17 00:00:00 2001 From: jack <212554440+jackjackbits@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:47:11 +0200 Subject: [PATCH] Show Bluetooth permission alerts on launch and foreground (#765) * Fix test suite peer ID collisions Use unique peer IDs for each test suite to prevent global registry collisions when Swift Testing runs suites in parallel. - PrivateChatE2ETests: PRIV_* prefix - PublicChatE2ETests: PUB_* prefix - Update all peer ID references to use actual instance IDs This fixes the race condition where simplePublicMessage() was receiving duplicate deliveries due to registry contamination. * Add Bluetooth permission & state alerts on launch and foreground Wire up existing Bluetooth alert infrastructure to show notifications when Bluetooth is off, unauthorized, or unsupported. Changes: - Add didUpdateBluetoothState() to BitchatDelegate protocol - BLEService now notifies delegate when Bluetooth state changes - ChatViewModel implements delegate method to show alerts - Check Bluetooth state on app launch (after 100ms delay) - Check Bluetooth state when app comes to foreground - Add getCurrentBluetoothState() method to BLEService The UI alert already existed but wasn't wired up. Now users will see appropriate alerts for: - Bluetooth turned off - Bluetooth permission denied - Bluetooth unsupported on device Alert includes a button to open Settings on iOS. --------- Co-authored-by: jack --- bitchat/Protocols/BitchatProtocol.swift | 8 ++++-- bitchat/Services/BLEService.swift | 11 +++++++- bitchat/ViewModels/ChatViewModel.swift | 27 +++++++++++++++++-- bitchatTests/BLEServiceTests.swift | 3 +++ .../Fragmentation/FragmentationTests.swift | 2 ++ 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index 0c27d3d5..7b21136a 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -59,6 +59,7 @@ /// import Foundation +import CoreBluetooth // MARK: - Message Types @@ -164,14 +165,17 @@ protocol BitchatDelegate: AnyObject { func didConnectToPeer(_ peerID: String) func didDisconnectFromPeer(_ peerID: String) func didUpdatePeerList(_ peers: [String]) - + // Optional method to check if a fingerprint belongs to a favorite peer func isFavorite(fingerprint: String) -> Bool - + func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) // Low-level events for better separation of concerns func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) + + // Bluetooth state updates for user notifications + func didUpdateBluetoothState(_ state: CBManagerState) func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) } diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 259cdf3b..bd612398 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -552,7 +552,11 @@ final class BLEService: NSObject { func getNoiseService() -> NoiseEncryptionService { return noiseService } - + + func getCurrentBluetoothState() -> CBManagerState { + return centralManager?.state ?? .unknown + } + // MARK: Messaging // Transport protocol conformance helper: simplified public message send @@ -694,6 +698,11 @@ extension BLEService: GossipSyncManager.Delegate { extension BLEService: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { + // Notify delegate about state change on main thread + Task { @MainActor in + self.delegate?.didUpdateBluetoothState(central.state) + } + if central.state == .poweredOn { // Start scanning - use allow duplicates for faster discovery when active startScanning() diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 8955181f..3fe260ea 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -549,7 +549,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Start mesh service immediately meshService.startServices() - + + // Check initial Bluetooth state after a brief delay to allow centralManager initialization + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + guard let self = self else { return } + if let bleService = self.meshService as? BLEService { + let state = bleService.getCurrentBluetoothState() + self.updateBluetoothState(state) + } + } + // Announce Tor status (geohash-only; do not show in mesh chat). Only when auto-start is allowed. if TorManager.shared.torEnforced && !torStatusAnnounced && TorManager.shared.isAutoStartAllowed() { torStatusAnnounced = true @@ -2834,6 +2843,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { @MainActor @objc private func appDidBecomeActive() { + // Check Bluetooth state and show alert if needed + if let bleService = meshService as? BLEService { + let currentState = bleService.getCurrentBluetoothState() + updateBluetoothState(currentState) + } + // When app becomes active, send read receipts for visible private chat if let peerID = selectedPrivateChatPeer { // Try immediately @@ -4597,8 +4612,16 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { } // Mention parsing moved from BLE – use the existing non-optional helper below + // MARK: - Bluetooth State Monitoring + + func didUpdateBluetoothState(_ state: CBManagerState) { + Task { @MainActor in + updateBluetoothState(state) + } + } + // MARK: - Peer Connection Events - + func didConnectToPeer(_ peerID: String) { SecureLogger.debug("🤝 Peer connected: \(peerID)", category: .session) diff --git a/bitchatTests/BLEServiceTests.swift b/bitchatTests/BLEServiceTests.swift index ee2a58a8..c691ca17 100644 --- a/bitchatTests/BLEServiceTests.swift +++ b/bitchatTests/BLEServiceTests.swift @@ -286,4 +286,7 @@ private final class MockBitchatDelegate: BitchatDelegate { func didUpdatePeerList(_ peers: [String]) {} func isFavorite(fingerprint: String) -> Bool { return false } func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {} + func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {} + func didUpdateBluetoothState(_ state: CBManagerState) {} + func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) {} } diff --git a/bitchatTests/Fragmentation/FragmentationTests.swift b/bitchatTests/Fragmentation/FragmentationTests.swift index b3b41e7a..c2ddfde1 100644 --- a/bitchatTests/Fragmentation/FragmentationTests.swift +++ b/bitchatTests/Fragmentation/FragmentationTests.swift @@ -8,6 +8,7 @@ import Testing import Foundation +import CoreBluetooth @testable import bitchat struct FragmentationTests { @@ -134,6 +135,7 @@ extension FragmentationTests { func isFavorite(fingerprint: String) -> Bool { false } func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {} func didReceiveNoisePayload(from peerID: String, type: NoisePayloadType, payload: Data, timestamp: Date) {} + func didUpdateBluetoothState(_ state: CBManagerState) {} func didReceivePublicMessage(from peerID: String, nickname: String, content: String, timestamp: Date) { publicMessages.append((peerID, nickname, content)) }