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
+44 -5
View File
@@ -10,6 +10,9 @@ import SwiftUI
struct DashboardView: View {
@Environment(HomeViewModel.self) private var vm
@Environment(AppViewModel.self) private var app
@State private var showQRScanner = false
@State private var scannedCardAccountNumber: String? = nil
@State private var tapToPayAccount: BankAccount? = nil
var body: some View {
NavigationStack {
@@ -118,8 +121,7 @@ struct DashboardView: View {
// Fixed bottom quick-action bar
HStack(spacing: 8) {
QuickActionButton(icon: "arrow.up.right", label: "Transfer") {}
QuickActionButton(icon: "qrcode", label: "PayMV QR") {}
}
QuickActionButton(icon: "qrcode", label: "PayMV QR") { showQRScanner = true } }
.padding(.horizontal, 16)
.padding(.top, 8)
.padding(.bottom, max(16, 0))
@@ -128,6 +130,16 @@ struct DashboardView: View {
.navigationTitle("Thijooree")
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbarItems }
.sheet(isPresented: $showQRScanner) {
QRScannerSheet { qrContent in
handleQR(qrContent)
}
}
.sheet(item: $tapToPayAccount) { account in
TapToPayView(account: account) {
tapToPayAccount = nil
}
}
}
}
@@ -137,7 +149,12 @@ struct DashboardView: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
ForEach(vm.cardAccounts) { card in
CardCarouselItem(account: card, hide: vm.hideAmounts)
CardCarouselItem(account: card, hide: vm.hideAmounts, onScanToPay: {
scannedCardAccountNumber = card.accountNumber
showQRScanner = true
}, onTapToPay: {
tapToPayAccount = card
})
.padding(.trailing, 12)
}
}
@@ -168,6 +185,26 @@ struct DashboardView: View {
}
}
// MARK: - QR handler
private func handleQR(_ content: String) {
let bmlUrl = PaymvQrParser.extractBmlGatewayUrl(content)
if content.hasPrefix("https://ebanking.bankofmaldives.com.mv/qrpay/") || bmlUrl != nil {
vm.pendingBmlQrUrl = bmlUrl ?? content
vm.pendingQrFromCard = scannedCardAccountNumber
return
}
if let result = PaymvQrParser.parse(content) {
vm.pendingTransferQRData = result
vm.pendingQrFromCard = scannedCardAccountNumber
} else {
let fallback = PaymvQrResult(account: content, amount: nil, merchantName: nil, purpose: nil)
vm.pendingTransferQRData = fallback
vm.pendingQrFromCard = scannedCardAccountNumber
}
}
// MARK: - Empty state
private var emptyState: some View {
@@ -232,6 +269,8 @@ private struct BalanceCard: View {
struct CardCarouselItem: View {
let account: BankAccount
let hide: Bool
let onScanToPay: () -> Void
let onTapToPay: () -> Void
var body: some View {
VStack(spacing: 0) {
@@ -274,8 +313,8 @@ struct CardCarouselItem: View {
// Action buttons
HStack(spacing: 4) {
CardActionButton(icon: "qrcode", label: "Scan to Pay") {}
CardActionButton(icon: "wave.3.right", label: "Tap to Pay") {}
CardActionButton(icon: "qrcode", label: "Scan to Pay") { onScanToPay() }
CardActionButton(icon: "wave.3.right", label: "Tap to Pay") { onTapToPay() }
}
.padding(.horizontal, 10)
.padding(.top, 8)