From eabb9a9d30ee3fe75934864d8b556170aa35e6ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:20:16 +0000 Subject: [PATCH] Add Background Activity onboarding step, firmware version screens, and security nag Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/449fe2d6-dec9-4509-920e-e6196ca11d65 Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com> --- .../Accessory Manager/AccessoryManager.swift | 3 +- Meshtastic/Views/Connect/Connect.swift | 16 +++ Meshtastic/Views/Connect/InvalidVersion.swift | 122 ++++++++++++------ .../Views/Connect/SecurityVersionNag.swift | 103 +++++++++++++++ .../Views/Onboarding/DeviceOnboarding.swift | 79 +++++++++++- Meshtastic/Views/Settings/Firmware.swift | 5 +- 6 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 Meshtastic/Views/Connect/SecurityVersionNag.swift diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 5e1a46bd..1f2a1727 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -116,7 +116,8 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate { // Constants let NONCE_ONLY_CONFIG = 69420 let NONCE_ONLY_DB = 69421 - let minimumVersion = "2.3.15" + let minimumVersion = "2.5.18" + let securityVersion = "2.6.0" // Global Objects // Chicken/Egg problem. Set in the App object immediately after diff --git a/Meshtastic/Views/Connect/Connect.swift b/Meshtastic/Views/Connect/Connect.swift index b66b1b59..ec1f3d78 100644 --- a/Meshtastic/Views/Connect/Connect.swift +++ b/Meshtastic/Views/Connect/Connect.swift @@ -25,6 +25,7 @@ struct Connect: View { @State var node: NodeInfoEntity? @State var isUnsetRegion = false @State var invalidFirmwareVersion = false + @State var showSecurityVersionNag = false @State var liveActivityStarted = false @ObservedObject var manualConnections = ManualConnectionList.shared @@ -347,6 +348,16 @@ struct Connect: View { // .onChange(of: accessoryManager) { // invalidFirmwareVersion = self.bleManager.invalidVersion // } + .sheet(isPresented: $invalidFirmwareVersion) { + InvalidVersion(minimumVersion: accessoryManager.minimumVersion, version: accessoryManager.activeConnection?.device.firmwareVersion ?? "?.?.?") + .presentationDetents([.large]) + .presentationDragIndicator(.automatic) + } + .sheet(isPresented: $showSecurityVersionNag) { + SecurityVersionNag(minimumSecureVersion: accessoryManager.securityVersion, version: accessoryManager.activeConnection?.device.firmwareVersion ?? "?.?.?") + .presentationDetents([.large]) + .presentationDragIndicator(.automatic) + } .onChange(of: self.accessoryManager.state) { _, state in if let deviceNum = accessoryManager.activeDeviceNum, UserDefaults.preferredPeripheralId.count > 0 && state == .subscribed { @@ -364,6 +375,11 @@ struct Connect: View { } catch { Logger.data.error("💥 Error fetching node info: \(error.localizedDescription, privacy: .public)") } + // Check firmware version on connection + invalidFirmwareVersion = !accessoryManager.checkIsVersionSupported(forVersion: accessoryManager.minimumVersion) + if !invalidFirmwareVersion { + showSecurityVersionNag = !accessoryManager.checkIsVersionSupported(forVersion: accessoryManager.securityVersion) + } } } } diff --git a/Meshtastic/Views/Connect/InvalidVersion.swift b/Meshtastic/Views/Connect/InvalidVersion.swift index 5d475756..c60873e2 100644 --- a/Meshtastic/Views/Connect/InvalidVersion.swift +++ b/Meshtastic/Views/Connect/InvalidVersion.swift @@ -14,51 +14,93 @@ struct InvalidVersion: View { @State var version = "" var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 20) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 60)) + .foregroundColor(.orange) + .padding(.top, 40) - VStack { - - Text("Update Your Firmware") - .font(.largeTitle) - .foregroundColor(.orange) - - Divider() - VStack { - Text("The Meshtastic Apple apps support firmware version \(minimumVersion) and above.") - .font(.title2) - .padding(.bottom) - Link("Firmware update docs", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/")!) - .font(.title) - .padding() - Link("Additional help", destination: URL(string: "https://meshtastic.org/docs/faq")!) - .font(.title) - .padding() - } - .padding() - Divider() - .padding(.top) - VStack { - Text("🦕 End of life Version 🦖 ☄️") - .font(.title3) - .foregroundColor(.orange) - .padding(.bottom) - Text("Version \(minimumVersion) includes substantial network optimizations and extensive changes to devices and client apps. Only nodes version \(minimumVersion) and above are supported.") - .font(.callout) - .padding([.leading, .trailing, .bottom]) - - #if targetEnvironment(macCatalyst) - Button { - dismiss() - } label: { - Label("Close", systemImage: "xmark") + Text("Firmware Update Required") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + VStack(spacing: 8) { + if !version.isEmpty { + Label { + Text("Connected firmware: **\(version)**") + } icon: { + Image(systemName: "wifi.slash") + .foregroundColor(.red) + } + .font(.body) + } + Label { + Text("Minimum required: **\(minimumVersion)**") + } icon: { + Image(systemName: "checkmark.shield.fill") + .foregroundColor(.green) + } + .font(.body) } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) .padding() - #endif + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) - }.padding() + Text("The Meshtastic Apple app requires firmware version \(minimumVersion) or later. Older firmware versions are no longer supported and may have compatibility issues or missing features.") + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 12) { + Text("How to Update") + .font(.headline) + Link(destination: URL(string: "https://flasher.meshtastic.org")!) { + Label("Open Web Flasher", systemImage: "bolt.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.regular) + .buttonBorderShape(.capsule) + Link(destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/")!) { + Label("Firmware Update Docs", systemImage: "book.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.regular) + .buttonBorderShape(.capsule) + Link(destination: URL(string: "https://meshtastic.org/docs/faq")!) { + Label("Additional Help", systemImage: "questionmark.circle.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.regular) + .buttonBorderShape(.capsule) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + .padding(.horizontal) + } + .padding(.bottom, 20) + } + + #if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("Close", systemImage: "xmark") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + #endif } } } diff --git a/Meshtastic/Views/Connect/SecurityVersionNag.swift b/Meshtastic/Views/Connect/SecurityVersionNag.swift new file mode 100644 index 00000000..7e8db097 --- /dev/null +++ b/Meshtastic/Views/Connect/SecurityVersionNag.swift @@ -0,0 +1,103 @@ +// +// SecurityVersionNag.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 2024. +// +import SwiftUI + +struct SecurityVersionNag: View { + + @Environment(\.dismiss) private var dismiss + + @State var minimumSecureVersion = "" + @State var version = "" + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(spacing: 20) { + Image(systemName: "shield.slash.fill") + .font(.system(size: 60)) + .foregroundColor(.red) + .padding(.top, 40) + + Text("Security Update Recommended") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + VStack(spacing: 8) { + if !version.isEmpty { + Label { + Text("Connected firmware: **\(version)**") + } icon: { + Image(systemName: "wifi.exclamationmark") + .foregroundColor(.orange) + } + .font(.body) + } + Label { + Text("Recommended secure version: **\(minimumSecureVersion)**") + } icon: { + Image(systemName: "checkmark.shield.fill") + .foregroundColor(.green) + } + .font(.body) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + + VStack(alignment: .leading, spacing: 12) { + Text("Security Advisory") + .font(.headline) + Text("Your connected device is running firmware older than **\(minimumSecureVersion)**, which contains known security vulnerabilities. Updating your firmware is strongly recommended to protect your device and mesh network.") + .font(.body) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + .padding(.horizontal) + + VStack(alignment: .leading, spacing: 12) { + Text("How to Update") + .font(.headline) + Link(destination: URL(string: "https://flasher.meshtastic.org")!) { + Label("Open Web Flasher", systemImage: "bolt.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.regular) + .buttonBorderShape(.capsule) + Link(destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/")!) { + Label("Firmware Update Docs", systemImage: "book.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.regular) + .buttonBorderShape(.capsule) + } + .padding() + .background(Color(.secondarySystemBackground)) + .cornerRadius(12) + .padding(.horizontal) + } + .padding(.bottom, 20) + } + + Button { + dismiss() + } label: { + Text("Dismiss") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + } + } +} diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index d95d6977..d82fe542 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -8,6 +8,7 @@ struct DeviceOnboarding: View { enum SetupGuide: Hashable { case notifications case location + case backgroundActivity case localNetwork case bluetooth } @@ -209,6 +210,69 @@ struct DeviceOnboarding: View { } } + var backgroundActivityView: some View { + VStack { + ScrollView(.vertical) { + VStack { + Text("Background Activity") + .font(.largeTitle.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + VStack(alignment: .leading, spacing: 16) { + Text(createBackgroundActivityString()) + .font(.body.bold()) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + makeRow( + icon: "location.fill", + title: String(localized: "Continuous Location Updates"), + subtitle: String(localized: "Keep the mesh map updated and send your position to the mesh even while using other apps.") + ) + makeRow( + icon: "antenna.radiowaves.left.and.right", + title: String(localized: "Background Mesh Tracking"), + subtitle: String(localized: "Receive position updates from other nodes and maintain an accurate picture of the mesh while in the background.") + ) + makeRow( + icon: "battery.100.bolt", + title: String(localized: "Battery Usage"), + subtitle: String(localized: "Enabling background activity may increase battery usage. You can toggle this at any time in the app settings.") + ) + Toggle(isOn: Binding( + get: { LocationsHandler.shared.backgroundActivity }, + set: { LocationsHandler.shared.backgroundActivity = $0 } + )) { + Label { + Text("Enable Background Activity") + } icon: { + Image(systemName: "location.circle") + } + } + .fixedSize() + .scaleEffect(0.85) + .padding(.leading, 52) + .tint(.accentColor) + } + .padding() + } + Spacer() + Button { + Task { + await goToNextStep(after: .backgroundActivity) + } + } label: { + Text("Continue") + .frame(maxWidth: .infinity) + } + .padding() + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .buttonStyle(.borderedProminent) + } + } + var localNetworkView: some View { VStack { ScrollView(.vertical) { @@ -313,6 +377,8 @@ struct DeviceOnboarding: View { notificationView case .location: locationView + case .backgroundActivity: + backgroundActivityView case .bluetooth: bluetoothView case .localNetwork: @@ -382,8 +448,10 @@ struct DeviceOnboarding: View { case .location: locationStatus = LocationsHandler.shared.manager.authorizationStatus if locationStatus != .notDetermined && locationStatus != .restricted { - navigationPath.append(.localNetwork) + navigationPath.append(.backgroundActivity) } + case .backgroundActivity: + navigationPath.append(.localNetwork) case .localNetwork: navigationPath.append(.bluetooth) @@ -393,6 +461,15 @@ struct DeviceOnboarding: View { } // MARK: Formatting + func createBackgroundActivityString() -> AttributedString { + var fullText = AttributedString("Meshtastic can track your location in the background to keep the mesh map updated and send your position to the mesh even when the app is not in the foreground. You can update this setting at any time from settings.") + if let range = fullText.range(of: "settings") { + fullText[range].link = URL(string: UIApplication.openSettingsURLString)! + fullText[range].foregroundColor = .blue + } + return fullText + } + func createLocationString() -> AttributedString { var fullText = AttributedString(localized: "Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from settings.") if let range = fullText.range(of: String(localized: "settings")) { diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 490f7e01..79ca14e1 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -13,14 +13,13 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var accessoryManager: AccessoryManager var node: NodeInfoEntity? - @State var minimumVersion = "2.5.4" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? @State private var latestAlpha: FirmwareRelease? var body: some View { - let supportedVersion = accessoryManager.checkIsVersionSupported(forVersion: minimumVersion) + let supportedVersion = accessoryManager.checkIsVersionSupported(forVersion: accessoryManager.minimumVersion) let connectedVersion = accessoryManager.activeConnection?.device.firmwareVersion ?? "Unknown" ScrollView { VStack(alignment: .leading) { @@ -63,7 +62,7 @@ struct Firmware: View { .foregroundStyle(.red) .font(.title2) .padding(.bottom) - Text("Current Firmware Version: \(connectedVersion), Latest Firmware Version: \(minimumVersion)") + Text("Current Firmware Version: \(connectedVersion), Latest Firmware Version: \(accessoryManager.minimumVersion)") .fixedSize(horizontal: false, vertical: true) .font(.title3) .padding(.bottom)