Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display branch, build date, latest version, blacklisted version #295

Merged
merged 2 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions LoopFollow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -644,6 +650,7 @@
FC97880B2485969B00A7906C = {
isa = PBXGroup;
children = (
DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */,
DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */,
DDB0AF4F2BB1A81F00AFA48B /* Scripts */,
DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */,
Expand Down Expand Up @@ -692,6 +699,8 @@
DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */,
DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */,
DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */,
DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */,
DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */,
);
path = helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
128 changes: 67 additions & 61 deletions LoopFollow/ViewControllers/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -320,30 +278,78 @@ 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
#else
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 {
Expand Down
97 changes: 97 additions & 0 deletions LoopFollow/helpers/AppVersionManager.swift
Original file line number Diff line number Diff line change
@@ -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 changes: 58 additions & 0 deletions LoopFollow/helpers/BuildDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 "TestFlight Expires"
} else {
return "App Expires"
}
}
}
11 changes: 11 additions & 0 deletions LoopFollow/helpers/DateTime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading