KasztelanKarta/Kasztelan/APIClient.swift

162 lines
6.2 KiB
Swift

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
}
}