payfrit-food-ios/PayfritFood/Services/APIService.swift
John Pinkyfloyd 71e7ec34f6 Initial commit: PayfritFood iOS app
- SwiftUI + async/await architecture
- Barcode scanning with AVFoundation
- Product display with score ring, NOVA badge, nutrition
- Alternatives with sort/filter
- Auth (login/register)
- Favorites & history
- Account management
- Dark theme
- Connected to food.payfrit.com API (Open Food Facts proxy)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-16 16:58:21 -07:00

281 lines
9.4 KiB
Swift

import Foundation
// MARK: - API Error
enum APIError: LocalizedError {
case invalidResponse
case httpError(statusCode: Int, message: String)
case serverError(String)
case decodingError(String)
case unauthorized
case noData
case invalidURL
var errorDescription: String? {
switch self {
case .invalidResponse: return "Invalid response from server"
case .httpError(let code, let msg): return "HTTP \(code): \(msg)"
case .serverError(let msg): return msg
case .decodingError(let msg): return "Failed to decode: \(msg)"
case .unauthorized: return "Please log in to continue"
case .noData: return "No data received"
case .invalidURL: return "Invalid URL"
}
}
}
// MARK: - API Service
actor APIService {
static let shared = APIService()
static let baseURL = "https://food.payfrit.com/api"
private let session: URLSession
private var userToken: String?
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
self.session = URLSession(configuration: config)
}
// MARK: - Token Management
func setToken(_ token: String) {
self.userToken = token
}
func clearToken() {
self.userToken = nil
}
// MARK: - HTTP Methods
private func request(_ endpoint: String, method: String = "GET", body: [String: Any]? = nil, includeAuth: Bool = true) async throws -> [String: Any] {
guard let url = URL(string: "\(Self.baseURL)/\(endpoint)") else {
throw APIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = method
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
if includeAuth, let token = userToken {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
if let body = body {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
}
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
if httpResponse.statusCode == 401 {
throw APIError.unauthorized
}
guard (200...299).contains(httpResponse.statusCode) else {
let message = String(data: data, encoding: .utf8) ?? "Unknown error"
throw APIError.httpError(statusCode: httpResponse.statusCode, message: message)
}
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw APIError.decodingError("Invalid JSON response")
}
// Check for API-level errors
if let ok = json["ok"] as? Bool, !ok {
let message = json["error"] as? String ?? json["message"] as? String ?? "Unknown error"
throw APIError.serverError(message)
}
return json
}
private func get(_ endpoint: String, includeAuth: Bool = true) async throws -> [String: Any] {
return try await request(endpoint, method: "GET", includeAuth: includeAuth)
}
private func post(_ endpoint: String, body: [String: Any], includeAuth: Bool = true) async throws -> [String: Any] {
return try await request(endpoint, method: "POST", body: body, includeAuth: includeAuth)
}
private func delete(_ endpoint: String) async throws -> [String: Any] {
return try await request(endpoint, method: "DELETE")
}
// MARK: - Auth Endpoints
func login(email: String, password: String) async throws -> (UserProfile, String) {
let json = try await post("user/login.php", body: ["email": email, "password": password], includeAuth: false)
guard let token = json["token"] as? String,
let userData = json["user"] as? [String: Any] else {
throw APIError.decodingError("Missing token or user data")
}
let profile = UserProfile(json: userData)
return (profile, token)
}
func register(email: String, password: String, name: String, zipCode: String) async throws -> (UserProfile, String) {
let body: [String: Any] = [
"email": email,
"password": password,
"name": name,
"zipCode": zipCode
]
let json = try await post("user/register.php", body: body, includeAuth: false)
guard let token = json["token"] as? String,
let userData = json["user"] as? [String: Any] else {
throw APIError.decodingError("Missing token or user data")
}
let profile = UserProfile(json: userData)
return (profile, token)
}
func logout() async throws {
_ = try await post("user/account.php", body: ["action": "logout"])
}
func deleteAccount() async throws {
_ = try await post("user/account.php", body: ["action": "delete"])
}
// MARK: - User Endpoints
func getProfile() async throws -> UserProfile {
let json = try await get("user/account.php")
guard let userData = json["user"] as? [String: Any] else {
throw APIError.decodingError("Missing user data")
}
return UserProfile(json: userData)
}
func exportData() async throws -> URL {
let json = try await post("user/account.php", body: ["action": "export"])
guard let urlString = json["downloadUrl"] as? String,
let url = URL(string: urlString) else {
throw APIError.decodingError("Missing download URL")
}
return url
}
// MARK: - Product Endpoints
func lookupProduct(barcode: String) async throws -> Product {
let json = try await get("scan.php?barcode=\(barcode)", includeAuth: false)
// API returns product data at root level or nested under "product"
let productData = (json["product"] as? [String: Any]) ?? json
return Product(json: productData)
}
func getProduct(id: Int) async throws -> Product {
let json = try await get("scan.php?id=\(id)", includeAuth: false)
// API returns product data at root level or nested under "product"
let productData = (json["product"] as? [String: Any]) ?? json
return Product(json: productData)
}
func getAlternatives(productId: Int, sort: String? = nil, filters: [String]? = nil) async throws -> [Alternative] {
var endpoint = "alternatives.php?productId=\(productId)"
if let sort = sort {
endpoint += "&sort=\(sort)"
}
if let filters = filters, !filters.isEmpty {
endpoint += "&filters=\(filters.joined(separator: ","))"
}
let json = try await get(endpoint, includeAuth: false)
guard let alternativesData = json["alternatives"] as? [[String: Any]] else {
return []
}
return alternativesData.map { Alternative(json: $0) }
}
// MARK: - Favorites Endpoints
func getFavorites() async throws -> [Product] {
let json = try await get("user/favorites.php")
guard let productsData = json["products"] as? [[String: Any]] else {
return []
}
return productsData.map { Product(json: $0) }
}
func addFavorite(productId: Int) async throws {
_ = try await post("user/favorites.php", body: ["action": "add", "productId": productId])
}
func removeFavorite(productId: Int) async throws {
_ = try await post("user/favorites.php", body: ["action": "remove", "productId": productId])
}
// MARK: - History Endpoints
func getHistory() async throws -> [ScanHistoryItem] {
let json = try await get("user/scans.php")
guard let itemsData = json["items"] as? [[String: Any]] else {
return []
}
return itemsData.map { ScanHistoryItem(json: $0) }
}
func addToHistory(productId: Int) async throws {
_ = try await post("user/scans.php", body: ["productId": productId])
}
}
// MARK: - JSON Helpers
enum JSON {
static func parseInt(_ value: Any?) -> Int {
if let i = value as? Int { return i }
if let s = value as? String, let i = Int(s) { return i }
if let d = value as? Double { return Int(d) }
return 0
}
static func parseDouble(_ value: Any?) -> Double {
if let d = value as? Double { return d }
if let i = value as? Int { return Double(i) }
if let s = value as? String, let d = Double(s) { return d }
return 0
}
static func parseString(_ value: Any?) -> String {
if let s = value as? String { return s }
if let i = value as? Int { return String(i) }
if let d = value as? Double { return String(d) }
return ""
}
static func parseBool(_ value: Any?) -> Bool {
if let b = value as? Bool { return b }
if let i = value as? Int { return i != 0 }
if let s = value as? String { return s.lowercased() == "true" || s == "1" }
return false
}
static func parseOptionalString(_ value: Any?) -> String? {
if let s = value as? String, !s.isEmpty { return s }
return nil
}
static func parseStringArray(_ value: Any?) -> [String] {
if let arr = value as? [String] { return arr }
if let s = value as? String { return s.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } }
return []
}
}