Aggiunge app iOS nativa con parità multi-sport e test unitari.

Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Emiliano Frascaro
2026-06-14 21:37:32 +02:00
co-authored by Cursor
parent b798e0ea06
commit 585332e32e
101 changed files with 7910 additions and 5 deletions
@@ -0,0 +1,111 @@
import Foundation
import Security
struct StoredSession: Equatable, Sendable {
let user: User
let accessToken: String
let refreshToken: String
}
@MainActor
final class TokenStore: ObservableObject {
@Published private(set) var session: StoredSession?
@Published private(set) var activeTeamId: String?
private let defaults = UserDefaults.standard
private let service = "com.matchlivetv.match-live-tv.auth"
init() {
session = loadSession()
activeTeamId = defaults.string(forKey: Keys.activeTeamId)
}
func saveSession(_ session: StoredSession) {
self.session = session
saveKeychain(key: Keys.accessToken, value: session.accessToken)
saveKeychain(key: Keys.refreshToken, value: session.refreshToken)
defaults.set(session.user.id, forKey: Keys.userId)
defaults.set(session.user.email, forKey: Keys.userEmail)
defaults.set(session.user.name, forKey: Keys.userName)
defaults.set(session.user.role, forKey: Keys.userRole)
}
func saveActiveTeamId(_ teamId: String) {
activeTeamId = teamId
defaults.set(teamId, forKey: Keys.activeTeamId)
}
func clear() {
session = nil
activeTeamId = nil
deleteKeychain(key: Keys.accessToken)
deleteKeychain(key: Keys.refreshToken)
defaults.removeObject(forKey: Keys.userId)
defaults.removeObject(forKey: Keys.userEmail)
defaults.removeObject(forKey: Keys.userName)
defaults.removeObject(forKey: Keys.userRole)
defaults.removeObject(forKey: Keys.activeTeamId)
}
private func loadSession() -> StoredSession? {
guard let access = loadKeychain(key: Keys.accessToken),
let refresh = loadKeychain(key: Keys.refreshToken),
let id = defaults.string(forKey: Keys.userId) else { return nil }
return StoredSession(
user: User(
id: id,
email: defaults.string(forKey: Keys.userEmail) ?? "",
name: defaults.string(forKey: Keys.userName) ?? "",
role: defaults.string(forKey: Keys.userRole) ?? "coach"
),
accessToken: access,
refreshToken: refresh
)
}
private enum Keys {
static let accessToken = "access_token"
static let refreshToken = "refresh_token"
static let userId = "user_id"
static let userEmail = "user_email"
static let userName = "user_name"
static let userRole = "user_role"
static let activeTeamId = "active_team_id"
}
private func saveKeychain(key: String, value: String) {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
var add = query
add[kSecValueData as String] = data
SecItemAdd(add as CFDictionary, nil)
}
private func loadKeychain(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
private func deleteKeychain(key: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
]
SecItemDelete(query as CFDictionary)
}
}