diff --git a/Localizable.xcstrings b/Localizable.xcstrings index c988b278..f2308346 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1437,6 +1437,9 @@ } } } + }, + "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device." : { + }, "Allow Position Requests" : { "localizations" : { @@ -1764,6 +1767,9 @@ } } } + }, + "App Notifications" : { + }, "App Settings" : { "localizations" : { @@ -2962,6 +2968,9 @@ } } } + }, + "Bluetooth" : { + }, "bluetooth.config" : { "localizations" : { @@ -6249,6 +6258,9 @@ } } } + }, + "Configure notification permissions" : { + }, "Connect to a Node" : { "localizations" : { @@ -6619,6 +6631,9 @@ } } } + }, + "Continue to next step" : { + }, "Control Type" : { "localizations" : { @@ -6831,6 +6846,9 @@ } } } + }, + "Critical Alerts" : { + }, "Current Firmware Version: %@" : { "localizations" : { @@ -9449,6 +9467,9 @@ } } } + }, + "Enable MQTT" : { + }, "Enable Notifications" : { "localizations" : { @@ -10848,6 +10869,9 @@ } } } + }, + "Get started" : { + }, "Get the latest alpha firmware" : { "localizations" : { @@ -19176,6 +19200,9 @@ } } } + }, + "Meshtastic" : { + }, "Meshtastic Node %@ has shared channels with you" : { "localizations" : { @@ -19192,6 +19219,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" : { @@ -21920,6 +21950,9 @@ } } } + }, + "Phone Location" : { + }, "phone.gps" : { "localizations" : { @@ -26105,6 +26138,9 @@ } } } + }, + "Send Notifications" : { + }, "Send Reboot OTA" : { "localizations" : { @@ -26732,6 +26768,9 @@ } } } + }, + "Set up later" : { + }, "set.region" : { "localizations" : { @@ -31368,6 +31407,9 @@ } } } + }, + "Welcome to" : { + }, "What does the lock mean?" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d9a03344..10c8a311 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 25A978BC2C13F90D0003AAE7 /* MeshtasticProtobufs in Frameworks */ = {isa = PBXBuildFile; productRef = 25A978BB2C13F90D0003AAE7 /* MeshtasticProtobufs */; }; 25AECD4F2C2F723200862C8E /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 25AECD4E2C2F723200862C8E /* Localizable.xcstrings */; }; 25C49D902C471AEA0024FBD1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25C49D8F2C471AEA0024FBD1 /* Constants.swift */; }; + 25CEC8E52C54536000B5F7C9 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25CEC8E42C54536000B5F7C9 /* OnboardingView.swift */; }; 25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB0C2C285F00007E03CA /* Logger.swift */; }; 25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD5BB152C28B1E4007E03CA /* AppData.swift */; }; 25F5D5BE2C3F6D87008036E3 /* NavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F5D5BD2C3F6D87008036E3 /* NavigationState.swift */; }; @@ -281,6 +282,7 @@ 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNodeButton.swift; sourceTree = ""; }; 25AECD4E2C2F723200862C8E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 25C49D8F2C471AEA0024FBD1 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 25CEC8E42C54536000B5F7C9 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; 25F5D5BD2C3F6D87008036E3 /* NavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationState.swift; sourceTree = ""; }; 25F5D5BF2C3F6DA6008036E3 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; 25F5D5C12C3F6E4B008036E3 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; @@ -605,6 +607,14 @@ path = Actions; sourceTree = ""; }; + 25CEC8E32C54532400B5F7C9 /* Onboarding */ = { + isa = PBXGroup; + children = ( + 25CEC8E42C54536000B5F7C9 /* OnboardingView.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; 25F5D5BC2C3F6D7B008036E3 /* Router */ = { isa = PBXGroup; children = ( @@ -956,12 +966,13 @@ DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( + DD47E3D726F2F21A00029299 /* Bluetooth */, + DDC2E18D26CE25CB0042C5E4 /* Helpers */, DD6D5A312CA1176A00ED3032 /* Layouts */, C9483F6B2773016700998F6B /* MapKitMap */, - DDC2E18D26CE25CB0042C5E4 /* Helpers */, - DD47E3D726F2F21A00029299 /* Bluetooth */, DDC2E18B26CE25A70042C5E4 /* Messages */, DD47E3CA26F0E50300029299 /* Nodes */, + 25CEC8E32C54532400B5F7C9 /* Onboarding */, DD4A911C2708C57100501B7E /* Settings */, DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */, ); @@ -1517,6 +1528,7 @@ DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, + 25CEC8E52C54536000B5F7C9 /* OnboardingView.swift in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, @@ -1741,7 +1753,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; @@ -1758,6 +1770,7 @@ MARKETING_VERSION = 2.5.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1775,7 +1788,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; @@ -1792,6 +1805,7 @@ MARKETING_VERSION = 2.5.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Meshtastic/AppState.swift b/Meshtastic/AppState.swift index 72f0d23a..1ccfd90a 100644 --- a/Meshtastic/AppState.swift +++ b/Meshtastic/AppState.swift @@ -2,14 +2,11 @@ import Combine import SwiftUI class AppState: ObservableObject { - @Published - var router: Router + @Published var router: Router - @Published - var unreadChannelMessages: Int + @Published var unreadChannelMessages: Int - @Published - var unreadDirectMessages: Int + @Published var unreadDirectMessages: Int var totalUnreadMessages: Int { unreadChannelMessages + unreadDirectMessages diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 740f04e2..cafbe8bf 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -72,6 +72,9 @@ extension UserDefaults { case firmwareVersion case environmentEnableWeatherKit case enableAdministration + case firstLaunch + case showOnboarding + case onboardingVersion case testIntEnum } @@ -166,6 +169,12 @@ extension UserDefaults { @UserDefault(.enableAdministration, defaultValue: false) static var enableAdministration: Bool + @UserDefault(.firstLaunch, defaultValue: true) + static var firstLaunch: Bool + + @UserDefault(.provideLocation, defaultValue: false) + static var showOnboarding: Bool + @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 1afb59be..5a305e74 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -995,7 +995,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)") + if UserDefaults.firstLaunch { + UserDefaults.showOnboarding = true + } if sendTime() { + } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index 7eb830c3..b6d97a49 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -12,7 +12,6 @@ class LocalNotificationManager { // Step 1 Request Permissions for notifications private func requestAuthorization() { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in - if granted == true && error == nil { self.scheduleNotifications() } diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index 978ae5a8..ee473bba 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -9,11 +9,20 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { // @Published var region = MKCoordinateRegion() @Published var authorizationStatus: CLAuthorizationStatus? + + // The continuation we will use to asynchronously ask the user permission to track their location. + private var permissionContinuation: CheckedContinuation? + + func requestLocationAlwaysPermissions() async -> CLAuthorizationStatus { + self.locationManager.requestAlwaysAuthorization() + return await withCheckedContinuation { continuation in + permissionContinuation = continuation + } + } override init() { super.init() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - locationManager.pausesLocationUpdatesAutomatically = true locationManager.allowsBackgroundLocationUpdates = true locationManager.activityType = .other } @@ -55,14 +64,12 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { authorizationStatus = .authorizedAlways case .authorizedWhenInUse: authorizationStatus = .authorizedWhenInUse - locationManager.requestLocation() case .restricted: authorizationStatus = .restricted case .denied: authorizationStatus = .denied case .notDetermined: authorizationStatus = .notDetermined - locationManager.requestAlwaysAuthorization() default: break } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 99d8b41a..5db92778 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -46,8 +46,9 @@ import OSLog } func startLocationUpdates() { - 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 { @@ -71,7 +72,6 @@ import OSLog } catch { Logger.services.error("💥 [App] Could not start location updates: \(error.localizedDescription)") } - return } } diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index b7b28f35..6d580093 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 ed9728c6..9a620766 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -26,20 +26,6 @@ struct Connect: View { @State var liveActivityStarted = 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)") - } - } - } - }) - } var body: some View { NavigationStack { VStack { diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index b122b0aa..3fc04715 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 isShowingOnboardingFlow: Bool = false var body: some View { TabView(selection: $appState.router.navigationState.selectedTab) { @@ -52,6 +52,21 @@ struct ContentView: View { .font(.title) } .tag(NavigationState.Tab.settings) + }.sheet( + isPresented: $isShowingOnboardingFlow, + onDismiss: { + //UserDefaults.firstLaunch = false + }, content: { + OnboardingView() + } + ) + .onAppear { + if UserDefaults.firstLaunch { + isShowingOnboardingFlow = true + } + } + .onChange(of: UserDefaults.showOnboarding) { newValue in + isShowingOnboardingFlow = newValue } } } diff --git a/Meshtastic/Views/Onboarding/OnboardingView.swift b/Meshtastic/Views/Onboarding/OnboardingView.swift new file mode 100644 index 00000000..ad516276 --- /dev/null +++ b/Meshtastic/Views/Onboarding/OnboardingView.swift @@ -0,0 +1,315 @@ +import CoreBluetooth +import OSLog +import SwiftUI +import Foundation +import MapKit + +struct OnboardingView: 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 LocationHelper.shared.requestLocationAlwaysPermissions() + } 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)") + } + } +} diff --git a/protobufs b/protobufs deleted file mode 160000 index 76f806e1..00000000 --- a/protobufs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 76f806e1bb1e2a7b157a14fadd095775f63db5e4