mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Device onboarding initial commit
This commit is contained in:
parent
2201bad314
commit
dfeaf848e0
9 changed files with 490 additions and 104 deletions
|
|
@ -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" : {
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.swift; sourceTree = "<group>"; };
|
||||
DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = "<group>"; };
|
||||
DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOnboarding.swift; sourceTree = "<group>"; };
|
||||
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = "<group>"; };
|
||||
DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = "<group>"; };
|
||||
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -837,6 +839,14 @@
|
|||
path = Help;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD74ED0B2DC6A0900059AC10 /* Onboarding */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD74ED0C2DC6A0B80059AC10 /* DeviceOnboarding.swift */,
|
||||
);
|
||||
path = Onboarding;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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<CLAuthorizationStatus, Never>?
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
316
Meshtastic/Views/Onboarding/DeviceOnboarding.swift
Normal file
316
Meshtastic/Views/Onboarding/DeviceOnboarding.swift
Normal file
|
|
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue