From 92c9efbf8c8bc0db12e39d55b3e5aec835a9eeca Mon Sep 17 00:00:00 2001
From: Gregor Longariva <gregor.longariva@fau.de>
Date: Mon, 24 Mar 2025 15:36:41 +0100
Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20improved=20ShareManager=20and=20?=
 =?UTF-8?q?AccountsManager?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../managers/AccountsManager.swift            |  18 +-
 .../managers/ShareManager.swift               | 326 ++++++++++--------
 networkShareMounter.xcodeproj/project.pbxproj |   8 +-
 3 files changed, 197 insertions(+), 155 deletions(-)

diff --git a/Network Share Mounter/managers/AccountsManager.swift b/Network Share Mounter/managers/AccountsManager.swift
index 53b7692..3db0753 100644
--- a/Network Share Mounter/managers/AccountsManager.swift	
+++ b/Network Share Mounter/managers/AccountsManager.swift	
@@ -33,23 +33,23 @@ actor AccountsManager {
         guard !isInitialized else { return }
         
         // Perform FAU-specific tasks if the Kerberos realm matches.
-        if await prefs.string(for: .kerberosRealm)?.lowercased() == FAU.kerberosRealm.lowercased() {
-            if await !prefs.bool(for: .keyChainPrefixManagerMigration) {
+        if prefs.string(for: .kerberosRealm)?.lowercased() == FAU.kerberosRealm.lowercased() {
+            if !prefs.bool(for: .keyChainPrefixManagerMigration) {
                 let migrator = Migrator()
                 await migrator.migrate()
             }
         }
         
         // Load accounts from persistent storage.
-        await loadAccounts()
+        loadAccounts()
         
         isInitialized = true
     }
     
     /// Loads accounts from UserDefaults and updates the internal accounts list.
-    private func loadAccounts() async {
+    private func loadAccounts() {
         let decoder = PropertyListDecoder()
-        if let accountsData = await prefs.data(for: .accounts),
+        if let accountsData = prefs.data(for: .accounts),
            let accountsList = try? decoder.decode(DogeAccounts.self, from: accountsData) {
             var uniqueAccounts = [DogeAccount]()
             var processedUPNs = Set<String>()
@@ -70,10 +70,10 @@ actor AccountsManager {
     }
     
     /// Saves the current accounts list to UserDefaults.
-    func saveAccounts() async {
+    func saveAccounts() {
         let encoder = PropertyListEncoder()
         if let accountData = try? encoder.encode(DogeAccounts(accounts: accounts)) {
-            await prefs.set(for: .accounts, value: accountData)
+            prefs.set(for: .accounts, value: accountData)
         }
         
         // Notify delegates about the updated accounts list.
@@ -83,13 +83,13 @@ actor AccountsManager {
     /// Adds a new account and updates the persistent storage.
     func addAccount(account: DogeAccount) async {
         accounts.append(account)
-        await saveAccounts()
+        saveAccounts()
     }
     
     /// Deletes an existing account and updates the persistent storage.
     func deleteAccount(account: DogeAccount) async {
         accounts.removeAll { $0 == account }
-        await saveAccounts()
+        saveAccounts()
     }
     
     /// Retrieves an account for a given principal.
diff --git a/Network Share Mounter/managers/ShareManager.swift b/Network Share Mounter/managers/ShareManager.swift
index 5e6e3c3..993beeb 100644
--- a/Network Share Mounter/managers/ShareManager.swift	
+++ b/Network Share Mounter/managers/ShareManager.swift	
@@ -11,6 +11,7 @@ import OSLog
 
 enum ShareError: Error {
     case invalidIndex(Int)
+    case invalidURL(String)
 }
 
 /// class `ShareManager` to manage the shares (array fo Share)
@@ -25,34 +26,59 @@ actor ShareManager {
             _shares.append(share)
             //
             // save password in keychain
-            if let password = share.password {
-                if let username = share.username {
-                    let pwm = KeychainManager()
-                    do {
-                        try pwm.saveCredential(forShare: URL(string: share.networkShare)!, withUsername: username, andPassword: password)
-                    } catch {
-                        Logger.shareManager.error("🛑 Cannot store password for share \(share.networkShare, privacy: .public) in user's keychain")
-                    }
-                }
+            if let password = share.password, let username = share.username {
+                savePasswordToKeychain(for: share.networkShare, username: username, password: password)
             }
         }
     }
     
+    /// Saves a password to the keychain for a share
+    /// - Parameters:
+    ///   - shareURL: The network share URL string
+    ///   - username: The username for authentication
+    ///   - password: The password to save
+    private func savePasswordToKeychain(for shareURL: String, username: String, password: String) {
+        let pwm = KeychainManager()
+        guard let url = URL(string: shareURL) else {
+            Logger.shareManager.error("🛑 Cannot create URL from share path: \(shareURL, privacy: .public)")
+            return
+        }
+        
+        do {
+            try pwm.saveCredential(forShare: url, withUsername: username, andPassword: password)
+        } catch {
+            Logger.shareManager.error("🛑 Cannot store password for share \(shareURL, privacy: .public) in user's keychain: \(error.localizedDescription)")
+        }
+    }
+    
     /// Remove a share at a specific index
     func removeShare(at index: Int) {
         // remove keychain entry for share
         if let username = _shares[index].username {
-            let pwm = KeychainManager()
-            do {
-                Logger.shareManager.debug("trying to remove keychain entry for \(self._shares[index].networkShare, privacy: .public) with username: \(username, privacy: .public)")
-                try pwm.removeCredential(forShare: URL(string: self._shares[index].networkShare)!, withUsername: username)
-            } catch {
-                Logger.shareManager.error("🛑 Cannot remove keychain entry for share \(self._shares[index].networkShare, privacy: .public)")
-            }
+            removePasswordFromKeychain(for: _shares[index].networkShare, username: username)
         }
         _shares.remove(at: index)
     }
     
+    /// Removes a password from the keychain for a share
+    /// - Parameters:
+    ///   - shareURL: The network share URL string
+    ///   - username: The username for authentication
+    private func removePasswordFromKeychain(for shareURL: String, username: String) {
+        let pwm = KeychainManager()
+        guard let url = URL(string: shareURL) else {
+            Logger.shareManager.error("🛑 Cannot create URL from share path: \(shareURL, privacy: .public)")
+            return
+        }
+        
+        do {
+            Logger.shareManager.debug("Trying to remove keychain entry for \(shareURL, privacy: .public) with username: \(username, privacy: .public)")
+            try pwm.removeCredential(forShare: url, withUsername: username)
+        } catch {
+            Logger.shareManager.error("🛑 Cannot remove keychain entry for share \(shareURL, privacy: .public): \(error.localizedDescription)")
+        }
+    }
+    
     /// Get all shares
     var allShares: [Share] {
         return _shares
@@ -73,22 +99,12 @@ actor ShareManager {
         //
         // remove existing keychain entry first since it wouldn't be found with the new data
         if let username = _shares[index].username {
-            let pwm = KeychainManager()
-            do {
-                try pwm.removeCredential(forShare: URL(string: _shares[index].networkShare)!, withUsername: username)
-            } catch {
-            }
+            removePasswordFromKeychain(for: _shares[index].networkShare, username: username)
         }
         //
         // save password in keychain
-        if let password = updatedShare.password {
-            if let username = updatedShare.username {
-                let pwm = KeychainManager()
-                do {
-                    try pwm.saveCredential(forShare: URL(string: updatedShare.networkShare)!, withUsername: username, andPassword: password)
-                } catch {
-                }
-            }
+        if let password = updatedShare.password, let username = updatedShare.username {
+            savePasswordToKeychain(for: updatedShare.networkShare, username: username, password: password)
         }
         _shares[index] = updatedShare
     }
@@ -155,9 +171,6 @@ actor ShareManager {
         // Replace username placeholder in share URL
         let shareRectified = shareUrlString.replacingOccurrences(of: "%USERNAME%", with: userName)
         
-        // Expanding tilde in shares does not make sense
-//        let shareRectified = NSString(string: expandenShare).expandingTildeInPath
-        
         // Configure authentication type, defaulting to Kerberos if not specified
         let shareAuthType = AuthType(rawValue: shareElement[Defaults.authType] ?? AuthType.krb.rawValue) ?? AuthType.krb
         var password: String?
@@ -165,13 +178,18 @@ actor ShareManager {
         
         // For password authentication, attempt to retrieve from keychain
         if shareAuthType == AuthType.pwd {
+            guard let url = URL(string: shareRectified) else {
+                Logger.shareManager.error("🛑 Invalid share URL: \(shareRectified, privacy: .public)")
+                return nil
+            }
+            
             do {
-                if let keychainPassword = try KeychainManager().retrievePassword(forShare: URL(string: shareRectified)!, withUsername: userName) {
+                if let keychainPassword = try KeychainManager().retrievePassword(forShare: url, withUsername: userName) {
                     password = keychainPassword
                 }
             } catch {
                 // Log warning if password retrieval fails and update mount status
-                Logger.shareManager.warning("Password for share \(shareRectified, privacy: .public) not found in user's keychain")
+                Logger.shareManager.warning("Password for share \(shareRectified, privacy: .public) not found in user's keychain: \(error.localizedDescription)")
                 mountStatus = MountStatus.missingPassword
                 password = nil
             }
@@ -209,119 +227,127 @@ actor ShareManager {
         }
         var password: String?
         var mountStatus = MountStatus.unmounted
+        
         if let username = shareElement[Defaults.username] {
+            guard let url = URL(string: shareUrlString) else {
+                Logger.shareManager.error("🛑 Invalid share URL: \(shareUrlString, privacy: .public)")
+                return nil
+            }
+            
             do {
-                if let keychainPassword = try KeychainManager().retrievePassword(forShare: URL(string: shareUrlString)!, withUsername: username) {
+                if let keychainPassword = try KeychainManager().retrievePassword(forShare: url, withUsername: username) {
                     password = keychainPassword
                 }
             } catch {
-                Logger.shareManager.warning("Password for share \(shareUrlString, privacy: .public) not found in user's keychain")
+                Logger.shareManager.warning("Password for share \(shareUrlString, privacy: .public) not found in user's keychain: \(error.localizedDescription)")
                 mountStatus = MountStatus.missingPassword
                 password = nil
             }
         }
+        
         let shareAuthType = AuthType(rawValue: shareElement[Defaults.authType] ?? AuthType.krb.rawValue) ?? AuthType.krb
         let newShare = Share.createShare(networkShare: shareUrlString, authType: shareAuthType, mountStatus: mountStatus, username: shareElement[Defaults.username], password: password, managed: false)
         return(newShare)
     }
     
     func updateShareArray() {
-        // read MDM shares
-        var usedNewMDMprofile = false
+        // Check for and process MDM shares first
+        let processingResult = processMDMShares()
+        
+        // If no MDM shares were processed, try legacy MDM shares
+        if !processingResult.usedNewMDMProfile {
+            processingLegacyShares()
+        }
+    }
+    
+    /// Processes MDM shares and updates the share array
+    /// - Returns: A tuple indicating whether MDM profile was used and the processed shares
+    private func processMDMShares() -> (usedNewMDMProfile: Bool, newShares: [Share]) {
         Logger.shareManager.debug("📜 Checking possible changes in MDM profile")
+        var usedNewMDMProfile = false
+        var newShares: [Share] = []
+        
+        // Process new MDM profile format if available
         if let sharesDict = userDefaults.array(forKey: Defaults.managedNetworkSharesKey) as? [[String: String]], !sharesDict.isEmpty {
-            var newShares: [Share] = []
+            usedNewMDMProfile = true
+            
             for shareElement in sharesDict {
-                if var newShare = self.getMDMShareConfig(forShare: shareElement) {
-                    usedNewMDMprofile = true
-                    // check if share exists and if not, add it to array of shares
-                    // addShare() would check if an element exists and skips it,
-                    // but the new share definition could differ from the new one get from MDM
-                    if !allShares.contains(where: { $0.networkShare == newShare.networkShare }) {
-                        Logger.shareManager.debug(" ▶︎ Reading MDM profile: adding new share \(newShare.networkShare, privacy: .public)")
-                        addShare(newShare)
-                    } else {
-                        if let index = allShares.firstIndex(where: { $0.networkShare == newShare.networkShare }) {
-                            // save some stati from actual share element and save them to new share
-                            // read from MDM. Then overwrite the share with the new data
-                            Logger.shareManager.debug(" ▶︎ Reading MDM profile: updating status for existing share \(newShare.networkShare, privacy: .public).")
-                            newShare.mountStatus = allShares[index].mountStatus
-                            newShare.id = allShares[index].id
-                            newShare.actualMountPoint  = allShares[index].actualMountPoint
-                            do {
-                                try updateShare(at: index, withUpdatedShare: newShare)
-                            } catch ShareError.invalidIndex(let index) {
-                                Logger.shareManager.error(" ▶︎ Reading MDM profile: could not update share \(newShare.networkShare, privacy: .public), index \(index, privacy: .public) is not valid.")
-                            } catch {
-                                Logger.shareManager.error(" ▶︎ Reading MDM profile: could not update share \(newShare.networkShare, privacy: .public), unknown error.")
-                            }
-                        }
-                    }
-                    newShares.append(newShare)
+                if let newShare = self.getMDMShareConfig(forShare: shareElement) {
+                    processShareForUpdate(newShare: newShare, isManaged: true, into: &newShares)
                 }
             }
-            // get the difference between _shares and the new share read from MDM
-            let differing = _shares.filter { _shares in
-                !newShares.contains { newShares in
-                    _shares.networkShare == newShares.networkShare
+            
+            // Remove managed shares that are no longer in the MDM profile
+            removeOrphanedManagedShares(newShares: newShares)
+        }
+        
+        return (usedNewMDMProfile, newShares)
+    }
+    
+    /// Processes legacy shares when no MDM shares are present
+    private func processingLegacyShares() {
+        var newShares: [Share] = []
+        
+        if let nwShares: [String] = userDefaults.array(forKey: Defaults.networkSharesKey) as? [String], !nwShares.isEmpty {
+            for share in nwShares {
+                if let newShare = self.getLegacyShareConfig(forShare: share) {
+                    processShareForUpdate(newShare: newShare, isManaged: true, into: &newShares)
                 }
             }
-            // remove found shares
-            for remove in differing {
-                if let index = allShares.firstIndex(where: { $0.networkShare == remove.networkShare }) {
-                    if _shares[index].managed == true {
-                        Logger.shareManager.debug(" ▶︎ Reading MDM profile: deleting share \(remove.networkShare, privacy: .public) at Index \(index, privacy: .public)")
-                        removeShare(at: index)
-                    }
+            
+            // Remove managed shares that are no longer in the legacy configuration
+            removeOrphanedManagedShares(newShares: newShares)
+        }
+    }
+    
+    /// Processes a share for update, adding it if new or updating existing one
+    /// - Parameters:
+    ///   - newShare: The share to process
+    ///   - isManaged: Whether the share is managed by MDM
+    ///   - sharesArray: The array to update with processed shares
+    private func processShareForUpdate(newShare: Share, isManaged: Bool, into sharesArray: inout [Share]) {
+        // Check if share exists
+        if !allShares.contains(where: { $0.networkShare == newShare.networkShare }) {
+            Logger.shareManager.debug(" ▶︎ Adding new share \(newShare.networkShare, privacy: .public)")
+            addShare(newShare)
+        } else {
+            if let index = allShares.firstIndex(where: { $0.networkShare == newShare.networkShare }) {
+                // Update existing share with new configuration while preserving state
+                var updatedShare = newShare
+                updatedShare.mountStatus = allShares[index].mountStatus
+                updatedShare.id = allShares[index].id
+                updatedShare.actualMountPoint = allShares[index].actualMountPoint
+                
+                do {
+                    try updateShare(at: index, withUpdatedShare: updatedShare)
+                    Logger.shareManager.debug(" ▶︎ Updated existing share \(newShare.networkShare, privacy: .public)")
+                } catch ShareError.invalidIndex(let index) {
+                    Logger.shareManager.error(" ▶︎ Could not update share \(newShare.networkShare, privacy: .public), index \(index, privacy: .public) is not valid.")
+                } catch {
+                    Logger.shareManager.error(" ▶︎ Could not update share \(newShare.networkShare, privacy: .public), error: \(error.localizedDescription)")
                 }
             }
         }
-        if !usedNewMDMprofile {
-            // the same as above with the legacy MDM profiles
-            if let nwShares: [String] = userDefaults.array(forKey: Defaults.networkSharesKey) as? [String], !nwShares.isEmpty {
-                var newShares: [Share] = []
-                for share in nwShares {
-                    if var newShare = self.getLegacyShareConfig(forShare: share) {
-                        usedNewMDMprofile = true
-                        // check if share exists and if not, add it to array of shares
-                        // addShare() would check if an element exists and skips it,
-                        // but the new share definition could differ from the new one get from MDM
-                        if !allShares.contains(where: { $0.networkShare == newShare.networkShare }) {
-                            addShare(newShare)
-                            newShares.append(newShare)
-                        } else {
-                            if let index = allShares.firstIndex(where: { $0.networkShare == newShare.networkShare }) {
-                                // save some stati from actual share element and save them to new share
-                                // read from MDM. Then overwrite the share with the new data
-                                newShare.mountStatus = allShares[index].mountStatus
-                                newShare.id = allShares[index].id
-                                newShare.actualMountPoint  = allShares[index].actualMountPoint
-                                do {
-                                    try updateShare(at: index, withUpdatedShare: newShare)
-                                } catch ShareError.invalidIndex(let index) {
-                                    Logger.shareManager.error(" ▶︎ Reading MDM profile: could not update share \(newShare.networkShare, privacy: .public), index \(index, privacy: .public) is not valid.")
-                                } catch {
-                                    Logger.shareManager.error(" ▶︎ Reading MDM profile: could not update share \(newShare.networkShare, privacy: .public), unknown error.")
-                                }
-                                newShares.append(newShare)
-                            }
-                        }
-                    }
-                }
-                // get the difference between _shares and the new share read from MDM
-                let differing = _shares.filter { _shares in
-                    !newShares.contains { newShares in
-                        _shares.networkShare == newShares.networkShare
-                    }
-                }
-                // remove found shares
-                for remove in differing {
-                    if let index = allShares.firstIndex(where: { $0.networkShare == remove.networkShare }) {
-                        if _shares[index].managed == true {
-                            Logger.shareManager.info(" ▶︎ Reading MDM profile: deleting share: \(remove.networkShare, privacy: .public) at Index \(index, privacy: .public)")
-                            removeShare(at: index)
-                        }
-                    }
+        
+        sharesArray.append(newShare)
+    }
+    
+    /// Removes managed shares that are no longer present in configuration
+    /// - Parameter newShares: The current list of shares from configuration
+    private func removeOrphanedManagedShares(newShares: [Share]) {
+        // Find shares that are in _shares but not in newShares
+        let differing = _shares.filter { share in
+            !newShares.contains { newShare in
+                share.networkShare == newShare.networkShare
+            }
+        }
+        
+        // Remove orphaned managed shares
+        for orphanedShare in differing {
+            if let index = allShares.firstIndex(where: { $0.networkShare == orphanedShare.networkShare }) {
+                if _shares[index].managed == true {
+                    Logger.shareManager.debug(" ▶︎ Deleting share \(orphanedShare.networkShare, privacy: .public) at index \(index, privacy: .public)")
+                    self.removeShare(at: index)
                 }
             }
         }
@@ -331,31 +357,49 @@ actor ShareManager {
     /// import configured shares from userDefaults for both mdm defined (legacy)`Settings.networkSharesKey`
     /// or `Settings.mdmNetworkSahresKey` and user defined `Settings.customSharesKey`.
     func createShareArray() {
-        /// **Important**:
-        /// - read only `Settings.mdmNetworkSahresKey` *OR* `Settings.networkSharesKey`, NOT both arrays
-        /// - then read user defined `Settings.customSharesKey`
-        ///
-        var usedNewMDMprofile = false
+        // Process MDM shares first
+        let processingResult = processInitialMDMShares()
+        
+        // If no MDM shares were found, try legacy MDM configuration
+        if !processingResult {
+            processInitialLegacyMDMShares()
+        }
+        
+        // Process user-defined shares
+        processUserDefinedShares()
+    }
+    
+    /// Processes initial MDM shares during application startup
+    /// - Returns: Whether MDM shares were processed
+    private func processInitialMDMShares() -> Bool {
+        var usedNewMDMProfile = false
+        
         if let sharesDict = userDefaults.array(forKey: Defaults.managedNetworkSharesKey) as? [[String: String]], !sharesDict.isEmpty {
             for shareElement in sharesDict {
                 if let newShare = self.getMDMShareConfig(forShare: shareElement) {
-                    usedNewMDMprofile = true
+                    usedNewMDMProfile = true
                     addShare(newShare)
                 }
             }
         }
-        /// alternatively try to get configured shares with now obsolete
-        /// Network Share Mounter 2 definitions
-        if !usedNewMDMprofile {
-            if let nwShares: [String] = userDefaults.array(forKey: Defaults.networkSharesKey) as? [String], !nwShares.isEmpty {
-                for share in nwShares {
-                    if let newShare = self.getLegacyShareConfig(forShare: share) {
-                        addShare(newShare)
-                    }
+        
+        return usedNewMDMProfile
+    }
+    
+    /// Processes legacy MDM shares during application startup
+    private func processInitialLegacyMDMShares() {
+        if let nwShares: [String] = userDefaults.array(forKey: Defaults.networkSharesKey) as? [String], !nwShares.isEmpty {
+            for share in nwShares {
+                if let newShare = self.getLegacyShareConfig(forShare: share) {
+                    addShare(newShare)
                 }
             }
         }
-        /// next look if there are some user-defined shares to import
+    }
+    
+    /// Processes user-defined shares during application startup
+    private func processUserDefinedShares() {
+        // Try new user-defined share format first
         if let privSharesDict = userDefaults.array(forKey: Defaults.userNetworkShares) as? [[String: String]], !privSharesDict.isEmpty {
             for share in privSharesDict {
                 if let newShare = self.getUserShareConfigs(forShare: share) {
@@ -363,7 +407,7 @@ actor ShareManager {
                 }
             }
         }
-        /// at last there may be legacy user defined share definitions
+        // Fall back to legacy user-defined share format
         else if let nwShares: [String] = userDefaults.array(forKey: Defaults.customSharesKey) as? [String], !nwShares.isEmpty {
             for share in nwShares {
                 addShare(Share.createShare(networkShare: share, authType: AuthType.krb, mountStatus: MountStatus.unmounted, managed: false))
@@ -388,7 +432,6 @@ actor ShareManager {
     func saveModifiedShareConfigs() {
         var userDefaultsConfigs: [[String: String]] = []
         
-        
         for share in _shares {
             //
             // save non-managed shares in userconfig
@@ -410,12 +453,11 @@ actor ShareManager {
             }
         }
         userDefaults.set(userDefaultsConfigs, forKey: Defaults.userNetworkShares)
-        userDefaults.synchronize()
+        // synchronize() is deprecated and unnecessary
     }
     
     private func removeLegacyShareConfigs() {
         userDefaults.removeObject(forKey: Defaults.customSharesKey)
-        userDefaults.synchronize()
+        // synchronize() is deprecated and unnecessary
     }
-
 }
diff --git a/networkShareMounter.xcodeproj/project.pbxproj b/networkShareMounter.xcodeproj/project.pbxproj
index ec3f74d..6e322e8 100644
--- a/networkShareMounter.xcodeproj/project.pbxproj
+++ b/networkShareMounter.xcodeproj/project.pbxproj
@@ -647,7 +647,7 @@
 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 201;
+				CURRENT_PROJECT_VERSION = 202;
 				DEVELOPMENT_TEAM = C8F68RFW4L;
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -677,7 +677,7 @@
 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 201;
+				CURRENT_PROJECT_VERSION = 202;
 				DEVELOPMENT_TEAM = C8F68RFW4L;
 				ENABLE_USER_SCRIPT_SANDBOXING = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -711,7 +711,7 @@
 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
 				CODE_SIGN_STYLE = Manual;
 				COMBINE_HIDPI_IMAGES = YES;
-				CURRENT_PROJECT_VERSION = 201;
+				CURRENT_PROJECT_VERSION = 202;
 				DEAD_CODE_STRIPPING = YES;
 				DEVELOPMENT_TEAM = "";
 				"DEVELOPMENT_TEAM[sdk=macosx*]" = C8F68RFW4L;
@@ -761,7 +761,7 @@
 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer";
 				CODE_SIGN_STYLE = Manual;
 				COMBINE_HIDPI_IMAGES = YES;
-				CURRENT_PROJECT_VERSION = 201;
+				CURRENT_PROJECT_VERSION = 202;
 				DEAD_CODE_STRIPPING = YES;
 				DEVELOPMENT_TEAM = "";
 				"DEVELOPMENT_TEAM[sdk=macosx*]" = C8F68RFW4L;
-- 
GitLab