Skip to content

Commit 8ecad6f

Browse files
authored
Merge pull request #2336 from SatoshiPortal/fix/recipients-not-loading
fix: recipients not loading
2 parents c3faac8 + 4e3d0e3 commit 8ecad6f

37 files changed

Lines changed: 314 additions & 221 deletions

lib/features/recipients/application/ports/recipients_gateway_port.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:bb_mobile/features/recipients/domain/entities/recipient.dart';
22
import 'package:bb_mobile/features/recipients/domain/value_objects/cad_biller.dart';
33
import 'package:bb_mobile/features/recipients/domain/value_objects/recipient_details.dart';
4+
import 'package:bb_mobile/features/recipients/domain/value_objects/recipient_type.dart';
45

56
abstract class RecipientsGatewayPort {
67
Future<Recipient> saveRecipient(
@@ -13,6 +14,9 @@ abstract class RecipientsGatewayPort {
1314
required bool isTestnet,
1415
int page = 1,
1516
int pageSize = 50,
17+
List<RecipientType>? recipientTypes,
18+
bool? isOwner,
19+
String? search,
1620
});
1721
Future<List<CadBiller>> listCadBillers({
1822
required String searchTerm,

lib/features/recipients/application/usecases/get_recipients_usecase.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
22
import 'package:bb_mobile/features/recipients/application/dtos/recipient_dto.dart';
33
import 'package:bb_mobile/features/recipients/application/ports/recipients_gateway_port.dart';
4+
import 'package:bb_mobile/features/recipients/domain/value_objects/recipient_type.dart';
45

56
class GetRecipientsParams {
67
final bool fiatOnly;
78
final int page;
89
final int pageSize;
10+
final List<RecipientType>? recipientTypes;
11+
final bool? isOwner;
12+
final String? search;
913

1014
GetRecipientsParams({
1115
this.fiatOnly = true,
1216
this.page = 1,
1317
this.pageSize = 50,
18+
this.recipientTypes,
19+
this.isOwner,
20+
this.search,
1421
});
1522
}
1623

@@ -45,6 +52,9 @@ class GetRecipientsUsecase {
4552
fiatOnly: params.fiatOnly,
4653
page: params.page,
4754
pageSize: params.pageSize,
55+
recipientTypes: params.recipientTypes,
56+
isOwner: params.isOwner,
57+
search: params.search,
4858
);
4959
return GetRecipientsResult(
5060
recipients:

lib/features/recipients/domain/value_objects/recipient_details.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ class BillPaymentCadDetails extends RecipientDetails {
103103
if (payeeCode.trim().isEmpty) {
104104
throw ArgumentError('Payee code cannot be empty');
105105
}
106-
if (payeeAccountNumber.trim().isEmpty) {
107-
throw ArgumentError('Payee account number cannot be empty');
108-
}
109106
return BillPaymentCadDetails._(
110107
label: label,
111108
isDefault: isDefault,
Lines changed: 93 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import 'dart:async';
2-
31
import 'package:bb_mobile/core/themes/app_theme.dart';
42
import 'package:bb_mobile/core/utils/build_context_x.dart';
3+
import 'package:bb_mobile/core/widgets/bb_refresh_indicator.dart';
54
import 'package:bb_mobile/core/widgets/buttons/button.dart';
65
import 'package:bb_mobile/features/recipients/frameworks/ui/widgets/jurisdiction_dropdown.dart';
76
import 'package:bb_mobile/features/recipients/frameworks/ui/widgets/recipients_list_tile.dart';
@@ -21,11 +20,7 @@ class RecipientsListTab extends StatefulWidget {
2120
}
2221

2322
class RecipientsListTabState extends State<RecipientsListTab> {
24-
String? _jurisdictionFilter;
25-
String _searchQuery = '';
26-
List<RecipientViewModel>? _recipients;
2723
RecipientViewModel? _selectedRecipient;
28-
late StreamSubscription<RecipientsState> _stateSubscription;
2924
late ScrollController _scrollController;
3025
late TextEditingController _searchController;
3126

@@ -35,26 +30,6 @@ class RecipientsListTabState extends State<RecipientsListTab> {
3530
_searchController = TextEditingController();
3631
_scrollController = ScrollController();
3732
_scrollController.addListener(_onScroll);
38-
39-
final bloc = context.read<RecipientsBloc>();
40-
// Listen for changes in the RecipientsBloc state to update the recipients list
41-
_stateSubscription = bloc.stream.listen((state) {
42-
setState(() {
43-
_recipients = _applyFilters(
44-
state.filteredRecipientsByJurisdiction(_jurisdictionFilter),
45-
);
46-
_jurisdictionFilter = state.availableJurisdictions.length == 1
47-
? state.availableJurisdictions.first
48-
: _jurisdictionFilter;
49-
});
50-
});
51-
// Initialize the recipients list
52-
_recipients = _applyFilters(
53-
bloc.state.filteredRecipientsByJurisdiction(_jurisdictionFilter),
54-
);
55-
_jurisdictionFilter = bloc.state.availableJurisdictions.length == 1
56-
? bloc.state.availableJurisdictions.first
57-
: _jurisdictionFilter;
5833
}
5934

6035
void _onScroll() {
@@ -64,38 +39,21 @@ class RecipientsListTabState extends State<RecipientsListTab> {
6439
}
6540
}
6641

67-
List<RecipientViewModel>? _applyFilters(
68-
List<RecipientViewModel>? recipients,
69-
) {
70-
if (recipients == null) return null;
71-
72-
if (_searchQuery.isEmpty) return recipients;
73-
74-
final searchLower = _searchQuery.toLowerCase();
75-
final filtered = recipients.where((recipient) {
76-
final displayName = recipient.displayName?.toLowerCase() ?? '';
77-
final label = recipient.label?.toLowerCase() ?? '';
78-
return displayName.contains(searchLower) || label.contains(searchLower);
79-
}).toList();
80-
81-
// If search returns no results and there are more recipients to load, trigger loading
82-
if (filtered.isEmpty &&
83-
_searchQuery.isNotEmpty &&
84-
context.read<RecipientsBloc>().state.hasMoreRecipientsToLoad) {
85-
// Schedule loading more recipients after this build completes
86-
WidgetsBinding.instance.addPostFrameCallback((_) {
87-
context.read<RecipientsBloc>().add(const RecipientsEvent.moreLoaded());
88-
});
89-
}
90-
91-
return filtered;
42+
Future<void> _onRefresh() async {
43+
final bloc = context.read<RecipientsBloc>();
44+
bloc.add(const RecipientsEvent.refreshed());
45+
// orElse guards against the bloc closing (route popped) before completion,
46+
// which would otherwise throw an unhandled StateError on this future.
47+
await bloc.stream.firstWhere(
48+
(state) => !state.isLoadingRecipients,
49+
orElse: () => bloc.state,
50+
);
9251
}
9352

9453
@override
9554
void dispose() {
9655
_searchController.dispose();
9756
_scrollController.dispose();
98-
_stateSubscription.cancel();
9957
super.dispose();
10058
}
10159

@@ -110,39 +68,28 @@ class RecipientsListTabState extends State<RecipientsListTab> {
11068
decoration: InputDecoration(
11169
hintText: context.loc.recipientsSearchHint,
11270
prefixIcon: const Icon(Icons.search),
113-
suffixIcon: _searchQuery.isNotEmpty
114-
? IconButton(
115-
icon: const Icon(Icons.clear),
116-
onPressed: () {
117-
setState(() {
71+
suffixIcon: ValueListenableBuilder<TextEditingValue>(
72+
valueListenable: _searchController,
73+
builder: (context, value, _) => value.text.isEmpty
74+
? const SizedBox.shrink()
75+
: IconButton(
76+
icon: const Icon(Icons.clear),
77+
onPressed: () {
11878
_searchController.clear();
119-
_searchQuery = '';
120-
_recipients = _applyFilters(
121-
context
122-
.read<RecipientsBloc>()
123-
.state
124-
.filteredRecipientsByJurisdiction(
125-
_jurisdictionFilter,
126-
),
79+
context.read<RecipientsBloc>().add(
80+
const RecipientsEvent.searchChanged(''),
12781
);
128-
});
129-
},
130-
)
131-
: null,
82+
},
83+
),
84+
),
13285
border: OutlineInputBorder(
13386
borderRadius: BorderRadius.circular(8.0),
13487
),
13588
),
13689
onChanged: (value) {
137-
setState(() {
138-
_searchQuery = value;
139-
_recipients = _applyFilters(
140-
context
141-
.read<RecipientsBloc>()
142-
.state
143-
.filteredRecipientsByJurisdiction(_jurisdictionFilter),
144-
);
145-
});
90+
context.read<RecipientsBloc>().add(
91+
RecipientsEvent.searchChanged(value),
92+
);
14693
},
14794
),
14895
const Gap(16.0),
@@ -151,54 +98,35 @@ class RecipientsListTabState extends State<RecipientsListTab> {
15198
style: context.font.bodyMedium,
15299
),
153100
const Gap(8.0),
154-
JurisdictionsDropdown(
155-
selectedJurisdiction: _jurisdictionFilter,
156-
includeAllOption: true,
157-
onChanged: (newJurisdiction) {
158-
setState(() {
159-
_jurisdictionFilter = newJurisdiction;
160-
_recipients = _applyFilters(
161-
context
162-
.read<RecipientsBloc>()
163-
.state
164-
.filteredRecipientsByJurisdiction(newJurisdiction),
165-
);
166-
_selectedRecipient =
167-
newJurisdiction == null ||
168-
_selectedRecipient?.jurisdictionCode == newJurisdiction
169-
? _selectedRecipient
170-
: null;
171-
});
101+
BlocSelector<RecipientsBloc, RecipientsState, String?>(
102+
selector: (state) => state.jurisdictionFilter,
103+
builder: (context, selected) {
104+
return JurisdictionsDropdown(
105+
selectedJurisdiction: selected,
106+
includeAllOption: true,
107+
onChanged: (newJurisdiction) {
108+
context.read<RecipientsBloc>().add(
109+
RecipientsEvent.jurisdictionChanged(newJurisdiction),
110+
);
111+
if (newJurisdiction != null &&
112+
_selectedRecipient?.jurisdictionCode != newJurisdiction) {
113+
setState(() => _selectedRecipient = null);
114+
}
115+
},
116+
);
172117
},
173118
),
174119
const Gap(16.0),
175120
Expanded(
176-
child: _recipients == null
177-
? const Center(child: CircularProgressIndicator())
178-
: _recipients!.isEmpty
179-
? Center(
180-
child: Text(
181-
context.loc.recipientsListEmpty,
182-
style: context.font.bodyLarge,
183-
),
184-
)
185-
: ListView.builder(
186-
controller: _scrollController,
187-
itemBuilder: (context, index) {
188-
final recipient = _recipients![index];
189-
return RecipientsListTile(
190-
recipient: recipient,
191-
selected: _selectedRecipient == recipient,
192-
onTap: () {
193-
setState(() {
194-
_selectedRecipient = recipient;
195-
});
196-
},
197-
);
198-
},
199-
shrinkWrap: true,
200-
itemCount: _recipients!.length,
201-
),
121+
child: BlocBuilder<RecipientsBloc, RecipientsState>(
122+
builder: (context, state) {
123+
final recipients = state.selectableRecipients ?? const [];
124+
return BBRefreshIndicator(
125+
onRefresh: _onRefresh,
126+
child: _buildListContent(state, recipients),
127+
);
128+
},
129+
),
202130
),
203131
if (widget.hookError != null)
204132
Padding(
@@ -240,4 +168,47 @@ class RecipientsListTabState extends State<RecipientsListTab> {
240168
],
241169
);
242170
}
171+
172+
Widget _buildListContent(
173+
RecipientsState state,
174+
List<RecipientViewModel> recipients,
175+
) {
176+
if (recipients.isEmpty) {
177+
if (state.isLoadingRecipients) {
178+
return const Center(child: CircularProgressIndicator());
179+
}
180+
181+
final message = state.failedToLoadRecipients != null
182+
? context.loc.recipientsListLoadError
183+
: context.loc.recipientsListEmpty;
184+
// Wrap in a scrollable so pull-to-refresh works while empty.
185+
return ListView(
186+
physics: const AlwaysScrollableScrollPhysics(),
187+
children: [
188+
const Gap(96.0),
189+
Center(
190+
child: Text(message, style: context.font.bodyLarge),
191+
),
192+
],
193+
);
194+
}
195+
196+
return ListView.builder(
197+
controller: _scrollController,
198+
physics: const AlwaysScrollableScrollPhysics(),
199+
itemBuilder: (context, index) {
200+
final recipient = recipients[index];
201+
return RecipientsListTile(
202+
recipient: recipient,
203+
selected: _selectedRecipient == recipient,
204+
onTap: () {
205+
setState(() {
206+
_selectedRecipient = recipient;
207+
});
208+
},
209+
);
210+
},
211+
itemCount: recipients.length,
212+
);
213+
}
243214
}

lib/features/recipients/frameworks/ui/widgets/jurisdiction_dropdown.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ class JurisdictionsDropdown extends StatelessWidget {
1818

1919
@override
2020
Widget build(BuildContext context) {
21-
// Get the possible jurisdictions
22-
final jurisdictions = context.select(
23-
(RecipientsBloc bloc) => bloc.state.availableJurisdictions,
21+
final filters = context.select(
22+
(RecipientsBloc bloc) => bloc.state.allowedRecipientFilters,
2423
);
24+
final jurisdictions = filters.types
25+
.map((type) => type.jurisdictionCode)
26+
.toSet();
2527

2628
return Material(
2729
elevation: 4,

0 commit comments

Comments
 (0)