summaryrefslogtreecommitdiff
path: root/Rune/ViewModels
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-13 22:37:22 -0500
committerChristian Cleberg <[email protected]>2026-04-13 22:37:22 -0500
commit5dcfe0f147c7871eaf5f3a88bd9e9fded38d4633 (patch)
tree6bcbda4f02a131d3e3b870a3904ccd42f4af2f26 /Rune/ViewModels
parentfcf864a15b70e4ecb5bd789b1db3116221c34394 (diff)
downloadrune-5dcfe0f147c7871eaf5f3a88bd9e9fded38d4633.tar.gz
rune-5dcfe0f147c7871eaf5f3a88bd9e9fded38d4633.tar.bz2
rune-5dcfe0f147c7871eaf5f3a88bd9e9fded38d4633.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.swift93
-rw-r--r--Rune/ViewModels/SettingsViewModel.swift20
-rw-r--r--Rune/ViewModels/WalletViewModel.swift65
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
+ }
+ }
+}