Skip to content

Commit fd4373e

Browse files
committed
feat: add announcements feature to exchange module
- Introduced GetAnnouncementsUsecase for fetching announcements. - Updated ExchangeLocator to register the new use case. - Implemented listAnnouncements method in ExchangeUserRepository. - Added API call in BullbitcoinApiDatasource to retrieve announcements. - Enhanced ExchangeCubit to load and manage announcements state. - Integrated AnnouncementBanner in ExchangeHomeScreen for display.
1 parent 3d78ddb commit fd4373e

13 files changed

Lines changed: 312 additions & 24 deletions

File tree

ios/Podfile.lock

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -260,45 +260,45 @@ EXTERNAL SOURCES:
260260
SPEC CHECKSUMS:
261261
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
262262
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
263-
ark_wallet: cb7a2af0c3a711d488709b7b91b6e639cfd441b1
264-
bdk_flutter: cef9180019b4c6b67a3e3dfb74611683fe140107
263+
ark_wallet: f985745c06c7cf93f368c61218c51ac56b46c71e
264+
bdk_flutter: 86c9ba59ee282dee08c3a29599abe867d10a8b10
265265
boltz: 6388ec2412f3753b63a9e65c97f87ea26f43bddc
266-
camera_avfoundation: 5675ca25298b6f81fa0a325188e7df62cc217741
267-
dart_bbqr: bfd89cc8a74538d94ef6d87d11e4a2ad55578e7d
266+
camera_avfoundation: 281867ff09f1da66f031a184ecfbc6f2e625c9f5
267+
dart_bbqr: 10143a83ef3919f9ac06974e3bbe23cac4abc39b
268268
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
269269
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
270-
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
270+
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
271271
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
272-
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
273-
flutter_nfc_kit: e1b71583eafd2c9650bc86844a7f2d185fb414f6
274-
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
275-
flutter_zxing: e8bcc43bd3056c70c271b732ed94e7a16fd62f93
276-
google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
272+
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
273+
flutter_nfc_kit: 3985c93f749b9cb4747479205c2f10bd2f877a11
274+
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
275+
flutter_zxing: d527c3ff9c7f3606dd29a7ec8c4055f7daa088c5
276+
google_sign_in_ios: 7411fab6948df90490dc4620ecbcabdc3ca04017
277277
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
278278
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
279279
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
280280
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
281-
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
282-
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
281+
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
282+
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
283283
lwk: 22e06bc5664247d6b2dac91cfe209b63b70dd580
284-
no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e
285-
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
286-
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
284+
no_screenshot: 67d110f12466f4913b488803d4e498d03ef2889e
285+
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
286+
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
287287
payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5
288-
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
288+
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
289289
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
290290
ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4
291291
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
292-
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
292+
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
293293
sqlite3: 8d708bc63e9f4ce48f0ad9d6269e478c5ced1d9b
294-
sqlite3_flutter_libs: d13b8b3003f18f596e542bcb9482d105577eff41
294+
sqlite3_flutter_libs: c7b32a4d17d2cbad19895873ccc815f276b1f125
295295
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
296-
tor: 767208930250ef7be241963b75568c55c0a81890
297-
universal_ble: ff19787898040d721109c6324472e5dd4bc86adc
298-
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
299-
webview_cookie_manager: d63a76cabdf42a7ea3d92768ac67d4853a1367f8
300-
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
301-
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
296+
tor: 662a9f5b980b5c86decb8ba611de9bcd4c8286eb
297+
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
298+
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
299+
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
300+
webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
301+
workmanager_apple: 7bac258335c310689a641e2d66e88d4845d372e9
302302

303303
PODFILE CHECKSUM: cc3fb9f3ad75d6f5db35855d6f129b8b2fb2d9f5
304304

lib/core/exchange/data/datasources/bullbitcoin_api_datasource.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class BullbitcoinApiDatasource implements BitcoinPriceDatasource {
2424
final _ordersPath = '/ak/api-orders';
2525
final _orderTriggerPath = '/ak/api-ordertrigger';
2626
final _recipientsPath = '/ak/api-recipients';
27+
final _messagesPath = '/ak/api-commcenter';
2728

2829
BullbitcoinApiDatasource({required Dio bullbitcoinApiHttpClient})
2930
: _http = bullbitcoinApiHttpClient;
@@ -583,6 +584,44 @@ class BullbitcoinApiDatasource implements BitcoinPriceDatasource {
583584

584585
return resp.data['result'] as Map<String, dynamic>;
585586
}
587+
588+
Future<List<Map<String, dynamic>>> listAnnouncements({
589+
required String apiKey,
590+
}) async {
591+
try {
592+
final resp = await _http.post(
593+
_messagesPath,
594+
data: {
595+
'jsonrpc': '2.0',
596+
'id': '1',
597+
'method': 'listAnnouncements',
598+
'params': {
599+
'paginator': {'pageSize': 5, 'page': 1},
600+
'sortBy': {'id': 'updatedAt', 'sort': 'desc'},
601+
},
602+
},
603+
options: Options(headers: {'X-API-Key': apiKey}),
604+
);
605+
606+
if (resp.statusCode == null || resp.statusCode != 200) {
607+
throw Exception('Failed to list announcements');
608+
}
609+
610+
final error = resp.data['error'];
611+
if (error != null) {
612+
throw Exception('Failed to list announcements: $error');
613+
}
614+
615+
final result = resp.data['result'] as Map<String, dynamic>?;
616+
if (result == null) {
617+
return [];
618+
}
619+
final items = result['elements'] as List<dynamic>? ?? [];
620+
return items.cast<Map<String, dynamic>>();
621+
} catch (e) {
622+
rethrow;
623+
}
624+
}
586625
}
587626

588627
class BullBitcoinApiMinAmountException extends BullException {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'package:bb_mobile/core/exchange/domain/entity/announcement.dart';
2+
import 'package:freezed_annotation/freezed_annotation.dart';
3+
4+
part 'announcement_model.freezed.dart';
5+
part 'announcement_model.g.dart';
6+
7+
@freezed
8+
sealed class AnnouncementModel with _$AnnouncementModel {
9+
const factory AnnouncementModel({
10+
required String title,
11+
required String description,
12+
required String updatedAt,
13+
}) = _AnnouncementModel;
14+
15+
factory AnnouncementModel.fromJson(Map<String, dynamic> json) =>
16+
_$AnnouncementModelFromJson(json);
17+
18+
const AnnouncementModel._();
19+
20+
Announcement toEntity() {
21+
return Announcement(
22+
title: title,
23+
description: description,
24+
updatedAt: DateTime.parse(updatedAt),
25+
);
26+
}
27+
}
28+

lib/core/exchange/data/repository/exchange_user_repository_impl.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import 'package:bb_mobile/core/errors/exchange_errors.dart';
22
import 'package:bb_mobile/core/exchange/data/datasources/bullbitcoin_api_datasource.dart';
33
import 'package:bb_mobile/core/exchange/data/datasources/bullbitcoin_api_key_datasource.dart';
44
import 'package:bb_mobile/core/exchange/data/mappers/user_summary_mapper.dart';
5+
import 'package:bb_mobile/core/exchange/data/models/announcement_model.dart';
56
import 'package:bb_mobile/core/exchange/data/models/user_preference_payload_model.dart';
7+
import 'package:bb_mobile/core/exchange/domain/entity/announcement.dart';
68
import 'package:bb_mobile/core/exchange/domain/entity/user_summary.dart';
79
import 'package:bb_mobile/core/exchange/domain/repositories/exchange_user_repository.dart';
810

@@ -90,4 +92,36 @@ class ExchangeUserRepositoryImpl implements ExchangeUserRepository {
9092
}
9193
}
9294
}
95+
96+
@override
97+
Future<List<Announcement>> listAnnouncements() async {
98+
try {
99+
final apiKey = await _bullbitcoinApiKeyDatasource.get(
100+
isTestnet: _isTestnet,
101+
);
102+
if (apiKey == null) {
103+
throw ApiKeyException(
104+
'API key not found. Please login to your Bull Bitcoin account.',
105+
);
106+
}
107+
try {
108+
final announcementDataList =
109+
await _bullbitcoinApiDatasource.listAnnouncements(
110+
apiKey: apiKey.key,
111+
);
112+
final announcements = announcementDataList
113+
.map((json) => AnnouncementModel.fromJson(json).toEntity())
114+
.toList();
115+
return announcements;
116+
} catch (e) {
117+
throw Exception('Failed to fetch announcements: $e');
118+
}
119+
} catch (e) {
120+
if (e is ApiKeyException) {
121+
rethrow;
122+
} else {
123+
throw Exception('Failed to fetch announcements: $e');
124+
}
125+
}
126+
}
93127
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import 'package:freezed_annotation/freezed_annotation.dart';
2+
3+
part 'announcement.freezed.dart';
4+
5+
@freezed
6+
sealed class Announcement with _$Announcement {
7+
const factory Announcement({
8+
required String title,
9+
required String description,
10+
required DateTime updatedAt,
11+
}) = _Announcement;
12+
13+
const Announcement._();
14+
}
15+

lib/core/exchange/domain/repositories/exchange_user_repository.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:bb_mobile/core/exchange/domain/entity/announcement.dart';
12
import 'package:bb_mobile/core/exchange/domain/entity/user_summary.dart';
23

34
abstract class ExchangeUserRepository {
@@ -8,4 +9,5 @@ abstract class ExchangeUserRepository {
89
bool? dcaEnabled,
910
String? autoBuyEnabled,
1011
});
12+
Future<List<Announcement>> listAnnouncements();
1113
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:bb_mobile/core/errors/bull_exception.dart';
2+
import 'package:bb_mobile/core/errors/exchange_errors.dart';
3+
import 'package:bb_mobile/core/exchange/domain/entity/announcement.dart';
4+
import 'package:bb_mobile/core/exchange/domain/repositories/exchange_user_repository.dart';
5+
import 'package:bb_mobile/core/settings/data/settings_repository.dart';
6+
7+
class GetAnnouncementsUsecase {
8+
final ExchangeUserRepository _mainnetExchangeUserRepository;
9+
final ExchangeUserRepository _testnetExchangeUserRepository;
10+
final SettingsRepository _settingsRepository;
11+
12+
GetAnnouncementsUsecase({
13+
required ExchangeUserRepository mainnetExchangeUserRepository,
14+
required ExchangeUserRepository testnetExchangeUserRepository,
15+
required SettingsRepository settingsRepository,
16+
}) : _mainnetExchangeUserRepository = mainnetExchangeUserRepository,
17+
_testnetExchangeUserRepository = testnetExchangeUserRepository,
18+
_settingsRepository = settingsRepository;
19+
20+
Future<List<Announcement>> execute() async {
21+
try {
22+
final settings = await _settingsRepository.fetch();
23+
final isTestnet = settings.environment.isTestnet;
24+
final repo =
25+
isTestnet
26+
? _testnetExchangeUserRepository
27+
: _mainnetExchangeUserRepository;
28+
final announcements = await repo.listAnnouncements();
29+
return announcements;
30+
} catch (e) {
31+
if (e is ApiKeyException) {
32+
rethrow;
33+
}
34+
throw GetAnnouncementsException('$e');
35+
}
36+
}
37+
}
38+
39+
class GetAnnouncementsException extends BullException {
40+
GetAnnouncementsException(super.message);
41+
}
42+

lib/core/exchange/exchange_locator.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:bb_mobile/core/exchange/domain/usecases/convert_sats_to_currency
2222
import 'package:bb_mobile/core/exchange/domain/usecases/delete_exchange_api_key_usecase.dart';
2323
import 'package:bb_mobile/core/exchange/domain/usecases/get_available_currencies_usecase.dart';
2424
import 'package:bb_mobile/core/exchange/domain/usecases/get_exchange_funding_details_usecase.dart';
25+
import 'package:bb_mobile/core/exchange/domain/usecases/get_announcements_usecase.dart';
2526
import 'package:bb_mobile/core/exchange/domain/usecases/get_exchange_user_summary_usecase.dart';
2627
import 'package:bb_mobile/core/exchange/domain/usecases/get_order_usercase.dart';
2728
import 'package:bb_mobile/core/exchange/domain/usecases/get_price_history_usecase.dart';
@@ -305,6 +306,18 @@ class ExchangeLocator {
305306
),
306307
);
307308

309+
locator.registerFactory<GetAnnouncementsUsecase>(
310+
() => GetAnnouncementsUsecase(
311+
mainnetExchangeUserRepository: locator<ExchangeUserRepository>(
312+
instanceName: 'mainnetExchangeUserRepository',
313+
),
314+
testnetExchangeUserRepository: locator<ExchangeUserRepository>(
315+
instanceName: 'testnetExchangeUserRepository',
316+
),
317+
settingsRepository: locator<SettingsRepository>(),
318+
),
319+
);
320+
308321
locator.registerFactory<CreateBuyOrderUsecase>(
309322
() => CreateBuyOrderUsecase(
310323
mainnetExchangeOrderRepository: locator<ExchangeOrderRepository>(

lib/features/exchange/exchange_locator.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:bb_mobile/core/exchange/domain/usecases/delete_exchange_api_key_usecase.dart';
2+
import 'package:bb_mobile/core/exchange/domain/usecases/get_announcements_usecase.dart';
23
import 'package:bb_mobile/core/exchange/domain/usecases/get_exchange_user_summary_usecase.dart';
34
import 'package:bb_mobile/core/exchange/domain/usecases/save_exchange_api_key_usecase.dart';
45
import 'package:bb_mobile/core/exchange/domain/usecases/save_user_preferences_usecase.dart';
@@ -18,6 +19,7 @@ class ExchangeLocator {
1819
locator.get<GetExchangeUserSummaryUsecase>(),
1920
saveUserPreferencesUsecase: locator.get<SaveUserPreferencesUsecase>(),
2021
deleteExchangeApiKeyUsecase: locator.get<DeleteExchangeApiKeyUsecase>(),
22+
getAnnouncementsUsecase: locator.get<GetAnnouncementsUsecase>(),
2123
),
2224
);
2325
}

lib/features/exchange/presentation/exchange_cubit.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22

33
import 'package:bb_mobile/core/errors/exchange_errors.dart';
44
import 'package:bb_mobile/core/exchange/domain/usecases/delete_exchange_api_key_usecase.dart';
5+
import 'package:bb_mobile/core/exchange/domain/usecases/get_announcements_usecase.dart';
56
import 'package:bb_mobile/core/exchange/domain/usecases/get_exchange_user_summary_usecase.dart';
67
import 'package:bb_mobile/core/exchange/domain/usecases/save_exchange_api_key_usecase.dart';
78
import 'package:bb_mobile/core/exchange/domain/usecases/save_user_preferences_usecase.dart';
@@ -16,10 +17,12 @@ class ExchangeCubit extends Cubit<ExchangeState> {
1617
required SaveExchangeApiKeyUsecase saveExchangeApiKeyUsecase,
1718
required SaveUserPreferencesUsecase saveUserPreferencesUsecase,
1819
required DeleteExchangeApiKeyUsecase deleteExchangeApiKeyUsecase,
20+
required GetAnnouncementsUsecase getAnnouncementsUsecase,
1921
}) : _getExchangeUserSummaryUsecase = getExchangeUserSummaryUsecase,
2022
_saveExchangeApiKeyUsecase = saveExchangeApiKeyUsecase,
2123
_saveUserPreferencesUsecase = saveUserPreferencesUsecase,
2224
_deleteExchangeApiKeyUsecase = deleteExchangeApiKeyUsecase,
25+
_getAnnouncementsUsecase = getAnnouncementsUsecase,
2326
super(const ExchangeState());
2427

2528
Timer? _pollingTimer;
@@ -28,6 +31,7 @@ class ExchangeCubit extends Cubit<ExchangeState> {
2831
final SaveExchangeApiKeyUsecase _saveExchangeApiKeyUsecase;
2932
final SaveUserPreferencesUsecase _saveUserPreferencesUsecase;
3033
final DeleteExchangeApiKeyUsecase _deleteExchangeApiKeyUsecase;
34+
final GetAnnouncementsUsecase _getAnnouncementsUsecase;
3135

3236
Future<void> fetchUserSummary() async {
3337
try {
@@ -41,6 +45,7 @@ class ExchangeCubit extends Cubit<ExchangeState> {
4145
emit(state.copyWith(selectedLanguage: userSummary.language));
4246
emit(state.copyWith(selectedCurrency: userSummary.currency));
4347

48+
loadAnnouncements();
4449
startPolling();
4550
} catch (e) {
4651
log.severe('$ExchangeCubit init: $e');
@@ -151,6 +156,29 @@ class ExchangeCubit extends Cubit<ExchangeState> {
151156
}
152157
}
153158

159+
void loadAnnouncements() async {
160+
emit(
161+
state.copyWith(loadingAnnouncements: true, errLoadingAnnouncements: null),
162+
);
163+
try {
164+
final announcements = await _getAnnouncementsUsecase.execute();
165+
emit(
166+
state.copyWith(
167+
announcements: announcements,
168+
loadingAnnouncements: false,
169+
),
170+
);
171+
} catch (e) {
172+
log.warning('Failed to load announcements: $e');
173+
emit(
174+
state.copyWith(
175+
errLoadingAnnouncements: e.toString(),
176+
loadingAnnouncements: false,
177+
),
178+
);
179+
}
180+
}
181+
154182
@override
155183
Future<void> close() {
156184
stopPolling();

0 commit comments

Comments
 (0)