ExportableSettings.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. // LoopFollow
  2. // ExportableSettings.swift
  3. import Foundation
  4. import HealthKit
  5. // MARK: - Nightscout Settings Export
  6. struct NightscoutSettingsExport: Codable {
  7. let version: String
  8. let url: String
  9. let token: String
  10. let units: String
  11. static func fromCurrentStorage() -> NightscoutSettingsExport {
  12. let storage = Storage.shared
  13. return NightscoutSettingsExport(
  14. version: AppVersionManager().version(),
  15. url: storage.url.value,
  16. token: storage.token.value,
  17. units: storage.units.value
  18. )
  19. }
  20. func applyToStorage() {
  21. let storage = Storage.shared
  22. storage.url.value = url
  23. storage.token.value = token
  24. storage.units.value = units
  25. }
  26. func encodeToJSON() -> String? {
  27. do {
  28. let data = try JSONEncoder().encode(self)
  29. return String(data: data, encoding: .utf8)
  30. } catch {
  31. return nil
  32. }
  33. }
  34. func hasValidSettings() -> Bool {
  35. return !url.isEmpty && !token.isEmpty
  36. }
  37. }
  38. // MARK: - Dexcom Settings Export
  39. struct DexcomSettingsExport: Codable {
  40. let version: String
  41. let userName: String
  42. let password: String
  43. let server: String
  44. static func fromCurrentStorage() -> DexcomSettingsExport {
  45. let storage = Storage.shared
  46. return DexcomSettingsExport(
  47. version: AppVersionManager().version(),
  48. userName: storage.shareUserName.value,
  49. password: storage.sharePassword.value,
  50. server: storage.shareServer.value
  51. )
  52. }
  53. func applyToStorage() {
  54. let storage = Storage.shared
  55. storage.shareUserName.value = userName
  56. storage.sharePassword.value = password
  57. storage.shareServer.value = server
  58. }
  59. func encodeToJSON() -> String? {
  60. do {
  61. let data = try JSONEncoder().encode(self)
  62. return String(data: data, encoding: .utf8)
  63. } catch {
  64. return nil
  65. }
  66. }
  67. func hasValidSettings() -> Bool {
  68. return !userName.isEmpty && !password.isEmpty
  69. }
  70. }
  71. // MARK: - Alarm Settings Export
  72. struct AlarmSettingsExport: Codable {
  73. let version: String
  74. let alarms: [Alarm]
  75. let alarmConfiguration: AlarmConfiguration
  76. static func fromCurrentStorage() -> AlarmSettingsExport {
  77. let storage = Storage.shared
  78. return AlarmSettingsExport(
  79. version: AppVersionManager().version(),
  80. alarms: storage.alarms.value,
  81. alarmConfiguration: storage.alarmConfiguration.value
  82. )
  83. }
  84. static func fromSelectedAlarms(_ selectedAlarms: [Alarm]) -> AlarmSettingsExport {
  85. let storage = Storage.shared
  86. return AlarmSettingsExport(
  87. version: AppVersionManager().version(),
  88. alarms: selectedAlarms,
  89. alarmConfiguration: storage.alarmConfiguration.value
  90. )
  91. }
  92. func applyToStorage() {
  93. let storage = Storage.shared
  94. // When importing, merge with existing alarms instead of replacing
  95. var existingAlarms = storage.alarms.value
  96. var updatedAlarms: [Alarm] = []
  97. // Keep existing alarms that aren't being imported
  98. for existingAlarm in existingAlarms {
  99. if !alarms.contains(where: { $0.id == existingAlarm.id }) {
  100. updatedAlarms.append(existingAlarm)
  101. }
  102. }
  103. // Add imported alarms
  104. updatedAlarms.append(contentsOf: alarms)
  105. storage.alarms.value = updatedAlarms
  106. storage.alarmConfiguration.value = alarmConfiguration
  107. }
  108. func encodeToJSON() -> String? {
  109. do {
  110. let data = try JSONEncoder().encode(self)
  111. return String(data: data, encoding: .utf8)
  112. } catch {
  113. return nil
  114. }
  115. }
  116. func hasValidSettings() -> Bool {
  117. return !alarms.isEmpty
  118. }
  119. }
  120. // MARK: - Remote Settings Export
  121. struct RemoteSettingsExport: Codable {
  122. let version: String
  123. let remoteType: RemoteType
  124. let user: String
  125. let sharedSecret: String
  126. let apnsKey: String
  127. let keyId: String
  128. let teamId: String?
  129. let maxBolus: Double
  130. let maxCarbs: Double
  131. let maxProtein: Double
  132. let maxFat: Double
  133. let mealWithBolus: Bool
  134. let mealWithFatProtein: Bool
  135. let productionEnvironment: Bool
  136. let loopAPNSQrCodeURL: String
  137. let device: String
  138. static func fromCurrentStorage() -> RemoteSettingsExport {
  139. let storage = Storage.shared
  140. return RemoteSettingsExport(
  141. version: AppVersionManager().version(),
  142. remoteType: storage.remoteType.value,
  143. user: storage.user.value,
  144. sharedSecret: storage.sharedSecret.value,
  145. apnsKey: storage.apnsKey.value,
  146. keyId: storage.keyId.value,
  147. teamId: storage.teamId.value,
  148. maxBolus: storage.maxBolus.value.doubleValue(for: .internationalUnit()),
  149. maxCarbs: storage.maxCarbs.value.doubleValue(for: .gram()),
  150. maxProtein: storage.maxProtein.value.doubleValue(for: .gram()),
  151. maxFat: storage.maxFat.value.doubleValue(for: .gram()),
  152. mealWithBolus: storage.mealWithBolus.value,
  153. mealWithFatProtein: storage.mealWithFatProtein.value,
  154. productionEnvironment: storage.productionEnvironment.value,
  155. loopAPNSQrCodeURL: storage.loopAPNSQrCodeURL.value,
  156. device: storage.device.value
  157. )
  158. }
  159. func applyToStorage() {
  160. let storage = Storage.shared
  161. storage.remoteType.value = remoteType
  162. storage.user.value = user
  163. storage.sharedSecret.value = sharedSecret
  164. storage.apnsKey.value = apnsKey
  165. storage.keyId.value = keyId
  166. storage.teamId.value = teamId
  167. storage.maxBolus.value = HKQuantity(unit: .internationalUnit(), doubleValue: maxBolus)
  168. storage.maxCarbs.value = HKQuantity(unit: .gram(), doubleValue: maxCarbs)
  169. storage.maxProtein.value = HKQuantity(unit: .gram(), doubleValue: maxProtein)
  170. storage.maxFat.value = HKQuantity(unit: .gram(), doubleValue: maxFat)
  171. storage.mealWithBolus.value = mealWithBolus
  172. storage.mealWithFatProtein.value = mealWithFatProtein
  173. storage.productionEnvironment.value = productionEnvironment
  174. storage.loopAPNSQrCodeURL.value = loopAPNSQrCodeURL
  175. // Set device temporarily from import (will be overridden by Nightscout connection)
  176. if !device.isEmpty {
  177. storage.device.value = device
  178. } else {
  179. // Fallback to automatic device type based on remote type
  180. switch remoteType {
  181. case .loopAPNS:
  182. storage.device.value = "Loop"
  183. case .trc:
  184. storage.device.value = "Trio"
  185. case .nightscout:
  186. // For Nightscout, we don't automatically set device type
  187. // as it should be determined by the actual connection
  188. break
  189. case .none:
  190. break
  191. }
  192. }
  193. }
  194. func encodeToJSON() -> String? {
  195. do {
  196. let data = try JSONEncoder().encode(self)
  197. return String(data: data, encoding: .utf8)
  198. } catch {
  199. return nil
  200. }
  201. }
  202. func hasValidSettings() -> Bool {
  203. switch remoteType {
  204. case .none:
  205. return true
  206. case .nightscout:
  207. return !user.isEmpty
  208. case .trc:
  209. return !user.isEmpty && !sharedSecret.isEmpty && !apnsKey.isEmpty && !keyId.isEmpty
  210. case .loopAPNS:
  211. return !keyId.isEmpty && !apnsKey.isEmpty && teamId != nil && !loopAPNSQrCodeURL.isEmpty
  212. }
  213. }
  214. /// Validates compatibility with current storage settings
  215. func validateCompatibilityWithCurrentStorage() -> (isCompatible: Bool, shouldPromptForURL: Bool, shouldPromptForToken: Bool, message: String) {
  216. let storage = Storage.shared
  217. var message = ""
  218. var shouldPromptForURL = false
  219. var shouldPromptForToken = false
  220. // Check if there are existing remote settings
  221. let currentRemoteType = storage.remoteType.value
  222. let currentUser = storage.user.value
  223. // If remote type is changing, warn user
  224. if currentRemoteType != .none && currentRemoteType != remoteType {
  225. message += "Remote type is changing from \(currentRemoteType.rawValue) to \(remoteType.rawValue). This may affect your remote commands.\n"
  226. }
  227. // If user is changing, warn user
  228. if !currentUser.isEmpty && currentUser != user {
  229. message += "Remote user is changing from '\(currentUser)' to '\(user)'. This may affect your remote commands.\n"
  230. }
  231. // For TRC and LoopAPNS, check if key details are changing
  232. if remoteType == .trc || remoteType == .loopAPNS {
  233. let currentKeyId = storage.keyId.value
  234. let currentApnsKey = storage.apnsKey.value
  235. if !currentKeyId.isEmpty, currentKeyId != keyId {
  236. message += "APNS Key ID is changing. This may affect your remote commands.\n"
  237. }
  238. if !currentApnsKey.isEmpty, currentApnsKey != apnsKey {
  239. message += "APNS Key is changing. This may affect your remote commands.\n"
  240. }
  241. }
  242. // For TRC, check shared secret
  243. if remoteType == .trc {
  244. let currentSharedSecret = storage.sharedSecret.value
  245. if !currentSharedSecret.isEmpty, currentSharedSecret != sharedSecret {
  246. message += "Shared secret is changing. This may affect your remote commands.\n"
  247. }
  248. }
  249. // For LoopAPNS, check team ID and QR code URL
  250. if remoteType == .loopAPNS {
  251. let currentTeamId = storage.teamId.value
  252. let currentQrCodeURL = storage.loopAPNSQrCodeURL.value
  253. if let teamId = teamId, let currentTeamId = currentTeamId, teamId != currentTeamId {
  254. message += "Team ID is changing. This may affect your remote commands.\n"
  255. }
  256. if !currentQrCodeURL.isEmpty, currentQrCodeURL != loopAPNSQrCodeURL {
  257. message += "Loop APNS QR Code URL is changing. This may affect your remote commands.\n"
  258. }
  259. }
  260. // If both have tokens but they don't match, show warning
  261. let hasCurrentToken = !storage.token.value.isEmpty
  262. if hasCurrentToken {
  263. message += "Note: This import does not include Nightscout token settings. Your current Nightscout token will be preserved.\n"
  264. }
  265. let isCompatible = !shouldPromptForURL && !shouldPromptForToken
  266. return (isCompatible, shouldPromptForURL, shouldPromptForToken, message)
  267. }
  268. }
  269. // MARK: - Combined Settings Export
  270. struct CombinedSettingsExport: Codable {
  271. let version: String
  272. let appVersion: String
  273. let nightscout: NightscoutSettingsExport?
  274. let dexcom: DexcomSettingsExport?
  275. let remote: RemoteSettingsExport?
  276. let alarms: AlarmSettingsExport?
  277. let exportType: String
  278. let timestamp: Date
  279. init(nightscout: NightscoutSettingsExport? = nil,
  280. dexcom: DexcomSettingsExport? = nil,
  281. remote: RemoteSettingsExport? = nil,
  282. alarms: AlarmSettingsExport? = nil,
  283. exportType: String)
  284. {
  285. version = "1.0"
  286. appVersion = AppVersionManager().version()
  287. self.nightscout = nightscout
  288. self.dexcom = dexcom
  289. self.remote = remote
  290. self.alarms = alarms
  291. self.exportType = exportType
  292. timestamp = Date()
  293. }
  294. func encodeToJSON() -> String? {
  295. do {
  296. let data = try JSONEncoder().encode(self)
  297. return String(data: data, encoding: .utf8)
  298. } catch {
  299. return nil
  300. }
  301. }
  302. static func decodeFromJSON(_ jsonString: String) -> CombinedSettingsExport? {
  303. guard let data = jsonString.data(using: .utf8) else { return nil }
  304. do {
  305. return try JSONDecoder().decode(CombinedSettingsExport.self, from: data)
  306. } catch {
  307. return nil
  308. }
  309. }
  310. }