import Foundation enum APIError: LocalizedError { case missingAppKey case invalidResponse case server(status: Int, code: String?, message: String) var errorDescription: String? { switch self { case .missingAppKey: return "Brak klucza aplikacji. Uzupełnij Config/Secrets.xcconfig." case .invalidResponse: return "Serwer zwrócił nieprawidłową odpowiedź." case let .server(_, _, message): return message } } var code: String? { guard case let .server(_, code, _) = self else { return nil } return code } } final class APIClient { private let baseURL = URL(string: "https://kasztelan.co.uk/wp-json/kasztelan/v1")! private let session: URLSession init(session: URLSession = .shared) { self.session = session } private var appKey: String { Bundle.main.object(forInfoDictionaryKey: "KASZTELANAppKey") as? String ?? "" } func login(login: String, password: String) async throws -> APIEnvelope { try await request("POST", path: "/login", body: ["login": login, "password": password]) } func register(_ form: RegistrationForm) async throws -> APIEnvelope { try await request("POST", path: "/register", body: [ "first_name": form.firstName, "last_name": form.lastName, "email": form.email, "password": form.password, "mobile": form.mobile, "kod_pocztowy": form.postcode, "dob": form.dob, "numer_karty": form.cardNumber, "register_tos": form.acceptsTerms ]) } func me(token: String) async throws -> APIEnvelope { try await request("GET", path: "/me", token: token) } func logout(token: String) async throws { let _: APIEnvelope = try await request("POST", path: "/logout", token: token, body: [:]) } func assignCard(_ number: String, token: String) async throws -> APIEnvelope { try await request("POST", path: "/card", token: token, body: ["numer_karty": number]) } func generateCard(token: String) async throws -> APIEnvelope { try await request("POST", path: "/card/generate", token: token, body: [:]) } func updateProfile(_ form: ProfileForm, token: String) async throws -> APIEnvelope { try await request("POST", path: "/profile", token: token, body: [ "first_name": form.firstName, "last_name": form.lastName, "email": form.email, "mobile": form.mobile, "kod_pocztowy": form.postcode, "dob": form.dob ]) } func changePassword(current: String, new: String, token: String) async throws { let _: APIEnvelope = try await request("POST", path: "/password", token: token, body: [ "current_password": current, "new_password": new ]) } func deleteAccount(password: String, token: String) async throws { let _: APIEnvelope = try await request("DELETE", path: "/account", token: token, body: ["password": password]) } func notifications(after: Int, token: String) async throws -> APIEnvelope { try await request("GET", path: "/notifications?after=\(max(0, after))", token: token) } func latestNotification(token: String) async throws -> APIEnvelope { try await request("GET", path: "/notifications/latest", token: token) } func registerPush(deviceToken: String, environment: String, token: String) async throws { let _: APIEnvelope = try await request("POST", path: "/push/device", token: token, body: [ "device_token": deviceToken, "environment": environment ]) } func unregisterPush(deviceToken: String, token: String) async throws { let _: APIEnvelope = try await request("DELETE", path: "/push/device", token: token, body: ["device_token": deviceToken]) } func wallet(token: String) async throws -> APIEnvelope { try await request("GET", path: "/wallet", token: token) } func saveWallet(payload: String, token: String) async throws -> APIEnvelope { try await request("POST", path: "/wallet", token: token, body: ["payload": payload]) } private func request( _ method: String, path: String, token: String? = nil, body: [String: Any]? = nil ) async throws -> APIEnvelope { guard !appKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw APIError.missingAppKey } guard let url = URL(string: baseURL.absoluteString + path) else { throw APIError.invalidResponse } var request = URLRequest(url: url) request.httpMethod = method request.timeoutInterval = 20 request.cachePolicy = .reloadIgnoringLocalCacheData request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue("no-cache, no-store", forHTTPHeaderField: "Cache-Control") request.setValue(appKey, forHTTPHeaderField: "X-Kasztelan-App-Key") request.setValue("iOS", forHTTPHeaderField: "X-Kasztelan-Platform") request.setValue( Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "", forHTTPHeaderField: "X-Kasztelan-App-Version" ) if let token, !token.isEmpty { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } if let body { request.httpBody = try JSONSerialization.data(withJSONObject: body) request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") } let (data, response) = try await session.data(for: request) guard let http = response as? HTTPURLResponse else { throw APIError.invalidResponse } let envelope = (try? JSONDecoder().decode(APIEnvelope.self, from: data)) ?? APIEnvelope() guard (200..<300).contains(http.statusCode), envelope.success != false else { throw APIError.server( status: http.statusCode, code: envelope.code, message: envelope.message ?? "Nie udało się połączyć z kontem." ) } return envelope } }