Jonas Björkert 1 год назад
Родитель
Сommit
9ad88d2abc
1 измененных файлов с 128 добавлено и 22 удалено
  1. 128 22
      Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift

+ 128 - 22
Trio/Sources/Services/AppVersionChecker/AppVersionChecker.swift

@@ -1,26 +1,42 @@
 import UIKit
 
+/// AppVersionChecker is a singleton responsible for checking the app's version status.
+/// It fetches version data from remote sources (GitHub), caches the results, and notifies the user
+/// if an update is available or if the current version is blacklisted.
 final class AppVersionChecker {
+    /// Shared singleton instance.
     static let shared = AppVersionChecker()
+
+    /// Private initializer to enforce the singleton pattern.
     private init() {}
 
     // MARK: - Persisted Properties
 
+    /// Cached app version for which data was last fetched.
     @Persisted(key: "cachedForVersion") private var cachedForVersion: String? = nil
+    /// The latest version fetched from GitHub.
     @Persisted(key: "latestVersion") private var persistedLatestVersion: String? = nil
+    /// The date when the latest version was checked.
     @Persisted(key: "latestVersionChecked") private var latestVersionChecked: Date? = .distantPast
+    /// Boolean flag indicating whether the current version is blacklisted.
     @Persisted(key: "currentVersionBlackListed") private var currentVersionBlackListed: Bool = false
+    /// Timestamp for the last time a blacklist notification was shown.
     @Persisted(key: "lastBlacklistNotificationShown") private var lastBlacklistNotificationShown: Date? = .distantPast
+    /// Timestamp for the last time a version update notification was shown.
     @Persisted(key: "lastVersionUpdateNotificationShown") private var lastVersionUpdateNotificationShown: Date? = .distantPast
+    /// Timestamp for the last time an expiration notification was shown.
     @Persisted(key: "lastExpirationNotificationShown") private var lastExpirationNotificationShown: Date? = .distantPast
 
     // MARK: - Nested Types
 
-    /// Types of data we fetch from GitHub for version checking.
+    /// GitHubDataType defines the type of data to fetch from GitHub for version checking.
     private enum GitHubDataType {
+        /// The configuration file containing version information.
         case versionConfig
+        /// The JSON file listing blacklisted versions.
         case blacklistedVersions
 
+        /// Returns the URL string associated with the data type.
         var url: String {
             switch self {
             case .versionConfig:
@@ -31,24 +47,35 @@ final class AppVersionChecker {
         }
     }
 
-    /// Structures for decoding GitHub JSON data.
+    /// Model for decoding the blacklist JSON from GitHub.
     private struct Blacklist: Decodable {
+        /// Array of blacklisted version entries.
         let blacklistedVersions: [VersionEntry]
     }
 
+    /// Model representing a single version entry in the blacklist.
     private struct VersionEntry: Decodable {
+        /// The version string that is blacklisted.
         let version: String
     }
 
     // MARK: - Public Methods
 
-    /// Checks for new or blacklisted versions and presents an alert if needed.
+    /**
+     Checks for a new or blacklisted version and presents an alert if necessary.
+
+     This method determines whether there is an update or if the current version is blacklisted.
+     Depending on the result, it displays an alert on the given view controller, ensuring that alerts
+     are not shown too frequently (24 hours for blacklist and 2 weeks for update notifications).
+
+     - Parameter viewController: The UIViewController on which to present any alerts.
+     */
     func checkAndNotifyVersionStatus(in viewController: UIViewController) {
         checkForNewVersion { [weak viewController] latestVersion, isNewer, isBlacklisted in
             guard let vc = viewController else { return }
             let now = Date()
 
-            // Check for critical (blacklisted) version.
+            // If the current version is blacklisted, show a critical update alert if not shown in the last 24 hours.
             if isBlacklisted {
                 let lastShown = self.lastBlacklistNotificationShown ?? .distantPast
                 if now.timeIntervalSince(lastShown) > 86400 { // 24 hours
@@ -61,7 +88,7 @@ final class AppVersionChecker {
                     self.lastVersionUpdateNotificationShown = now
                 }
             }
-            // Check for a new version available.
+            // Otherwise, if a newer version is available, show an update alert if not shown in the last 2 weeks.
             else if isNewer {
                 let lastShown = self.lastVersionUpdateNotificationShown ?? .distantPast
                 if now.timeIntervalSince(lastShown) > 1_209_600 { // 2 weeks
@@ -77,11 +104,24 @@ final class AppVersionChecker {
         }
     }
 
+    /**
+     Refreshes the version information and returns the current state.
+
+     This method triggers a version check (using cached values if valid or fetching fresh data)
+     and then returns the current app version along with the latest version info, a flag indicating
+     whether the latest version is newer, and a flag indicating if the current version is blacklisted.
+
+     - Parameter completion: A closure that receives the following parameters:
+     - currentVersion: The current app version.
+     - latestVersion: The latest version fetched from GitHub (if available).
+     - isNewer: `true` if the fetched version is newer than the current version.
+     - isBlacklisted: `true` if the current version is blacklisted.
+     */
     func refreshVersionInfo(completion: @escaping (
-        String /* currentVersion */,
-        String? /* latestVersion */,
-        Bool /* isNewer */,
-        Bool /* isBlacklisted */
+        String,    /* currentVersion */
+        String?,   /* latestVersion */
+        Bool,      /* isNewer */
+        Bool       /* isBlacklisted */
     ) -> Void) {
         let currentVersion = version()
         checkForNewVersion { latestVersion, isNewer, isBlacklisted in
@@ -91,7 +131,18 @@ final class AppVersionChecker {
 
     // MARK: - Core Version Checking Logic
 
-    /// Checks if there is a new or blacklisted version.
+    /**
+     Checks whether there is a new or blacklisted version.
+
+     This method attempts to use cached version data if it is less than 24 hours old and
+     corresponds to the current app version. If the cache is invalid or outdated,
+     it fetches fresh data from GitHub.
+
+     - Parameter completion: A closure that receives:
+     - latestVersion: The latest version string (if available).
+     - isNewer: `true` if the fetched version is newer than the current version.
+     - isBlacklisted: `true` if the current version is blacklisted.
+     */
     private func checkForNewVersion(completion: @escaping (String?, Bool, Bool) -> Void) {
         let currentVersion = version()
         let now = Date()
@@ -102,13 +153,13 @@ final class AppVersionChecker {
         let persistedLatest = persistedLatestVersion
         let isBlacklistedCached = currentVersionBlackListed
 
-        // Reset notifications if the current app version differs from the cached one.
+        // If the current app version has changed, reset notification timestamps.
         if let cachedVersion = cachedVersion, cachedVersion != currentVersion {
             lastBlacklistNotificationShown = .distantPast
             lastVersionUpdateNotificationShown = .distantPast
         }
 
-        // If cache is valid (<24 hours old) and for the current version, use it.
+        // Use cached data if it is valid (less than 24 hours old) and matches the current version.
         if let cachedVersion = cachedVersion,
            cachedVersion == currentVersion,
            now.timeIntervalSince(lastChecked) < 24 * 3600,
@@ -119,26 +170,39 @@ final class AppVersionChecker {
             return
         }
 
-        // Otherwise, fetch fresh data and update the cache.
+        // Otherwise, fetch fresh data from GitHub and update the cache.
         fetchDataAndUpdateCache(currentVersion: currentVersion, completion: completion)
     }
 
-    /// Fetches version and blacklist data from GitHub, updates persisted values, and then calls completion.
+    /**
+     Fetches version and blacklist data from GitHub, updates persisted values, and invokes the completion handler.
+
+     This method performs two sequential network requests: first for the version configuration and then for the
+     blacklisted versions. After parsing the fetched data and comparing version values, it updates the cache and calls
+     the completion handler with the results.
+
+     - Parameters:
+     - currentVersion: The current app version.
+     - completion: A closure that receives:
+     - latestVersion: The latest version string from GitHub (if available).
+     - isNewer: `true` if the fetched version is newer than the current version.
+     - isBlacklisted: `true` if the current version is blacklisted.
+     */
     private func fetchDataAndUpdateCache(currentVersion: String, completion: @escaping (String?, Bool, Bool) -> Void) {
         fetchData(for: .versionConfig) { versionData in
             self.fetchData(for: .blacklistedVersions) { blacklistData in
                 DispatchQueue.main.async {
-                    // Parse the version from the fetched config.
+                    // Parse the version from the fetched configuration data.
                     let fetchedVersion = versionData
                         .flatMap { String(data: $0, encoding: .utf8) }
                         .flatMap { self.parseVersionFromConfig(contents: $0) }
 
-                    // Compare versions.
+                    // Determine if the fetched version is newer than the current version.
                     let isNewer = fetchedVersion.map {
                         self.isVersion($0, newerThan: currentVersion)
                     } ?? false
 
-                    // Parse and determine if current version is blacklisted.
+                    // Determine if the current version is blacklisted.
                     let isBlacklisted = (try? blacklistData.flatMap {
                         try JSONDecoder().decode(Blacklist.self, from: $0)
                     })?.blacklistedVersions
@@ -159,7 +223,16 @@ final class AppVersionChecker {
 
     // MARK: - Data Fetching Helper
 
-    /// Fetches data from GitHub for a given data type.
+    /**
+     Fetches data from GitHub for a specified data type.
+
+     This helper method builds a URL from the provided GitHubDataType and executes a network request.
+     If the request is successful and returns valid data (HTTP status 200), the data is passed to the completion handler.
+
+     - Parameters:
+     - dataType: The type of GitHub data to fetch (version configuration or blacklisted versions).
+     - completion: A closure that receives the fetched data as an optional `Data` object.
+     */
     private func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) {
         guard let url = URL(string: dataType.url) else {
             completion(nil)
@@ -180,7 +253,15 @@ final class AppVersionChecker {
 
     // MARK: - Helpers
 
-    /// Parses the version string from a configuration file's content.
+    /**
+     Parses the version string from the contents of a configuration file.
+
+     The method scans each line of the provided content for an occurrence of "APP_VERSION" and then
+     extracts the version number following the "=" delimiter.
+
+     - Parameter contents: A string containing the contents of the configuration file.
+     - Returns: The extracted version string if found; otherwise, `nil`.
+     */
     private func parseVersionFromConfig(contents: String) -> String? {
         let lines = contents.split(separator: "\n")
         for line in lines {
@@ -196,7 +277,18 @@ final class AppVersionChecker {
         return nil
     }
 
-    /// Compares two version strings to determine if the fetched version is newer.
+    /**
+     Compares two version strings to determine if the fetched version is newer than the current version.
+
+     The version strings are split into numeric components and compared sequentially.
+     If any component of the fetched version is greater than its counterpart in the current version,
+     the function returns `true`; if lower, it returns `false`.
+
+     - Parameters:
+     - fetchedVersion: The version string obtained from GitHub.
+     - currentVersion: The current app version.
+     - Returns: `true` if the fetched version is newer than the current version; otherwise, `false`.
+     */
     private func isVersion(_ fetchedVersion: String, newerThan currentVersion: String) -> Bool {
         let fetchedComponents = fetchedVersion.split(separator: ".").map { Int($0) ?? 0 }
         let currentComponents = currentVersion.split(separator: ".").map { Int($0) ?? 0 }
@@ -214,12 +306,26 @@ final class AppVersionChecker {
         return false
     }
 
-    /// Returns the current app version.
+    /**
+     Retrieves the current app version from the main bundle.
+
+     - Returns: The current app version as defined in the app's Info.plist under "CFBundleShortVersionString",
+     or `"Unknown"` if not available.
+     */
     private func version() -> String {
         Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
     }
 
-    /// Presents an alert on the provided view controller.
+    /**
+     Presents an alert on the specified view controller with a given title and message.
+
+     The alert is dispatched to the main thread to ensure UI updates occur correctly.
+
+     - Parameters:
+     - viewController: The UIViewController on which the alert should be presented.
+     - title: The title text for the alert.
+     - message: The body message of the alert.
+     */
     private func showAlert(on viewController: UIViewController, title: String, message: String) {
         DispatchQueue.main.async {
             let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)