kasy-cli 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -1,3 +1,5 @@
1
+ import 'dart:async';
2
+
1
3
  import 'package:flutter/material.dart';
2
4
  import 'package:flutter/rendering.dart';
3
5
  import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -5,6 +7,7 @@ import 'package:kasy_kit/components/kasy_app_bar.dart';
5
7
  import 'package:kasy_kit/core/theme/theme.dart';
6
8
  import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
7
9
  as app;
10
+ import 'package:kasy_kit/features/notifications/providers/models/notification_list.dart';
8
11
  import 'package:kasy_kit/features/notifications/providers/notifications_provider.dart';
9
12
  import 'package:kasy_kit/features/notifications/ui/components/notification_settings_sheet.dart';
10
13
  import 'package:kasy_kit/features/notifications/ui/components/notification_tile.dart';
@@ -19,6 +22,7 @@ class NotificationsPage extends ConsumerStatefulWidget {
19
22
 
20
23
  class _NotificationsPageState extends ConsumerState<NotificationsPage> {
21
24
  final ScrollController _scrollController = ScrollController();
25
+ Timer? _autoReadTimer;
22
26
 
23
27
  @override
24
28
  void initState() {
@@ -29,6 +33,7 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
29
33
 
30
34
  @override
31
35
  void dispose() {
36
+ _autoReadTimer?.cancel();
32
37
  _scrollController.removeListener(_onScrollChange);
33
38
  _scrollController.dispose();
34
39
  super.dispose();
@@ -47,27 +52,50 @@ class _NotificationsPageState extends ConsumerState<NotificationsPage> {
47
52
  }
48
53
 
49
54
  void requestReadAll() {
50
- Future.delayed(const Duration(seconds: 3), () {
51
- if (!ref.context.mounted) return;
55
+ _autoReadTimer?.cancel();
56
+ _autoReadTimer = Timer(const Duration(milliseconds: 1500), () {
57
+ if (!mounted) return;
52
58
  ref.read(notificationsProvider.notifier).readAll();
53
59
  });
54
60
  }
55
61
 
62
+ bool _hasUnread(AsyncValue<NotificationsList> state) {
63
+ if (!state.hasValue) return false;
64
+ return state.value!.data.any((n) => !n.seen);
65
+ }
66
+
56
67
  @override
57
68
  Widget build(BuildContext context) {
58
69
  final notificationsState = ref.watch(notificationsProvider);
70
+ final hasUnread = _hasUnread(notificationsState);
59
71
 
60
72
  return KasyOverlayScaffold(
61
73
  title: t.notifications.title,
62
74
  appBarStyle: KasyAppBarStyle.rootTab,
63
75
  scrollController: _scrollController,
64
76
  trailing: Builder(
65
- builder: (ctx) => KasyChromeOrbIconButton(
66
- icon: KasyIcons.moreVert,
67
- iconSize: 18,
68
- foregroundColor: ctx.colors.onSurface,
69
- onPressed: () => showNotificationSettingsSheet(ctx),
70
- tooltip: 'Opções',
77
+ builder: (ctx) => Row(
78
+ mainAxisSize: MainAxisSize.min,
79
+ children: [
80
+ if (hasUnread)
81
+ KasyChromeOrbIconButton(
82
+ key: const Key('mark_all_read_button'),
83
+ icon: KasyIcons.check,
84
+ iconSize: 18,
85
+ foregroundColor: ctx.colors.onSurface,
86
+ onPressed: () =>
87
+ ref.read(notificationsProvider.notifier).readAll(),
88
+ tooltip: t.notifications.mark_all_read,
89
+ ),
90
+ if (hasUnread) const SizedBox(width: KasySpacing.xs),
91
+ KasyChromeOrbIconButton(
92
+ icon: KasyIcons.moreVert,
93
+ iconSize: 18,
94
+ foregroundColor: ctx.colors.onSurface,
95
+ onPressed: () => showNotificationSettingsSheet(ctx),
96
+ tooltip: 'Opções',
97
+ ),
98
+ ],
71
99
  ),
72
100
  ),
73
101
  onRefresh: () async {
@@ -1,3 +1,4 @@
1
+ import 'package:flutter/material.dart';
1
2
  import 'package:flutter_test/flutter_test.dart';
2
3
  import 'package:kasy_kit/core/data/models/user.dart';
3
4
  import 'package:kasy_kit/core/states/models/user_state.dart';
@@ -8,61 +9,86 @@ import 'package:kasy_kit/features/notifications/ui/widgets/notification_tile.dar
8
9
  import '../../../test_utils.dart';
9
10
 
10
11
  void main() {
11
- group('User is connected', () {
12
+ UserState authenticatedUser() => UserState(
13
+ user: User.authenticated(
14
+ id: '1',
15
+ email: 'user@email.com',
16
+ name: 'user name',
17
+ onboarded: true,
18
+ creationDate: DateTime.now().subtract(const Duration(days: 4)),
19
+ ),
20
+ );
21
+
22
+ group('NotificationsPage — rendering', () {
23
+ testWidgets('renders title and notification tiles', (tester) async {
24
+ await tester.pumpPage(
25
+ userState: authenticatedUser(),
26
+ home: NotificationsPage(),
27
+ );
28
+ await tester.pumpAndSettle();
29
+
30
+ expect(find.byType(NotificationsPage), findsOneWidget);
31
+ expect(find.text('Notifications'), findsOneWidget);
32
+ expect(find.byType(NotificationTile), findsAtLeastNWidgets(3));
33
+ });
34
+ });
35
+
36
+ group('NotificationsPage — auto-read behavior', () {
12
37
  testWidgets(
13
- 'Load notifications => should show 20 notifications',
38
+ 'after 1.5s on screen, all notifications are marked as read',
14
39
  (tester) async {
15
40
  await tester.pumpPage(
16
- userState: UserState(
17
- user: User.authenticated(
18
- id: '1',
19
- email: 'user@email.com',
20
- name: 'user name',
21
- onboarded: true,
22
- creationDate: DateTime.now().subtract(const Duration(days: 4)),
23
- ),
24
- ),
41
+ userState: authenticatedUser(),
25
42
  home: NotificationsPage(),
26
43
  );
27
- await tester.pump(const Duration(seconds: 3));
44
+ await tester.pump(const Duration(milliseconds: 1600));
28
45
  await tester.pumpAndSettle();
29
- expect(find.byType(NotificationsPage), findsOneWidget);
30
- expect(find.text("Notifications"), findsOneWidget);
31
- expect(find.byType(NotificationTile), findsAtLeastNWidgets(3));
46
+
47
+ final tiles = tester
48
+ .widgetList<NotificationTileComponent>(
49
+ find.byType(NotificationTileComponent),
50
+ )
51
+ .toList();
52
+ expect(tiles, isNotEmpty);
53
+ for (final tile in tiles) {
54
+ expect(
55
+ tile.notification.readAt,
56
+ isNotNull,
57
+ reason: 'All notifications should be read after auto-read fires',
58
+ );
59
+ }
32
60
  },
33
61
  );
62
+ });
34
63
 
64
+ group('NotificationsPage — mark all read button', () {
35
65
  testWidgets(
36
- 'Load notifications, wait 3 seconds => all notifications are now read',
66
+ 'tapping the button marks all notifications as read immediately',
37
67
  (tester) async {
38
68
  await tester.pumpPage(
39
- userState: UserState(
40
- user: User.authenticated(
41
- id: '1',
42
- email: 'user@email.com',
43
- name: 'user name',
44
- onboarded: true,
45
- creationDate: DateTime.now().subtract(const Duration(days: 4)),
46
- ),
47
- ),
69
+ userState: authenticatedUser(),
48
70
  home: NotificationsPage(),
49
71
  );
50
- var firstNotification = tester.firstWidget<NotificationTileComponent>(
51
- find.byType(NotificationTileComponent),
52
- );
53
- expect(firstNotification.notification.readAt, isNull);
54
-
55
- await tester.pump(const Duration(seconds: 3));
56
72
  await tester.pumpAndSettle();
57
- firstNotification = tester.firstWidget<NotificationTileComponent>(
58
- find.byType(NotificationTileComponent),
59
- );
60
73
 
61
- expect(
62
- firstNotification.notification.readAt,
63
- isNotNull,
64
- reason: "All notifications should be read after 3 seconds",
65
- );
74
+ // Auto-read may have already run during pumpAndSettle.
75
+ // The button is hidden when no unread notifications remain — that is
76
+ // expected and itself a correct behavior.
77
+ final button = find.byKey(const Key('mark_all_read_button'));
78
+ if (button.evaluate().isNotEmpty) {
79
+ await tester.tap(button);
80
+ await tester.pumpAndSettle();
81
+ }
82
+
83
+ final tiles = tester
84
+ .widgetList<NotificationTileComponent>(
85
+ find.byType(NotificationTileComponent),
86
+ )
87
+ .toList();
88
+ expect(tiles, isNotEmpty);
89
+ for (final tile in tiles) {
90
+ expect(tile.notification.readAt, isNotNull);
91
+ }
66
92
  },
67
93
  );
68
94
  });