import Foundation private let BML_API_BASE = "https://www.bankofmaldives.com.mv/internetbanking" private let BML_API_UA = "bml-mobile-banking/348 (Apple; iOS 17.0; iPhone)" private let BML_API_VERSION = "2.1.44.348" final class BmlTapToPayClient { private let urlSession: URLSession = { let cfg = URLSessionConfiguration.default cfg.timeoutIntervalForRequest = 30 return URLSession(configuration: cfg) }() func fetchTokens( session: BmlSession, cardId: String, otp: String, quantity: Int = 3 ) async throws -> [BmlWalletToken] { let url = "\(BML_API_BASE)/api/mobile/walletpayments/gettoken" // Step 1: initiate let base: [String: Any] = ["type": "track2", "cardid": cardId, "quantity": quantity] let step1 = try await post(session: session, url: url, body: base) if step1.code == 0 { return step1.tokens } if step1.code != 99 { throw BmlError.serverError(step1.message ?? "Token request failed") } // Step 2: request OTP channel var body2 = base body2["channel"] = "token" let step2 = try await post(session: session, url: url, body: body2) if step2.code != 22 { throw BmlError.serverError(step2.message ?? "OTP channel request failed") } // Step 3: submit TOTP var body3 = body2 body3["otp"] = otp let step3 = try await post(session: session, url: url, body: body3) if step3.code != 0 { throw BmlError.serverError(step3.message ?? "Token fetch failed") } return step3.tokens } private func post(session: BmlSession, url: String, body: [String: Any]) async throws -> TokenResponse { var req = URLRequest(url: URL(string: url)!) req.httpMethod = "POST" req.setValue("Bearer \(session.accessToken)", forHTTPHeaderField: "Authorization") req.setValue(BML_API_UA, forHTTPHeaderField: "User-Agent") req.setValue(BML_API_VERSION, forHTTPHeaderField: "x-app-version") req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await urlSession.data(for: req) let code = (response as? HTTPURLResponse)?.statusCode ?? 0 if code == 401 || code == 419 { throw BmlError.sessionExpired } if code >= 500 { throw BmlError.networkError("Server error HTTP \(code)") } guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw BmlError.invalidResponse } let respCode = root["code"] as? Int ?? -1 let message = root["message"] as? String let payload = root["payload"] as? [[String: Any]] ?? [] let tokens: [BmlWalletToken] = payload.map { item in BmlWalletToken( token: item["token"] as? String ?? "", expiry: item["expiry"] as? String ?? "", appCode: item["app_code"] as? String ?? "", serviceCode: item["service_code"] as? String ?? "", data: item["data"] as? String ?? "", validUntil: item["valid_until"] as? String ?? "" ) } return TokenResponse(code: respCode, message: message, tokens: tokens) } private struct TokenResponse { let code: Int let message: String? let tokens: [BmlWalletToken] } }