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>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-06 00:20:16 +00:00 committed by GitHub
parent 218412f82a
commit eabb9a9d30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 283 additions and 45 deletions

View file

@ -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

View file

@ -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)
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}
}

View file

@ -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")) {

View file

@ -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)