payfrit-beacon-ios/_backup/Services/APIService.swift
John Pinkyfloyd 8c2320da44 Add ios-marketing idiom, iPad orientations, launch screen
- Fixed App Store icon display with ios-marketing idiom
- Added iPad orientation support for multitasking
- Added UILaunchScreen for iPad requirements
- Removed unused BLE permissions and files from build

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-10 19:38:11 -08:00

416 lines
14 KiB
Swift

import Foundation
// MARK: - API Errors
enum APIError: LocalizedError, Equatable {
case invalidURL
case noData
case decodingError(String)
case serverError(String)
case unauthorized
case networkError(String)
var errorDescription: String? {
switch self {
case .invalidURL: return "Invalid URL"
case .noData: return "No data received"
case .decodingError(let msg): return "Decoding error: \(msg)"
case .serverError(let msg): return msg
case .unauthorized: return "Unauthorized"
case .networkError(let msg): return msg
}
}
}
// MARK: - Login Response
struct LoginResponse {
let userId: Int
let userFirstName: String
let token: String
let photoUrl: String
}
// MARK: - API Service
actor APIService {
static let shared = APIService()
private enum Environment {
case development, production
var baseURL: String {
switch self {
case .development: return "https://dev.payfrit.com/api"
case .production: return "https://biz.payfrit.com/api"
}
}
}
private let environment: Environment = .development
var isDev: Bool { environment == .development }
private var userToken: String?
private var userId: Int?
private var businessId: Int = 0
var baseURL: String { environment.baseURL }
// MARK: - Configuration
func setAuth(token: String?, userId: Int?) {
self.userToken = token
self.userId = userId
}
func setBusinessId(_ id: Int) {
self.businessId = id
}
func getToken() -> String? { userToken }
func getUserId() -> Int? { userId }
func getBusinessId() -> Int { businessId }
// MARK: - Core Request
private func postJSON(_ path: String, payload: [String: Any]) async throws -> [String: Any] {
let urlString = environment.baseURL + (path.hasPrefix("/") ? path : "/\(path)")
guard let url = URL(string: urlString) else { throw APIError.invalidURL }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
if let token = userToken, !token.isEmpty {
request.setValue(token, forHTTPHeaderField: "X-User-Token")
}
if businessId > 0 {
request.setValue(String(businessId), forHTTPHeaderField: "X-Business-ID")
}
request.httpBody = try JSONSerialization.data(withJSONObject: payload)
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 401 { throw APIError.unauthorized }
guard (200...299).contains(httpResponse.statusCode) else {
throw APIError.serverError("HTTP \(httpResponse.statusCode)")
}
}
if let json = tryDecodeJSON(data) {
return json
}
throw APIError.decodingError("Non-JSON response")
}
private func tryDecodeJSON(_ data: Data) -> [String: Any]? {
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
return json
}
guard let body = String(data: data, encoding: .utf8),
let start = body.firstIndex(of: "{"),
let end = body.lastIndex(of: "}") else { return nil }
let jsonStr = String(body[start...end])
guard let jsonData = jsonStr.data(using: .utf8) else { return nil }
return try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
}
private func ok(_ json: [String: Any]) -> Bool {
for key in ["OK", "ok", "Ok"] {
if let b = json[key] as? Bool { return b }
if let i = json[key] as? Int { return i == 1 }
if let s = json[key] as? String {
let lower = s.lowercased()
return lower == "true" || lower == "1" || lower == "yes"
}
}
return false
}
private func err(_ json: [String: Any]) -> String {
let msg = (json["ERROR"] as? String) ?? (json["error"] as? String)
?? (json["Error"] as? String) ?? (json["message"] as? String) ?? ""
return msg.isEmpty ? "Unknown error" : msg
}
nonisolated static func findArray(_ json: [String: Any], _ keys: [String]) -> [[String: Any]]? {
for key in keys {
if let arr = json[key] as? [[String: Any]] { return arr }
}
for (_, value) in json {
if let arr = value as? [[String: Any]], !arr.isEmpty { return arr }
}
return nil
}
// MARK: - Auth
func login(username: String, password: String) async throws -> LoginResponse {
let json = try await postJSON("/auth/login.cfm", payload: [
"username": username,
"password": password
])
guard ok(json) else {
let e = err(json)
if e == "bad_credentials" {
throw APIError.serverError("Invalid email/phone or password")
}
throw APIError.serverError("Login failed: \(e)")
}
let uid = (json["UserID"] as? Int)
?? Int(json["UserID"] as? String ?? "")
?? (json["UserId"] as? Int)
?? 0
let token = (json["Token"] as? String)
?? (json["token"] as? String)
?? ""
guard uid > 0 else {
throw APIError.serverError("Login failed: no user ID returned")
}
guard !token.isEmpty else {
throw APIError.serverError("Login failed: no token returned")
}
let firstName = (json["UserFirstName"] as? String)
?? (json["FirstName"] as? String)
?? (json["firstName"] as? String)
?? (json["Name"] as? String)
?? (json["name"] as? String)
?? ""
let photoUrl = (json["UserPhotoUrl"] as? String)
?? (json["PhotoUrl"] as? String)
?? (json["photoUrl"] as? String)
?? ""
self.userToken = token
self.userId = uid
return LoginResponse(userId: uid, userFirstName: firstName, token: token, photoUrl: photoUrl)
}
func logout() {
userToken = nil
userId = nil
businessId = 0
}
// MARK: - Businesses
func getMyBusinesses() async throws -> [Employment] {
guard let uid = userId, uid > 0 else {
throw APIError.serverError("User not logged in")
}
let json = try await postJSON("/workers/myBusinesses.cfm", payload: [
"UserID": uid
])
guard ok(json) else {
throw APIError.serverError("Failed to load businesses: \(err(json))")
}
guard let arr = Self.findArray(json, ["BUSINESSES", "Businesses", "businesses"]) else {
return []
}
return arr.map { Employment(json: $0) }
}
// MARK: - Beacons
func listBeacons() async throws -> [Beacon] {
let json = try await postJSON("/beacons/list.cfm", payload: [
"BusinessID": businessId
])
guard ok(json) else {
throw APIError.serverError("Failed to load beacons: \(err(json))")
}
guard let arr = Self.findArray(json, ["BEACONS", "Beacons", "beacons"]) else { return [] }
return arr.map { Beacon(json: $0) }
}
func getBeacon(beaconId: Int) async throws -> Beacon {
let json = try await postJSON("/beacons/get.cfm", payload: [
"BeaconID": beaconId,
"BusinessID": businessId
])
guard ok(json) else {
throw APIError.serverError("Failed to load beacon: \(err(json))")
}
var beaconJson: [String: Any]?
for key in ["BEACON", "Beacon", "beacon"] {
if let d = json[key] as? [String: Any] { beaconJson = d; break }
}
if beaconJson == nil {
for (_, value) in json {
if let d = value as? [String: Any], d.count > 3 { beaconJson = d; break }
}
}
guard let beaconJson = beaconJson else {
throw APIError.serverError("Invalid beacon response")
}
return Beacon(json: beaconJson)
}
func createBeacon(name: String, uuid: String) async throws -> Int {
let json = try await postJSON("/beacons/create.cfm", payload: [
"BusinessID": businessId,
"Name": name,
"UUID": uuid
])
guard ok(json) else {
throw APIError.serverError("Failed to create beacon: \(err(json))")
}
return (json["BeaconID"] as? Int)
?? (json["ID"] as? Int)
?? Int(json["BeaconID"] as? String ?? "")
?? 0
}
func updateBeacon(beaconId: Int, name: String, uuid: String, isActive: Bool) async throws {
let json = try await postJSON("/beacons/update.cfm", payload: [
"BeaconID": beaconId,
"BusinessID": businessId,
"Name": name,
"UUID": uuid,
"IsActive": isActive
])
guard ok(json) else {
throw APIError.serverError("Failed to update beacon: \(err(json))")
}
}
func deleteBeacon(beaconId: Int) async throws {
let json = try await postJSON("/beacons/delete.cfm", payload: [
"BeaconID": beaconId,
"BusinessID": businessId
])
guard ok(json) else {
throw APIError.serverError("Failed to delete beacon: \(err(json))")
}
}
// MARK: - Service Points
func listServicePoints() async throws -> [ServicePoint] {
let json = try await postJSON("/servicePoints/list.cfm", payload: [
"BusinessID": businessId
])
guard ok(json) else {
throw APIError.serverError("Failed to load service points: \(err(json))")
}
guard let arr = Self.findArray(json, ["SERVICE_POINTS", "ServicePoints", "servicePoints", "SERVICEPOINTS"]) else { return [] }
return arr.map { ServicePoint(json: $0) }
}
func assignBeaconToServicePoint(servicePointId: Int, beaconId: Int?) async throws {
var payload: [String: Any] = [
"ServicePointID": servicePointId,
"BusinessID": businessId
]
if let bid = beaconId {
payload["BeaconID"] = bid
}
let json = try await postJSON("/servicePoints/assignBeacon.cfm", payload: payload)
guard ok(json) else {
throw APIError.serverError("Failed to assign beacon: \(err(json))")
}
}
func listServicePointTypes() async throws -> [ServicePointType] {
let json = try await postJSON("/servicePoints/types.cfm", payload: [
"BusinessID": businessId
])
guard ok(json) else {
throw APIError.serverError("Failed to load service point types: \(err(json))")
}
guard let arr = Self.findArray(json, ["TYPES", "Types", "types"]) else { return [] }
return arr.map { ServicePointType(json: $0) }
}
// MARK: - URL Helpers
func resolvePhotoUrl(_ rawUrl: String) -> String {
let trimmed = rawUrl.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return "" }
if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { return trimmed }
let baseDomain = environment == .development ? "https://dev.payfrit.com" : "https://biz.payfrit.com"
if trimmed.hasPrefix("/") { return baseDomain + trimmed }
return baseDomain + "/" + trimmed
}
// MARK: - Date Parsing
private static let iso8601Formatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
private static let iso8601NoFrac: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime]
return f
}()
private static let simpleDateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
f.locale = Locale(identifier: "en_US_POSIX")
return f
}()
private static let cfmlDateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "MMMM, dd yyyy HH:mm:ss Z"
f.locale = Locale(identifier: "en_US_POSIX")
return f
}()
private static let cfmlShortFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
f.locale = Locale(identifier: "en_US_POSIX")
return f
}()
private static let cfmlAltFormatters: [DateFormatter] = {
let formats = [
"MMM dd, yyyy HH:mm:ss",
"MM/dd/yyyy HH:mm:ss",
"yyyy-MM-dd HH:mm:ss.S",
"yyyy-MM-dd'T'HH:mm:ss.S",
"yyyy-MM-dd'T'HH:mm:ssZ",
"yyyy-MM-dd'T'HH:mm:ss.SZ",
]
return formats.map { fmt in
let f = DateFormatter()
f.dateFormat = fmt
f.locale = Locale(identifier: "en_US_POSIX")
return f
}
}()
nonisolated static func parseDate(_ string: String) -> Date? {
let s = string.trimmingCharacters(in: .whitespacesAndNewlines)
if s.isEmpty { return nil }
if let epoch = Double(s) {
if epoch > 1_000_000_000_000 { return Date(timeIntervalSince1970: epoch / 1000) }
if epoch > 1_000_000_000 { return Date(timeIntervalSince1970: epoch) }
}
if let d = iso8601Formatter.date(from: s) { return d }
if let d = iso8601NoFrac.date(from: s) { return d }
if let d = simpleDateFormatter.date(from: s) { return d }
if let d = cfmlDateFormatter.date(from: s) { return d }
if let d = cfmlShortFormatter.date(from: s) { return d }
for formatter in cfmlAltFormatters {
if let d = formatter.date(from: s) { return d }
}
return nil
}
}