Skip to content

Commit 5ae462e

Browse files
committed
merge #2497 review fixes, dedupe duplicated files
2 parents 09b5ec0 + abcc731 commit 5ae462e

15 files changed

Lines changed: 252 additions & 63 deletions

lib/features/announcements/announcements_locator.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import 'package:bb_mobile/core/wallet/domain/usecases/get_wallet_transactions_us
66
import 'package:bb_mobile/core/wallet/domain/usecases/watch_finished_wallet_syncs_usecase.dart';
77
import 'package:bb_mobile/features/announcements/data/announcement_dismissal_repository_impl.dart';
88
import 'package:bb_mobile/features/announcements/data/datasources/announcement_dismissal_datasource.dart';
9-
import 'package:bb_mobile/features/announcements/domain/dismiss_announcement_usecase.dart';
10-
import 'package:bb_mobile/features/announcements/domain/get_visible_announcements_usecase.dart';
9+
import 'package:bb_mobile/features/announcements/domain/usecases/dismiss_announcement_usecase.dart';
10+
import 'package:bb_mobile/features/announcements/domain/usecases/get_visible_announcements_usecase.dart';
1111
import 'package:bb_mobile/features/announcements/domain/repositories/announcement_dismissal_repository.dart';
1212
import 'package:bb_mobile/features/announcements/presentation/announcements_cubit.dart';
1313
import 'package:get_it/get_it.dart';

lib/features/announcements/data/announcement_dismissal_repository_impl.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class AnnouncementDismissalRepositoryImpl
2222

2323
@override
2424
Future<void> dismiss(AnnouncementId id) async {
25-
await _datasource.upsert(id.name, DateTime.now());
25+
// Persist in UTC, per the `dismissed_announcements.dismissedAt` contract.
26+
await _datasource.upsert(id.name, DateTime.now().toUtc());
2627
}
2728
}

lib/features/announcements/domain/entities/announcement.dart

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@ sealed class AnnouncementAction {
2828
const AnnouncementAction();
2929
}
3030

31-
/// Navigate to a named GoRouter route (by route *name*, not path, to avoid
32-
/// coupling to another feature's path layout).
31+
/// Tapping the announcement navigates somewhere. The concrete destination is
32+
/// resolved in the ui layer (`ui/announcement_navigation.dart`) from the
33+
/// [Announcement]'s id, so `domain/` never imports another feature's router.
3334
final class NavigateAction extends AnnouncementAction {
34-
final String routeName;
35-
36-
const NavigateAction(this.routeName)
37-
: assert(routeName != '', 'routeName must not be empty');
35+
const NavigateAction();
3836
}
3937

4038
/// How re-display works after the user dismisses an announcement.
@@ -53,8 +51,15 @@ final class PermanentDismiss extends DismissPolicy {
5351
final class SnoozeDismiss extends DismissPolicy {
5452
final Duration interval;
5553

56-
SnoozeDismiss(this.interval)
57-
: assert(interval.inMicroseconds > 0, 'snooze interval must be positive');
54+
SnoozeDismiss(this.interval) {
55+
if (interval.inMicroseconds <= 0) {
56+
throw ArgumentError.value(
57+
interval,
58+
'interval',
59+
'snooze interval must be positive',
60+
);
61+
}
62+
}
5863
}
5964

6065
/// A rich, self-validating announcement definition.
@@ -78,7 +83,11 @@ class Announcement {
7883
required this.tone,
7984
required this.action,
8085
required this.dismissPolicy,
81-
}) : assert(priority >= 0, 'priority must be non-negative');
86+
}) {
87+
if (priority < 0) {
88+
throw ArgumentError.value(priority, 'priority', 'must be non-negative');
89+
}
90+
}
8291

8392
/// Whether a dismissal recorded at [dismissedAt] still suppresses this
8493
/// announcement as of [now]. Permanent dismissals always suppress; snooze

lib/features/announcements/domain/entities/announcement_catalog.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'package:bb_mobile/features/announcements/domain/entities/announcement.dart';
2-
import 'package:bb_mobile/features/settings/ui/settings_router.dart';
32

43
/// The runtime signals a trigger can read to decide whether it fires.
54
///
@@ -45,7 +44,7 @@ final List<AnnouncementCatalogEntry> announcementCatalog = [
4544
id: AnnouncementId.payjoinPrivacy,
4645
priority: 0,
4746
tone: AnnouncementTone.info,
48-
action: NavigateAction(SettingsRoute.payjoinSettings.name),
47+
action: const NavigateAction(),
4948
dismissPolicy: const PermanentDismiss(),
5049
),
5150
// Show once the wallet has received/transacted (first UTXO or history after
@@ -57,7 +56,7 @@ final List<AnnouncementCatalogEntry> announcementCatalog = [
5756
id: AnnouncementId.autoswapActive,
5857
priority: 1,
5958
tone: AnnouncementTone.success,
60-
action: NavigateAction(SettingsRoute.autoswapSettings.name),
59+
action: const NavigateAction(),
6160
dismissPolicy: const PermanentDismiss(),
6261
),
6362
// Show while autoswap is enabled, letting the user learn what it does.

lib/features/announcements/domain/dismiss_announcement_usecase.dart renamed to lib/features/announcements/domain/usecases/dismiss_announcement_usecase.dart

File renamed without changes.

lib/features/announcements/domain/get_visible_announcements_usecase.dart renamed to lib/features/announcements/domain/usecases/get_visible_announcements_usecase.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ class GetVisibleAnnouncementsUsecase {
3030

3131
Future<Result<List<Announcement>, AnnouncementsFailure>> execute() async {
3232
try {
33-
final settings = await _settingsRepository.fetch();
34-
final transactions = await _getWalletTransactionsUsecase.execute();
35-
final autoSwap = await _getAutoSwapSettingsUsecase.execute();
36-
final dismissals = await _dismissalRepository.getDismissals();
33+
// The four sources are independent, so gather them concurrently.
34+
final (settings, transactions, autoSwap, dismissals) = await (
35+
_settingsRepository.fetch(),
36+
_getWalletTransactionsUsecase.execute(),
37+
_getAutoSwapSettingsUsecase.execute(),
38+
_dismissalRepository.getDismissals(),
39+
).wait;
3740

3841
final signals = AnnouncementSignals(
3942
isPayjoinEnabled: settings.isPayjoinEnabled,
@@ -42,7 +45,7 @@ class GetVisibleAnnouncementsUsecase {
4245
);
4346

4447
final dismissedAtById = {for (final d in dismissals) d.id: d.dismissedAt};
45-
final now = DateTime.now();
48+
final now = DateTime.now().toUtc();
4649

4750
final visible = <Announcement>[];
4851
for (final entry in announcementCatalog) {
@@ -60,7 +63,9 @@ class GetVisibleAnnouncementsUsecase {
6063
visible.sort((a, b) => a.priority.compareTo(b.priority));
6164
return Ok(visible);
6265
} catch (e) {
63-
return Err(AnnouncementStorageFailure(e.toString()));
66+
// Sources span settings/tx/autoswap/storage, so this is a genuine
67+
// catch-all rather than a storage-only failure.
68+
return Err(AnnouncementUnexpectedFailure(e.toString()));
6469
}
6570
}
6671
}

lib/features/announcements/presentation/announcements_cubit.dart

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import 'package:bb_mobile/core/settings/domain/watch_payjoin_enabled_changes_use
44
import 'package:bb_mobile/core/wallet/domain/entities/wallet.dart';
55
import 'package:bb_mobile/core/wallet/domain/usecases/watch_finished_wallet_syncs_usecase.dart';
66
import 'package:bb_mobile/features/announcements/domain/announcements_failure.dart';
7-
import 'package:bb_mobile/features/announcements/domain/dismiss_announcement_usecase.dart';
7+
import 'package:bb_mobile/features/announcements/domain/usecases/dismiss_announcement_usecase.dart';
88
import 'package:bb_mobile/features/announcements/domain/entities/announcement.dart';
9-
import 'package:bb_mobile/features/announcements/domain/get_visible_announcements_usecase.dart';
9+
import 'package:bb_mobile/features/announcements/domain/usecases/get_visible_announcements_usecase.dart';
1010
import 'package:flutter_bloc/flutter_bloc.dart';
1111
import 'package:freezed_annotation/freezed_annotation.dart';
1212

@@ -29,6 +29,9 @@ class AnnouncementsCubit extends Cubit<AnnouncementsState> {
2929
StreamSubscription<bool>? _payjoinEnabledSub;
3030
StreamSubscription<Wallet>? _walletSyncSub;
3131

32+
bool _refreshing = false;
33+
bool _refreshQueued = false;
34+
3235
AnnouncementsCubit({
3336
required this._getVisibleAnnouncementsUsecase,
3437
required this._dismissAnnouncementUsecase,
@@ -47,18 +50,37 @@ class AnnouncementsCubit extends Cubit<AnnouncementsState> {
4750

4851
/// (Re)loads the visible announcements. Called on mount and whenever a
4952
/// trigger signal changes.
53+
///
54+
/// Overlapping calls are coalesced: a request arriving while a load is in
55+
/// flight re-runs once after it completes, so several wallets syncing
56+
/// back-to-back can't spawn redundant, out-of-order loads.
5057
Future<void> refresh() async {
51-
final result = await _getVisibleAnnouncementsUsecase.execute();
52-
result.fold(
53-
(announcements) => emit(AnnouncementsState(announcements: announcements)),
54-
(failure) => emit(state.copyWith(failure: failure)),
55-
);
58+
if (_refreshing) {
59+
_refreshQueued = true;
60+
return;
61+
}
62+
_refreshing = true;
63+
try {
64+
do {
65+
_refreshQueued = false;
66+
final result = await _getVisibleAnnouncementsUsecase.execute();
67+
if (isClosed) return;
68+
result.fold(
69+
(announcements) =>
70+
emit(AnnouncementsState(announcements: announcements)),
71+
(failure) => emit(state.copyWith(failure: failure)),
72+
);
73+
} while (_refreshQueued);
74+
} finally {
75+
_refreshing = false;
76+
}
5677
}
5778

5879
/// Records a dismissal and refreshes the list (which collapses the section
5980
/// when the last card is dismissed).
6081
Future<void> dismiss(AnnouncementId id) async {
6182
final result = await _dismissAnnouncementUsecase.execute(id);
83+
if (isClosed) return;
6284
await result.fold(
6385
(_) => refresh(),
6486
(failure) async => emit(state.copyWith(failure: failure)),
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:bb_mobile/features/announcements/domain/entities/announcement.dart';
2+
import 'package:bb_mobile/features/settings/ui/settings_router.dart';
3+
4+
/// Resolves each announcement to the route its [NavigateAction] opens.
5+
///
6+
/// Lives in the ui layer so `domain/` never imports another feature's router
7+
/// (AGENTS.md rule #1 + Flutter-free domain). The exhaustive `switch` over the
8+
/// closed [AnnouncementId] enum makes a missing mapping a compile-time warning.
9+
extension AnnouncementNavigation on Announcement {
10+
SettingsRoute get route => switch (id) {
11+
AnnouncementId.payjoinPrivacy => SettingsRoute.payjoinSettings,
12+
AnnouncementId.autoswapActive => SettingsRoute.autoswapSettings,
13+
};
14+
}

lib/features/announcements/ui/widgets/announcement_carousel.dart

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import 'package:bb_mobile/core/widgets/snackbar_utils.dart';
12
import 'package:bb_mobile/features/announcements/domain/entities/announcement.dart';
23
import 'package:bb_mobile/features/announcements/presentation/announcements_cubit.dart';
4+
import 'package:bb_mobile/features/announcements/presentation/announcements_failure_l10n.dart';
5+
import 'package:bb_mobile/features/announcements/ui/announcement_navigation.dart';
36
import 'package:bb_mobile/features/announcements/ui/widgets/announcement_card.dart';
47
import 'package:bb_mobile/features/announcements/ui/widgets/announcement_dismiss_dialog.dart';
58
import 'package:bull_ui/bull_ui.dart';
@@ -16,26 +19,41 @@ class AnnouncementCarousel extends StatelessWidget {
1619

1720
@override
1821
Widget build(BuildContext context) {
19-
// Narrow rebuild: only when the visible set changes.
20-
return BlocSelector<
21-
AnnouncementsCubit,
22-
AnnouncementsState,
23-
List<Announcement>
24-
>(
25-
selector: (state) => state.announcements,
26-
builder: (context, announcements) {
27-
return AnimatedSize(
28-
duration: const Duration(milliseconds: 200),
29-
curve: Curves.easeInOut,
30-
alignment: Alignment.topCenter,
31-
child: announcements.isEmpty
32-
? const SizedBox(width: double.infinity)
33-
: Padding(
34-
padding: const EdgeInsets.only(left: 13, right: 13, top: 13),
35-
child: _CarouselBody(announcements: announcements),
36-
),
37-
);
38-
},
22+
return BlocListener<AnnouncementsCubit, AnnouncementsState>(
23+
// Surface a dismissal/refresh failure (the card otherwise just stays).
24+
// Fires only when a new failure appears, not on every rebuild.
25+
listenWhen: (previous, current) =>
26+
current.failure != null && previous.failure != current.failure,
27+
listener: (context, state) => SnackBarUtils.showSnackBar(
28+
context,
29+
state.failure!.toTranslated(context),
30+
),
31+
// Narrow rebuild: only when the visible set changes.
32+
child:
33+
BlocSelector<
34+
AnnouncementsCubit,
35+
AnnouncementsState,
36+
List<Announcement>
37+
>(
38+
selector: (state) => state.announcements,
39+
builder: (context, announcements) {
40+
return AnimatedSize(
41+
duration: const Duration(milliseconds: 200),
42+
curve: Curves.easeInOut,
43+
alignment: Alignment.topCenter,
44+
child: announcements.isEmpty
45+
? const SizedBox(width: double.infinity)
46+
: Padding(
47+
padding: const EdgeInsets.only(
48+
left: 13,
49+
right: 13,
50+
top: 13,
51+
),
52+
child: _CarouselBody(announcements: announcements),
53+
),
54+
);
55+
},
56+
),
3957
);
4058
}
4159
}
@@ -72,8 +90,8 @@ class _CarouselBodyState extends State<_CarouselBody> {
7290

7391
void _onTap(Announcement announcement) {
7492
switch (announcement.action) {
75-
case NavigateAction(:final routeName):
76-
context.pushNamed(routeName);
93+
case NavigateAction():
94+
context.pushNamed(announcement.route.name);
7795
}
7896
}
7997

lib/features/wallet/presentation/bloc/wallet_state.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,4 @@ sealed class WalletState with _$WalletState {
7979
liquidWallet.balanceSat.toInt(),
8080
);
8181
}
82-
83-
bool showAutoSwapActiveStatus() {
84-
return autoSwapSettings != null && autoSwapSettings!.enabled;
85-
}
8682
}

0 commit comments

Comments
 (0)