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:
2026-06-08 01:27:27 +05:00
co-authored by Claude Sonnet 4.6
parent 160ef97e68
commit 2bbb280380
10 changed files with 1080 additions and 49 deletions
@@ -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]
}
}