From 8e953520273e3d6ff906b6aa11230a570fddaad5 Mon Sep 17 00:00:00 2001 From: joshuakrueger-dfx Date: Thu, 4 Jun 2026 00:35:39 +0200 Subject: [PATCH] fix(dashboard): guard pending-transactions emits against post-close StateError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PendingTransactionsCubit fires an un-awaited fetch from its constructor and emits the result (or [] on error) with no isClosed guard. If the cubit is closed (page popped) before the fetch resolves, emit throws 'Cannot emit new states after calling close' — and the catch's emit([]) throws again, uncaught. Guard both emits with isClosed. Regression: test/screens/dashboard/pending_transactions_cubit_test.dart ("does not emit if closed before the fetch resolves") Issue #657 — Part 3, finding #16. --- .../bloc/pending_transactions_cubit.dart | 5 +++++ .../pending_transactions_cubit_test.dart | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/lib/screens/dashboard/bloc/pending_transactions_cubit.dart b/lib/screens/dashboard/bloc/pending_transactions_cubit.dart index 7db47c80..c34fc8b5 100644 --- a/lib/screens/dashboard/bloc/pending_transactions_cubit.dart +++ b/lib/screens/dashboard/bloc/pending_transactions_cubit.dart @@ -14,9 +14,14 @@ class PendingTransactionsCubit extends Cubit> { Future _loadPendingTransactions() async { try { final transactions = await _transactionHistoryService.fetchPendingTransactions(); + // The fetch is started in the constructor and not awaited, so the cubit + // can be closed (page popped) before it resolves. Guard the emit to avoid + // a StateError after close (issue #657 P3 #16). + if (isClosed) return; emit(transactions); } catch (e) { developer.log('Failed to load pending transactions: $e', name: '$PendingTransactionsCubit'); + if (isClosed) return; emit([]); } } diff --git a/test/screens/dashboard/pending_transactions_cubit_test.dart b/test/screens/dashboard/pending_transactions_cubit_test.dart index 2564da29..dc9e61c9 100644 --- a/test/screens/dashboard/pending_transactions_cubit_test.dart +++ b/test/screens/dashboard/pending_transactions_cubit_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:realunit_wallet/packages/service/dfx/models/transactions/dto/transactions_dto.dart'; @@ -36,6 +38,25 @@ void main() { expect(cubit.state, [tx1, tx2]); }); + test('does not emit (no StateError) if closed before the fetch resolves ' + '(issue #657 P3 #16 regression)', () async { + final completer = Completer>(); + when(() => service.fetchPendingTransactions()) + .thenAnswer((_) => completer.future); + + final cubit = PendingTransactionsCubit(service); + // Close while the constructor-started fetch is still in flight. + await cubit.close(); + + // Resolving now would, without the isClosed guard, emit after close and + // throw 'Cannot emit new states after calling close'. + completer.complete([_StubTx()]); + await Future.delayed(Duration.zero); + + // No exception escaped; the closed cubit kept its last state. + expect(cubit.isClosed, isTrue); + }); + test('falls back to an empty list when the service throws', () async { when(() => service.fetchPendingTransactions()) .thenAnswer((_) async => throw Exception('network'));