Adds BML contactless token fetching, APDU emulation utility, TapToPayViewModel, and TapToPayView with NFC animation. Also updates QR scanner, transfer flow, dashboard, and home views. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
3.4 KiB
Swift
86 lines
3.4 KiB
Swift
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]
|
|
}
|
|
}
|