From 5dcfe0f147c7871eaf5f3a88bd9e9fded38d4633 Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Mon, 13 Apr 2026 22:37:22 -0500 Subject: v1.2.0 domain management and wallet expansion Complete v1.1-aligned domain flows with forwards and glue CRUD, improve API payload resilience, and add wallet transaction/payment views. Harden error handling and UX states across domain screens, remove out-of-scope DNSSEC/renewal record flows for now, and keep auth/session behavior aligned with token-based usage. --- Rune/Views/Settings/SettingsView.swift | 19 ++++-- Rune/Views/Settings/WalletPaymentDetailView.swift | 45 ++++++++++++++ Rune/Views/Settings/WalletTransactionsView.swift | 75 +++++++++++++++++++++++ 3 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 Rune/Views/Settings/WalletPaymentDetailView.swift create mode 100644 Rune/Views/Settings/WalletTransactionsView.swift (limited to 'Rune/Views/Settings') diff --git a/Rune/Views/Settings/SettingsView.swift b/Rune/Views/Settings/SettingsView.swift index 4d07887..a776956 100644 --- a/Rune/Views/Settings/SettingsView.swift +++ b/Rune/Views/Settings/SettingsView.swift @@ -2,6 +2,7 @@ import SwiftUI struct SettingsView: View { @ObservedObject var viewModel: SettingsViewModel + @ObservedObject var walletViewModel: WalletViewModel let onLogout: () -> Void @State private var showingLogoutConfirmation = false @@ -33,6 +34,12 @@ struct SettingsView: View { } } .disabled(viewModel.client == nil || viewModel.isLoadingBalance) + + if let client = viewModel.client { + NavigationLink("Transactions") { + WalletTransactionsView(viewModel: walletViewModel, client: client) + } + } } Section("Account") { @@ -57,11 +64,13 @@ struct SettingsView: View { titleVisibility: .visible ) { Button("Log Out", role: .destructive) { - do { - try viewModel.logout() - onLogout() - } catch { - viewModel.errorMessage = error.localizedDescription + Task { + do { + try await viewModel.logout() + onLogout() + } catch { + viewModel.errorMessage = error.localizedDescription + } } } diff --git a/Rune/Views/Settings/WalletPaymentDetailView.swift b/Rune/Views/Settings/WalletPaymentDetailView.swift new file mode 100644 index 0000000..f5ef605 --- /dev/null +++ b/Rune/Views/Settings/WalletPaymentDetailView.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct WalletPaymentDetailView: View { + let transactionID: String + @ObservedObject var viewModel: WalletViewModel + let client: NjallaClient + + var body: some View { + Group { + if viewModel.isLoadingPayment && viewModel.selectedPayment == nil { + ProgressView("Loading Payment") + } else if let payment = viewModel.selectedPayment { + List { + Section("Payment") { + detailRow(label: "ID", value: payment.id ?? transactionID) + detailRow(label: "Status", value: payment.status ?? "Not available") + detailRow(label: "Amount", value: payment.amount.map { "€\($0)" } ?? "Not available") + detailRow(label: "Address", value: payment.address ?? "Not available") + detailRow(label: "URL", value: payment.url ?? "Not available") + } + } + .listStyle(.insetGrouped) + } else if let errorMessage = viewModel.paymentErrorMessage { + ContentUnavailableView("Payment Unavailable", systemImage: "exclamationmark.triangle", description: Text(errorMessage)) + } else { + ContentUnavailableView("Payment Unavailable", systemImage: "creditcard", description: Text("No payment details were returned for this transaction.")) + } + } + .navigationTitle("Payment") + .navigationBarTitleDisplayMode(.inline) + .task { + await viewModel.loadPayment(id: transactionID, client: client) + } + } + + private func detailRow(label: String, value: String) -> some View { + HStack { + Text(label) + Spacer() + Text(value) + .foregroundStyle(.secondary) + .multilineTextAlignment(.trailing) + } + } +} diff --git a/Rune/Views/Settings/WalletTransactionsView.swift b/Rune/Views/Settings/WalletTransactionsView.swift new file mode 100644 index 0000000..f0106de --- /dev/null +++ b/Rune/Views/Settings/WalletTransactionsView.swift @@ -0,0 +1,75 @@ +import SwiftUI + +struct WalletTransactionsView: View { + @ObservedObject var viewModel: WalletViewModel + let client: NjallaClient + + var body: some View { + List { + if let errorMessage = viewModel.transactionsErrorMessage { + Section { + InlineErrorView(message: errorMessage, retryTitle: "Retry Transactions") { + Task { + await viewModel.loadTransactions(client: client) + } + } + .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) + } + } + + if viewModel.isLoadingTransactions && viewModel.transactions.isEmpty { + Section { + HStack { + Spacer() + ProgressView("Loading Transactions") + Spacer() + } + } + } else if viewModel.transactions.isEmpty { + Section { + ContentUnavailableView( + "No Transactions", + systemImage: "eurosign.circle", + description: Text("No wallet transactions were returned for this account.") + ) + } + } else { + ForEach(viewModel.transactions) { transaction in + NavigationLink { + WalletPaymentDetailView(transactionID: transaction.id, viewModel: viewModel, client: client) + } label: { + VStack(alignment: .leading, spacing: 4) { + Text(transaction.type ?? "Transaction") + .font(.headline) + Text(transactionDateText(transaction)) + .font(.subheadline) + .foregroundStyle(.secondary) + if let amount = transaction.amount { + Text("Amount: €\(amount)") + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 4) + } + } + } + } + .listStyle(.insetGrouped) + .navigationTitle("Transactions") + .navigationBarTitleDisplayMode(.inline) + .task { + await viewModel.loadTransactions(client: client) + } + .refreshable { + await viewModel.loadTransactions(client: client) + } + } + + private func transactionDateText(_ transaction: WalletTransaction) -> String { + if let date = transaction.date, !date.isEmpty { + return date + } + return "ID: \(transaction.id)" + } +} -- cgit v1.2.3