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:
27
FantasyWatch/Shared/Networking/Core/Endpoint.swift
Normal file
27
FantasyWatch/Shared/Networking/Core/Endpoint.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Endpoint.swift
|
||||
// FantasyWatch
|
||||
//
|
||||
// Created by Claude Code
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Endpoint {
|
||||
var path: String { get }
|
||||
var method: HTTPMethod { get }
|
||||
var headers: [String: String]? { get }
|
||||
var queryParameters: [String: String]? { get }
|
||||
}
|
||||
|
||||
enum HTTPMethod: String {
|
||||
case get = "GET"
|
||||
case post = "POST"
|
||||
case put = "PUT"
|
||||
case delete = "DELETE"
|
||||
}
|
||||
|
||||
extension Endpoint {
|
||||
var headers: [String: String]? { nil }
|
||||
var queryParameters: [String: String]? { nil }
|
||||
}
|
||||
35
FantasyWatch/Shared/Networking/Core/NetworkError.swift
Normal file
35
FantasyWatch/Shared/Networking/Core/NetworkError.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// NetworkError.swift
|
||||
// FantasyWatch
|
||||
//
|
||||
// Created by Claude Code
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NetworkError: Error, Equitable {
|
||||
case unauthorized
|
||||
case serverError(statusCode: Int)
|
||||
case decodingError(Error)
|
||||
case networkFailure(Error)
|
||||
case invalidURL
|
||||
case invalidResponse
|
||||
case noData
|
||||
|
||||
static func == (lhs: NetworkError, rhs: NetworkError) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.unauthorized, .unauthorized):
|
||||
return true
|
||||
case let (.serverError(lhsCode), .serverError(rhsCode)):
|
||||
return lhsCode == rhsCode
|
||||
case (.invalidURL, .invalidURL):
|
||||
return true
|
||||
case (.invalidResponse, .invalidResponse):
|
||||
return true
|
||||
case (.noData, .noData):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
78
FantasyWatch/Shared/Networking/Core/NetworkService.swift
Normal file
78
FantasyWatch/Shared/Networking/Core/NetworkService.swift
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// NetworkService.swift
|
||||
// FantasyWatch
|
||||
//
|
||||
// Created by Claude Code
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol NetworkService {
|
||||
func request(
|
||||
_ endpoint: Endpoint,
|
||||
baseURL: String,
|
||||
bearerToken: String?
|
||||
) async throws -> Data
|
||||
}
|
||||
|
||||
final class DefaultNetworkService: NetworkService {
|
||||
private let session: URLSession
|
||||
|
||||
init(session: URLSession = .shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
func request(
|
||||
_ endpoint: Endpoint,
|
||||
baseURL: String,
|
||||
bearerToken: String? = nil
|
||||
) async throws -> Data {
|
||||
guard var urlComponents = URLComponents(string: baseURL + endpoint.path) else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
|
||||
if let queryParameters = endpoint.queryParameters {
|
||||
urlComponents.queryItems = queryParameters.map {
|
||||
URLQueryItem(name: $0.key, value: $0.value)
|
||||
}
|
||||
}
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = endpoint.method.rawValue
|
||||
|
||||
if let headers = endpoint.headers {
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
}
|
||||
|
||||
if let token = bearerToken {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try await session.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
switch httpResponse.statusCode {
|
||||
case 200...299:
|
||||
return data
|
||||
case 401:
|
||||
throw NetworkError.unauthorized
|
||||
default:
|
||||
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
|
||||
}
|
||||
} catch let error as NetworkError {
|
||||
throw error
|
||||
} catch {
|
||||
throw NetworkError.networkFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user