Skip to content

Commit

Permalink
Merge pull request #295 from loopandlearn/show-branch
Browse files Browse the repository at this point in the history
Display branch, build date, latest version, blacklisted version
  • Loading branch information
marionbarker committed May 11, 2024
2 parents cbe8bfb + c982f46 commit 873f1bd
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 78 deletions.
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

0 comments on commit 873f1bd

Please sign in to comment.