fix: align OTP auth with actual API response format

Three bugs found and fixed:
1. sendOTP was sending "ContactNumber" but API expects "Phone"
2. APIResponse expected {"Success":true,"Data":{}} but API returns {"OK":true,"UUID":"..."}
3. verifyOTP was sending "Code" but API expects "OTP"

Now decodes the raw API format directly instead of going through the
generic APIResponse wrapper (which doesn't match auth endpoints).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Schwifty 2026-03-22 20:46:55 +00:00
parent fd4b1bf8ca
commit 2242260f5a

View file

@ -36,39 +36,76 @@ actor APIClient {
// MARK: - Auth
struct OTPResponse: Codable {
let uuid: String?
/// Raw response from /auth/loginOTP.php
/// API returns: { "OK": true, "UUID": "...", "MESSAGE": "..." }
private struct OTPRawResponse: Codable {
let OK: Bool
let UUID: String?
var otpUUID: String { uuid ?? UUID ?? "" }
let MESSAGE: String?
let ERROR: String?
}
func sendOTP(phone: String) async throws -> String {
let body: [String: Any] = ["ContactNumber": phone]
let body: [String: Any] = ["Phone": phone]
let data = try await post(path: "/auth/loginOTP.php", body: body)
let resp = try JSONDecoder().decode(APIResponse<OTPResponse>.self, from: data)
guard resp.success, let payload = resp.data else {
throw APIError.serverError(resp.message ?? "Failed to send OTP")
let resp = try JSONDecoder().decode(OTPRawResponse.self, from: data)
guard resp.OK, let uuid = resp.UUID, !uuid.isEmpty else {
throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Failed to send OTP")
}
return payload.otpUUID
return uuid
}
struct LoginResponse: Codable {
let token: String?
/// Raw response from /auth/verifyLoginOTP.php
/// API returns: { "OK": true, "UserID": 123, "Token": "...", "FirstName": "..." }
private struct VerifyOTPRawResponse: Codable {
let OK: Bool
let Token: String?
let userId: String?
let UserID: String?
var authToken: String { token ?? Token ?? "" }
var authUserId: String { userId ?? UserID ?? "" }
let UserID: IntOrString?
let MESSAGE: String?
let ERROR: String?
}
/// Handles UserID coming as either int or string from API
enum IntOrString: Codable {
case int(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let i = try? container.decode(Int.self) {
self = .int(i)
} else if let s = try? container.decode(String.self) {
self = .string(s)
} else {
self = .string("")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let i): try container.encode(i)
case .string(let s): try container.encode(s)
}
}
var stringValue: String {
switch self {
case .int(let i): return String(i)
case .string(let s): return s
}
}
}
func verifyOTP(uuid: String, code: String) async throws -> (token: String, userId: String) {
let body: [String: Any] = ["UUID": uuid, "Code": code]
let body: [String: Any] = ["UUID": uuid, "OTP": code]
let data = try await post(path: "/auth/verifyLoginOTP.php", body: body)
let resp = try JSONDecoder().decode(APIResponse<LoginResponse>.self, from: data)
guard resp.success, let payload = resp.data else {
throw APIError.serverError(resp.message ?? "Invalid OTP")
let resp = try JSONDecoder().decode(VerifyOTPRawResponse.self, from: data)
guard resp.OK, let token = resp.Token, !token.isEmpty else {
throw APIError.serverError(resp.MESSAGE ?? resp.ERROR ?? "Invalid OTP")
}
return (payload.authToken, payload.authUserId)
let userId = resp.UserID?.stringValue ?? ""
return (token, userId)
}
// MARK: - Businesses