ImportExportSettingsViewModel.swift 16 KB

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