ImportExportSettingsViewModel.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. // LoopFollow
  2. // ImportExportSettingsViewModel.swift
  3. import Foundation
  4. import SwiftUI
  5. struct ImportPreview {
  6. let nightscoutURL: String?
  7. let dexcomUsername: String?
  8. let remoteType: String?
  9. let alarmCount: Int
  10. let alarmNames: [String]
  11. }
  12. class ImportExportSettingsViewModel: ObservableObject {
  13. // MARK: - Published Properties
  14. @Published var isShowingQRCodeScanner = false
  15. @Published var isShowingQRCodeDisplay = false
  16. @Published var isShowingAlarmSelection = false
  17. @Published var qrCodeErrorMessage = ""
  18. @Published var qrCodeString = ""
  19. @Published var exportType: ExportType = .nightscout
  20. @Published var exportedAlarmIds: Set<UUID> = []
  21. @Published var importPreview: ImportPreview?
  22. @Published var showImportConfirmation = false
  23. @Published var pendingImportSettings: CombinedSettingsExport?
  24. @Published var pendingImportSource: String = ""
  25. @Published var showExportSuccessAlert = false
  26. @Published var exportSuccessMessage = ""
  27. @Published var exportSuccessDetails: [String] = []
  28. @Published var showImportNotFoundAlert = false
  29. @Published var importNotFoundMessage = ""
  30. // MARK: - Export Types
  31. enum ExportType: String, CaseIterable {
  32. case nightscout = "Nightscout Settings"
  33. case dexcom = "Dexcom Share Settings"
  34. case remote = "Remote Settings"
  35. case alarms = "Alarm Settings"
  36. var icon: String {
  37. switch self {
  38. case .nightscout: return "network"
  39. case .dexcom: return "network"
  40. case .remote: return "antenna.radiowaves.left.and.right"
  41. case .alarms: return "bell"
  42. }
  43. }
  44. }
  45. // MARK: - QR Code Methods
  46. func handleQRCodeScanResult(_ result: Result<String, Error>) {
  47. DispatchQueue.main.async {
  48. switch result {
  49. case let .success(jsonString):
  50. self.processImportedSettings(jsonString)
  51. case let .failure(error):
  52. self.qrCodeErrorMessage = "Scanning failed: \(error.localizedDescription)"
  53. }
  54. self.isShowingQRCodeScanner = false
  55. }
  56. }
  57. private func processImportedSettings(_ jsonString: String) {
  58. do {
  59. LogManager.shared.log(category: .general, message: "Processing QR code data: \(LogRedactor.fingerprint(jsonString))")
  60. guard let data = jsonString.data(using: .utf8) else {
  61. qrCodeErrorMessage = "Invalid QR code data"
  62. return
  63. }
  64. LogManager.shared.log(category: .general, message: "QR code data converted to Data, size: \(data.count) bytes")
  65. // Try to decode as JSON first to see what we get
  66. do {
  67. let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
  68. let topLevelKeys = (jsonObject as? [String: Any]).map { Array($0.keys).sorted() } ?? []
  69. LogManager.shared.log(category: .general, message: "JSON parsing successful: keys=\(topLevelKeys)")
  70. } catch {
  71. LogManager.shared.log(category: .general, message: "JSON parsing failed: \(error.localizedDescription)")
  72. }
  73. // Use migration manager to handle version compatibility
  74. guard let settings = SettingsMigrationManager.migrateSettings(data) else {
  75. LogManager.shared.log(category: .general, message: "SettingsMigrationManager.migrateSettings returned nil")
  76. qrCodeErrorMessage = "Failed to decode or migrate settings from QR code"
  77. return
  78. }
  79. LogManager.shared.log(category: .general, message: "QR code decoded successfully. Components: nightscout=\(settings.nightscout != nil), remote=\(settings.remote != nil), alarms=\(settings.alarms != nil)")
  80. // Check version compatibility
  81. let currentVersion = AppVersionManager().version()
  82. if !SettingsMigrationManager.isCompatibleVersion(settings.appVersion) {
  83. qrCodeErrorMessage = SettingsMigrationManager.getCompatibilityMessage(for: settings.appVersion)
  84. // Still try to apply settings, but warn user
  85. }
  86. // Store settings and create preview for confirmation
  87. pendingImportSettings = settings
  88. pendingImportSource = "QR code"
  89. createImportPreview(from: settings)
  90. } catch {
  91. let currentVersion = AppVersionManager().version()
  92. qrCodeErrorMessage = "Import failed. This might be due to a version change (current: \(currentVersion)). Please try exporting settings from the source device again."
  93. LogManager.shared.log(category: .general, message: "QR code import failed: \(error.localizedDescription)")
  94. }
  95. }
  96. private func applyImportedSettings(_ settings: CombinedSettingsExport, source: String) throws {
  97. var importedComponents: [String] = []
  98. // Apply settings based on what's available
  99. if let nightscout = settings.nightscout {
  100. // Check if Nightscout settings are already configured
  101. let currentNightscout = NightscoutSettingsExport.fromCurrentStorage()
  102. if currentNightscout.hasValidSettings() {
  103. // Nightscout is already configured, warn user about overwrite
  104. LogManager.shared.log(category: .general, message: "Warning: Nightscout settings are already configured. Import will overwrite existing Nightscout settings.")
  105. }
  106. nightscout.applyToStorage()
  107. importedComponents.append("Nightscout settings")
  108. LogManager.shared.log(category: .general, message: "Nightscout settings imported from \(source) (version: \(nightscout.version))")
  109. }
  110. if let dexcom = settings.dexcom {
  111. // Check if Dexcom settings are already configured
  112. let currentDexcom = DexcomSettingsExport.fromCurrentStorage()
  113. if currentDexcom.hasValidSettings() {
  114. LogManager.shared.log(category: .general, message: "Warning: Dexcom settings are already configured. Import will overwrite existing Dexcom settings.")
  115. }
  116. dexcom.applyToStorage()
  117. importedComponents.append("Dexcom settings")
  118. LogManager.shared.log(category: .general, message: "Dexcom settings imported from \(source) (version: \(dexcom.version))")
  119. }
  120. if let remote = settings.remote {
  121. if remote.hasValidSettings() {
  122. let validation = remote.validateCompatibilityWithCurrentStorage()
  123. if validation.isCompatible {
  124. remote.applyToStorage()
  125. importedComponents.append("Remote settings (\(remote.remoteType.rawValue))")
  126. LogManager.shared.log(category: .general, message: "Remote settings imported from \(source) (version: \(remote.version), type: \(remote.remoteType.rawValue), device: \(remote.device))")
  127. } else {
  128. throw NSError(domain: "SettingsImport", code: 1, userInfo: [NSLocalizedDescriptionKey: "Remote settings conflict: \(validation.message)"])
  129. }
  130. } else {
  131. throw NSError(domain: "SettingsImport", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid remote settings in \(source)"])
  132. }
  133. }
  134. if let alarms = settings.alarms {
  135. LogManager.shared.log(category: .general, message: "Attempting to import alarm settings: \(alarms.alarms.count) alarms")
  136. alarms.applyToStorage()
  137. importedComponents.append("Alarm settings (\(alarms.alarms.count) alarms)")
  138. LogManager.shared.log(category: .general, message: "Alarm settings imported from \(source) (version: \(alarms.version), \(alarms.alarms.count) alarms)")
  139. }
  140. // Update the success message with what was imported
  141. if !importedComponents.isEmpty {
  142. let componentsList = importedComponents.joined(separator: ", ")
  143. qrCodeErrorMessage = "Successfully imported: \(componentsList)"
  144. }
  145. }
  146. func generateQRCodeForExport() -> String? {
  147. let settings: CombinedSettingsExport?
  148. switch exportType {
  149. case .nightscout:
  150. let nightscoutSettings = NightscoutSettingsExport.fromCurrentStorage()
  151. if !nightscoutSettings.hasValidSettings() {
  152. qrCodeErrorMessage = "Please configure your Nightscout settings first (URL and Token)"
  153. return nil
  154. }
  155. settings = CombinedSettingsExport(
  156. nightscout: nightscoutSettings,
  157. exportType: exportType.rawValue
  158. )
  159. case .remote:
  160. let remoteSettings = RemoteSettingsExport.fromCurrentStorage()
  161. if !remoteSettings.hasValidSettings() {
  162. let currentRemoteType = Storage.shared.remoteType.value
  163. if currentRemoteType == .none {
  164. qrCodeErrorMessage = "Please configure your Remote settings first (select a remote type and configure required fields)"
  165. } else {
  166. qrCodeErrorMessage = "Please complete your Remote settings configuration (check required fields for \(currentRemoteType.rawValue))"
  167. }
  168. return nil
  169. }
  170. settings = CombinedSettingsExport(
  171. remote: remoteSettings,
  172. exportType: exportType.rawValue
  173. )
  174. case .alarms:
  175. let alarmSettings = AlarmSettingsExport.fromCurrentStorage()
  176. LogManager.shared.log(category: .general, message: "Generating alarm export: \(alarmSettings.alarms.count) alarms")
  177. if !alarmSettings.hasValidSettings() {
  178. qrCodeErrorMessage = "Please configure your Alarm settings first"
  179. return nil
  180. }
  181. settings = CombinedSettingsExport(
  182. alarms: alarmSettings,
  183. exportType: exportType.rawValue
  184. )
  185. case .dexcom:
  186. let dexcomSettings = DexcomSettingsExport.fromCurrentStorage()
  187. if !dexcomSettings.hasValidSettings() {
  188. qrCodeErrorMessage = "Please configure your Dexcom Share settings first (username and password)"
  189. return nil
  190. }
  191. settings = CombinedSettingsExport(
  192. dexcom: dexcomSettings,
  193. exportType: exportType.rawValue
  194. )
  195. }
  196. return settings?.encodeToJSON()
  197. }
  198. func showAlarmSelection() {
  199. exportType = .alarms
  200. isShowingAlarmSelection = true
  201. }
  202. func exportSelectedAlarms(_ selectedAlarms: [Alarm]) {
  203. let settings = AlarmSettingsExport.fromSelectedAlarms(selectedAlarms)
  204. if let qrString = settings.encodeToJSON() {
  205. qrCodeString = qrString
  206. isShowingQRCodeDisplay = true
  207. // Track which alarms were exported
  208. let exportedIds = Set(selectedAlarms.map { $0.id })
  209. exportedAlarmIds.formUnion(exportedIds)
  210. }
  211. isShowingAlarmSelection = false
  212. }
  213. func cancelAlarmSelection() {
  214. isShowingAlarmSelection = false
  215. }
  216. func resetExportedAlarms() {
  217. exportedAlarmIds.removeAll()
  218. }
  219. private func createImportPreview(from settings: CombinedSettingsExport) {
  220. let nightscoutURL = settings.nightscout?.url.isEmpty == false ? settings.nightscout?.url : nil
  221. let dexcomUsername = settings.dexcom?.userName.isEmpty == false ? settings.dexcom?.userName : nil
  222. let remoteType = settings.remote?.remoteType != .none ? settings.remote?.remoteType.rawValue : nil
  223. let alarmCount = settings.alarms?.alarms.count ?? 0
  224. let alarmNames = settings.alarms?.alarms.map { $0.name } ?? []
  225. // Check if any settings are actually present
  226. let hasAnySettings = (nightscoutURL != nil && !nightscoutURL!.isEmpty) ||
  227. (dexcomUsername != nil && !dexcomUsername!.isEmpty) ||
  228. (remoteType != nil && !remoteType!.isEmpty && remoteType != "None") ||
  229. alarmCount > 0
  230. LogManager.shared.log(category: .general, message: "Import preview check - nightscoutURL: \(nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), dexcomUsername: \(dexcomUsername.map { LogRedactor.username($0) } ?? "nil"), remoteType: \(remoteType ?? "nil"), alarmCount: \(alarmCount), hasAnySettings: \(hasAnySettings)")
  231. if hasAnySettings {
  232. LogManager.shared.log(category: .general, message: "Creating import preview with settings")
  233. importPreview = ImportPreview(
  234. nightscoutURL: nightscoutURL,
  235. dexcomUsername: dexcomUsername,
  236. remoteType: remoteType,
  237. alarmCount: alarmCount,
  238. alarmNames: alarmNames
  239. )
  240. LogManager.shared.log(category: .general, message: "Created importPreview - nightscoutURL: \(importPreview?.nightscoutURL.map { LogRedactor.url($0) } ?? "nil"), remoteType: \(importPreview?.remoteType ?? "nil"), alarmCount: \(importPreview?.alarmCount ?? 0)")
  241. showImportConfirmation = true
  242. LogManager.shared.log(category: .general, message: "Set showImportConfirmation = true")
  243. } else {
  244. LogManager.shared.log(category: .general, message: "No settings found, clearing import data")
  245. // No settings found, show alert and clear any pending data
  246. importNotFoundMessage = "The settings file exists but contains no valid settings to import."
  247. showImportNotFoundAlert = true
  248. showImportConfirmation = false
  249. importPreview = nil
  250. pendingImportSettings = nil
  251. pendingImportSource = ""
  252. LogManager.shared.log(category: .general, message: "Set showImportConfirmation = false")
  253. }
  254. }
  255. func confirmImport() {
  256. guard let settings = pendingImportSettings else { return }
  257. do {
  258. try applyImportedSettings(settings, source: pendingImportSource)
  259. } catch {
  260. qrCodeErrorMessage = "Import failed: \(error.localizedDescription)"
  261. }
  262. // Reset confirmation state
  263. showImportConfirmation = false
  264. importPreview = nil
  265. pendingImportSettings = nil
  266. pendingImportSource = ""
  267. }
  268. func cancelImport() {
  269. showImportConfirmation = false
  270. importPreview = nil
  271. pendingImportSettings = nil
  272. pendingImportSource = ""
  273. }
  274. }