RemoteCommandSettings.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. // LoopFollow
  2. // RemoteCommandSettings.swift
  3. import Foundation
  4. import HealthKit
  5. struct RemoteCommandSettings: Codable {
  6. let remoteType: RemoteType
  7. let user: String
  8. let sharedSecret: String
  9. let remoteApnsKey: String
  10. let remoteKeyId: String
  11. let teamId: String?
  12. let maxBolus: Double
  13. let maxCarbs: Double
  14. let maxProtein: Double
  15. let maxFat: Double
  16. let mealWithBolus: Bool
  17. let mealWithFatProtein: Bool
  18. let productionEnvironment: Bool
  19. let loopAPNSQrCodeURL: String
  20. let url: String
  21. let token: String
  22. let version: String
  23. init(
  24. remoteType: RemoteType,
  25. user: String,
  26. sharedSecret: String,
  27. remoteApnsKey: String,
  28. remoteKeyId: String,
  29. teamId: String?,
  30. maxBolus: Double,
  31. maxCarbs: Double,
  32. maxProtein: Double,
  33. maxFat: Double,
  34. mealWithBolus: Bool,
  35. mealWithFatProtein: Bool,
  36. productionEnvironment: Bool,
  37. loopAPNSQrCodeURL: String,
  38. url: String,
  39. token: String
  40. ) {
  41. self.remoteType = remoteType
  42. self.user = user
  43. self.sharedSecret = sharedSecret
  44. self.remoteApnsKey = remoteApnsKey
  45. self.remoteKeyId = remoteKeyId
  46. self.teamId = teamId
  47. self.maxBolus = maxBolus
  48. self.maxCarbs = maxCarbs
  49. self.maxProtein = maxProtein
  50. self.maxFat = maxFat
  51. self.mealWithBolus = mealWithBolus
  52. self.mealWithFatProtein = mealWithFatProtein
  53. self.productionEnvironment = productionEnvironment
  54. self.loopAPNSQrCodeURL = loopAPNSQrCodeURL
  55. self.url = url
  56. self.token = token
  57. version = "1.0"
  58. }
  59. /// Creates RemoteCommandSettings from the current Storage values
  60. static func fromCurrentStorage() -> RemoteCommandSettings {
  61. let storage = Storage.shared
  62. return RemoteCommandSettings(
  63. remoteType: storage.remoteType.value,
  64. user: storage.user.value,
  65. sharedSecret: storage.sharedSecret.value,
  66. remoteApnsKey: storage.remoteApnsKey.value,
  67. remoteKeyId: storage.remoteKeyId.value,
  68. teamId: storage.teamId.value,
  69. maxBolus: storage.maxBolus.value.doubleValue(for: .internationalUnit()),
  70. maxCarbs: storage.maxCarbs.value.doubleValue(for: .gram()),
  71. maxProtein: storage.maxProtein.value.doubleValue(for: .gram()),
  72. maxFat: storage.maxFat.value.doubleValue(for: .gram()),
  73. mealWithBolus: storage.mealWithBolus.value,
  74. mealWithFatProtein: storage.mealWithFatProtein.value,
  75. productionEnvironment: storage.productionEnvironment.value,
  76. loopAPNSQrCodeURL: storage.loopAPNSQrCodeURL.value,
  77. url: storage.url.value,
  78. token: storage.token.value
  79. )
  80. }
  81. /// Applies the settings to the current Storage
  82. func applyToStorage() {
  83. let storage = Storage.shared
  84. storage.remoteType.value = remoteType
  85. storage.user.value = user
  86. storage.sharedSecret.value = sharedSecret
  87. storage.remoteApnsKey.value = remoteApnsKey
  88. storage.remoteKeyId.value = remoteKeyId
  89. storage.teamId.value = teamId
  90. storage.maxBolus.value = HKQuantity(unit: .internationalUnit(), doubleValue: maxBolus)
  91. storage.maxCarbs.value = HKQuantity(unit: .gram(), doubleValue: maxCarbs)
  92. storage.maxProtein.value = HKQuantity(unit: .gram(), doubleValue: maxProtein)
  93. storage.maxFat.value = HKQuantity(unit: .gram(), doubleValue: maxFat)
  94. storage.mealWithBolus.value = mealWithBolus
  95. storage.mealWithFatProtein.value = mealWithFatProtein
  96. storage.productionEnvironment.value = productionEnvironment
  97. storage.loopAPNSQrCodeURL.value = loopAPNSQrCodeURL
  98. storage.url.value = url
  99. storage.token.value = token
  100. // Automatically set device type based on remote type
  101. switch remoteType {
  102. case .loopAPNS:
  103. storage.device.value = "Loop"
  104. case .trc:
  105. storage.device.value = "Trio"
  106. case .none:
  107. // For none, we don't change the device type
  108. break
  109. }
  110. }
  111. /// Encodes the settings to a JSON string for QR code generation
  112. func encodeToJSON() -> String? {
  113. do {
  114. let data = try JSONEncoder().encode(self)
  115. return String(data: data, encoding: .utf8)
  116. } catch {
  117. return nil
  118. }
  119. }
  120. /// Decodes settings from a JSON string
  121. static func decodeFromJSON(_ jsonString: String) -> RemoteCommandSettings? {
  122. guard let data = jsonString.data(using: .utf8) else {
  123. return nil
  124. }
  125. do {
  126. return try JSONDecoder().decode(RemoteCommandSettings.self, from: data)
  127. } catch {
  128. return nil
  129. }
  130. }
  131. /// Checks if the settings are valid for the given remote type
  132. func hasValidSettings() -> Bool {
  133. switch remoteType {
  134. case .none:
  135. return true
  136. case .trc:
  137. return !user.isEmpty && !sharedSecret.isEmpty && !remoteApnsKey.isEmpty && !remoteKeyId.isEmpty
  138. case .loopAPNS:
  139. return !remoteKeyId.isEmpty && !remoteApnsKey.isEmpty && teamId != nil && !loopAPNSQrCodeURL.isEmpty
  140. }
  141. }
  142. /// Validates URL and token compatibility with current storage
  143. /// Returns a tuple with (isCompatible, shouldPromptForURL, shouldPromptForToken, message)
  144. func validateCompatibilityWithCurrentStorage() -> (isCompatible: Bool, shouldPromptForURL: Bool, shouldPromptForToken: Bool, message: String) {
  145. let storage = Storage.shared
  146. let currentURL = storage.url.value
  147. let currentToken = storage.token.value
  148. var shouldPromptForURL = false
  149. var shouldPromptForToken = false
  150. var message = ""
  151. // Check if current user has URL set
  152. let hasCurrentURL = !currentURL.isEmpty
  153. let hasCurrentToken = !currentToken.isEmpty
  154. // Check if scanned settings have URL/token
  155. let hasScannedURL = !url.isEmpty
  156. let hasScannedToken = !token.isEmpty
  157. // If current user doesn't have URL but scanned settings do, prompt to set it
  158. if !hasCurrentURL, hasScannedURL {
  159. shouldPromptForURL = true
  160. message = String(localized: "The scanned settings include a Nightscout URL. Would you like to set this as your Nightscout address?")
  161. }
  162. // If current user doesn't have token but scanned settings do, prompt to set it
  163. if !hasCurrentToken, hasScannedToken {
  164. shouldPromptForToken = true
  165. if !message.isEmpty {
  166. message += "\n\n" + String(localized: "The scanned settings also include a token. Would you like to set this as your access token?")
  167. } else {
  168. message = String(localized: "The scanned settings include a token. Would you like to set this as your access token?")
  169. }
  170. }
  171. // If both have URLs but they don't match, show warning
  172. if hasCurrentURL, hasScannedURL, currentURL != url {
  173. shouldPromptForURL = true
  174. 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?"
  175. }
  176. // If both have tokens but they don't match, show warning
  177. if hasCurrentToken, hasScannedToken, currentToken != token {
  178. shouldPromptForToken = true
  179. if !message.isEmpty {
  180. message += "\n\n" + String(localized: "The scanned token doesn't match your current access token. Would you like to update your token?")
  181. } else {
  182. message = String(localized: "The scanned token doesn't match your current access token. Would you like to update your token?")
  183. }
  184. }
  185. let isCompatible = !shouldPromptForURL && !shouldPromptForToken
  186. return (isCompatible, shouldPromptForURL, shouldPromptForToken, message)
  187. }
  188. }