import Foundation import Security struct AuthCredentials { let userId: Int let token: String let userName: String? let photoUrl: String? } actor AuthStorage { static let shared = AuthStorage() private let userIdKey = "payfrit_beacon_user_id" private let userNameKey = "payfrit_beacon_user_name" private let userPhotoKey = "payfrit_beacon_user_photo" private let serviceName = "com.payfrit.beacon" private let tokenAccount = "auth_token" // MARK: - Save func saveAuth(userId: Int, token: String, userName: String? = nil, photoUrl: String? = nil) { UserDefaults.standard.set(userId, forKey: userIdKey) // Always overwrite name/photo to prevent stale data from previous user if let name = userName, !name.isEmpty { UserDefaults.standard.set(name, forKey: userNameKey) } else { UserDefaults.standard.removeObject(forKey: userNameKey) } if let photo = photoUrl, !photo.isEmpty { UserDefaults.standard.set(photo, forKey: userPhotoKey) } else { UserDefaults.standard.removeObject(forKey: userPhotoKey) } saveToKeychain(token) } // MARK: - Load func loadAuth() -> AuthCredentials? { let userId = UserDefaults.standard.integer(forKey: userIdKey) guard userId > 0 else { return nil } guard let token = loadFromKeychain(), !token.isEmpty else { return nil } let userName = UserDefaults.standard.string(forKey: userNameKey) let photoUrl = UserDefaults.standard.string(forKey: userPhotoKey) return AuthCredentials(userId: userId, token: token, userName: userName, photoUrl: photoUrl) } // MARK: - Clear func clearAuth() { UserDefaults.standard.removeObject(forKey: userIdKey) UserDefaults.standard.removeObject(forKey: userNameKey) UserDefaults.standard.removeObject(forKey: userPhotoKey) deleteFromKeychain() } // MARK: - Keychain private func saveToKeychain(_ token: String) { deleteFromKeychain() guard let data = token.data(using: .utf8) else { return } let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, kSecAttrAccount as String: tokenAccount, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] SecItemAdd(query as CFDictionary, nil) } private func loadFromKeychain() -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, kSecAttrAccount as String: tokenAccount, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let data = result as? Data else { return nil } return String(data: data, encoding: .utf8) } private func deleteFromKeychain() { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: serviceName, kSecAttrAccount as String: tokenAccount ] SecItemDelete(query as CFDictionary) } }