Fill in payments by scanning a QR code or picking one from photos

This commit is contained in:
2026-09-20 02:00:52 +08:00
parent 399502cf72
commit c0903f4f02
8 changed files with 423 additions and 11 deletions
+80
View File
@@ -1,3 +1,4 @@
import PhotosUI
import SwiftUI
struct DashboardView: View {
@@ -124,6 +125,9 @@ struct SendView: View {
@State private var tag = ""
@State private var priority: TransactionPriority = .defaultPriority
@State private var localError: String?
@State private var status: String?
@State private var showScanner = false
@State private var photoItem: PhotosPickerItem?
var body: some View {
VStack(alignment: .leading, spacing: 18) {
@@ -131,6 +135,20 @@ struct SendView: View {
subtitle: "Balance \(model.balance.total) \(model.symbol)",
onBack: { route = .home })
HStack(spacing: 10) {
SecondaryButton(title: "Scan QR") { showScanner = true }
PhotosPicker(selection: $photoItem, matching: .images, photoLibrary: .shared()) {
Text("Photo")
.font(.system(size: 15, weight: .medium))
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Theme.surface)
.foregroundStyle(Theme.text)
.overlay(RoundedRectangle(cornerRadius: 14, style: .continuous).stroke(Theme.border))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
}
}
VStack(alignment: .leading, spacing: 6) {
FieldLabel(text: model.chain == .xrp ? "Destination address" : "Recipient address")
KoinsTextField(placeholder: model.chain == .xrp ? "r…" : "4…", text: $destination)
@@ -163,6 +181,9 @@ struct SendView: View {
if let message = localError ?? model.errorMessage {
MessageBanner(message: message, isError: true)
}
if let message = status {
MessageBanner(message: message, isError: false)
}
if let message = model.statusMessage {
MessageBanner(message: message, isError: false)
}
@@ -184,6 +205,65 @@ struct SendView: View {
Spacer(minLength: 0)
}
.sheet(isPresented: $showScanner) {
QRScannerView(onScan: { value in
showScanner = false
applyScanned(value)
}, onCancel: {
showScanner = false
})
.ignoresSafeArea()
}
.onChange(of: photoItem) { item in
guard let item else { return }
Task {
if let data = try? await item.loadTransferable(type: Data.self) {
if let value = QRCodeReader.decodeText(in: data) {
applyScanned(value)
} else {
localError = "No QR code found in that image"
}
} else {
localError = "That image could not be read"
}
photoItem = nil
}
}
}
/// Fills the form from a scanned code, accepting both plain addresses and
/// wallet payment URIs (`monero:…?tx_amount=`, `ripple:…?amount=&dt=`).
private func applyScanned(_ value: String) {
localError = nil
status = nil
guard let payload = QRCodeReader.parse(value) else {
localError = "That QR code is not a payment request"
return
}
let isForThisChain = model.chain == .xrp
? AddressFormat.isXRP(payload.address)
: AddressFormat.isMonero(payload.address)
guard isForThisChain else {
localError = model.chain == .xrp
? "That looks like a Monero address. Switch to Monero to use it."
: "That looks like an XRP address. Switch to XRP to use it."
return
}
destination = payload.address
if let scannedAmount = payload.amount, !scannedAmount.isEmpty {
amount = scannedAmount
}
if model.chain == .xrp, let scannedTag = payload.tag {
tag = String(scannedTag)
}
var filled: [String] = ["address"]
if payload.amount?.isEmpty == false { filled.append("amount") }
if payload.tag != nil { filled.append("tag") }
if payload.paymentId?.isEmpty == false { filled.append("payment ID") }
status = "Filled \(filled.joined(separator: ", ")) from the QR code"
}
}