Files
modeazimandClaude Sonnet 4.6 2bbb280380 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>
2026-06-08 01:27:27 +05:00

116 lines
3.8 KiB
Swift

import Foundation
import Observation
@MainActor
@Observable
final class TapToPayViewModel {
let account: BankAccount
var phase: Phase = .loading
var token: BmlWalletToken?
var errorMessage: String?
var isReady = false
enum Phase {
case loading
case ready
case error
}
init(account: BankAccount) {
self.account = account
}
func armToken() async {
phase = .loading
errorMessage = nil
BmlContactlessUtil.clearToken()
let loginId = account.loginTag.replacingOccurrences(of: "bml_", with: "")
guard let otpSeed = CredentialStore.shared.load(
forKey: CredentialStore.Keys.bmlOtpSeed(loginId)
) else {
errorMessage = "OTP seed not found for \(loginId). Please log in again."
phase = .error
return
}
let profileId = account.profileId ?? ""
guard let accessToken = CredentialStore.shared.load(
forKey: CredentialStore.Keys.bmlAccessToken(profileId)
), !accessToken.isEmpty else {
errorMessage = "Session not found. Please log in again."
phase = .error
return
}
let deviceId = CredentialStore.shared.load(forKey: CredentialStore.Keys.bmlDeviceId(profileId)) ?? ""
let refreshTok = CredentialStore.shared.load(forKey: CredentialStore.Keys.bmlRefreshToken(profileId)) ?? ""
let expStr = CredentialStore.shared.load(forKey: CredentialStore.Keys.bmlExpTime(profileId)) ?? "0"
let expiresAt = Int64(expStr) ?? 0
let now = Int64(Date().timeIntervalSince1970 * 1000)
var session = BmlSession(
accessToken: accessToken,
deviceId: deviceId,
refreshToken: refreshTok,
expiresAt: expiresAt
)
// Refresh if expired or expiring within 60s
if expiresAt > 0 && now >= expiresAt - 60_000 {
do {
let fresh = try await BmlLoginFlow().refreshSession(session)
saveSession(fresh, profileId: profileId)
session = fresh
} catch {
errorMessage = "Session expired. Please log in again. (\(error.localizedDescription))"
phase = .error
return
}
}
let otp = Totp.generate(otpSeed)
let internalId = account.internalId ?? ""
do {
let tokens = try await BmlTapToPayClient().fetchTokens(
session: session,
cardId: internalId,
otp: otp
)
guard let firstToken = tokens.first else {
errorMessage = "No payment tokens received from server."
phase = .error
return
}
token = firstToken
BmlContactlessUtil.setToken(firstToken)
phase = .ready
isReady = true
} catch {
if error is CancellationError { return }
errorMessage = error.localizedDescription
phase = .error
}
}
func cancel() {
BmlContactlessUtil.clearToken()
}
private func saveSession(_ sess: BmlSession, profileId: String) {
try? CredentialStore.shared.save(sess.accessToken, forKey: CredentialStore.Keys.bmlAccessToken(profileId))
try? CredentialStore.shared.save(sess.deviceId, forKey: CredentialStore.Keys.bmlDeviceId(profileId))
if !sess.refreshToken.isEmpty {
try? CredentialStore.shared.save(sess.refreshToken, forKey: CredentialStore.Keys.bmlRefreshToken(profileId))
}
if sess.expiresAt > 0 {
try? CredentialStore.shared.save(String(sess.expiresAt), forKey: CredentialStore.Keys.bmlExpTime(profileId))
}
}
}