Initial implementation of Fantasy Hockey watchOS app
Implemented complete TCA architecture for iOS and watchOS targets: - Authentication flow (Sign in with Apple + Yahoo OAuth) - OAuth token management with iCloud Key-Value Storage - Yahoo Fantasy Sports API client with async/await - Watch Connectivity for iPhone ↔ Watch data sync - Complete UI for both iPhone and Watch platforms Core features: - Matchup score display - Category breakdown with win/loss/tie indicators - Roster status tracking - Manual refresh functionality - Persistent data caching on Watch Technical stack: - The Composable Architecture for state management - Swift Concurrency (async/await, actors) - WatchConnectivity framework - Sign in with Apple - OAuth 2.0 authentication flow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
110
FantasyWatch/Shared/Networking/OAuth/OAuthManager.swift
Normal file
110
FantasyWatch/Shared/Networking/OAuth/OAuthManager.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// OAuthManager.swift
|
||||
// FantasyWatch
|
||||
//
|
||||
// Created by Claude Code
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
actor OAuthManager {
|
||||
private let tokenStorage: OAuthTokenStorage
|
||||
private let clientID: String
|
||||
private let clientSecret: String
|
||||
private var refreshTask: Task<TokenPair, Error>?
|
||||
private var currentUserID: String?
|
||||
|
||||
init(
|
||||
tokenStorage: OAuthTokenStorage,
|
||||
clientID: String,
|
||||
clientSecret: String
|
||||
) {
|
||||
self.tokenStorage = tokenStorage
|
||||
self.clientID = clientID
|
||||
self.clientSecret = clientSecret
|
||||
}
|
||||
|
||||
func setCurrentUser(_ userID: String) {
|
||||
self.currentUserID = userID
|
||||
}
|
||||
|
||||
func validToken() async throws -> String {
|
||||
guard let userID = currentUserID else {
|
||||
throw OAuthError.authorizationFailed
|
||||
}
|
||||
|
||||
if await tokenStorage.isTokenValid(for: userID) {
|
||||
guard let token = await tokenStorage.getAccessToken(for: userID) else {
|
||||
throw OAuthError.tokenExpired
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
let refreshedTokenPair = try await refreshToken()
|
||||
return refreshedTokenPair.accessToken
|
||||
}
|
||||
|
||||
func refreshToken() async throws -> TokenPair {
|
||||
if let existingTask = refreshTask {
|
||||
return try await existingTask.value
|
||||
}
|
||||
|
||||
guard let userID = currentUserID else {
|
||||
throw OAuthError.authorizationFailed
|
||||
}
|
||||
|
||||
guard let refreshToken = await tokenStorage.getRefreshToken(for: userID) else {
|
||||
throw OAuthError.noRefreshToken
|
||||
}
|
||||
|
||||
let task = Task<TokenPair, Error> {
|
||||
let url = URL(string: "https://api.login.yahoo.com/oauth2/get_token")!
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let bodyParams = [
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refreshToken,
|
||||
"client_id": clientID,
|
||||
"client_secret": clientSecret
|
||||
]
|
||||
|
||||
let bodyString = bodyParams
|
||||
.map { "\($0.key)=\($0.value)" }
|
||||
.joined(separator: "&")
|
||||
|
||||
request.httpBody = bodyString.data(using: .utf8)
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
(200...299).contains(httpResponse.statusCode) else {
|
||||
throw OAuthError.invalidResponse
|
||||
}
|
||||
|
||||
let tokenResponse = try JSONDecoder().decode(OAuthTokenResponse.self, from: data)
|
||||
let tokenPair = tokenResponse.toTokenPair()
|
||||
|
||||
await tokenStorage.saveTokenPair(tokenPair, for: userID)
|
||||
|
||||
return tokenPair
|
||||
}
|
||||
|
||||
refreshTask = task
|
||||
|
||||
defer { refreshTask = nil }
|
||||
|
||||
return try await task.value
|
||||
}
|
||||
|
||||
func saveTokenPair(_ tokenPair: TokenPair) async {
|
||||
guard let userID = currentUserID else { return }
|
||||
await tokenStorage.saveTokenPair(tokenPair, for: userID)
|
||||
}
|
||||
|
||||
func clearTokens() async {
|
||||
guard let userID = currentUserID else { return }
|
||||
await tokenStorage.clearTokens(for: userID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user