Преглед изворни кода

Display branch, build date, latest version, blacklisted version

Jonas Björkert пре 2 година
родитељ
комит
9d1f0b6e9b

+ 12 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -37,6 +37,9 @@
 		DDCF979C24C14EFB002C9752 /* AdvancedSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979B24C14EFB002C9752 /* AdvancedSettingsViewController.swift */; };
 		DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979D24C2382A002C9752 /* AppStateController.swift */; };
 		DDCFCAF22B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */; };
+		DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */; };
+		DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */; };
+		DDF2C0142BEFD468007A20E6 /* blacklisted-versions.json in Resources */ = {isa = PBXBuildFile; fileRef = DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */; };
 		DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF9676D2AD08C6E00C5EB95 /* SiteChange.swift */; };
 		FC16A97A24996673003D6245 /* NightScout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97924996673003D6245 /* NightScout.swift */; };
 		FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */; };
@@ -225,6 +228,9 @@
 		DDCF979B24C14EFB002C9752 /* AdvancedSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsViewController.swift; sourceTree = "<group>"; };
 		DDCF979D24C2382A002C9752 /* AppStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateController.swift; sourceTree = "<group>"; };
 		DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = LoopFollowDisplayNameConfig.xcconfig; sourceTree = "<group>"; };
+		DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubService.swift; sourceTree = "<group>"; };
+		DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionManager.swift; sourceTree = "<group>"; };
+		DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blacklisted-versions.json"; sourceTree = "<group>"; };
 		DDF9676D2AD08C6E00C5EB95 /* SiteChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteChange.swift; sourceTree = "<group>"; };
 		ECA3EFB4037410B4973BB632 /* Pods-LoopFollow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.debug.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig"; sourceTree = "<group>"; };
 		FC16A97924996673003D6245 /* NightScout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScout.swift; sourceTree = "<group>"; };
@@ -644,6 +650,7 @@
 		FC97880B2485969B00A7906C = {
 			isa = PBXGroup;
 			children = (
+				DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */,
 				DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */,
 				DDB0AF4F2BB1A81F00AFA48B /* Scripts */,
 				DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */,
@@ -692,6 +699,8 @@
 				DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */,
 				DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */,
 				DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */,
+				DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */,
+				DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */,
 			);
 			path = helpers;
 			sourceTree = "<group>";
@@ -818,6 +827,7 @@
 				FC7CE55D248ABE37001F83B8 /* Metallic.caf in Resources */,
 				FC7CE568248ABE37001F83B8 /* Sci-Fi_Engine_Shut_Down.caf in Resources */,
 				FC7CE580248ABE37001F83B8 /* Sci-Fi_Alarm.caf in Resources */,
+				DDF2C0142BEFD468007A20E6 /* blacklisted-versions.json in Resources */,
 				FC7CE533248ABE37001F83B8 /* Ending_Reached.caf in Resources */,
 				FC7CE558248ABE37001F83B8 /* Rush.caf in Resources */,
 				FC7CE52A248ABE37001F83B8 /* Nightguard.caf in Resources */,
@@ -977,9 +987,11 @@
 				FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */,
 				FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */,
 				DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */,
+				DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */,
 				FC16A97A24996673003D6245 /* NightScout.swift in Sources */,
 				DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */,
 				FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */,
+				DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */,
 				DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */,
 				FC16A97D24996747003D6245 /* Alarms.swift in Sources */,
 				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,

+ 67 - 61
LoopFollow/ViewControllers/SettingsViewController.swift

@@ -39,50 +39,6 @@ class SettingsViewController: FormViewController {
         guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return }
         nightscoutTab.isEnabled = isEnabled
     }
-   
-    // Determine if the build is from TestFlight
-    func isTestFlightBuild() -> Bool {
-#if targetEnvironment(simulator)
-        return false
-#else
-        if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil {
-            return false
-        }
-        guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else {
-            return false
-        }
-        return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame
-#endif
-    }
-    
-    // Get the build date from the build details
-    func buildDate() -> Date? {
-        let dateFormatter = DateFormatter()
-        dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy"
-        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
-        dateFormatter.timeZone = TimeZone(identifier: "UTC")
-        
-        guard let dateString = BuildDetails.default.buildDateString,
-              let date = dateFormatter.date(from: dateString) else {
-            return nil
-        }
-        return date
-    }
-    
-    // Calculate the expiration date based on the build type
-    func calculateExpirationDate() -> Date {
-        if isTestFlightBuild(), let buildDate = buildDate() {
-            // For TestFlight, add 90 days to the build date
-            return Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
-        } else {
-            // For Xcode builds, use the provisioning profile's expiration date
-            if let provision = MobileProvision.read() {
-                return provision.expirationDate
-            } else {
-                return Date() // Fallback to current date if unable to read provisioning profile
-            }
-        }
-    }
     
    override func viewDidLoad() {
         super.viewDidLoad()
@@ -92,12 +48,14 @@ class SettingsViewController: FormViewController {
        UserDefaultsRepository.showNS.value = false
        UserDefaultsRepository.showDex.value = false
     
-       let expiration = calculateExpirationDate()
-       var expirationHeaderString = "App Expiration"
-       if isTestFlightBuild() {
-          expirationHeaderString = "Beta (TestFlight) Expiration"
-       }
-       
+       let buildDetails = BuildDetails.default
+       let formattedBuildDate = dateTimeUtils.formattedDate(from: buildDetails.buildDate())
+       let branchAndSha = buildDetails.branchAndSha
+       let expiration = dateTimeUtils.formattedDate(from: buildDetails.calculateExpirationDate())
+       let expirationHeaderString = buildDetails.expirationHeaderString
+       let versionManager = AppVersionManager()
+       let version = versionManager.version()
+
         form
         +++ Section(header: "Data Settings", footer: "")
        <<< SegmentedRow<String>("units") { row in
@@ -320,16 +278,71 @@ class SettingsViewController: FormViewController {
             
         }
 
-       +++ Section(header: getAppVersion(), footer: "")
-       
-       if !isMacApp() {
-           form +++ Section(header: expirationHeaderString, footer: String(expiration.description))
+       +++ Section("Build Information")
+       <<< LabelRow() {
+           $0.title = "Version"
+           $0.value = version
+           $0.tag = "currentVersionRow"
+       }
+       <<< LabelRow() {
+           $0.title = "Latest version"
+           $0.value = "Fetching..."
+           $0.tag = "latestVersionRow"
+       }
+       <<< LabelRow() {
+           $0.title = expirationHeaderString
+           $0.value = expiration
+           $0.hidden = Condition(booleanLiteral: isMacApp())
+       }
+       <<< LabelRow() {
+           $0.title = "Built"
+           $0.value = formattedBuildDate
+       }
+       <<< LabelRow() {
+           $0.title = "Branch"
+           $0.value = branchAndSha
        }
        
        showHideNSDetails()
        checkNightscoutStatus()
     }
     
+    override func viewWillAppear(_ animated: Bool) {
+        super.viewWillAppear(animated)
+        
+        refreshVersionInfo()
+        checkNightscoutStatus()
+    }
+    
+    func refreshVersionInfo() {
+        let versionManager = AppVersionManager()
+        versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in
+            DispatchQueue.main.async {
+                if let currentVersionRow = self.form.rowBy(tag: "currentVersionRow") as? LabelRow {
+                    currentVersionRow.cell.detailTextLabel?.textColor = self.getColor(isBlacklisted: isBlacklisted, isNewer: isNewer, isCurrent: latestVersion == versionManager.version())
+                    currentVersionRow.updateCell()
+                }
+                
+                if let latestVersionRow = self.form.rowBy(tag: "latestVersionRow") as? LabelRow {
+                    latestVersionRow.value = latestVersion ?? "Unknown"
+                    latestVersionRow.updateCell()
+                }
+            }
+        }
+    }
+    
+    private func getColor(isBlacklisted: Bool, isNewer: Bool, isCurrent: Bool) -> UIColor {
+        if isBlacklisted {
+            return .red
+        } else if isNewer {
+            return .orange
+        } else if isCurrent {
+            return .green
+        } else {
+            return .secondaryLabel
+        }
+    }
+    
     func isMacApp() -> Bool {
 #if targetEnvironment(macCatalyst)
         return true
@@ -337,13 +350,6 @@ class SettingsViewController: FormViewController {
         return false
 #endif
     }
-
-    func getAppVersion() -> String {
-        if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
-            return "App Version: \(version)"
-        }
-        return "Version Unknown"
-    }
     
     func updateStatusLabel(error: NightscoutUtils.NightscoutError?) {
         if let error = error {

+ 97 - 0
LoopFollow/helpers/AppVersionManager.swift

@@ -0,0 +1,97 @@
+//
+//  AppVersionManager.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-05-11.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class AppVersionManager {
+    private let githubService = GitHubService()
+    
+    func checkForNewVersion(completion: @escaping (String?, Bool, Bool) -> Void) {
+        let currentVersion = version()
+        let now = Date()
+        
+        // Retrieve cache
+        let lastChecked = UserDefaults.standard.object(forKey: "latestVersionChecked") as? Date ?? Date.distantPast
+        let cachedLatestVersion = UserDefaults.standard.string(forKey: "latestVersion")
+        let isBlacklistedCached = UserDefaults.standard.bool(forKey: "isCurrentVersionBlacklisted")
+
+        // Check if the cache is still valid
+        if now.timeIntervalSince(lastChecked) < 24 * 3600, let latestVersion = cachedLatestVersion {
+            let isNewer = isVersion(latestVersion, newerThan: currentVersion)
+            completion(latestVersion, isNewer, isBlacklistedCached)
+            return
+        }
+        
+        // Fetch new data if cache is outdated
+        githubService.fetchData(for: .versionConfig) { versionData in
+            self.githubService.fetchData(for: .blacklistedVersions) { blacklistData in
+                DispatchQueue.main.async {
+                    let fetchedVersion = versionData.flatMap { String(data: $0, encoding: .utf8) }
+                        .flatMap { self.parseVersionFromConfig(contents: $0) }
+                    let isNewer = fetchedVersion.map { self.isVersion($0, newerThan: currentVersion) } ?? false
+                    
+                    let isBlacklisted = (try? blacklistData.flatMap { try JSONDecoder().decode(Blacklist.self, from: $0) })
+                        .map { $0.blacklistedVersions.map { $0.version }.contains(currentVersion) } ?? false
+                    
+                    // Update cache with new data
+                    UserDefaults.standard.set(fetchedVersion, forKey: "latestVersion")
+                    UserDefaults.standard.set(Date(), forKey: "latestVersionChecked")
+                    UserDefaults.standard.set(isBlacklisted, forKey: "isCurrentVersionBlacklisted")
+                    
+                    // Call completion with new data
+                    completion(fetchedVersion, isNewer, isBlacklisted)
+                }
+            }
+        }
+    }
+
+    private func parseVersionFromConfig(contents: String) -> String? {
+        let lines = contents.split(separator: "\n")
+        for line in lines {
+            if line.contains("LOOP_FOLLOW_MARKETING_VERSION") {
+                let components = line.split(separator: "=").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+                if components.count > 1 {
+                    return components[1]
+                }
+            }
+        }
+        return nil
+    }
+    
+    private func isVersion(_ fetchedVersion: String, newerThan currentVersion: String) -> Bool {
+        let fetchedVersionComponents = fetchedVersion.split(separator: ".").map { Int($0) ?? 0 }
+        let currentVersionComponents = currentVersion.split(separator: ".").map { Int($0) ?? 0 }
+        
+        let maxCount = max(fetchedVersionComponents.count, currentVersionComponents.count)
+        for i in 0..<maxCount {
+            let fetched = i < fetchedVersionComponents.count ? fetchedVersionComponents[i] : 0
+            let current = i < currentVersionComponents.count ? currentVersionComponents[i] : 0
+            if fetched > current {
+                return true
+            } else if fetched < current {
+                return false
+            }
+        }
+        return false
+    }
+    
+    func version() -> String {
+        if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
+            return version
+        }
+        return "Unknown"
+    }
+    
+    struct Blacklist: Decodable {
+        let blacklistedVersions: [VersionEntry]
+    }
+    
+    struct VersionEntry: Decodable {
+        let version: String
+    }
+}

+ 58 - 0
LoopFollow/helpers/BuildDetails.swift

@@ -26,4 +26,62 @@ class BuildDetails {
     var buildDateString: String? {
         return dict["com-LoopFollow-build-date"] as? String
     }
+
+    var branch: String? {
+        return dict["com-LoopFollow-branch"] as? String
+    }
+
+    var branchAndSha: String {
+        let branch = branch ?? "Unknown"
+        let sha = dict["com-LoopFollow-commit-sha"] as? String ?? "Unknown"
+        return "\(branch) \(sha)"
+    }
+        
+    // Determine if the build is from TestFlight
+    func isTestFlightBuild() -> Bool {
+#if targetEnvironment(simulator)
+        return false
+#else
+        if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil {
+            return false
+        }
+        guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else {
+            return false
+        }
+        return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame
+#endif
+    }
+        
+    // Parse the build date string into a Date object
+    func buildDate() -> Date? {
+        guard let dateString = dict["com-LoopFollow-build-date"] as? String else {
+            return nil
+        }
+        let formatter = ISO8601DateFormatter()
+        return formatter.date(from: dateString)
+    }
+    
+    // Calculate the expiration date based on the build type
+    func calculateExpirationDate() -> Date {
+        if isTestFlightBuild(), let buildDate = buildDate() {
+            // For TestFlight, add 90 days to the build date
+            return Calendar.current.date(byAdding: .day, value: 90, to: buildDate)!
+        } else {
+            // For Xcode builds, use the provisioning profile's expiration date
+            if let provision = MobileProvision.read() {
+                return provision.expirationDate
+            } else {
+                return Date()
+            }
+        }
+    }
+    
+    // Expiration header based on build type
+    var expirationHeaderString: String {
+        if isTestFlightBuild() {
+            return "Beta (TestFlight) Expires"
+        } else {
+            return "App Expires"
+        }
+    }
 }

+ 11 - 0
LoopFollow/helpers/DateTime.swift

@@ -90,4 +90,15 @@ class dateTimeUtils {
 
         return dateFormat.firstIndex(of: "a") == nil
     }
+
+    static func formattedDate(from date: Date?) -> String {
+        guard let date = date else {
+            return "Unknown"
+        }
+        let dateFormatter = DateFormatter()
+        dateFormatter.dateStyle = .short
+        dateFormatter.timeStyle = .short
+        dateFormatter.locale = Locale.current
+        return dateFormatter.string(from: date)
+    }
 }

+ 45 - 0
LoopFollow/helpers/GitHubService.swift

@@ -0,0 +1,45 @@
+//
+//  GitHubService.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-05-11.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+class GitHubService {
+    enum GitHubDataType {
+        case versionConfig
+        case blacklistedVersions
+        
+        var url: String {
+            switch self {
+            case .versionConfig:
+                return "https://raw.githubusercontent.com/loopandlearn/LoopFollow/main/Config.xcconfig"
+            case .blacklistedVersions:
+                return "https://raw.githubusercontent.com/loopandlearn/LoopFollow/main/blacklisted-versions.json"
+            }
+        }
+    }
+    
+    func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) {
+        let urlString = dataType.url
+        guard let url = URL(string: urlString) else {
+            completion(nil)
+            return
+        }
+        
+        URLSession.shared.dataTask(with: url) { data, response, error in
+            guard let data = data, error == nil else {
+                completion(nil)
+                return
+            }
+            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
+                completion(data)
+            } else {
+                completion(nil)
+            }
+        }.resume()
+    }
+}

+ 17 - 10
LoopFollow/repository/UserDefaults.swift

@@ -331,7 +331,7 @@ class UserDefaultsRepository {
     static let alertSAGEAutosnooze = UserDefaultsValue<String>(key: "alertSAGEAutosnooze", default: "At night")
     static let alertSAGEAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertSAGEAutosnoozeDay", default: false)
     static let alertSAGEAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertSAGEAutosnoozeNight", default: true)
-    
+
     static let alertCAGEActive = UserDefaultsValue<Bool>(key: "alertCAGEActive", default: false)
     static let alertCAGE = UserDefaultsValue<Int>(key: "alertCAGE", default: 4) //Hours
     static let alertCAGEQuiet = UserDefaultsValue<Bool>(key: "alertCAGEQuiet", default: false)
@@ -348,16 +348,16 @@ class UserDefaultsRepository {
     static let alertCAGEAutosnooze = UserDefaultsValue<String>(key: "alertCAGEAutosnooze", default: "At night")
     static let alertCAGEAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertCAGEAutosnoozeDay", default: false)
     static let alertCAGEAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertCAGEAutosnoozeNight", default: true)
-    
+
     static let alertAppInactive = UserDefaultsValue<Bool>(key: "alertAppInactive", default: false)
-    
+
     static let alertTemporaryActive = UserDefaultsValue<Bool>(key: "alertTemporaryActive", default: false)
     static let alertTemporaryBelow = UserDefaultsValue<Bool>(key: "alertTemporaryBelow", default: true)
     static let alertTemporaryBG = UserDefaultsValue<Float>(key: "alertTemporaryBG", default: 90.0)
     static let alertTemporaryBGRepeat = UserDefaultsValue<Bool>(key: "alertTemporaryBGRepeat", default: false)
     static let alertTemporaryBGAudible = UserDefaultsValue<Bool>(key: "alertTemporaryBGRepeatAudible", default: true)
     static let alertTemporarySound = UserDefaultsValue<String>(key: "alertTemporarySound", default: "Indeed")
-    
+
     static let alertOverrideStart = UserDefaultsValue<Bool>(key: "alertOverrideStart", default: false)
     static let alertOverrideStartQuiet = UserDefaultsValue<Bool>(key: "alertOverrideStartQuiet", default: false)
     static let alertOverrideStartRepeat = UserDefaultsValue<String>(key: "alertOverrideStartRepeat", default: "Never")
@@ -372,7 +372,7 @@ class UserDefaultsRepository {
     static let alertOverrideStartAutosnooze = UserDefaultsValue<String>(key: "alertOverrideStartAutosnooze", default: "Never")
     static let alertOverrideStartAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertOverrideStartAutosnoozeDay", default: false)
     static let alertOverrideStartAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertOverrideStartAutosnoozeNight", default: false)
-    
+
     static let alertOverrideEnd = UserDefaultsValue<Bool>(key: "alertOverrideEnd", default: false)
     static let alertOverrideEndQuiet = UserDefaultsValue<Bool>(key: "alertOverrideEndQuiet", default: false)
     static let alertOverrideEndRepeat = UserDefaultsValue<String>(key: "alertOverrideEndRepeat", default: "Never")
@@ -387,7 +387,7 @@ class UserDefaultsRepository {
     static let alertOverrideEndAutosnooze = UserDefaultsValue<String>(key: "alertOverrideEndAutosnooze", default: "Never")
     static let alertOverrideEndAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertOverrideEndAutosnoozeDay", default: false)
     static let alertOverrideEndAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertOverrideEndAutosnoozeNight", default: false)
-    
+
     static let alertPump = UserDefaultsValue<Bool>(key: "alertPump", default: false)
     static let alertPumpAt = UserDefaultsValue<Int>(key: "alertPumpAt", default: 10) //Units
     static let alertPumpQuiet = UserDefaultsValue<Bool>(key: "alertPumpQuiet", default: false)
@@ -404,7 +404,7 @@ class UserDefaultsRepository {
     static let alertPumpAutosnooze = UserDefaultsValue<String>(key: "alertPumpAutosnooze", default: "Never")
     static let alertPumpAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertPumpAutosnoozeDay", default: false)
     static let alertPumpAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertPumpAutosnoozeNight", default: false)
-    
+
     static let alertIOB = UserDefaultsValue<Bool>(key: "alertIOB", default: false)
     static let alertIOBAt = UserDefaultsValue<Double>(key: "alertIOBAt", default: 1.5) //Units
     static let alertIOBNumber = UserDefaultsValue<Int>(key: "alertIOBNumber", default: 3) //Number
@@ -424,7 +424,7 @@ class UserDefaultsRepository {
     static let alertIOBAutosnooze = UserDefaultsValue<String>(key: "alertIOBAutosnooze", default: "Never")
     static let alertIOBAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertIOBAutosnoozeDay", default: false)
     static let alertIOBAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertIOBAutosnoozeNight", default: false)
-    
+
     static let alertCOB = UserDefaultsValue<Bool>(key: "alertCOB", default: false)
     static let alertCOBAt = UserDefaultsValue<Int>(key: "alertCOBAt", default: 50) //Units
     static let alertCOBQuiet = UserDefaultsValue<Bool>(key: "alertCOBQuiet", default: false)
@@ -441,7 +441,7 @@ class UserDefaultsRepository {
     static let alertCOBAutosnooze = UserDefaultsValue<String>(key: "alertCOBAutosnooze", default: "Never")
     static let alertCOBAutosnoozeDay = UserDefaultsValue<Bool>(key: "alertCOBAutosnoozeDay", default: false)
     static let alertCOBAutosnoozeNight = UserDefaultsValue<Bool>(key: "alertCOBAutosnoozeNight", default: false)
-    
+
     static let alertBatteryActive = UserDefaultsValue<Bool>(key: "alertBatteryActive", default: false)
     static let alertBatteryLevel = UserDefaultsValue<Int>(key: "alertBatteryLevel", default: 25)
     static let alertBatterySound = UserDefaultsValue<String>(key: "alertBatterySound", default: "Machine_Charge")
@@ -450,7 +450,7 @@ class UserDefaultsRepository {
     static let alertBatterySnoozedTime = UserDefaultsValue<Date?>(key: "alertBatterySnoozedTime", default: nil)
     static let alertBatterySnoozeHours = UserDefaultsValue<Int>(key: "alertBatterySnoozeHours", default: 1)
     static var deviceBatteryLevel: UserDefaultsValue<Double> = UserDefaultsValue(key: "deviceBatteryLevel", default: 100.0)
-    
+
     static let alertRecBolusActive = UserDefaultsValue<Bool>(key: "alertRecBolusActive", default: false)
     static let alertRecBolusLevel = UserDefaultsValue<Double>(key: "alertRecBolusLevel", default: 1)  //Unit[s]
     static let alertRecBolusSound = UserDefaultsValue<String>(key: "alertRecBolusSound", default: "Dhol_Shuffleloop")
@@ -459,4 +459,11 @@ class UserDefaultsRepository {
     static let alertRecBolusSnooze = UserDefaultsValue<Int>(key: "alertRecBolusSnooze", default: 5)
     static let alertRecBolusSnoozedTime = UserDefaultsValue<Date?>(key: "alertRecBolusSnoozedTime", default: nil)
     static var deviceRecBolus: UserDefaultsValue<Double> = UserDefaultsValue(key: "deviceRecBolus", default: 0.0)
+
+    //Caching of latest version
+    static let latestVersion = UserDefaultsValue<String?>(key: "latestVersion", default: nil)
+    static let latestVersionChecked = UserDefaultsValue<Date?>(key: "latestVersionChecked", default: nil)
+
+    //Caching of blacklisted version
+    static let currentVersionBlackListed = UserDefaultsValue<Bool>(key: "currentVersionBlackListed", default: false)
 }

+ 38 - 7
Scripts/capture-build-details.sh

@@ -3,17 +3,48 @@
 #  capture-build-details.sh
 #  LoopFollow
 #
-#  Created by Jonas Björkert on 2024-03-25.
-#  Copyright © 2024 Jon Fawcett. All rights reserved.
+#  Created by Jonas Björkert on 2024-05-08.
 
-info_plist_path="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/BuildDetails.plist"
+# Enable debugging if needed
+#set -x
+
+# Define the base path for the BuildDetails.plist
+info_plist_base="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}"
+
+# Adjust the path for macOS Catalyst builds to include the Resources directory
+if [[ "$PLATFORM_NAME" == *"macosx"* ]]; then
+  info_plist_base+="/Resources"
+fi
+
+info_plist_path="${info_plist_base}/BuildDetails.plist"
 
 # Ensure the path to BuildDetails.plist is valid.
 if [ "${info_plist_path}" == "/" -o ! -e "${info_plist_path}" ]; then
-  echo "BuildDetails.plist file does not exist at path: ${info_plist_path}" >&2
+    echo "$PLATFORM_NAME"
+    echo "BuildDetails.plist file does not exist at path: ${info_plist_path}" >&2
+    exit 1
 else
-  echo "Gathering build date..."
+    echo "Gathering build details..."
+
+    # Capture the current date and write it to BuildDetails.plist
+    formatted_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+    plutil -replace com-LoopFollow-build-date -string "$formatted_date" "${info_plist_path}"
+
+    # Retrieve the current branch
+    git_branch=$(git symbolic-ref --short -q HEAD)
+
+    # Attempt to retrieve the current tag
+    git_tag=$(git describe --tags --exact-match 2>/dev/null || echo "")
+
+    # Retrieve the current SHA of the latest commit
+    git_commit_sha=$(git log -1 --format="%h" --abbrev=7)
+
+    # Determine the branch or tag information
+    git_branch_or_tag="${git_branch:-${git_tag}}"
+
+    # Update BuildDetails.plist with the branch or tag information
+    plutil -replace com-LoopFollow-branch -string "${git_branch_or_tag}" "${info_plist_path}"
 
-  # Capture the current date and write it to BuildDetails.plist
-  plutil -replace com-LoopFollow-build-date -string "$(date)" "${info_plist_path}"
+    # Update BuildDetails.plist with the SHA information
+    plutil -replace com-LoopFollow-commit-sha -string "${git_commit_sha}" "${info_plist_path}"
 fi

+ 10 - 0
blacklisted-versions.json

@@ -0,0 +1,10 @@
+{
+    "blacklistedVersions": [
+        {
+            "version": "1.0.0"
+        },
+        {
+            "version": "2.0.0"
+        }
+    ]
+}