From dfeaf848e090e2e9b6a5f54782153a42d86c8513 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 3 May 2025 20:44:12 -0700 Subject: [PATCH 01/14] Device onboarding initial commit --- Localizable.xcstrings | 192 ++++++----- Meshtastic.xcodeproj/project.pbxproj | 16 +- Meshtastic/Extensions/UserDefaults.swift | 8 + Meshtastic/Helpers/BLEManager.swift | 3 + Meshtastic/Helpers/LocationsHandler.swift | 16 +- Meshtastic/MeshtasticApp.swift | 6 +- Meshtastic/Views/Bluetooth/Connect.swift | 14 - Meshtastic/Views/ContentView.swift | 23 +- .../Views/Onboarding/DeviceOnboarding.swift | 316 ++++++++++++++++++ 9 files changed, 490 insertions(+), 104 deletions(-) create mode 100644 Meshtastic/Views/Onboarding/DeviceOnboarding.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 95f524d2..e6f2f5de 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -3055,6 +3055,9 @@ } } } + }, + "App Notifications" : { + }, "App Settings" : { "localizations" : { @@ -6533,6 +6536,9 @@ } } } + }, + "Configure notification permissions" : { + }, "Confirm" : { "localizations" : { @@ -7027,6 +7033,9 @@ } } } + }, + "Continue to next step" : { + }, "Control Type" : { "localizations" : { @@ -7371,6 +7380,9 @@ } } } + }, + "Critical Alerts" : { + }, "Current" : { "localizations" : { @@ -10999,6 +11011,9 @@ } } } + }, + "Enable MQTT" : { + }, "Enable Notifications" : { "localizations" : { @@ -11428,6 +11443,34 @@ } } }, + "Environment Metrics" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Metriche dei sensori" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Метрике сензора" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "传感器指标" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "傳感器數據" + } + } + } + }, "Environment Metrics Log" : { "localizations" : { "it" : { @@ -13113,6 +13156,9 @@ } } } + }, + "Get started" : { + }, "Get the latest alpha firmware" : { "localizations" : { @@ -14370,34 +14416,6 @@ } } }, - "How often power metrics are sent out over the mesh. Default is 30 minutes." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Con quale frequenza vengono inviate le metriche di potenza attraverso la rete. L'impostazione predefinita è 30 minuti." - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Колико често се метрике снаге шаљу преко мреже. Подразумевано је 30 минута." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "通过网格发送功率指标的频率。默认为 30 分钟。" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "電力指標透過網狀網路發送的頻率。預設為每 30 分鐘一次。" - } - } - } - }, "How often environment metrics are sent out over the mesh. Default is 30 minutes." : { "localizations" : { "it" : { @@ -14426,6 +14444,34 @@ } } }, + "How often power metrics are sent out over the mesh. Default is 30 minutes." : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Con quale frequenza vengono inviate le metriche di potenza attraverso la rete. L'impostazione predefinita è 30 minuti." + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Колико често се метрике снаге шаљу преко мреже. Подразумевано је 30 минута." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "通过网格发送功率指标的频率。默认为 30 分钟。" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "電力指標透過網狀網路發送的頻率。預設為每 30 分鐘一次。" + } + } + } + }, "How often should we try to get a GPS position." : { "localizations" : { "it" : { @@ -18592,28 +18638,6 @@ } } }, - "Long Range - Fast" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "A lungo raggio - Veloce" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Дугачки домет - Брзо" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "Long Range - Fast" - } - } - } - }, "Long Name" : { "localizations" : { "de" : { @@ -18710,6 +18734,28 @@ } } }, + "Long Range - Fast" : { + "localizations" : { + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "A lungo raggio - Veloce" + } + }, + "sr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Дугачки домет - Брзо" + } + }, + "zh-Hant-TW" : { + "stringUnit" : { + "state" : "translated", + "value" : "Long Range - Fast" + } + } + } + }, "Long Range - Slow" : { "localizations" : { "it" : { @@ -22085,6 +22131,9 @@ } } } + }, + "Meshtastic" : { + }, "Meshtastic Node %@ has shared channels with you" : { "localizations" : { @@ -22113,6 +22162,9 @@ } } } + }, + "Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings." : { + }, "Meshtastic® Copyright Meshtastic LLC" : { "localizations" : { @@ -25421,6 +25473,9 @@ } } } + }, + "Phone Location" : { + }, "phone.gps" : { "localizations" : { @@ -31018,6 +31073,9 @@ } } } + }, + "Send Notifications" : { + }, "Send Reboot OTA" : { "localizations" : { @@ -31075,34 +31133,6 @@ } } }, - "Environment Metrics" : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Metriche dei sensori" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Метрике сензора" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "传感器指标" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "傳感器數據" - } - } - } - }, "Sensor options" : { "localizations" : { "it" : { @@ -31802,6 +31832,9 @@ } } } + }, + "Set up later" : { + }, "set.region" : { "localizations" : { @@ -37850,6 +37883,9 @@ } } } + }, + "Welcome to" : { + }, "What does the lock mean?" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index bbdd3417..3e1a842c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */; }; DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F657A2C6EC2900053C113 /* LockLegend.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; + DD74ED0D2DC6A0C90059AC10 /* DeviceOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */; }; @@ -418,6 +419,7 @@ DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesHelp.swift; sourceTree = ""; }; DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; + DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOnboarding.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = ""; }; @@ -837,6 +839,14 @@ path = Help; sourceTree = ""; }; + DD74ED0B2DC6A0900059AC10 /* Onboarding */ = { + isa = PBXGroup; + children = ( + DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( @@ -974,11 +984,12 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( - DD6D5A312CA1176A00ED3032 /* Layouts */, - DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD47E3D726F2F21A00029299 /* Bluetooth */, + DDC2E18D26CE25CB0042C5E4 /* Helpers */, + DD6D5A312CA1176A00ED3032 /* Layouts */, DDC2E18B26CE25A70042C5E4 /* Messages */, DD47E3CA26F0E50300029299 /* Nodes */, + DD74ED0B2DC6A0900059AC10 /* Onboarding */, DD4A911C2708C57100501B7E /* Settings */, DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */, ); @@ -1416,6 +1427,7 @@ DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDD5BB0B2C285E45007E03CA /* LogDetail.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, + DD74ED0D2DC6A0C90059AC10 /* DeviceOnboarding.swift in Sources */, DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */, B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index e1ca67f9..c255cec2 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -73,6 +73,8 @@ extension UserDefaults { case environmentEnableWeatherKit case enableAdministration case mapReportingOptIn + case firstLaunch + case showDeviceOnboarding case testIntEnum } @@ -151,6 +153,12 @@ extension UserDefaults { @UserDefault(.mapReportingOptIn, defaultValue: false) static var mapReportingOptIn: Bool + + @UserDefault(.firstLaunch, defaultValue: true) + static var firstLaunch: Bool + + @UserDefault(.showDeviceOnboarding, defaultValue: false) + static var showDeviceOnboarding: Bool @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 63a4cc68..29e80735 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -982,6 +982,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") + if UserDefaults.firstLaunch { + UserDefaults.showDeviceOnboarding = true + } if sendTime() { } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 75830805..62cbd841 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -13,7 +13,7 @@ import OSLog @MainActor class LocationsHandler: ObservableObject { static let shared = LocationsHandler() // Create a single, shared instance of the object. - private let manager: CLLocationManager + public let manager: CLLocationManager private var background: CLBackgroundActivitySession? var enableSmartPosition: Bool = UserDefaults.enableSmartPosition @@ -38,6 +38,16 @@ import OSLog UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted") } } + + // The continuation we will use to asynchronously ask the user permission to track their location. + private var permissionContinuation: CheckedContinuation? + + func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { + self.manager.requestAlwaysAuthorization() + return await withCheckedContinuation { continuation in + permissionContinuation = continuation + } + } private init() { self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. @@ -49,6 +59,10 @@ import OSLog if self.manager.authorizationStatus == .notDetermined { self.manager.requestWhenInUseAuthorization() } + let status = self.manager.authorizationStatus + guard status == .authorizedAlways || status == .authorizedWhenInUse else { + return + } Logger.services.info("📍 [App] Starting location updates") Task { do { diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 5c82c257..0145a7a7 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -11,11 +11,7 @@ struct MeshtasticAppleApp: App { @UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) private var appDelegate - @ObservedObject - var appState: AppState - -// @ObservedObject -// private var bleManager: BLEManager + @ObservedObject var appState: AppState private let persistenceController: PersistenceController diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 8e04f857..20da78dc 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -27,20 +27,6 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" - init () { - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.getNotificationSettings(completionHandler: { (settings) in - if settings.authorizationStatus == .notDetermined { - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .criticalAlert]) { success, error in - if success { - Logger.services.info("Notifications are all set!") - } else if let error = error { - Logger.services.error("\(error.localizedDescription, privacy: .public)") - } - } - } - }) - } var body: some View { NavigationStack { VStack { diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 1c8a395d..94666c0c 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -5,11 +5,11 @@ import SwiftUI struct ContentView: View { - @ObservedObject - var appState: AppState + @ObservedObject var appState: AppState - @ObservedObject - var router: Router + @ObservedObject var router: Router + + @State var isShowingDeviceOnboardingFlow: Bool = false init(appState: AppState, router: Router) { self.appState = appState @@ -58,6 +58,21 @@ struct ContentView: View { .font(.title) } .tag(NavigationState.Tab.settings) + }.sheet( + isPresented: $isShowingDeviceOnboardingFlow, + onDismiss: { + //UserDefaults.firstLaunch = false + }, content: { + DeviceOnboarding() + } + ) + .onAppear { + if UserDefaults.firstLaunch { + isShowingDeviceOnboardingFlow = true + } + } + .onChange(of: UserDefaults.showDeviceOnboarding) { newValue in + isShowingDeviceOnboardingFlow = newValue } } } diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift new file mode 100644 index 00000000..7f2274aa --- /dev/null +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -0,0 +1,316 @@ +import CoreBluetooth +import OSLog +import SwiftUI +import Foundation +import MapKit + +struct DeviceOnboarding: View { + enum SetupGuide: Hashable { + case notifications + case location + case mqtt + } + + @State var navigationPath: [SetupGuide] = [] + @EnvironmentObject var bleManager: BLEManager + + @Environment(\.dismiss) var dismiss + + /// The Title View + var title: some View { + VStack { + Text("Welcome to") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + Text("Meshtastic") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + } + + var welcomeView: some View { + VStack { + ScrollView(.vertical, showsIndicators: false) { + VStack { + // Title + title + .padding(.top) + // Onboarding + VStack(alignment: .leading, spacing: 16) { + makeRow( + icon: "antenna.radiowaves.left.and.right", + title: "Stay Connected Anywhere", + subtitle: "Communicate off-the-grid with your friends and community without cell service." + ) + + makeRow( + icon: "point.3.connected.trianglepath.dotted", + title: "Create Your Own Networks", + subtitle: "Easily set up private mesh networks for secure and reliable communication in remote areas." + ) + + makeRow( + icon: "location", + title: "Track and Share Locations", + subtitle: "Share your location in real-time and keep your group coordinated with integrated GPS features." + ) + } + .padding() + } + .interactiveDismissDisabled() + } + Spacer() + + Button { + Task { + await goToNextStep(after: nil) + } + } label: { + Text("Get started") + .frame(maxWidth: .infinity) + } + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + + var notificationView: some View { + VStack { + VStack { + Text("App Notifications") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() + VStack(alignment: .leading, spacing: 16) { + Text("Send Notifications") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "message", + title: "Incoming Messages", + subtitle: "Meshtastic notifications for channel messages and direct messages" + ) + makeRow( + icon: "flipphone", + title: "New Nodes", + subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." + ) + makeRow( + icon: "battery.25percent", + title: "Low Battery", + subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." + ) + Text("Critical Alerts") + .font(.title2.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "exclamationmark.triangle.fill", + subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center." + ) + } + .padding() + Spacer() + Button { + Task { + await requestNotificationsPermissions() + await goToNextStep(after: .notifications) + } + } label: { + Text("Configure notification permissions") + .frame(maxWidth: .infinity) + } + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + + var locationView: some View { + VStack { + VStack { + Text("Phone Location") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + VStack(alignment: .leading, spacing: 16) { + Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.") + .font(.body.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "location", + title: "Share Location", + subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node." + ) + makeRow( + icon: "lines.measurement.horizontal", + title: "Distance Measurements", + subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available." + ) + makeRow( + icon: "line.3.horizontal.decrease.circle", + title: "Distance Filters", + subtitle: "Filter the node list and mesh map based on proximity to your phone." + ) + makeRow( + icon: "mappin", + title: "Mesh Map Location", + subtitle: "Enables the blue location dot for your phone in the mesh map." + ) + } + .padding() + Spacer() + if LocationHelper.shared.locationManager.authorizationStatus != .notDetermined { + Button { + Task { + await goToNextStep(after: .location) + } + } label: { + Text("Continue to next step") + .frame(maxWidth: .infinity) + } + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + } + + var mqttView: some View { + VStack { + VStack { + Text("MQTT") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() + Button { + Task { + + } + } label: { + Text("Enable MQTT") + .frame(maxWidth: .infinity) + } + .padding() + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + + Button { + dismiss() + } label: { + Text("Set up later") + .frame(maxWidth: .infinity) + } + } + } + + var body: some View { + NavigationStack(path: $navigationPath) { + welcomeView + .navigationDestination(for: SetupGuide.self) { guide in + switch guide { + case .notifications: + notificationView + case .location: + locationView + case .mqtt: + mqttView + } + } + } + .toolbar(.hidden) + } + + @ViewBuilder + func makeRow( + icon: String, + title: String = "", + subtitle: String + ) -> some View { + HStack(alignment: .center) { + Image(systemName: icon) + .resizable() + .symbolRenderingMode(.multicolor) + .font(.subheadline) + .aspectRatio(contentMode: .fit) + .padding() + .frame(width: 72, height: 72) + + VStack(alignment: .leading) { + Text(title) + .font(.subheadline.weight(.semibold)) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + + Text(subtitle) + .font(.subheadline) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + }.multilineTextAlignment(.leading) + }.accessibilityElement(children: .combine) + } + + // MARK: Navigation + func goToNextStep(after step: SetupGuide?) async { + switch step { + case .none: + let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus + let criticalAlert = await UNUserNotificationCenter.current().notificationSettings().criticalAlertSetting + if status == .notDetermined && criticalAlert == .notSupported { + navigationPath.append(.notifications) + } else { + fallthrough + } + case .notifications: + let status = LocationHelper.shared.locationManager.authorizationStatus + if status == .notDetermined { + navigationPath.append(.location) + let newStatus = await LocationsHandler.shared.manager.requestAlwaysAuthorization() + } else { + fallthrough + } + case .location: + + if true { + navigationPath.append(.mqtt) + } else { + fallthrough + } + case .mqtt: + dismiss() + } + } + + // MARK: Permission Checks + + func requestNotificationsPermissions() async { + let center = UNUserNotificationCenter.current() + do { + let success = try await center.requestAuthorization(options: [.alert, .badge, .sound, .criticalAlert]) + if success { + Logger.services.info("Notification permissions are enabled") + } else { + Logger.services.info("Notification permissions denied") + } + } catch { + Logger.services.error("Notification permissions error: \(error.localizedDescription)") + } + } +} From 151c89835d5878cf9e1ae445a3944b37db24e49f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 May 2025 13:35:48 -0700 Subject: [PATCH 02/14] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- protobufs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 3e1a842c..a49af10d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1797,7 +1797,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.1; + MARKETING_VERSION = 2.6.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1830,7 +1830,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.6.1; + MARKETING_VERSION = 2.6.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1861,7 +1861,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.1; + MARKETING_VERSION = 2.6.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1893,7 +1893,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.6.1; + MARKETING_VERSION = 2.6.2; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/protobufs b/protobufs index 27fac391..06864665 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 27fac39141d99fe727a0a1824c5397409b1aea75 +Subproject commit 068646653e8375fc145988026ad242a3cf70f7ab From 964948fb7e298290e5a50ab695577c448d4ff412 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Jul 2025 14:13:43 -0700 Subject: [PATCH 03/14] Cleaned up onboarding flow --- Localizable.xcstrings | 17 +++-- Meshtastic.xcodeproj/project.pbxproj | 4 - .../xcshareddata/swiftpm/Package.resolved | 29 ++++++- Meshtastic/Helpers/LocationHelper.swift | 76 ------------------- Meshtastic/Helpers/LocationsHandler.swift | 2 +- .../Views/Onboarding/DeviceOnboarding.swift | 61 +++------------ 6 files changed, 49 insertions(+), 140 deletions(-) delete mode 100644 Meshtastic/Helpers/LocationHelper.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 96f34279..06fe2491 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -8108,6 +8108,9 @@ } } } + }, + "Configure Location Permissions" : { + }, "Configure notification permissions" : { @@ -8611,9 +8614,6 @@ } } } - }, - "Continue to next step" : { - }, "Control Type" : { "localizations" : { @@ -12450,9 +12450,6 @@ } } } - }, - "Enable MQTT" : { - }, "Enable Notifications" : { "localizations" : { @@ -25664,6 +25661,9 @@ } } } + }, + "Phone Location" : { + }, "Pin %lld" : { "localizations" : { @@ -31638,6 +31638,9 @@ } } } + }, + "Send Notifications" : { + }, "Send Reboot OTA" : { "localizations" : { @@ -41578,4 +41581,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 52ffbb86..938cdfb5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -201,7 +201,6 @@ DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; }; - DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; @@ -516,7 +515,6 @@ DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = ""; }; DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; @@ -1101,7 +1099,6 @@ DDD43FE12A78C86B0083A3E9 /* Mqtt */, DDAF8C5226EB1DF10058C060 /* BLEManager.swift */, DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, - DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, @@ -1430,7 +1427,6 @@ 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, 231B3F272D0885240069A07D /* MetricsColumnDetail.swift in Sources */, - DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */, 6D825E622C34786C008DBEE4 /* CommonRegex.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8cb1b6ba..4a0652bc 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "a3033aea781828906c453276e3723177901ce64df5757de7ada28c854c9662eb", + "originHash" : "fd71b247ba909b0eb360db5530e1068363839c5e169dea6f6a9974b2d98276f4", "pins" : [ { "identity" : "cocoamqtt", @@ -10,6 +10,15 @@ "version" : "2.1.8" } }, + { + "identity" : "dd-sdk-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/dd-sdk-ios.git", + "state" : { + "revision" : "d0a42d8067665cb6ee86af51251ccc071f62bd54", + "version" : "2.29.0" + } + }, { "identity" : "mqttcocoaasyncsocket", "kind" : "remoteSourceControl", @@ -19,6 +28,24 @@ "version" : "1.0.8" } }, + { + "identity" : "opentelemetry-swift-packages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DataDog/opentelemetry-swift-packages.git", + "state" : { + "revision" : "4a7295600d4ebb9525a23c11586c5fdb74ae8b7e", + "version" : "1.13.1" + } + }, + { + "identity" : "plcrashreporter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/microsoft/plcrashreporter.git", + "state" : { + "revision" : "8c61e5e38e9f737dd68512ed1ea5ab081244ad65", + "version" : "1.12.0" + } + }, { "identity" : "starscream", "kind" : "remoteSourceControl", diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift deleted file mode 100644 index 978ae5a8..00000000 --- a/Meshtastic/Helpers/LocationHelper.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import CoreLocation -import MapKit -import OSLog - -class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { - static let shared = LocationHelper() - var locationManager = CLLocationManager() - - // @Published var region = MKCoordinateRegion() - @Published var authorizationStatus: CLAuthorizationStatus? - override init() { - super.init() - locationManager.delegate = self - locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - locationManager.pausesLocationUpdatesAutomatically = true - locationManager.allowsBackgroundLocationUpdates = true - locationManager.activityType = .other - } - // Apple Park - static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) - static var currentLocation: CLLocationCoordinate2D { - guard let location = shared.locationManager.location else { - return DefaultLocation - } - return location.coordinate - } - static var satsInView: Int { - // If we have a position we have a sat - var sats = 1 - if shared.locationManager.location?.verticalAccuracy ?? 0 > 0 { - sats = 4 - if 0...5 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 12 - } else if 6...15 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 10 - } else if 16...30 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 9 - } else if 31...45 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 7 - } else if 46...60 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 5 - } - } else if shared.locationManager.location?.verticalAccuracy ?? 0 < 0 && 60...300 ~= shared.locationManager.location?.horizontalAccuracy ?? 0 { - sats = 3 - } else if shared.locationManager.location?.verticalAccuracy ?? 0 < 0 && shared.locationManager.location?.horizontalAccuracy ?? 0 > 300 { - sats = 2 - } - return sats - } - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - switch manager.authorizationStatus { - case .authorizedAlways: - authorizationStatus = .authorizedAlways - case .authorizedWhenInUse: - authorizationStatus = .authorizedWhenInUse - locationManager.requestLocation() - case .restricted: - authorizationStatus = .restricted - case .denied: - authorizationStatus = .denied - case .notDetermined: - authorizationStatus = .notDetermined - locationManager.requestAlwaysAuthorization() - default: - break - } - } - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - - } - func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - Logger.services.error("Location manager error: \(error.localizedDescription, privacy: .public)") - } -} diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index beaefe17..7ec4c77a 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -57,7 +57,7 @@ import OSLog func startLocationUpdates() { if self.manager.authorizationStatus == .notDetermined { - self.manager.requestWhenInUseAuthorization() + // self.manager.requestWhenInUseAuthorization() } let status = self.manager.authorizationStatus guard status == .authorizedAlways || status == .authorizedWhenInUse else { diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 7f2274aa..d48a1015 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -8,7 +8,6 @@ struct DeviceOnboarding: View { enum SetupGuide: Hashable { case notifications case location - case mqtt } @State var navigationPath: [SetupGuide] = [] @@ -170,54 +169,22 @@ struct DeviceOnboarding: View { } .padding() Spacer() - if LocationHelper.shared.locationManager.authorizationStatus != .notDetermined { - Button { - Task { - await goToNextStep(after: .location) - } - } label: { - Text("Continue to next step") - .frame(maxWidth: .infinity) - } - .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) - } - } - } - - var mqttView: some View { - VStack { - VStack { - Text("MQTT") - .font(.largeTitle.bold()) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - Spacer() Button { Task { - + let status = await LocationsHandler.shared.requestLocationAlwaysPermissions() + if status != .notDetermined { + dismiss() + } } } label: { - Text("Enable MQTT") + Text("Configure Location Permissions") .frame(maxWidth: .infinity) } .padding() - .padding() .buttonBorderShape(.capsule) .controlSize(.large) .padding() .buttonStyle(.borderedProminent) - - Button { - dismiss() - } label: { - Text("Set up later") - .frame(maxWidth: .infinity) - } } } @@ -230,8 +197,6 @@ struct DeviceOnboarding: View { notificationView case .location: locationView - case .mqtt: - mqttView } } } @@ -279,27 +244,21 @@ struct DeviceOnboarding: View { fallthrough } case .notifications: - let status = LocationHelper.shared.locationManager.authorizationStatus - if status == .notDetermined { + let status = LocationsHandler.shared.manager.authorizationStatus + if status == .notDetermined || status == .restricted || status == .denied { navigationPath.append(.location) - let newStatus = await LocationsHandler.shared.manager.requestAlwaysAuthorization() } else { fallthrough } case .location: - - if true { - navigationPath.append(.mqtt) - } else { - fallthrough + let status = LocationsHandler.shared.manager.authorizationStatus + if status != .notDetermined && status != .restricted && status != .denied { + dismiss() } - case .mqtt: - dismiss() } } // MARK: Permission Checks - func requestNotificationsPermissions() async { let center = UNUserNotificationCenter.current() do { From d6245e33b1ad1ca9d2ec7d4dc6bd6c834c3593e2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Jul 2025 19:42:59 -0700 Subject: [PATCH 04/14] A little more cleanup --- Meshtastic/Helpers/LocationsHandler.swift | 2 +- .../Views/Onboarding/DeviceOnboarding.swift | 67 +++++++++++-------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 7ec4c77a..efd817db 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -40,7 +40,7 @@ import OSLog } // The continuation we will use to asynchronously ask the user permission to track their location. - private var permissionContinuation: CheckedContinuation? + var permissionContinuation: CheckedContinuation? func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { self.manager.requestAlwaysAuthorization() diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index d48a1015..f48d1af7 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -10,8 +10,9 @@ struct DeviceOnboarding: View { case location } - @State var navigationPath: [SetupGuide] = [] @EnvironmentObject var bleManager: BLEManager + @State var navigationPath: [SetupGuide] = [] + @State var locationStatus = LocationsHandler.shared.manager.authorizationStatus @Environment(\.dismiss) var dismiss @@ -61,19 +62,20 @@ struct DeviceOnboarding: View { .interactiveDismissDisabled() } Spacer() - - Button { - Task { - await goToNextStep(after: nil) + if bleManager.isSwitchedOn { + Button { + Task { + await goToNextStep(after: nil) + } + } label: { + Text("Get started") + .frame(maxWidth: .infinity) } - } label: { - Text("Get started") - .frame(maxWidth: .infinity) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) } - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) } } @@ -169,22 +171,33 @@ struct DeviceOnboarding: View { } .padding() Spacer() - Button { - Task { - let status = await LocationsHandler.shared.requestLocationAlwaysPermissions() - if status != .notDetermined { - dismiss() + if locationStatus == .notDetermined { + Button { + Task { + locationStatus = await LocationsHandler.shared.requestLocationAlwaysPermissions() // LocationsHandler.shared.requestLocationAlwaysPermissions() } + } label: { + Text("Configure Location Permissions") + .frame(maxWidth: .infinity) } - } label: { - Text("Configure Location Permissions") - .frame(maxWidth: .infinity) + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } else { + Button { + dismiss() + } label: { + Text("Finish") + .frame(maxWidth: .infinity) + } + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) } - .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) } } @@ -244,8 +257,8 @@ struct DeviceOnboarding: View { fallthrough } case .notifications: - let status = LocationsHandler.shared.manager.authorizationStatus - if status == .notDetermined || status == .restricted || status == .denied { + locationStatus = LocationsHandler.shared.manager.authorizationStatus + if locationStatus == .notDetermined || locationStatus == .restricted || locationStatus == .denied { navigationPath.append(.location) } else { fallthrough From 72979888da80a1ebec0caba39ea2ff655ea8ad2a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Jul 2025 20:09:19 -0700 Subject: [PATCH 05/14] Close location view hack --- .../Views/Onboarding/DeviceOnboarding.swift | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index f48d1af7..7c4e58ed 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -171,33 +171,21 @@ struct DeviceOnboarding: View { } .padding() Spacer() - if locationStatus == .notDetermined { - Button { - Task { - locationStatus = await LocationsHandler.shared.requestLocationAlwaysPermissions() // LocationsHandler.shared.requestLocationAlwaysPermissions() - } - } label: { - Text("Configure Location Permissions") - .frame(maxWidth: .infinity) + Button { + Task { + await requestLocationPermissions() + await goToNextStep(after: .location) } - .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) - } else { - Button { - dismiss() - } label: { - Text("Finish") - .frame(maxWidth: .infinity) - } - .padding() - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .buttonStyle(.borderedProminent) + dismiss() + } label: { + Text("Configure Location Permissions") + .frame(maxWidth: .infinity) } + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) } } @@ -285,4 +273,13 @@ struct DeviceOnboarding: View { Logger.services.error("Notification permissions error: \(error.localizedDescription)") } } + + func requestLocationPermissions() async { + locationStatus = await LocationsHandler.shared.requestLocationAlwaysPermissions() + if locationStatus != .notDetermined { + Logger.services.info("Notification permissions are enabled") + } else { + Logger.services.info("Notification permissions denied") + } + } } From a0f2c897d82b3b610f0d5b98697ec07280261445 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 11 Jul 2025 20:17:44 -0700 Subject: [PATCH 06/14] set first launch value after device onboarding is complete --- Meshtastic/Views/ContentView.swift | 1 - Meshtastic/Views/Onboarding/DeviceOnboarding.swift | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 19106a26..641cec94 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -8,7 +8,6 @@ struct ContentView: View { @ObservedObject var appState: AppState @ObservedObject var router: Router - @State var isShowingDeviceOnboardingFlow: Bool = false init(appState: AppState, router: Router) { diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 7c4e58ed..bfd97908 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -174,8 +174,9 @@ struct DeviceOnboarding: View { Button { Task { await requestLocationPermissions() - await goToNextStep(after: .location) + } + UserDefaults.firstLaunch = false dismiss() } label: { Text("Configure Location Permissions") From 9b29e62c9e9690cbaeb10625098f6088c2689e4f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 12 Jul 2025 23:00:42 -0700 Subject: [PATCH 07/14] Removed method unused with device onboarding --- Meshtastic/Helpers/LocationsHandler.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index efd817db..ddcb5d4b 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -38,10 +38,8 @@ import OSLog UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted") } } - // The continuation we will use to asynchronously ask the user permission to track their location. var permissionContinuation: CheckedContinuation? - func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { self.manager.requestAlwaysAuthorization() return await withCheckedContinuation { continuation in @@ -56,9 +54,6 @@ import OSLog } func startLocationUpdates() { - if self.manager.authorizationStatus == .notDetermined { - // self.manager.requestWhenInUseAuthorization() - } let status = self.manager.authorizationStatus guard status == .authorizedAlways || status == .authorizedWhenInUse else { return From 8a5eac9b03cff45cafd23c8e8978381d33f12b65 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 13 Jul 2025 22:30:39 -0700 Subject: [PATCH 08/14] guard connected peripheral --- Meshtastic/Helpers/BLEManager.swift | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index cf5c385e..ee8bd26c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -60,9 +60,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let NONCE_ONLY_DB = 69421 private var isWaitingForWantConfigResponse = false - private var wantConfigTimer: Timer? - private var wantConfigRetryCount = 0 - private let maxWantConfigRetries = 6 + private var wantConfigTimer: Timer? + private var wantConfigRetryCount = 0 + private let maxWantConfigRetries = 6 private let wantConfigTimeoutInterval: TimeInterval = 6.0 // MARK: init @@ -799,42 +799,42 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } } + guard let cp = connectedPeripheral else { + return + } // Channels - if decodedInfo.channel.isInitialized && connectedPeripheral != nil { + if decodedInfo.channel.isInitialized { nowKnown = true - channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context) + channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: cp.num), context: context) } // Config - if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil && self.connectedPeripheral?.num != 0 { + if decodedInfo.config.isInitialized && !invalidVersion && cp.num != 0 { nowKnown = true - localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: cp.num), nodeLongName: cp.longName) } // Module Config - if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { + if decodedInfo.moduleConfig.isInitialized && !invalidVersion && cp.num != 0 { onWantConfigResponseReceived() nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral?.longName ?? "Unknown") + moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: cp.num), nodeLongName: cp.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { - if let connectedNum = self.connectedPeripheral?.num, connectedNum > 0 { - _ = self.getCannedMessageModuleMessages(destNum: connectedNum, wantResponse: true) - } + _ = self.getCannedMessageModuleMessages(destNum: cp.num, wantResponse: true) + } } if decodedInfo.config.payloadVariant == Config.OneOf_PayloadVariant.device(decodedInfo.config.device) { var dc = decodedInfo.config.device if dc.tzdef.isEmpty { dc.tzdef = TimeZone.current.posixDescription - if let connectedNum = self.connectedPeripheral?.num, connectedNum > 0 { - let adminMessageId = self.saveTimeZone(config: dc, user: connectedNum) - } + let adminMessageId = self.saveTimeZone(config: dc, user: cp.num) } } } // Device Metadata if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion { nowKnown = true - deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context) + deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: cp.num, context: context) connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".") if lastDotIndex == nil { From 3e3f154a8ecfc96604c5759100f36b554cf83de7 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 14 Jul 2025 10:25:26 -0500 Subject: [PATCH 09/14] Fix break for 2 admin keys instead of 3 --- Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 8788c696..63f9efbc 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -924,6 +924,8 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s fetchedNode[0].securityConfig?.adminKey = config.adminKey[0] if config.adminKey.count > 1 { fetchedNode[0].securityConfig?.adminKey2 = config.adminKey[1] + } + if config.adminKey.count > 2 { fetchedNode[0].securityConfig?.adminKey3 = config.adminKey[2] } } From da40b4b0b23456a00a72d55f39008799b89bad1b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 14 Jul 2025 20:27:02 -0700 Subject: [PATCH 10/14] Resume continuation --- Meshtastic/Helpers/LocationsHandler.swift | 22 +++++++++++-------- .../Views/Onboarding/DeviceOnboarding.swift | 3 +-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index ddcb5d4b..645959f3 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -10,14 +10,14 @@ import CoreLocation import OSLog // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. -@MainActor class LocationsHandler: ObservableObject { +@MainActor class LocationsHandler: NSObject, ObservableObject, @preconcurrency CLLocationManagerDelegate { static let shared = LocationsHandler() // Create a single, shared instance of the object. - public let manager: CLLocationManager + public var manager = CLLocationManager() private var background: CLBackgroundActivitySession? var enableSmartPosition: Bool = UserDefaults.enableSmartPosition - @Published var locationsArray: [CLLocation] + @Published var locationsArray: [CLLocation] = [CLLocation]() @Published var isStationary = false @Published var count = 0 @Published var isRecording = false @@ -39,18 +39,22 @@ import OSLog } } // The continuation we will use to asynchronously ask the user permission to track their location. - var permissionContinuation: CheckedContinuation? + private var permissionContinuation: CheckedContinuation? func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { - self.manager.requestAlwaysAuthorization() return await withCheckedContinuation { continuation in - permissionContinuation = continuation + self.permissionContinuation = continuation + manager.requestAlwaysAuthorization() } } + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + // This is the line you need to add + permissionContinuation?.resume(returning: manager.authorizationStatus) + } - private init() { - self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. + override init() { + super.init() + self.manager.delegate = self self.manager.allowsBackgroundLocationUpdates = true - locationsArray = [CLLocation]() } func startLocationUpdates() { diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index bfd97908..8157b33c 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -174,10 +174,8 @@ struct DeviceOnboarding: View { Button { Task { await requestLocationPermissions() - } UserDefaults.firstLaunch = false - dismiss() } label: { Text("Configure Location Permissions") .frame(maxWidth: .infinity) @@ -282,5 +280,6 @@ struct DeviceOnboarding: View { } else { Logger.services.info("Notification permissions denied") } + dismiss() } } From bf5454069ec2737a735e8f9c3b7e6ff7006edd3e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 14 Jul 2025 20:39:34 -0700 Subject: [PATCH 11/14] Update Meshtastic/Views/Onboarding/DeviceOnboarding.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/Onboarding/DeviceOnboarding.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 8157b33c..c7ecdc92 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -276,9 +276,9 @@ struct DeviceOnboarding: View { func requestLocationPermissions() async { locationStatus = await LocationsHandler.shared.requestLocationAlwaysPermissions() if locationStatus != .notDetermined { - Logger.services.info("Notification permissions are enabled") + Logger.services.info("Location permissions are enabled") } else { - Logger.services.info("Notification permissions denied") + Logger.services.info("Location permissions denied") } dismiss() } From 4ee9e719c397869ab96f6d4e3c9ed6ec82811610 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 14 Jul 2025 20:40:10 -0700 Subject: [PATCH 12/14] Update Meshtastic/Views/ContentView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Views/ContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 641cec94..05f227b5 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -60,7 +60,7 @@ struct ContentView: View { }.sheet( isPresented: $isShowingDeviceOnboardingFlow, onDismiss: { - //UserDefaults.firstLaunch = false + UserDefaults.firstLaunch = false }, content: { DeviceOnboarding() } From 0d1c744d12065c420c72db070378a34da5e84718 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 14 Jul 2025 20:40:52 -0700 Subject: [PATCH 13/14] Remove duplicate user default setting --- Meshtastic/Views/Onboarding/DeviceOnboarding.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 8157b33c..382d2f24 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -175,7 +175,6 @@ struct DeviceOnboarding: View { Task { await requestLocationPermissions() } - UserDefaults.firstLaunch = false } label: { Text("Configure Location Permissions") .frame(maxWidth: .infinity) From 4b7dfc3af229dc1aa3c568f2e3d8d6b3a5a7abe9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 14 Jul 2025 22:41:32 -0700 Subject: [PATCH 14/14] Point emoji notifications to the message they are part of --- Meshtastic/Helpers/MeshPackets.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 10a3d69b..24a577b2 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1027,7 +1027,7 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)", + path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)", messageId: newMessage.messageId, channel: newMessage.channel, userNum: Int64(packet.from), @@ -1058,7 +1058,7 @@ func textMessageAppPacket( subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, target: "messages", - path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)", + path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)", messageId: newMessage.messageId, channel: newMessage.channel, userNum: Int64(newMessage.fromUser?.userId ?? "0"),