Fix wallet view hanging on loading placeholder indefinitely

Resolves a race condition in wallet data fetching that caused views to
hang on loading placeholders. The issue occurred due to:

1. Multiple state updates triggering view re-renders mid-fetch
2. Refreshable tasks getting cancelled before completion

Changes:
- Remove premature state reset in refreshWalletInformation()
- Atomically update balance and transactions together after fetching
- Replace onAppear + manual task cancellation with SwiftUI .task modifier
- Simplify refresh flow to use proper async/await without explicit task management

This ensures the wallet view completes data loading in a single atomic
operation, preventing intermediate loading states from persisting.

Closes: https://github.com/damus-io/damus/issues/2999
Changelog-Fixed: Wallet view no longer hangs on loading placeholder
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2026-01-28 18:16:45 -08:00
parent fa4b7a7518
commit c88d881801
2 changed files with 19 additions and 29 deletions

View File

@@ -122,25 +122,20 @@ class WalletModel: ObservableObject {
func refreshWalletInformation() async throws {
await self.resetWalletStateInformation()
// Implementation note: Do not reset wallet information here
// This is important to avoid re-rendering the view twice (waste),
// and to avoid refreshable tasks to be cancelled before updating everything
try await loadWalletInformation()
}
func loadWalletInformation() async throws {
try await loadBalance()
try await loadTransactionList()
}
func loadBalance() async throws {
// Implementation note: Get all needed info first, then atomically set the new state.
// This is important to avoid re-rendering the view twice (waste),
// and to avoid refreshable tasks to be cancelled before updating everything
let balance = try await fetchBalance()
DispatchQueue.main.async {
self.balance = balance
}
}
func loadTransactionList() async throws {
let transactions = try await fetchTransactions(from: nil, until: nil, limit: 50, offset: 0, unpaid: false, type: "")
DispatchQueue.main.async {
self.balance = balance
self.transactions = transactions
}
}

View File

@@ -16,7 +16,6 @@ struct WalletView: View {
@ObservedObject var model: WalletModel
@ObservedObject var settings: UserSettingsStore
@State private var showBalance: Bool = false
@State private var walletRefreshTask: Task<Void, Never>? = nil
init(damus_state: DamusState, model: WalletModel? = nil) {
self.damus_state = damus_state
@@ -104,11 +103,11 @@ struct WalletView: View {
)
}
}
.onAppear() {
self.refreshWalletInformation()
.task {
await self.refreshWalletInformation()
}
.refreshable {
self.refreshWalletInformation()
await self.refreshWalletInformation()
}
.sheet(isPresented: $show_settings, onDismiss: { self.show_settings = false }) {
ScrollView {
@@ -126,20 +125,16 @@ struct WalletView: View {
}
}
@MainActor
func refreshWalletInformation() {
walletRefreshTask?.cancel()
walletRefreshTask = Task {
do {
try await self.model.refreshWalletInformation()
}
catch {
guard let error = error as? ErrorView.UserPresentableErrorProtocol else {
Log.error("Error while refreshing wallet: %s", for: .nwc, error.localizedDescription)
return
}
present_sheet(.error(error.userPresentableError))
func refreshWalletInformation() async {
do {
try await self.model.refreshWalletInformation()
}
catch {
guard let error = error as? ErrorView.UserPresentableErrorProtocol else {
Log.error("Error while refreshing wallet: %s", for: .nwc, error.localizedDescription)
return
}
present_sheet(.error(error.userPresentableError))
}
}
}