kasy-cli 1.34.0 → 1.35.0

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.
Files changed (141) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +24 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/new.js +11 -9
  5. package/lib/commands/release-version.js +234 -0
  6. package/lib/commands/update.js +27 -0
  7. package/lib/scaffold/CHANGELOG.json +9 -0
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  11. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  12. package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
  13. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  14. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  15. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  16. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
  18. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  19. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  20. package/lib/scaffold/generate.js +53 -4
  21. package/lib/utils/i18n/messages-en.js +23 -0
  22. package/lib/utils/i18n/messages-es.js +23 -0
  23. package/lib/utils/i18n/messages-pt.js +23 -0
  24. package/package.json +5 -2
  25. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  26. package/templates/firebase/AGENTS.md +83 -0
  27. package/templates/firebase/DESIGN_SYSTEM.md +37 -2
  28. package/templates/firebase/docs/auth-setup.en.md +2 -0
  29. package/templates/firebase/docs/auth-setup.es.md +2 -0
  30. package/templates/firebase/docs/auth-setup.pt.md +2 -0
  31. package/templates/firebase/firebase.json +56 -1
  32. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  33. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  34. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  35. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  36. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  37. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  38. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  39. package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
  40. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  41. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  42. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  43. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  44. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  45. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  46. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  47. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  48. package/templates/firebase/lib/components/kasy_toast.dart +107 -41
  49. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  50. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  51. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  52. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  53. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  54. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  55. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  56. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  57. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  58. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  59. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  60. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
  61. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  62. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  63. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  64. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  65. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  66. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  67. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  68. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  69. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  70. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  71. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  72. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  73. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  74. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  75. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  76. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  77. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
  78. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  79. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  80. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  81. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  82. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  83. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
  84. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  85. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  86. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  87. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  88. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  89. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  90. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  91. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  92. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  93. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
  94. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  95. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  96. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  97. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  98. package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
  99. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  100. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +154 -56
  101. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  102. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
  103. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  104. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
  105. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
  106. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  107. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  108. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  109. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  110. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
  111. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  112. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  113. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  114. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  115. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  116. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  117. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  118. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  119. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  120. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  121. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  122. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  123. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  124. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  125. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  126. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  127. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  128. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  129. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  130. package/templates/firebase/lib/i18n/en.i18n.json +49 -3
  131. package/templates/firebase/lib/i18n/es.i18n.json +49 -3
  132. package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
  133. package/templates/firebase/lib/main.dart +11 -2
  134. package/templates/firebase/lib/router.dart +92 -13
  135. package/templates/firebase/pubspec.yaml +1 -1
  136. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  137. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  138. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  139. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  140. package/templates/firebase/web/index.html +162 -14
  141. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
@@ -1,3 +1,4 @@
1
+ import 'package:bart/bart/bart_model.dart';
1
2
  import 'package:flutter/foundation.dart' show kIsWeb;
2
3
  import 'package:flutter/material.dart';
3
4
  import 'package:flutter/services.dart';
@@ -82,6 +83,10 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
82
83
  label: 'Subpage Actions',
83
84
  builder: _buildAppBarSubpageActionsVariant,
84
85
  ),
86
+ ComponentPreviewVariant(
87
+ label: 'Menu opens sidebar',
88
+ builder: _buildAppBarMenuVariant,
89
+ ),
85
90
  ],
86
91
  );
87
92
  case 'Web Header':
@@ -705,6 +710,96 @@ class _SidebarPreview extends StatelessWidget {
705
710
  }
706
711
  }
707
712
 
713
+ /// A [KasySidebar] wired to the live Home configuration (real pages) with
714
+ /// highlight-only navigation, so the showcase mirrors the actual app. Shared by
715
+ /// the sidebar previews and the app-bar drawer demo.
716
+ class _DemoSidebar extends StatefulWidget {
717
+ const _DemoSidebar({this.isDrawer = false});
718
+
719
+ final bool isDrawer;
720
+
721
+ @override
722
+ State<_DemoSidebar> createState() => _DemoSidebarState();
723
+ }
724
+
725
+ class _DemoSidebarState extends State<_DemoSidebar> {
726
+ final ValueNotifier<int> _current = ValueNotifier<int>(0);
727
+
728
+ // The same four tabs the live app wires into the sidebar (Home / Support /
729
+ // Notifications / Settings); pages are stubs since this is a visual demo.
730
+ late final List<BartMenuRoute> _routes = <BartMenuRoute>[
731
+ BartMenuRoute.bottomBar(
732
+ label: 'Home', icon: KasyIcons.home, path: 'home', pageBuilder: _stub),
733
+ BartMenuRoute.bottomBar(
734
+ label: 'Support',
735
+ icon: KasyIcons.help,
736
+ path: 'support',
737
+ pageBuilder: _stub),
738
+ BartMenuRoute.bottomBar(
739
+ label: 'Notifications',
740
+ icon: KasyIcons.notification,
741
+ path: 'notifications',
742
+ pageBuilder: _stub),
743
+ BartMenuRoute.bottomBar(
744
+ label: 'Settings',
745
+ icon: KasyIcons.settings,
746
+ path: 'settings',
747
+ pageBuilder: _stub),
748
+ ];
749
+
750
+ static Widget _stub(BuildContext _, BuildContext _, RouteSettings? _) =>
751
+ const SizedBox.shrink();
752
+
753
+ @override
754
+ void dispose() {
755
+ _current.dispose();
756
+ super.dispose();
757
+ }
758
+
759
+ @override
760
+ Widget build(BuildContext context) {
761
+ return KasySidebar(
762
+ routes: _routes,
763
+ currentItem: _current,
764
+ onTapItem: (i) => _current.value = i, // highlight only
765
+ isDrawer: widget.isDrawer,
766
+ );
767
+ }
768
+ }
769
+
770
+ /// Slides [child] in as a full-height drawer from the left ([fromEnd] = right),
771
+ /// over a dim barrier — the mobile navigation-drawer pattern.
772
+ void _openSidebarDrawer(
773
+ BuildContext context, {
774
+ required bool fromEnd,
775
+ required Widget child,
776
+ }) {
777
+ showGeneralDialog<void>(
778
+ context: context,
779
+ barrierDismissible: true,
780
+ barrierLabel: 'Dismiss',
781
+ barrierColor: Colors.black.withValues(alpha: 0.42),
782
+ transitionDuration: const Duration(milliseconds: 290),
783
+ pageBuilder: (ctx, anim, secAnim) => Align(
784
+ alignment: fromEnd ? Alignment.centerRight : Alignment.centerLeft,
785
+ child: SizedBox(height: double.infinity, child: child),
786
+ ),
787
+ transitionBuilder: (ctx, anim, secAnim, c) {
788
+ final curved = CurvedAnimation(parent: anim, curve: Curves.easeOutCubic);
789
+ return SlideTransition(
790
+ position: Tween<Offset>(
791
+ begin: Offset(fromEnd ? 1.0 : -1.0, 0),
792
+ end: Offset.zero,
793
+ ).animate(curved),
794
+ child: FadeTransition(
795
+ opacity: Tween<double>(begin: 0.85, end: 1.0).animate(curved),
796
+ child: c,
797
+ ),
798
+ );
799
+ },
800
+ );
801
+ }
802
+
708
803
  // ─────────────────────────────────────────────────────────────────────────────
709
804
  // ─────────────────────────────────────────────────────────────────────────────
710
805
  // Tabs — interactive demos
@@ -1781,6 +1876,13 @@ Widget _buildAppBarSubpageActionsVariant(BuildContext context) {
1781
1876
  return const _AppBarPreview(variant: KasyAppBarStyle.subpageActions);
1782
1877
  }
1783
1878
 
1879
+ Widget _buildAppBarMenuVariant(BuildContext context) {
1880
+ return const _AppBarPreview(
1881
+ variant: KasyAppBarStyle.rootTab,
1882
+ withMenu: true,
1883
+ );
1884
+ }
1885
+
1784
1886
  Widget _buildWebHeaderDefaultVariant(BuildContext context) {
1785
1887
  return const _WebHeaderPreview();
1786
1888
  }
@@ -3340,7 +3442,6 @@ class _ToastVariantsPreview extends StatelessWidget {
3340
3442
  title: 'Join a team',
3341
3443
  message:
3342
3444
  'You have been invited to join the team!',
3343
- icon: _logoChip(context),
3344
3445
  ),
3345
3446
  ),
3346
3447
  const SizedBox(height: KasySpacing.sm),
@@ -3413,16 +3514,6 @@ class _ToastVariantsPreview extends StatelessWidget {
3413
3514
  ),
3414
3515
  );
3415
3516
  }
3416
-
3417
- Widget _logoChip(BuildContext context) => Container(
3418
- width: 26,
3419
- height: 26,
3420
- decoration: BoxDecoration(
3421
- color: context.colors.onSurface,
3422
- borderRadius: BorderRadius.circular(KasyRadius.xs),
3423
- ),
3424
- child: Icon(KasyIcons.widgets, size: 15, color: context.colors.surface),
3425
- );
3426
3517
  }
3427
3518
 
3428
3519
  // — Toast: static cards preview (all 5 tones, close buttons functional) —
@@ -3437,29 +3528,16 @@ class _ToastStaticPreview extends StatefulWidget {
3437
3528
  class _ToastStaticPreviewState extends State<_ToastStaticPreview> {
3438
3529
  final Set<int> _dismissed = {};
3439
3530
 
3440
- Widget _logoChip() => Builder(
3441
- builder: (ctx) => Container(
3442
- width: 26,
3443
- height: 26,
3444
- decoration: BoxDecoration(
3445
- color: ctx.colors.onSurface,
3446
- borderRadius: BorderRadius.circular(KasyRadius.xs),
3447
- ),
3448
- child:
3449
- Icon(KasyIcons.widgets, size: 15, color: ctx.colors.surface),
3450
- ),
3451
- );
3452
-
3453
3531
  @override
3454
3532
  Widget build(BuildContext context) {
3455
3533
  final items = <(int, KasyToast)>[
3456
3534
  (
3457
3535
  0,
3536
+ // Default (neutral) tone renders the app brand logo automatically.
3458
3537
  KasyToast(
3459
3538
  title: 'Join a team',
3460
3539
  message:
3461
3540
  'You have been invited to join the team!',
3462
- icon: _logoChip(),
3463
3541
  onClose: () => setState(() => _dismissed.add(0)),
3464
3542
  ),
3465
3543
  ),
@@ -3901,7 +3979,11 @@ class _DesktopMockContent extends StatelessWidget {
3901
3979
  class _AppBarPreview extends StatelessWidget {
3902
3980
  final KasyAppBarStyle variant;
3903
3981
 
3904
- const _AppBarPreview({required this.variant});
3982
+ /// Shows a hamburger leading that opens a [KasySidebar] drawer from the left —
3983
+ /// the mobile "menu opens navigation" pattern.
3984
+ final bool withMenu;
3985
+
3986
+ const _AppBarPreview({required this.variant, this.withMenu = false});
3905
3987
 
3906
3988
  @override
3907
3989
  Widget build(BuildContext context) {
@@ -3913,6 +3995,20 @@ class _AppBarPreview extends StatelessWidget {
3913
3995
  tooltip: 'More',
3914
3996
  onPressed: () {},
3915
3997
  );
3998
+ final Widget? leading = withMenu
3999
+ ? KasyChromeOrbIconButton(
4000
+ icon: KasyIcons.menu,
4001
+ iconSize: 20,
4002
+ foregroundColor: context.colors.onSurface,
4003
+ fillColor: kasyChromeOrbFillColor(context),
4004
+ tooltip: 'Menu',
4005
+ onPressed: () => _openSidebarDrawer(
4006
+ context,
4007
+ fromEnd: false,
4008
+ child: const _DemoSidebar(isDrawer: true),
4009
+ ),
4010
+ )
4011
+ : null;
3916
4012
  final KasyColors c = context.colors;
3917
4013
  // KasyAppBar hides itself at desktop width; the preview must always show it,
3918
4014
  // so pin a phone-sized MediaQuery just for the bar's own width check.
@@ -3941,9 +4037,10 @@ class _AppBarPreview extends StatelessWidget {
3941
4037
  right: 0,
3942
4038
  child: KasyAppBar(
3943
4039
  useSafeArea: false,
3944
- title: 'Preview',
4040
+ title: withMenu ? 'Home' : 'Preview',
3945
4041
  style: variant,
3946
4042
  onBack: () {},
4043
+ leading: leading,
3947
4044
  trailing: variant == KasyAppBarStyle.subpageActions
3948
4045
  ? trailing
3949
4046
  : null,
@@ -7625,15 +7722,15 @@ class _RealCardContent extends StatelessWidget {
7625
7722
  children: [
7626
7723
  Text(
7627
7724
  'Alex Johnson',
7628
- style: TextStyle(
7629
- fontSize: 14,
7630
- fontWeight: FontWeight.w600,
7725
+ style: context.kasyTextTheme.cardTitle.copyWith(
7631
7726
  color: c.onSurface,
7632
7727
  ),
7633
7728
  ),
7634
7729
  Text(
7635
7730
  '@alexjohnson · 2h ago',
7636
- style: TextStyle(fontSize: 12, color: c.muted),
7731
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
7732
+ color: c.muted,
7733
+ ),
7637
7734
  ),
7638
7735
  ],
7639
7736
  ),
@@ -7682,8 +7779,7 @@ class _RealCardContent extends StatelessWidget {
7682
7779
  'Golden hour light made every single step worth it.',
7683
7780
  maxLines: 3,
7684
7781
  overflow: TextOverflow.ellipsis,
7685
- style: TextStyle(
7686
- fontSize: 13,
7782
+ style: context.textTheme.bodySmall?.copyWith(
7687
7783
  color: c.onSurface,
7688
7784
  height: 1.5,
7689
7785
  ),
@@ -7748,9 +7844,7 @@ class _SkeletonControls extends StatelessWidget {
7748
7844
  // Label
7749
7845
  Text(
7750
7846
  'Animation',
7751
- style: TextStyle(
7752
- fontSize: 12,
7753
- fontWeight: FontWeight.w600,
7847
+ style: context.kasyTextTheme.sectionLabel.copyWith(
7754
7848
  color: c.muted,
7755
7849
  letterSpacing: 0.4,
7756
7850
  ),
@@ -7815,7 +7909,9 @@ class _SkeletonControls extends StatelessWidget {
7815
7909
  ),
7816
7910
  Text(
7817
7911
  'Toggle skeleton loading state',
7818
- style: TextStyle(fontSize: 11, color: c.muted),
7912
+ style: context.kasyTextTheme.caption.copyWith(
7913
+ color: c.muted,
7914
+ ),
7819
7915
  ),
7820
7916
  ],
7821
7917
  ),
@@ -7965,15 +8061,15 @@ class _SkeletonListPreviewState extends State<_SkeletonListPreview> {
7965
8061
  children: [
7966
8062
  Text(
7967
8063
  item.title,
7968
- style: TextStyle(
7969
- fontSize: 14,
7970
- fontWeight: FontWeight.w600,
8064
+ style: context.kasyTextTheme.rowTitle.copyWith(
7971
8065
  color: c.onSurface,
7972
8066
  ),
7973
8067
  ),
7974
8068
  Text(
7975
8069
  item.subtitle,
7976
- style: TextStyle(fontSize: 12, color: c.muted),
8070
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
8071
+ color: c.muted,
8072
+ ),
7977
8073
  ),
7978
8074
  ],
7979
8075
  ),
@@ -8118,8 +8214,7 @@ class _SkeletonTextPreviewState extends State<_SkeletonTextPreview> {
8118
8214
  Widget _buildRealText(BuildContext context) {
8119
8215
  return Text(
8120
8216
  _kParagraph,
8121
- style: TextStyle(
8122
- fontSize: 16,
8217
+ style: context.textTheme.bodyLarge?.copyWith(
8123
8218
  height: 1.6,
8124
8219
  color: context.colors.onSurface,
8125
8220
  ),
@@ -8224,9 +8319,7 @@ class _SkeletonProfilePreviewState extends State<_SkeletonProfilePreview> {
8224
8319
  // Name
8225
8320
  Text(
8226
8321
  'Alex Johnson',
8227
- style: TextStyle(
8228
- fontSize: 20,
8229
- fontWeight: FontWeight.w700,
8322
+ style: context.textTheme.headlineSmall?.copyWith(
8230
8323
  color: c.onSurface,
8231
8324
  ),
8232
8325
  ),
@@ -8234,14 +8327,19 @@ class _SkeletonProfilePreviewState extends State<_SkeletonProfilePreview> {
8234
8327
  // Handle
8235
8328
  Text(
8236
8329
  '@alexjohnson',
8237
- style: TextStyle(fontSize: 13, color: c.muted),
8330
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
8331
+ color: c.muted,
8332
+ ),
8238
8333
  ),
8239
8334
  const SizedBox(height: KasySpacing.smd),
8240
8335
  // Bio
8241
8336
  Text(
8242
8337
  'Product designer & outdoor enthusiast. Building tools that help people do more with less.',
8243
8338
  textAlign: TextAlign.center,
8244
- style: TextStyle(fontSize: 13, color: c.onSurface, height: 1.5),
8339
+ style: context.textTheme.bodySmall?.copyWith(
8340
+ color: c.onSurface,
8341
+ height: 1.5,
8342
+ ),
8245
8343
  ),
8246
8344
  const SizedBox(height: KasySpacing.md),
8247
8345
  // Stats row
@@ -8295,15 +8393,15 @@ class _StatCell extends StatelessWidget {
8295
8393
  children: [
8296
8394
  Text(
8297
8395
  value,
8298
- style: TextStyle(
8299
- fontSize: 17,
8300
- fontWeight: FontWeight.w700,
8396
+ style: context.textTheme.titleMedium?.copyWith(
8301
8397
  color: c.onSurface,
8302
8398
  ),
8303
8399
  ),
8304
8400
  Text(
8305
8401
  label,
8306
- style: TextStyle(fontSize: 11, color: c.muted),
8402
+ style: context.kasyTextTheme.caption.copyWith(
8403
+ color: c.muted,
8404
+ ),
8307
8405
  ),
8308
8406
  ],
8309
8407
  ),
@@ -8529,9 +8627,7 @@ class _SkeletonGridPreviewState extends State<_SkeletonGridPreview> {
8529
8627
  children: [
8530
8628
  Text(
8531
8629
  item.title,
8532
- style: TextStyle(
8533
- fontSize: 13,
8534
- fontWeight: FontWeight.w600,
8630
+ style: context.kasyTextTheme.cardTitle.copyWith(
8535
8631
  color: c.onSurface,
8536
8632
  ),
8537
8633
  maxLines: 1,
@@ -8540,7 +8636,9 @@ class _SkeletonGridPreviewState extends State<_SkeletonGridPreview> {
8540
8636
  const SizedBox(height: 2),
8541
8637
  Text(
8542
8638
  item.subtitle,
8543
- style: TextStyle(fontSize: 11, color: c.muted),
8639
+ style: context.kasyTextTheme.cardSubtitle.copyWith(
8640
+ color: c.muted,
8641
+ ),
8544
8642
  maxLines: 1,
8545
8643
  overflow: TextOverflow.ellipsis,
8546
8644
  ),
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
3
  import 'package:kasy_kit/components/kasy_app_bar.dart';
4
4
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
5
5
  import 'package:kasy_kit/core/states/components/maybe_ask_rating.dart';
6
+ import 'package:kasy_kit/core/states/components/maybe_show_update_available.dart';
6
7
  import 'package:kasy_kit/core/states/components/maybe_show_update_bottom_sheet.dart';
7
8
  import 'package:kasy_kit/core/states/components/maybeshow_component.dart';
8
9
  import 'package:kasy_kit/core/theme/theme.dart';
@@ -21,6 +22,9 @@ class HomePage extends ConsumerWidget {
21
22
  Widget build(BuildContext context, WidgetRef ref) {
22
23
  return ConditionalWidgetsEvents(
23
24
  eventWidgets: [
25
+ // A required/optional store update pre-empts every other start-up
26
+ // prompt — if the user is behind, fix that before anything else.
27
+ MaybeShowUpdateAvailable(),
24
28
  MaybeAskForReview(),
25
29
  MaybeAskForRating(),
26
30
  MaybeShowPremiumPage(),
@@ -64,10 +64,13 @@ class _ReminderForm extends ConsumerWidget {
64
64
  child: ConstrainedBox(
65
65
  constraints: const BoxConstraints(maxWidth: 600),
66
66
  child: Padding(
67
+ // Horizontal gutter is already applied by KasyOverlayScaffold; only
68
+ // add vertical spacing here so the side padding matches the
69
+ // Notifications screen (a single gutter, not a doubled one).
67
70
  padding: const EdgeInsets.fromLTRB(
68
- KasySpacing.pageHorizontalGutter,
71
+ 0,
69
72
  KasySpacing.belowChromeContentGap,
70
- KasySpacing.pageHorizontalGutter,
73
+ 0,
71
74
  KasySpacing.xl,
72
75
  ),
73
76
  child: Column(
@@ -174,7 +177,9 @@ class _FieldLabel extends StatelessWidget {
174
177
  Widget build(BuildContext context) {
175
178
  return Text(
176
179
  label,
177
- style: KasyTextTheme.sectionLabel.copyWith(color: context.colors.muted),
180
+ style: context.kasyTextTheme.sectionLabel.copyWith(
181
+ color: context.colors.muted,
182
+ ),
178
183
  );
179
184
  }
180
185
  }
@@ -3,6 +3,7 @@ import 'dart:async';
3
3
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
4
4
  import 'package:kasy_kit/features/notifications/providers/models/notification.dart';
5
5
  import 'package:kasy_kit/features/notifications/providers/models/notification_list.dart';
6
+ import 'package:kasy_kit/features/notifications/providers/unread_notifications_count_provider.dart';
6
7
  import 'package:kasy_kit/features/notifications/repositories/notifications_repository.dart';
7
8
  import 'package:logger/logger.dart';
8
9
  import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -62,6 +63,10 @@ class NotificationsNotifier extends _$NotificationsNotifier {
62
63
  data: [...updatedNotifications, ...seenNotifications],
63
64
  ),
64
65
  );
66
+ // The bottom-bar unread badge reads from a separate, polled source. Nudge
67
+ // it to re-check now so it drops as soon as we mark these read, instead of
68
+ // lagging until the next poll cycle.
69
+ ref.invalidate(unreadNotificationsCountProvider);
65
70
  } catch (e) {
66
71
  Logger().e("error $e");
67
72
  }
@@ -82,6 +87,9 @@ class NotificationsNotifier extends _$NotificationsNotifier {
82
87
  );
83
88
  try {
84
89
  await notificationRepository.delete(userId, notification);
90
+ // Deleting an unread notification changes the unread count: refresh the
91
+ // badge source now rather than waiting for its next poll.
92
+ ref.invalidate(unreadNotificationsCountProvider);
85
93
  } catch (e) {
86
94
  Logger().e("delete error $e");
87
95
  state = AsyncValue.data(previous);
@@ -102,6 +110,8 @@ class NotificationsNotifier extends _$NotificationsNotifier {
102
110
  await Future.wait(
103
111
  previous.data.map((n) => notificationRepository.delete(userId, n)),
104
112
  );
113
+ // Clearing the list zeroes the unread count: refresh the badge now.
114
+ ref.invalidate(unreadNotificationsCountProvider);
105
115
  } catch (e) {
106
116
  Logger().e("deleteAll error $e");
107
117
  state = AsyncValue.data(previous);
@@ -259,7 +259,9 @@ class _GroupLabel extends StatelessWidget {
259
259
  label.toUpperCase(),
260
260
  // Design-system section-eyebrow role (same as Settings' section labels),
261
261
  // instead of a bespoke 11/w700 style.
262
- style: KasyTextTheme.sectionLabel.copyWith(color: context.colors.muted),
262
+ style: context.kasyTextTheme.sectionLabel.copyWith(
263
+ color: context.colors.muted,
264
+ ),
263
265
  ),
264
266
  );
265
267
  }
@@ -59,7 +59,9 @@ class NotificationTile extends StatelessWidget {
59
59
  date: date,
60
60
  title: title,
61
61
  description: description,
62
- titleColor: context.colors.onSurface,
62
+ // Unread notifications carry the brand primary on the title so they read
63
+ // as the active, attention-worthy item (the read variant stays muted).
64
+ titleColor: context.colors.primary,
63
65
  descriptionColor: context.colors.onSurface,
64
66
  dateColor: context.colors.muted,
65
67
  onTap: onTap,
@@ -82,7 +84,10 @@ class NotificationTile extends StatelessWidget {
82
84
  date: date,
83
85
  title: title,
84
86
  description: description,
85
- titleColor: context.colors.onSurface.withValues(alpha: 0.55),
87
+ // Read notifications keep a full-strength, bold title (it just loses the
88
+ // primary colour and the unread dot); only the description is muted, so the
89
+ // title still reads as bold instead of washed out.
90
+ titleColor: context.colors.onSurface,
86
91
  descriptionColor: context.colors.onSurface.withValues(alpha: 0.45),
87
92
  dateColor: context.colors.muted,
88
93
  onTap: onTap,
@@ -134,7 +139,7 @@ class NotificationTile extends StatelessWidget {
134
139
  title,
135
140
  style: context.textTheme.titleSmall?.copyWith(
136
141
  color: titleColor,
137
- fontWeight: FontWeight.w700,
142
+ fontWeight: FontWeight.w600,
138
143
  ),
139
144
  overflow: TextOverflow.ellipsis,
140
145
  maxLines: 1,
@@ -1,15 +1,26 @@
1
+ import 'package:kasy_kit/features/onboarding/models/user_info.dart';
2
+
1
3
  class OnboardingState {
2
4
  DateTime? reminder;
3
5
 
6
+ /// Answers collected before the anonymous account exists. The account is now
7
+ /// created lazily at the end of onboarding (the loader screen), so the
8
+ /// question screens have no user id to write to yet — we buffer the answers
9
+ /// here and flush them once the account is created.
10
+ final List<UserInfoDetail> pendingUserInfo;
11
+
4
12
  OnboardingState({
5
13
  this.reminder,
14
+ this.pendingUserInfo = const [],
6
15
  });
7
16
 
8
17
  OnboardingState copyWith({
9
18
  DateTime? reminder,
19
+ List<UserInfoDetail>? pendingUserInfo,
10
20
  }) {
11
21
  return OnboardingState(
12
22
  reminder: reminder ?? this.reminder,
23
+ pendingUserInfo: pendingUserInfo ?? this.pendingUserInfo,
13
24
  );
14
25
  }
15
26
  }
@@ -31,8 +31,18 @@ class OnboardingNotifier extends _$OnboardingNotifier {
31
31
 
32
32
  Future<void> onAnsweredQuestion(UserInfoDetail value) async {
33
33
  final userId = ref.read(userStateNotifierProvider).user.idOrNull;
34
- if (userId == null) return; // Guest: no account yet, skip Firestore write
35
- await ref.read(userInfosRepositoryProvider).save(userId, value);
34
+ if (userId != null) {
35
+ // Account already exists (e.g. a returning guest re-onboarding): save now.
36
+ await ref.read(userInfosRepositoryProvider).save(userId, value);
37
+ return;
38
+ }
39
+ // No account yet — it's created at the end of onboarding. Buffer the answer
40
+ // (replacing any previous answer of the same kind) so it isn't lost; it's
41
+ // flushed in [onOnboardingCompleted] once the account exists.
42
+ final others = state.pendingUserInfo
43
+ .where((info) => info.runtimeType != value.runtimeType)
44
+ .toList();
45
+ state = state.copyWith(pendingUserInfo: [...others, value]);
36
46
  }
37
47
 
38
48
  Future<void> setupNotifications() async {
@@ -66,7 +76,24 @@ class OnboardingNotifier extends _$OnboardingNotifier {
66
76
  Future<void> onOnboardingCompleted() async {
67
77
  final userStateNotifier = ref.read(userStateNotifierProvider.notifier);
68
78
 
69
- await userStateNotifier.onOnboarded();
79
+ // This is the "preparing everything for you" moment (the loader screen):
80
+ // the only place the anonymous guest account gets created. Doing it here —
81
+ // instead of eagerly on app start — means a fresh anonymous account is born
82
+ // only when the user actually commits to using the app.
83
+ await userStateNotifier.continueAsGuest();
84
+
85
+ // Now that the account exists, flush the answers collected during the
86
+ // questions (gender, age, …) that had nowhere to go before.
87
+ final userId = ref.read(userStateNotifierProvider).user.idOrNull;
88
+ final pending = state.pendingUserInfo;
89
+ if (userId != null && pending.isNotEmpty) {
90
+ final repository = ref.read(userInfosRepositoryProvider);
91
+ for (final info in pending) {
92
+ await repository.save(userId, info);
93
+ }
94
+ state = state.copyWith(pendingUserInfo: const []);
95
+ }
96
+
70
97
  await userStateNotifier.refresh();
71
98
  }
72
99
 
@@ -20,10 +20,19 @@ class _OnboardingJournalLoaderState extends ConsumerState<OnboardingLoader> {
20
20
  @override
21
21
  void initState() {
22
22
  super.initState();
23
- Future.delayed(const Duration(milliseconds: 3500), () async {
24
- await ref.onboardingNotifier.onOnboardingCompleted();
25
- widget.onCompleted();
26
- });
23
+ _prepare();
24
+ }
25
+
26
+ Future<void> _prepare() async {
27
+ // Run the real work (which now lazily creates the guest account) while the
28
+ // loader animation plays, so its network latency is hidden under the
29
+ // minimum display time instead of being added on top of it.
30
+ await Future.wait([
31
+ ref.onboardingNotifier.onOnboardingCompleted(),
32
+ Future<void>.delayed(const Duration(milliseconds: 3500)),
33
+ ]);
34
+ if (!mounted) return;
35
+ widget.onCompleted();
27
36
  }
28
37
 
29
38
  @override