SettingsViewController.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. //
  2. // SettingsViewController.swift
  3. // LoopFollow
  4. //
  5. // Created by Jon Fawcett on 6/3/20.
  6. // Copyright © 2020 Jon Fawcett. All rights reserved.
  7. //
  8. import UIKit
  9. import Eureka
  10. import EventKit
  11. import EventKitUI
  12. class SettingsViewController: FormViewController {
  13. var appStateController: AppStateController?
  14. var statusLabelRow: LabelRow!
  15. func showHideNSDetails() {
  16. var isHidden = false
  17. var isEnabled = true
  18. var isLoopHidden = false;
  19. if UserDefaultsRepository.url.value == "" || !UserDefaultsRepository.loopUser.value {
  20. isHidden = true
  21. isEnabled = false
  22. }
  23. if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow {
  24. row1.hidden = .function(["hide"], {form in
  25. return isHidden
  26. })
  27. row1.evaluateHidden()
  28. }
  29. if UserDefaultsRepository.url.value != "" {
  30. isEnabled = true
  31. }
  32. guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
  33. nightscoutTab.isEnabled = isEnabled
  34. }
  35. // Determine if the build is from TestFlight
  36. func isTestFlightBuild() -> Bool {
  37. #if targetEnvironment(simulator)
  38. return false
  39. #else
  40. if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil {
  41. return false
  42. }
  43. guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else {
  44. return false
  45. }
  46. return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame
  47. #endif
  48. }
  49. // Get the build date from the build details
  50. func buildDate() -> Date? {
  51. let dateFormatter = DateFormatter()
  52. dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy"
  53. dateFormatter.locale = Locale(identifier: "en_US_POSIX")
  54. dateFormatter.timeZone = TimeZone(identifier: "UTC")
  55. guard let dateString = BuildDetails.default.buildDateString,
  56. let date = dateFormatter.date(from: dateString) else {
  57. return nil
  58. }
  59. return date
  60. }
  61. // Calculate the expiration date based on the build type
  62. func calculateExpirationDate() -> Date {
  63. if isTestFlightBuild(), let buildDate = buildDate() {
  64. // For TestFlight, add 90 days to the build date
  65. return Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
  66. } else {
  67. // For Xcode builds, use the provisioning profile's expiration date
  68. if let provision = MobileProvision.read() {
  69. return provision.expirationDate
  70. } else {
  71. return Date() // Fallback to current date if unable to read provisioning profile
  72. }
  73. }
  74. }
  75. override func viewDidLoad() {
  76. super.viewDidLoad()
  77. if UserDefaultsRepository.forceDarkMode.value {
  78. overrideUserInterfaceStyle = .dark
  79. }
  80. UserDefaultsRepository.showNS.value = false
  81. UserDefaultsRepository.showDex.value = false
  82. let expiration = calculateExpirationDate()
  83. var expirationHeaderString = "App Expiration"
  84. if isTestFlightBuild() {
  85. expirationHeaderString = "Beta (TestFlight) Expiration"
  86. }
  87. form
  88. +++ Section(header: "Data Settings", footer: "")
  89. <<< SegmentedRow<String>("units") { row in
  90. row.title = "Units"
  91. row.options = ["mg/dL", "mmol/L"]
  92. row.value = UserDefaultsRepository.units.value
  93. }.onChange { row in
  94. guard let value = row.value else { return }
  95. UserDefaultsRepository.units.value = value
  96. }
  97. <<< SwitchRow("showNS"){ row in
  98. row.title = "Show Nightscout Settings"
  99. row.value = UserDefaultsRepository.showNS.value
  100. }.onChange { [weak self] row in
  101. guard let value = row.value else { return }
  102. UserDefaultsRepository.showNS.value = value
  103. }
  104. <<< TextRow() { row in
  105. row.title = "URL"
  106. row.placeholder = "https://mycgm.herokuapp.com"
  107. row.value = UserDefaultsRepository.url.value
  108. row.hidden = "$showNS == false"
  109. }.cellSetup { (cell, row) in
  110. cell.textField.autocorrectionType = .no
  111. cell.textField.autocapitalizationType = .none
  112. }.onChange { row in
  113. guard let value = row.value else {
  114. UserDefaultsRepository.url.value = ""
  115. self.showHideNSDetails()
  116. return
  117. }
  118. // Normalize input: remove unwanted characters and lowercase
  119. let filtered = value.replacingOccurrences(of: "[^A-Za-z0-9:/._-]", with: "", options: .regularExpression).lowercased()
  120. // Further clean-up: Remove trailing slashes
  121. var cleanURL = filtered
  122. while cleanURL.last == "/" {
  123. cleanURL = String(cleanURL.dropLast())
  124. }
  125. // Set the cleaned URL
  126. UserDefaultsRepository.url.value = cleanURL
  127. row.value = cleanURL
  128. self.showHideNSDetails()
  129. globalVariables.nsVerifiedAlert = 0
  130. // Verify Nightscout URL and token
  131. self.checkNightscoutStatus()
  132. }
  133. <<< TextRow() { row in
  134. row.title = "NS Token"
  135. row.placeholder = "Leave blank if not using tokens"
  136. row.value = UserDefaultsRepository.token.value
  137. row.hidden = "$showNS == false"
  138. }.cellSetup { (cell, row) in
  139. cell.textField.autocorrectionType = .no
  140. cell.textField.autocapitalizationType = .none
  141. cell.textField.textContentType = .password
  142. }.onChange { row in
  143. if row.value == nil {
  144. UserDefaultsRepository.token.value = ""
  145. }
  146. guard let value = row.value else { return }
  147. UserDefaultsRepository.token.value = value
  148. globalVariables.nsVerifiedAlert = 0
  149. // Verify Nightscout URL and token
  150. self.checkNightscoutStatus()
  151. }
  152. <<< LabelRow() { row in
  153. row.title = "NS Status"
  154. row.value = "Checking..."
  155. statusLabelRow = row
  156. row.hidden = "$showNS == false"
  157. }
  158. <<< SwitchRow("loopUser"){ row in
  159. row.title = "Download Loop/iAPS Data"
  160. row.tag = "loopUser"
  161. row.value = UserDefaultsRepository.loopUser.value
  162. row.hidden = "$showNS == false"
  163. }.onChange { row in
  164. guard let value = row.value else { return }
  165. UserDefaultsRepository.loopUser.value = value
  166. }
  167. <<< SwitchRow("showDex"){ row in
  168. row.title = "Show Dexcom Settings"
  169. row.value = UserDefaultsRepository.showDex.value
  170. }.onChange { row in
  171. guard let value = row.value else { return }
  172. UserDefaultsRepository.showDex.value = value
  173. }
  174. <<< TextRow(){ row in
  175. row.title = "User Name"
  176. row.value = UserDefaultsRepository.shareUserName.value
  177. row.hidden = "$showDex == false"
  178. }.cellSetup { (cell, row) in
  179. cell.textField.autocorrectionType = .no
  180. cell.textField.autocapitalizationType = .none
  181. }.onChange { row in
  182. if row.value == nil {
  183. UserDefaultsRepository.shareUserName.value = ""
  184. }
  185. guard let value = row.value else { return }
  186. UserDefaultsRepository.shareUserName.value = value
  187. globalVariables.dexVerifiedAlert = 0
  188. }
  189. <<< TextRow(){ row in
  190. row.title = "Password"
  191. row.value = UserDefaultsRepository.sharePassword.value
  192. row.hidden = "$showDex == false"
  193. }.cellSetup { (cell, row) in
  194. cell.textField.autocorrectionType = .no
  195. cell.textField.isSecureTextEntry = true
  196. cell.textField.autocapitalizationType = .none
  197. }.onChange { row in
  198. if row.value == nil {
  199. UserDefaultsRepository.sharePassword.value = ""
  200. }
  201. guard let value = row.value else { return }
  202. UserDefaultsRepository.sharePassword.value = value
  203. globalVariables.dexVerifiedAlert = 0
  204. }
  205. <<< SegmentedRow<String>("shareServer") { row in
  206. row.title = "Server"
  207. row.options = ["US", "NON-US"]
  208. row.value = UserDefaultsRepository.shareServer.value
  209. row.hidden = "$showDex == false"
  210. }.onChange { row in
  211. guard let value = row.value else { return }
  212. UserDefaultsRepository.shareServer.value = value
  213. }
  214. +++ Section("App Settings")
  215. <<< ButtonRow() {
  216. $0.title = "General Settings"
  217. $0.presentationMode = .show(
  218. controllerProvider: .callback(builder: {
  219. let controller = GeneralSettingsViewController()
  220. controller.appStateController = self.appStateController
  221. return controller
  222. }
  223. ), onDismiss: nil)
  224. }
  225. <<< ButtonRow("graphSettings") {
  226. $0.title = "Graph Settings"
  227. $0.presentationMode = .show(
  228. controllerProvider: .callback(builder: {
  229. let controller = GraphSettingsViewController()
  230. controller.appStateController = self.appStateController
  231. return controller
  232. }
  233. ), onDismiss: nil)
  234. }
  235. <<< ButtonRow("informationDisplaySettings") {
  236. $0.title = "Information Display Settings"
  237. $0.presentationMode = .show(
  238. controllerProvider: .callback(builder: {
  239. let controller = InfoDisplaySettingsViewController()
  240. controller.appStateController = self.appStateController
  241. return controller
  242. }
  243. ), onDismiss: nil)
  244. }
  245. +++ Section("Integrations")
  246. <<< ButtonRow() {
  247. $0.title = "Apple Watch and Carplay"
  248. $0.presentationMode = .show(
  249. controllerProvider: .callback(builder: {
  250. let controller = WatchSettingsViewController()
  251. controller.appStateController = self.appStateController
  252. return controller
  253. }
  254. ), onDismiss: nil)
  255. }
  256. +++ Section("Advanced Settings")
  257. <<< ButtonRow() {
  258. $0.title = "Advanced Settings"
  259. $0.presentationMode = .show(
  260. controllerProvider: .callback(builder: {
  261. let controller = AdvancedSettingsViewController()
  262. controller.appStateController = self.appStateController
  263. return controller
  264. }
  265. ), onDismiss: nil)
  266. }
  267. +++ Section(header: getAppVersion(), footer: "")
  268. if !isMacApp() {
  269. form +++ Section(header: expirationHeaderString, footer: String(expiration.description))
  270. }
  271. showHideNSDetails()
  272. checkNightscoutStatus()
  273. }
  274. func isMacApp() -> Bool {
  275. #if targetEnvironment(macCatalyst)
  276. return true
  277. #else
  278. return false
  279. #endif
  280. }
  281. func getAppVersion() -> String {
  282. if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
  283. return "App Version: \(version)"
  284. }
  285. return "Version Unknown"
  286. }
  287. func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
  288. if let error = error {
  289. switch error {
  290. case .invalidURL:
  291. statusLabelRow.value = "Invalid URL"
  292. case .networkError:
  293. statusLabelRow.value = "Network Error"
  294. case .invalidToken:
  295. statusLabelRow.value = "Invalid Token"
  296. case .tokenRequired:
  297. statusLabelRow.value = "Token Required"
  298. case .siteNotFound:
  299. statusLabelRow.value = "Site Not Found"
  300. case .unknown:
  301. statusLabelRow.value = "Unknown Error"
  302. case .emptyAddress:
  303. statusLabelRow.value = "Address Empty"
  304. }
  305. } else {
  306. statusLabelRow.value = "OK"
  307. NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
  308. }
  309. statusLabelRow.updateCell()
  310. }
  311. func checkNightscoutStatus() {
  312. NightscoutUtils.verifyURLAndToken(urlUser: UserDefaultsRepository.url.value, token: UserDefaultsRepository.token.value) { error in
  313. DispatchQueue.main.async {
  314. self.updateStatusLabel(error: error)
  315. }
  316. }
  317. }
  318. }