From c88d88180196fd6a40b368f1510e88b61040568e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Wed, 28 Jan 2026 18:16:45 -0800 Subject: [PATCH] Fix wallet view hanging on loading placeholder indefinitely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Features/Wallet/Models/WalletModel.swift | 19 +++++------- damus/Features/Wallet/Views/WalletView.swift | 29 ++++++++----------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/damus/Features/Wallet/Models/WalletModel.swift b/damus/Features/Wallet/Models/WalletModel.swift index 240b8bcc..dda05fda 100644 --- a/damus/Features/Wallet/Models/WalletModel.swift +++ b/damus/Features/Wallet/Models/WalletModel.swift @@ -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 } } diff --git a/damus/Features/Wallet/Views/WalletView.swift b/damus/Features/Wallet/Views/WalletView.swift index 2c967ceb..ae052567 100644 --- a/damus/Features/Wallet/Views/WalletView.swift +++ b/damus/Features/Wallet/Views/WalletView.swift @@ -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? = 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)) } } }