Skip to content

Commit bd6230a

Browse files
authored
refactor(cat-voices): delete keychain dialog (#3618)
* wip * wip * chore: show dialog on proper page * chore: remove old way of showing confirmation dialog * chore: cleanup * fix: format * chore: review update * fix: format * chore: rename dialog to buildDialog
1 parent c95310e commit bd6230a

File tree

12 files changed

+222
-200
lines changed

12 files changed

+222
-200
lines changed

catalyst_voices/README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,26 @@
55
This repository contains the Catalyst Voices app and packages.
66

77
* [Catalyst Voices](#catalyst-voices)
8-
* [Requirements](#requirements)
9-
* [Recommended VS code plugins](#recommended-vs-code-plugins)
10-
* [Platforms](#platforms)
11-
* [Getting Started](#getting-started)
12-
* [Bootstrapping](#bootstrapping)
13-
* [Packages](#packages)
14-
* [Environment Type vs Flavor](#environment-type-vs-flavor)
15-
* [Environment types](#environment-types)
16-
* [Stress Test](#stress-test)
17-
* [Debug Performance Flags](#debug-performance-flags)
18-
* [Flavor types](#flavor-types)
19-
* [Environment variables](#environment-variables)
20-
* [Environment config](#environment-config)
8+
* [Requirements](#requirements)
9+
* [Recommended VS code plugins](#recommended-vs-code-plugins)
10+
* [Platforms](#platforms)
11+
* [Getting Started](#getting-started)
12+
* [Bootstrapping](#bootstrapping)
13+
* [Packages](#packages)
14+
* [Environment Type vs Flavor](#environment-type-vs-flavor)
15+
* [Environment types](#environment-types)
16+
* [Stress Test](#stress-test)
17+
* [Debug Performance Flags](#debug-performance-flags)
18+
* [Flavor types](#flavor-types)
19+
* [Environment variables](#environment-variables)
20+
* [Environment config](#environment-config)
2121
* [Code Generation](#code-generation)
22-
* [Running Code Generation](#running-code-generation)
23-
* [Basic Generation](#basic-generation)
24-
* [Local Saving](#local-saving)
25-
* [GitHub Token / PAT Setup](#github-token--pat-setup)
26-
* [Security Notes](#security-notes)
27-
* [Running Tests](#running-tests)
22+
*[Running Code Generation](#running-code-generation)
23+
* [Basic Generation](#basic-generation)
24+
*[Local Saving](#local-saving)
25+
* [GitHub Token / PAT Setup](#github-token--pat-setup)
26+
* [Security Notes](#security-notes)
27+
* [Running Tests](#running-tests)
2828

2929
## Requirements
3030

catalyst_voices/apps/voices/lib/notification/catalyst_messenger.dart

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:catalyst_voices/notification/banner_close_button.dart';
55
import 'package:catalyst_voices/notification/banner_content.dart';
66
import 'package:catalyst_voices/notification/catalyst_notification.dart';
77
import 'package:catalyst_voices/routes/app_router_factory.dart';
8+
import 'package:catalyst_voices/widgets/modals/voices_dialog.dart';
89
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
910
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
1011
import 'package:flutter/material.dart';
@@ -37,9 +38,12 @@ class CatalystMessenger extends StatefulWidget {
3738
}
3839

3940
class CatalystMessengerState extends State<CatalystMessenger> {
40-
final _pending = <CatalystNotification>[];
41+
final _pendingDialogs = <DialogNotification>[];
42+
final _pendingBanners = <BannerNotification>[];
4143
bool _isShowingBanner = false;
44+
bool _isShowingDialog = false;
4245
BannerNotification? _activeBanner;
46+
DialogNotification? _activeDialog;
4347

4448
GoRouter? __router;
4549

@@ -50,18 +54,26 @@ class CatalystMessengerState extends State<CatalystMessenger> {
5054
/// Adds a notification to the queue if it is not already present.
5155
///
5256
/// This method ensures that duplicate notifications are not added to the queue.
53-
/// If the notification already exists in the `_pending` queue, it logs a message
54-
/// and skips adding it. Otherwise, the notification is added to the queue in a
55-
/// sorted order, and the queue is processed to display notifications.
57+
/// Notifications are routed to the appropriate queue (dialogs or banners)
58+
/// and sorted by priority within each queue.
5659
void add(CatalystNotification notification) {
57-
if (_pending.contains(notification)) {
58-
_logger.fine('$notification already in queue, skipping add');
59-
return;
60-
}
61-
6260
_logger.finest('Adding $notification to queue');
6361

64-
_addSorted(notification);
62+
switch (notification) {
63+
case DialogNotification():
64+
if (_pendingDialogs.any((n) => n.id == notification.id)) {
65+
_logger.fine('$notification already in dialogs queue, skipping add');
66+
return;
67+
}
68+
_addToQueue(notification);
69+
case BannerNotification():
70+
if (_pendingBanners.any((n) => n.id == notification.id)) {
71+
_logger.fine('$notification already in banners queue, skipping add');
72+
return;
73+
}
74+
_addToQueue(notification);
75+
}
76+
6577
_processQueue();
6678
}
6779

@@ -71,7 +83,13 @@ class CatalystMessengerState extends State<CatalystMessenger> {
7183
}
7284

7385
void cancelWhere(CatalystNotificationPredicate test) {
74-
_pending.removeWhere(test);
86+
_pendingDialogs.removeWhere(test);
87+
_pendingBanners.removeWhere(test);
88+
89+
final activeDialog = _activeDialog;
90+
if (activeDialog != null && test(activeDialog)) {
91+
_hideCurrentDialog();
92+
}
7593

7694
final activeBanner = _activeBanner;
7795
if (activeBanner != null && test(activeBanner)) {
@@ -87,10 +105,18 @@ class CatalystMessengerState extends State<CatalystMessenger> {
87105
super.dispose();
88106
}
89107

90-
void _addSorted(CatalystNotification notification) {
91-
_pending
92-
..add(notification)
93-
..sort();
108+
void _addToQueue(CatalystNotification notification) {
109+
switch (notification) {
110+
case BannerNotification():
111+
_pendingBanners
112+
..add(notification)
113+
..sort();
114+
break;
115+
case DialogNotification():
116+
_pendingDialogs
117+
..add(notification)
118+
..sort();
119+
}
94120
}
95121

96122
GoRouter _findRouter() {
@@ -103,16 +129,24 @@ class CatalystMessengerState extends State<CatalystMessenger> {
103129
void _handleRouterChange() {
104130
final routerState = _router.state;
105131

132+
// Handle active dialog
133+
final activeDialog = _activeDialog;
134+
if (activeDialog != null && !activeDialog.routerPredicate(routerState)) {
135+
_logger.finer('Hiding dialog(${activeDialog.id}). Not valid for router state');
136+
_addToQueue(activeDialog);
137+
_hideCurrentDialog();
138+
}
139+
106140
// Handle active banner
107141
final activeBanner = _activeBanner;
108142
if (activeBanner != null && !activeBanner.routerPredicate(routerState)) {
109143
_logger.finer('Hiding banner(${activeBanner.id}). Not valid for router state');
110-
_addSorted(activeBanner);
144+
_addToQueue(activeBanner);
111145
_hideCurrentBanner();
112146
}
113147

114148
// Process queue if there are pending notifications
115-
if (_pending.isNotEmpty) {
149+
if (_pendingDialogs.isNotEmpty || _pendingBanners.isNotEmpty) {
116150
_processQueue();
117151
}
118152
}
@@ -126,6 +160,15 @@ class CatalystMessengerState extends State<CatalystMessenger> {
126160
messengerState.removeCurrentMaterialBanner(reason: MaterialBannerClosedReason.hide);
127161
}
128162

163+
/// Hiding current dialog will trigger _onDialogCompleted and process queue.
164+
void _hideCurrentDialog() {
165+
final navigatorContext = _router.routerDelegate.navigatorKey.currentContext;
166+
if (navigatorContext == null) {
167+
return;
168+
}
169+
Navigator.of(navigatorContext, rootNavigator: true).pop();
170+
}
171+
129172
void _onBannerCompleted() {
130173
assert(_activeBanner != null, 'Completed banner but active was null');
131174
final activeBanner = _activeBanner!;
@@ -138,29 +181,56 @@ class CatalystMessengerState extends State<CatalystMessenger> {
138181
_processQueue();
139182
}
140183

184+
void _onDialogCompleted() {
185+
assert(_activeDialog != null, 'Completed dialog but active was null');
186+
final activeDialog = _activeDialog!;
187+
188+
_logger.finer('Completed dialog $activeDialog');
189+
190+
_isShowingDialog = false;
191+
_activeDialog = null;
192+
193+
_processQueue();
194+
}
195+
141196
void _processQueue() {
142197
final routerState = _router.state;
143-
final allowed = _pending.where((notification) => notification.routerPredicate(routerState));
144198

145-
if (allowed.isEmpty) {
146-
if (_pending.isNotEmpty) {
147-
_logger.finest('Found ${_pending.length} notification but none allow for router state');
199+
// Filter notifications that are allowed for current router state
200+
final allowedDialogs = _pendingDialogs.where((n) => n.routerPredicate(routerState));
201+
final allowedBanners = _pendingBanners.where((n) => n.routerPredicate(routerState));
202+
203+
if (allowedDialogs.isEmpty && allowedBanners.isEmpty) {
204+
final totalPending = _pendingDialogs.length + _pendingBanners.length;
205+
if (totalPending > 0) {
206+
_logger.finest('Found $totalPending notification(s) but none allow for router state');
148207
}
149208
return;
150209
}
151210

152-
// Process banners
153-
if (!_isShowingBanner) {
154-
final banner = allowed.whereType<BannerNotification>().firstOrNull;
155-
if (banner != null) {
156-
_pending.removeWhere((element) => element.id == banner.id);
157-
_activeBanner = banner;
158-
_isShowingBanner = true;
211+
// Get next notification respecting priority across both queues
212+
final nextDialog = allowedDialogs.firstOrNull;
213+
final nextBanner = allowedBanners.firstOrNull;
159214

160-
_logger.finer('Showing banner $banner');
215+
// Banners and dialog can be shown at the same time
216+
if (!_isShowingDialog && nextDialog != null) {
217+
_pendingDialogs.removeWhere((element) => element.id == nextDialog.id);
218+
_activeDialog = nextDialog;
219+
_isShowingDialog = true;
161220

162-
unawaited(_showBanner(banner).whenComplete(_onBannerCompleted));
163-
}
221+
_logger.finer('Showing dialog $nextDialog');
222+
223+
unawaited(_showDialog(nextDialog).whenComplete(_onDialogCompleted));
224+
}
225+
226+
if (!_isShowingBanner && nextBanner != null) {
227+
_pendingBanners.removeWhere((element) => element.id == nextBanner.id);
228+
_activeBanner = nextBanner;
229+
_isShowingBanner = true;
230+
231+
_logger.finer('Showing banner $nextBanner');
232+
233+
unawaited(_showBanner(nextBanner).whenComplete(_onBannerCompleted));
164234
}
165235
}
166236

@@ -190,4 +260,37 @@ class CatalystMessengerState extends State<CatalystMessenger> {
190260
},
191261
);
192262
}
263+
264+
Future<void> _showDialog(DialogNotification notification) async {
265+
// Wait for all pending frames to complete. Navigation (especially with
266+
// redirects) can take multiple frames. By waiting while Flutter has
267+
// scheduled frames, we ensure navigation has fully completed.
268+
await WidgetsBinding.instance.endOfFrame;
269+
270+
// Check if the dialog is still active after navigation
271+
if (_activeDialog?.id != notification.id) {
272+
return;
273+
}
274+
275+
// Verify the dialog is still allowed for the current route
276+
if (!notification.routerPredicate(_router.state)) {
277+
_logger.finer('Dialog ${notification.id} no longer valid for current route after navigation');
278+
return;
279+
}
280+
281+
final navigatorContext = _router.routerDelegate.navigatorKey.currentContext;
282+
if (navigatorContext == null || !navigatorContext.mounted) {
283+
return;
284+
}
285+
286+
final widget = notification.buildDialog(navigatorContext);
287+
288+
if (navigatorContext.mounted) {
289+
await VoicesDialog.show<void>(
290+
context: navigatorContext,
291+
routeSettings: RouteSettings(name: '/dialog-${notification.id}'),
292+
builder: (context) => widget,
293+
);
294+
}
295+
}
193296
}

catalyst_voices/apps/voices/lib/notification/catalyst_notification.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
66

77
part 'banner_notification.dart';
88
part 'catalyst_notification_text.dart';
9+
part 'dialog_notification.dart';
910

1011
bool _alwaysAllowRouterPredicate(GoRouterState state) => true;
1112

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
part of 'catalyst_notification.dart';
2+
3+
abstract base class DialogNotification extends CatalystNotification {
4+
const DialogNotification({
5+
required super.id,
6+
super.priority,
7+
super.routerPredicate,
8+
super.type,
9+
});
10+
11+
Widget buildDialog(BuildContext context);
12+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'package:catalyst_voices/notification/catalyst_notification.dart';
2+
import 'package:catalyst_voices/routes/routing/root_route.dart';
3+
import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart';
4+
import 'package:catalyst_voices/widgets/modals/voices_info_dialog.dart';
5+
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
6+
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
7+
import 'package:flutter/material.dart';
8+
9+
final class KeychainDeletedDialogNotification extends DialogNotification {
10+
KeychainDeletedDialogNotification()
11+
: super(
12+
id: 'keychainDeleteDialog',
13+
routerPredicate: (state) => RootRoute.rootRouteNameOptions.contains(state.name),
14+
);
15+
16+
@override
17+
Widget buildDialog(BuildContext context) {
18+
return const _KeychainDeletedDialog();
19+
}
20+
}
21+
22+
class _KeychainDeletedDialog extends StatelessWidget {
23+
const _KeychainDeletedDialog();
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
return VoicesInfoDialog(
28+
icon: VoicesAssets.icons.checkCircle.buildIcon(),
29+
title: Text(context.l10n.keychainDeletedDialogTitle),
30+
message: Text(context.l10n.keychainDeletedDialogSubtitle),
31+
action: VoicesFilledButton(
32+
key: const Key('KeychainDeletedDialogCloseButton'),
33+
onTap: () => Navigator.of(context).pop(),
34+
child: Text(context.l10n.close),
35+
),
36+
);
37+
}
38+
}

0 commit comments

Comments
 (0)