Add Tap to Pay feature and misc fixes
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>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user