// LoopFollow // RemoteCommandSettings.swift import Foundation import HealthKit struct RemoteCommandSettings: Codable { let remoteType: RemoteType let user: String let sharedSecret: String let apnsKey: String let keyId: String let teamId: String? let maxBolus: Double let maxCarbs: Double let maxProtein: Double let maxFat: Double let mealWithBolus: Bool let mealWithFatProtein: Bool let productionEnvironment: Bool let loopAPNSQrCodeURL: String let url: String let token: String let version: String init( remoteType: RemoteType, user: String, sharedSecret: String, apnsKey: String, keyId: String, teamId: String?, maxBolus: Double, maxCarbs: Double, maxProtein: Double, maxFat: Double, mealWithBolus: Bool, mealWithFatProtein: Bool, productionEnvironment: Bool, loopAPNSQrCodeURL: String, url: String, token: String ) { self.remoteType = remoteType self.user = user self.sharedSecret = sharedSecret self.apnsKey = apnsKey self.keyId = keyId self.teamId = teamId self.maxBolus = maxBolus self.maxCarbs = maxCarbs self.maxProtein = maxProtein self.maxFat = maxFat self.mealWithBolus = mealWithBolus self.mealWithFatProtein = mealWithFatProtein self.productionEnvironment = productionEnvironment self.loopAPNSQrCodeURL = loopAPNSQrCodeURL self.url = url self.token = token version = "1.0" } /// Creates RemoteCommandSettings from the current Storage values static func fromCurrentStorage() -> RemoteCommandSettings { let storage = Storage.shared return RemoteCommandSettings( remoteType: storage.remoteType.value, user: storage.user.value, sharedSecret: storage.sharedSecret.value, apnsKey: storage.apnsKey.value, keyId: storage.keyId.value, teamId: storage.teamId.value, maxBolus: storage.maxBolus.value.doubleValue(for: .internationalUnit()), maxCarbs: storage.maxCarbs.value.doubleValue(for: .gram()), maxProtein: storage.maxProtein.value.doubleValue(for: .gram()), maxFat: storage.maxFat.value.doubleValue(for: .gram()), mealWithBolus: storage.mealWithBolus.value, mealWithFatProtein: storage.mealWithFatProtein.value, productionEnvironment: storage.productionEnvironment.value, loopAPNSQrCodeURL: storage.loopAPNSQrCodeURL.value, url: storage.url.value, token: storage.token.value ) } /// Applies the settings to the current Storage func applyToStorage() { let storage = Storage.shared storage.remoteType.value = remoteType storage.user.value = user storage.sharedSecret.value = sharedSecret storage.apnsKey.value = apnsKey storage.keyId.value = keyId storage.teamId.value = teamId storage.maxBolus.value = HKQuantity(unit: .internationalUnit(), doubleValue: maxBolus) storage.maxCarbs.value = HKQuantity(unit: .gram(), doubleValue: maxCarbs) storage.maxProtein.value = HKQuantity(unit: .gram(), doubleValue: maxProtein) storage.maxFat.value = HKQuantity(unit: .gram(), doubleValue: maxFat) storage.mealWithBolus.value = mealWithBolus storage.mealWithFatProtein.value = mealWithFatProtein storage.productionEnvironment.value = productionEnvironment storage.loopAPNSQrCodeURL.value = loopAPNSQrCodeURL storage.url.value = url storage.token.value = token // Automatically set device type based on remote type switch remoteType { case .loopAPNS: storage.device.value = "Loop" case .trc: storage.device.value = "Trio" case .nightscout: // For Nightscout, we don't automatically set device type // as it should be determined by the actual connection break case .none: // For none, we don't change the device type break } } /// Encodes the settings to a JSON string for QR code generation func encodeToJSON() -> String? { do { let data = try JSONEncoder().encode(self) return String(data: data, encoding: .utf8) } catch { return nil } } /// Decodes settings from a JSON string static func decodeFromJSON(_ jsonString: String) -> RemoteCommandSettings? { guard let data = jsonString.data(using: .utf8) else { return nil } do { return try JSONDecoder().decode(RemoteCommandSettings.self, from: data) } catch { return nil } } /// Checks if the settings are valid for the given remote type func hasValidSettings() -> Bool { switch remoteType { case .none: return true case .nightscout: return !user.isEmpty case .trc: return !user.isEmpty && !sharedSecret.isEmpty && !apnsKey.isEmpty && !keyId.isEmpty case .loopAPNS: return !keyId.isEmpty && !apnsKey.isEmpty && teamId != nil && !loopAPNSQrCodeURL.isEmpty } } /// Validates URL and token compatibility with current storage /// Returns a tuple with (isCompatible, shouldPromptForURL, shouldPromptForToken, message) func validateCompatibilityWithCurrentStorage() -> (isCompatible: Bool, shouldPromptForURL: Bool, shouldPromptForToken: Bool, message: String) { let storage = Storage.shared let currentURL = storage.url.value let currentToken = storage.token.value var shouldPromptForURL = false var shouldPromptForToken = false var message = "" // Check if current user has URL set let hasCurrentURL = !currentURL.isEmpty let hasCurrentToken = !currentToken.isEmpty // Check if scanned settings have URL/token let hasScannedURL = !url.isEmpty let hasScannedToken = !token.isEmpty // If current user doesn't have URL but scanned settings do, prompt to set it if !hasCurrentURL, hasScannedURL { shouldPromptForURL = true message = "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?" } // If current user doesn't have token but scanned settings do, prompt to set it if !hasCurrentToken, hasScannedToken { shouldPromptForToken = true if !message.isEmpty { message += "\n\nThe scanned settings also include a token. Would you like to set this as your access token?" } else { message = "The scanned settings include a token. Would you like to set this as your access token?" } } // If both have URLs but they don't match, show warning if hasCurrentURL, hasScannedURL, currentURL != url { shouldPromptForURL = true message = "The scanned Nightscout URL (\(url)) doesn't match your current Nightscout address (\(currentURL)). Would you like to change your Nightscout address to match the scanned settings?" } // If both have tokens but they don't match, show warning if hasCurrentToken, hasScannedToken, currentToken != token { shouldPromptForToken = true if !message.isEmpty { message += "\n\nThe scanned token doesn't match your current access token. Would you like to update your token?" } else { message = "The scanned token doesn't match your current access token. Would you like to update your token?" } } let isCompatible = !shouldPromptForURL && !shouldPromptForToken return (isCompatible, shouldPromptForURL, shouldPromptForToken, message) } }