diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 22:37:22 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 22:37:22 -0500 |
| commit | 5dcfe0f147c7871eaf5f3a88bd9e9fded38d4633 (patch) | |
| tree | 6bcbda4f02a131d3e3b870a3904ccd42f4af2f26 /Rune/ViewModels | |
| parent | fcf864a15b70e4ecb5bd789b1db3116221c34394 (diff) | |
| download | rune-1.2.0.tar.gz rune-1.2.0.tar.bz2 rune-1.2.0.zip | |
v1.2.0 domain management and wallet expansionv1.2.0
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.
Diffstat (limited to 'Rune/ViewModels')
| -rw-r--r-- | Rune/ViewModels/DomainViewModel.swift | 93 | ||||
| -rw-r--r-- | Rune/ViewModels/SettingsViewModel.swift | 20 | ||||
| -rw-r--r-- | Rune/ViewModels/WalletViewModel.swift | 65 |
3 files changed, 169 insertions, 9 deletions
diff --git a/Rune/ViewModels/DomainViewModel.swift b/Rune/ViewModels/DomainViewModel.swift index 1da1bbe..37fb7a1 100644 --- a/Rune/ViewModels/DomainViewModel.swift +++ b/Rune/ViewModels/DomainViewModel.swift @@ -7,16 +7,19 @@ final class DomainViewModel: ObservableObject { @Published private(set) var selectedDomain: Domain? @Published private(set) var records: [DNSRecord] = [] @Published private(set) var forwards: [EmailForward] = [] + @Published private(set) var glueRecords: [GlueRecord] = [] @Published private(set) var isLoadingDomains = false @Published private(set) var isLoadingDetail = false @Published private(set) var isLoadingRecords = false @Published private(set) var isLoadingForwards = false + @Published private(set) var isLoadingGlue = false @Published private(set) var isSaving = false @Published private(set) var hasLoadedDomains = false @Published var domainsErrorMessage: String? @Published var detailErrorMessage: String? @Published var recordsErrorMessage: String? @Published var forwardsErrorMessage: String? + @Published var glueErrorMessage: String? @Published var mutationErrorMessage: String? private var recordsRefreshTask: Task<Void, Never>? @@ -26,16 +29,19 @@ final class DomainViewModel: ObservableObject { selectedDomain = nil records = [] forwards = [] + glueRecords = [] isLoadingDomains = false isLoadingDetail = false isLoadingRecords = false isLoadingForwards = false + isLoadingGlue = false isSaving = false hasLoadedDomains = false domainsErrorMessage = nil detailErrorMessage = nil recordsErrorMessage = nil forwardsErrorMessage = nil + glueErrorMessage = nil mutationErrorMessage = nil stopAutoRefreshRecords() } @@ -167,6 +173,85 @@ final class DomainViewModel: ObservableObject { } } + func loadGlue(for domain: String, client: NjallaClient) async { + guard !isLoadingGlue else { return } + + isLoadingGlue = true + defer { + isLoadingGlue = false + } + + do { + glueRecords = try await client.listGlue(for: domain).sorted { $0.name < $1.name } + glueErrorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + glueErrorMessage = error.userFacingMessage + } + } + + func addGlue(for domain: String, name: String, address4: String?, address6: String?, client: NjallaClient) async throws { + guard !isSaving else { return } + + isSaving = true + mutationErrorMessage = nil + + do { + try await client.addGlue(for: domain, name: name, address4: address4, address6: address6) + isSaving = false + Task { + await loadGlue(for: domain, client: client) + } + } catch { + isSaving = false + mutationErrorMessage = error.userFacingMessage + throw error + } + } + + func editGlue(for domain: String, name: String, address4: String?, address6: String?, client: NjallaClient) async throws { + guard !isSaving else { return } + + isSaving = true + mutationErrorMessage = nil + + do { + try await client.editGlue(for: domain, name: name, address4: address4, address6: address6) + isSaving = false + Task { + await loadGlue(for: domain, client: client) + } + } catch { + isSaving = false + mutationErrorMessage = error.userFacingMessage + throw error + } + } + + func removeGlue(_ record: GlueRecord, client: NjallaClient) async throws { + guard !isSaving else { return } + + isSaving = true + mutationErrorMessage = nil + + do { + try await client.removeGlue(for: record.domain, name: record.name) + glueRecords.removeAll { $0.id == record.id } + isSaving = false + Task { + await loadGlue(for: record.domain, client: client) + } + } catch { + isSaving = false + mutationErrorMessage = error.userFacingMessage + throw error + } + } + private func fetchRecords(for domain: String, client: NjallaClient) async { guard !isLoadingRecords else { return } @@ -255,10 +340,12 @@ final class DomainViewModel: ObservableObject { do { let updatedRecords = try await client.removeRecord(record) - guard updatedRecords.contains(where: { $0.id == record.id }) == false else { - throw NjallaError.api(message: "The API response indicates the record was not removed.") + if updatedRecords.contains(where: { $0.id == record.id }) { + // Some API responses can be eventually consistent; treat successful call as authoritative. + records.removeAll { $0.id == record.id } + } else { + records = sortedRecords(updatedRecords) } - records = sortedRecords(updatedRecords) recordsErrorMessage = nil isSaving = false Task { diff --git a/Rune/ViewModels/SettingsViewModel.swift b/Rune/ViewModels/SettingsViewModel.swift index 6b773eb..c253ab8 100644 --- a/Rune/ViewModels/SettingsViewModel.swift +++ b/Rune/ViewModels/SettingsViewModel.swift @@ -83,12 +83,12 @@ final class SettingsViewModel: ObservableObject { } } - func logout() throws { + func logout() async throws { + if let client { + try await client.logout() + } try keychainManager.deleteToken() - client = nil - balance = nil - isAuthenticated = false - errorMessage = nil + clearLocalSession() } func logoutIfCurrentTokenDeleted(_ key: String) throws -> Bool { @@ -96,7 +96,15 @@ final class SettingsViewModel: ObservableObject { return false } - try logout() + try keychainManager.deleteToken() + clearLocalSession() return true } + + private func clearLocalSession() { + client = nil + balance = nil + isAuthenticated = false + errorMessage = nil + } } diff --git a/Rune/ViewModels/WalletViewModel.swift b/Rune/ViewModels/WalletViewModel.swift new file mode 100644 index 0000000..cbe0538 --- /dev/null +++ b/Rune/ViewModels/WalletViewModel.swift @@ -0,0 +1,65 @@ +import Combine +import Foundation + +@MainActor +final class WalletViewModel: ObservableObject { + @Published private(set) var transactions: [WalletTransaction] = [] + @Published private(set) var selectedPayment: WalletPayment? + @Published private(set) var isLoadingTransactions = false + @Published private(set) var isLoadingPayment = false + @Published var transactionsErrorMessage: String? + @Published var paymentErrorMessage: String? + + func reset() { + transactions = [] + selectedPayment = nil + isLoadingTransactions = false + isLoadingPayment = false + transactionsErrorMessage = nil + paymentErrorMessage = nil + } + + func loadTransactions(client: NjallaClient) async { + guard !isLoadingTransactions else { return } + + isLoadingTransactions = true + defer { + isLoadingTransactions = false + } + + do { + transactions = try await client.listTransactions().sorted { + ($0.date ?? "", $0.id) > ($1.date ?? "", $1.id) + } + transactionsErrorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + transactionsErrorMessage = error.userFacingMessage + } + } + + func loadPayment(id: String, client: NjallaClient) async { + guard !isLoadingPayment else { return } + + isLoadingPayment = true + defer { + isLoadingPayment = false + } + + do { + selectedPayment = try await client.getPayment(id: id) + paymentErrorMessage = nil + } catch is CancellationError { + return + } catch { + if (error as? URLError)?.code == .cancelled { + return + } + paymentErrorMessage = error.userFacingMessage + } + } +} |
