From e53c6aa7a9bd5b6052f20d487b285d234455f4d1 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 20 Sep 2026 00:57:51 +0800 Subject: [PATCH] Show each address QR in a bottom drawer --- apps/ios/Koins/Views/AccountsView.swift | 49 ++++++++------------- apps/ios/Koins/Views/AddressQRSheet.swift | 53 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 apps/ios/Koins/Views/AddressQRSheet.swift diff --git a/apps/ios/Koins/Views/AccountsView.swift b/apps/ios/Koins/Views/AccountsView.swift index 6da9891..499d716 100644 --- a/apps/ios/Koins/Views/AccountsView.swift +++ b/apps/ios/Koins/Views/AccountsView.swift @@ -6,7 +6,7 @@ struct AccountsView: View { @State private var error: String? @State private var busy = false - @State private var expandedAddress: String? + @State private var qrAddress: AddressQR? @State private var showAccountPrompt = false @State private var showSubaddressPrompt = false @State private var pendingLabel = "" @@ -37,6 +37,9 @@ struct AccountsView: View { } } .task(id: model.moneroReady) { await model.loadAccounts() } + .sheet(item: $qrAddress) { item in + AddressQRSheet(title: item.title, address: item.address) { qrAddress = nil } + } .alert("Account name", isPresented: $showAccountPrompt) { TextField("e.g. Savings", text: $pendingLabel) Button("Create") { createAccount() } @@ -64,16 +67,14 @@ struct AccountsView: View { Spacer() } - addressRow(id: "\(account.index)/primary", - title: "Primary address", + addressRow(title: "Primary address", address: account.address, isUsed: true) if !account.subaddresses.isEmpty { Divider().overlay(Theme.border) ForEach(account.subaddresses.filter { $0.index != 0 }) { subaddress in - addressRow(id: subaddress.id, - title: subaddress.label.isEmpty ? "Address \(subaddress.index)" : subaddress.label, + addressRow(title: subaddress.label.isEmpty ? "Address \(subaddress.index)" : subaddress.label, address: subaddress.address, isUsed: subaddress.isUsed, balance: subaddress.balance) @@ -89,8 +90,10 @@ struct AccountsView: View { } } - private func addressRow(id: String, title: String, address: String, isUsed: Bool, balance: String? = nil) -> some View { - VStack(spacing: 10) { + private func addressRow(title: String, address: String, isUsed: Bool, balance: String? = nil) -> some View { + Button { + qrAddress = AddressQR(title: title, address: address) + } label: { HStack(spacing: 8) { VStack(alignment: .leading, spacing: 2) { Text(title) @@ -108,31 +111,17 @@ struct AccountsView: View { } } Spacer(minLength: 6) - Button { - expandedAddress = expandedAddress == id ? nil : id - } label: { - Image(systemName: expandedAddress == id ? "xmark" : "qrcode") - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(Theme.accent) - .frame(width: 32, height: 32) - .background(Theme.surfaceAlt) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - } - } - - if expandedAddress == id { - VStack(spacing: 10) { - QRCodeView(text: address) - .frame(maxWidth: 180) - HStack(spacing: 8) { - SecondaryButton(title: "Copy") { - UIPasteboard.general.string = address - } - } - } - .frame(maxWidth: .infinity) + Image(systemName: "qrcode") + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(Theme.accent) + .frame(width: 32, height: 32) + .background(Theme.surfaceAlt) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) } + .contentShape(Rectangle()) } + .buttonStyle(.plain) + .disabled(address.isEmpty) } private func createAccount() { diff --git a/apps/ios/Koins/Views/AddressQRSheet.swift b/apps/ios/Koins/Views/AddressQRSheet.swift new file mode 100644 index 0000000..6e6c1bd --- /dev/null +++ b/apps/ios/Koins/Views/AddressQRSheet.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct AddressQR: Identifiable { + let id = UUID() + let title: String + let address: String +} + +/// Drawer that shows one address as a scannable QR, presented from the bottom +/// like the transaction detail sheet. +struct AddressQRSheet: View { + var title: String + var address: String + var onDone: () -> Void + + @State private var copied = false + + var body: some View { + ZStack { + Theme.background.ignoresSafeArea() + VStack(spacing: 16) { + Text(title) + .font(.system(size: 16, weight: .semibold)) + .foregroundStyle(Theme.text) + .frame(maxWidth: .infinity, alignment: .leading) + + QRCodeView(text: address) + .frame(maxWidth: 220) + + Text(address) + .font(.system(size: 12, design: .monospaced)) + .foregroundStyle(Theme.muted) + .multilineTextAlignment(.center) + .lineLimit(4) + .truncationMode(.middle) + .textSelection(.enabled) + + SecondaryButton(title: copied ? "Copied" : "Copy address") { + UIPasteboard.general.string = address + copied = true + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { copied = false } + } + + PrimaryButton(title: "Done", action: onDone) + } + .padding(22) + .frame(maxHeight: .infinity, alignment: .top) + } + .preferredColorScheme(.dark) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + } +} \ No newline at end of file