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)) } } }