// // UserAccountsManager.swift // UserAccounts // // Created by Shadowfacts on 3/5/23. // import Foundation import Combine public class UserAccountsManager: ObservableObject { public static let shared = UserAccountsManager() let defaults: UserDefaults private init() { if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING") { defaults = UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).uitesting")! defaults.removePersistentDomain(forName: "\(Bundle.main.bundleIdentifier!).uitesting") if ProcessInfo.processInfo.environment.keys.contains("UI_TESTING_LOGIN") { accounts = [ UserAccountInfo( instanceURL: URL(string: "http://localhost:8080")!, clientID: "client_id", clientSecret: "client_secret", username: "admin", accessToken: "access_token", scopes: [] ) ] } } else { defaults = UserDefaults(suiteName: "group.space.vaccor.Tusker")! } migrateAccountIDsIfNecessary() } private let accountsKey = "accounts" public private(set) var accounts: [UserAccountInfo] { get { if let array = defaults.array(forKey: accountsKey) as? [[String: Any]] { return array.compactMap(UserAccountInfo.init(userDefaultsDict:)) } else { return [] } } set { objectWillChange.send() let array = newValue.map(\.userDefaultsDict) defaults.set(array, forKey: accountsKey) } } private let mostRecentAccountKey = "mostRecentAccount" public private(set) var mostRecentAccountID: String? { get { return defaults.string(forKey: mostRecentAccountKey) } set { objectWillChange.send() defaults.set(newValue, forKey: mostRecentAccountKey) } } private let usesAccountIDHashesKey = "usesAccountIDHashes" private var usesAccountIDHashes: Bool { get { return defaults.bool(forKey: usesAccountIDHashesKey) } set { return defaults.set(newValue, forKey: usesAccountIDHashesKey) } } private func migrateAccountIDsIfNecessary() { if usesAccountIDHashes { return } if let mostRecentAccount = getMostRecentAccount() { let hashedMostRecentID = UserAccountInfo.id(instanceURL: mostRecentAccount.instanceURL, username: mostRecentAccount.username) mostRecentAccountID = hashedMostRecentID } if let array = defaults.array(forKey: accountsKey) as? [[String: String]] { accounts = array.compactMap { guard let urlString = $0["instanceURL"], let url = URL(string: urlString), let username = $0["username"] else { return nil } var dict = $0 dict["id"] = UserAccountInfo.id(instanceURL: url, username: username) return UserAccountInfo(userDefaultsDict: dict) } } usesAccountIDHashes = true } // MARK: Account Management public var onboardingComplete: Bool { return !accounts.isEmpty } public func addAccount(instanceURL url: URL, clientID: String, clientSecret secret: String, username: String?, accessToken: String, scopes: [String]) -> UserAccountInfo { var accounts = self.accounts if let index = accounts.firstIndex(where: { $0.instanceURL == url && $0.username == username }) { accounts.remove(at: index) } let info = UserAccountInfo(instanceURL: url, clientID: clientID, clientSecret: secret, username: username, accessToken: accessToken, scopes: scopes) accounts.append(info) self.accounts = accounts return info } public func removeAccount(_ info: UserAccountInfo) { accounts.removeAll(where: { $0.id == info.id }) } public func getAccount(id: String) -> UserAccountInfo? { return accounts.first(where: { $0.id == id }) } public func getMostRecentAccount() -> UserAccountInfo? { guard onboardingComplete else { return nil } let mostRecent: UserAccountInfo? if let id = mostRecentAccountID { mostRecent = accounts.first { $0.id == id } } else { mostRecent = nil } return mostRecent ?? accounts.first! } public func setMostRecentAccount(_ account: UserAccountInfo?) { mostRecentAccountID = account?.id } public func updateServerPreferences(_ account: UserAccountInfo, defaultLanguage: String?, defaultVisibility: String?, defaultFederation: Bool?) { guard let index = accounts.firstIndex(where: { $0.id == account.id }) else { return } var account = account account.serverDefaultLanguage = defaultLanguage account.serverDefaultVisibility = defaultVisibility account.serverDefaultFederation = defaultFederation accounts[index] = account } public func updateCredentials(_ account: UserAccountInfo, clientID: String, clientSecret: String, accessToken: String, scopes: [String]) { guard let index = accounts.firstIndex(where: { $0.id == account.id }) else { return } var account = account account.clientID = clientID account.clientSecret = clientSecret account.accessToken = accessToken account.scopes = scopes accounts[index] = account } } public extension Notification.Name { static let userLoggedOut = Notification.Name("Tusker.userLoggedOut") static let addAccount = Notification.Name("Tusker.addAccount") static let activateAccount = Notification.Name("Tusker.activateAccount") }