kasy-cli 1.20.0 → 1.21.0-beta.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 (62) hide show
  1. package/README.md +11 -3
  2. package/lib/commands/docs.js +0 -10
  3. package/lib/commands/ios.js +3 -2
  4. package/lib/commands/new.js +98 -58
  5. package/lib/commands/run.js +7 -0
  6. package/lib/scaffold/CHANGELOG.json +14 -0
  7. package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
  8. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  9. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
  11. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
  12. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
  14. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  18. package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  19. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
  20. package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
  21. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
  22. package/lib/scaffold/backends/firebase/setup-from-scratch.js +10 -8
  23. package/lib/scaffold/backends/supabase/deploy.js +56 -3
  24. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
  25. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
  26. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
  27. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
  28. package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
  29. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
  30. package/lib/scaffold/catalog.js +2 -2
  31. package/lib/scaffold/generate.js +19 -3
  32. package/lib/scaffold/shared/generator-utils.js +265 -55
  33. package/lib/scaffold/shared/post-build.js +22 -6
  34. package/lib/utils/apple-release.js +1 -10
  35. package/lib/utils/browser.js +61 -0
  36. package/lib/utils/checks.js +189 -69
  37. package/lib/utils/env-tools.js +101 -0
  38. package/lib/utils/i18n/messages-en.js +13 -1
  39. package/lib/utils/i18n/messages-es.js +13 -1
  40. package/lib/utils/i18n/messages-pt.js +13 -1
  41. package/package.json +1 -1
  42. package/templates/firebase/lib/components/kasy_sidebar_pro.dart +8 -14
  43. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +38 -128
  44. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -125
  45. package/templates/firebase/lib/core/states/user_state_notifier.dart +8 -10
  46. package/templates/firebase/lib/core/widgets/kasy_hover.dart +9 -1
  47. package/templates/firebase/lib/features/home/home_components_page.dart +8 -14
  48. package/templates/firebase/lib/features/home/home_page.dart +7 -8
  49. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  50. package/templates/firebase/lib/router.dart +60 -0
  51. package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
  52. package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  53. package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
  54. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  55. package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
  56. package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
  57. package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
  58. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
  59. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
  60. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
  61. package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
  62. package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
@@ -122,6 +122,14 @@ module.exports = {
122
122
  'checks.install.failed': 'instalación automática falló — ejecuta el comando manualmente',
123
123
  'checks.waitPrompt.gcloud.install': 'Tras instalar gcloud, presiona Enter para verificar de nuevo',
124
124
  'checks.waitPrompt.gcloud.auth': 'Tras ejecutar gcloud auth login, presiona Enter para verificar de nuevo',
125
+ 'checks.thenRun': 'Luego ejecuta',
126
+ 'checks.recheck': 'Tras instalar {name}, presiona Enter para verificar de nuevo',
127
+ 'checks.install.confirm': '¿Instalar {name} ahora? ({cmd})',
128
+ 'checks.install.running': 'Instalando {name}…',
129
+ 'checks.install.failedManual': 'No pude instalar {name} automáticamente. Hazlo manualmente:',
130
+ 'checks.install.openDocs': '¿Abrir la página de instalación en el navegador?',
131
+ 'browser.open.intro': 'Abre este enlace en el navegador:',
132
+ 'browser.open.confirm': '¿Abrir en el navegador ahora?',
125
133
  'error.hint.notFlutterProject': 'No estás dentro de un proyecto Flutter. Usa `kasy new` para crear uno, o entra en la carpeta de un proyecto existente.',
126
134
  'error.hint.flutterMissing': 'Flutter no está instalado o no está en el PATH. Ejecuta `kasy doctor` para diagnosticar.',
127
135
  'error.hint.permission': 'Un archivo/carpeta es de solo lectura. Verifica los permisos de la carpeta padre o intenta desde tu carpeta home.',
@@ -524,7 +532,7 @@ module.exports = {
524
532
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
525
533
  'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
526
534
  'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
527
- 'new.firebase.module.web': '🌐 Web Support (PWA, solo Firebase)',
535
+ 'new.firebase.module.web': '🌐 Web Support (PWA)',
528
536
  'new.firebase.module.widget': '📱 Home Widget (iOS/Android)',
529
537
  'new.firebase.module.llm_chat': '🤖 AI Chat (OpenAI/Gemini)',
530
538
  'new.firebase.module.local_notifications': '🔔 Local Reminders (sin servidor)',
@@ -692,6 +700,10 @@ module.exports = {
692
700
  'new.google.refreshConfigs': 'Actualizando google-services.json y GoogleService-Info.plist con los Client IDs de Google…',
693
701
  'new.google.manualHint': 'Inicio de sesión con Google: actívalo manualmente en la consola (proveedor Google):',
694
702
  'new.google.manualHint.noEmail': 'Inicio de sesión con Google: no detecté un email de soporte (gcloud sin cuenta). Actívalo manualmente en la consola:',
703
+ 'new.google.supabaseManual': 'Inicio de sesión con Google: cliente creado, pero el secret aún no estaba disponible. Actívalo luego en el panel de Supabase (Authentication > Providers > Google).',
704
+ 'new.fcm.ok': 'generada automáticamente',
705
+ 'new.fcm.failSupabase': 'no generada (permiso de GCP aún propagando); define FIREBASE_SERVICE_ACCOUNT_JSON en los secrets de Supabase',
706
+ 'new.fcm.failApi': 'no generada (permiso de GCP aún propagando); ejecuta el comando de nuevo en unos minutos',
695
707
  'new.sha1.registering': 'Registrando SHA-1 para Google Sign-In (Android)…',
696
708
  'new.sha1.failed': 'SHA-1 no añadido automaticamente: {error}',
697
709
  'new.sha1.manual': 'Agregalo manualmente para que Google Sign-In funcione en Android:',
@@ -122,6 +122,14 @@ module.exports = {
122
122
  'checks.install.failed': 'instalação automática falhou — execute o comando manualmente',
123
123
  'checks.waitPrompt.gcloud.install': 'Após instalar o gcloud, pressione Enter para verificar novamente',
124
124
  'checks.waitPrompt.gcloud.auth': 'Após rodar gcloud auth login, pressione Enter para verificar novamente',
125
+ 'checks.thenRun': 'Depois rode',
126
+ 'checks.recheck': 'Após instalar {name}, pressione Enter para verificar novamente',
127
+ 'checks.install.confirm': 'Instalar {name} agora? ({cmd})',
128
+ 'checks.install.running': 'Instalando {name}…',
129
+ 'checks.install.failedManual': 'Não consegui instalar {name} automaticamente. Faça manualmente:',
130
+ 'checks.install.openDocs': 'Abrir a página de instalação no navegador?',
131
+ 'browser.open.intro': 'Abra este link no navegador:',
132
+ 'browser.open.confirm': 'Abrir no navegador agora?',
125
133
  'error.hint.notFlutterProject': 'Você não está dentro de um projeto Flutter. Use `kasy new` para criar um, ou entre na pasta de um projeto existente.',
126
134
  'error.hint.flutterMissing': 'Flutter não está instalado ou não está no PATH. Rode `kasy doctor` para diagnosticar.',
127
135
  'error.hint.permission': 'Algum arquivo/pasta está somente leitura. Verifique as permissões da pasta pai ou tente de novo a partir da sua pasta home.',
@@ -524,7 +532,7 @@ module.exports = {
524
532
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
525
533
  'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
526
534
  'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
527
- 'new.firebase.module.web': '🌐 Web Support (PWA, somente Firebase)',
535
+ 'new.firebase.module.web': '🌐 Web Support (PWA)',
528
536
  'new.firebase.module.widget': '📱 Home Widget (iOS/Android)',
529
537
  'new.firebase.module.llm_chat': '🤖 AI Chat (OpenAI/Gemini)',
530
538
  'new.firebase.module.local_notifications': '🔔 Local Reminders (sem servidor)',
@@ -692,6 +700,10 @@ module.exports = {
692
700
  'new.google.refreshConfigs': 'Atualizando google-services.json e GoogleService-Info.plist com Client IDs do Google…',
693
701
  'new.google.manualHint': 'Login com Google: ative manualmente no Console (provedor Google):',
694
702
  'new.google.manualHint.noEmail': 'Login com Google: não consegui detectar um e-mail de suporte (gcloud sem conta). Ative manualmente no Console:',
703
+ 'new.google.supabaseManual': 'Login com Google: client criado, mas o secret ainda não estava disponível. Ative depois no painel do Supabase (Authentication > Providers > Google).',
704
+ 'new.fcm.ok': 'gerada automaticamente',
705
+ 'new.fcm.failSupabase': 'não gerada (permissão do GCP ainda propagando); defina FIREBASE_SERVICE_ACCOUNT_JSON nos secrets do Supabase',
706
+ 'new.fcm.failApi': 'não gerada (permissão do GCP ainda propagando); rode o comando de novo em alguns minutos',
695
707
  'new.sha1.registering': 'Registrando SHA-1 para Google Sign-In (Android)…',
696
708
  'new.sha1.failed': 'SHA-1 não adicionado automaticamente: {error}',
697
709
  'new.sha1.manual': 'Adicione manualmente para o Google Sign-In funcionar no Android:',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.20.0",
3
+ "version": "1.21.0-beta.0",
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"
@@ -244,10 +244,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
244
244
  final c = _colors;
245
245
 
246
246
  final bool anchoredLeft = widget.side == KasySidebarProSide.left;
247
- const Radius r = Radius.circular(12);
248
- final BorderRadius borderRadius = anchoredLeft
249
- ? const BorderRadius.only(topRight: r, bottomRight: r)
250
- : const BorderRadius.only(topLeft: r, bottomLeft: r);
251
247
  final Border border = anchoredLeft
252
248
  ? Border(right: BorderSide(color: c.border))
253
249
  : Border(left: BorderSide(color: c.border));
@@ -282,16 +278,12 @@ class _KasySidebarProState extends State<KasySidebarPro> {
282
278
  right: anchoredLeft ? halfBtn : 0,
283
279
  top: 0,
284
280
  bottom: 0,
285
- child: ClipRRect(
286
- borderRadius: borderRadius,
287
- child: DecoratedBox(
288
- decoration: BoxDecoration(
289
- color: c.bg,
290
- border: border,
291
- borderRadius: borderRadius,
292
- ),
293
- child: _buildScrollableContent(context, c),
281
+ child: DecoratedBox(
282
+ decoration: BoxDecoration(
283
+ color: c.bg,
284
+ border: border,
294
285
  ),
286
+ child: _buildScrollableContent(context, c),
295
287
  ),
296
288
  ),
297
289
  // Collapse button — centered on the sidebar edge, fully within
@@ -309,7 +301,9 @@ class _KasySidebarProState extends State<KasySidebarPro> {
309
301
  // Use a Column that fills the sidebar height.
310
302
  // Top section scrolls if content overflows; bottom items (Help + Logout)
311
303
  // stay pinned to the bottom via Spacer in the outer Column.
312
- return Positioned.fill(
304
+ // SizedBox.expand (not Positioned.fill) because the parent here is a
305
+ // DecoratedBox, not a Stack — Positioned only works as a direct Stack child.
306
+ return SizedBox.expand(
313
307
  child: Column(
314
308
  crossAxisAlignment: CrossAxisAlignment.start,
315
309
  children: [
@@ -1,5 +1,4 @@
1
1
  import 'package:bart/bart.dart' as bart;
2
- import 'package:bart/bart/bart_bottombar_actions.dart';
3
2
  import 'package:flutter/foundation.dart';
4
3
  import 'package:flutter/material.dart';
5
4
  import 'package:flutter/services.dart';
@@ -11,79 +10,18 @@ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
11
10
 
12
11
  /// Bottom navigation host powered by Bart (https://pub.dev/packages/bart).
13
12
  ///
14
- /// Bart's [bart.BartScaffold] owns a [ValueNotifier<bool>] for bottom-bar
15
- /// visibility, created in its constructor. Two issues fall out of that:
16
- ///
17
- /// 1. [ResponsiveLayout] swaps between three [bart.BartScaffold]s (small /
18
- /// medium / large). Any rebuild (theme toggle, locale switch,
19
- /// device-preview resize) instantiates a fresh notifier with the initial
20
- /// value, dropping whatever visibility we had set.
21
- /// 2. Bart's [bart.BartScaffold.onRouteChanged] only fires when the active
22
- /// *tab index* changes — pushing an inner route inside the same tab
23
- /// (e.g. `home → home/components`) is silent. We can't drive visibility
24
- /// from that callback alone.
25
- ///
26
- /// We solve both by treating inner-route presence as the source of truth.
27
- /// [BottomBarVisibilityScope] exposes a counter; the [HomeInnerRoute] wrapper
28
- /// increments while mounted and decrements on dispose. This widget reacts to
29
- /// the counter (and to lifecycle resume + every rebuild) and re-asserts the
30
- /// matching [BottomBarIntent] on Bart.
31
- class BottomMenu extends StatefulWidget {
13
+ /// [ResponsiveLayout] swaps between three [bart.BartScaffold]s (small / medium /
14
+ /// large) based on width. The bottom bar shows on every tab; detail screens
15
+ /// (Features, Components, …) are pushed on the root navigator from the pages
16
+ /// themselves, so they cover this menu and there's nothing to toggle here.
17
+ class BottomMenu extends StatelessWidget {
32
18
  final String? initialRoute;
33
19
 
34
20
  const BottomMenu({super.key, this.initialRoute});
35
21
 
36
- @override
37
- State<BottomMenu> createState() => _BottomMenuState();
38
- }
39
-
40
- class _BottomMenuState extends State<BottomMenu>
41
- with WidgetsBindingObserver, BartNotifier {
42
- final ValueNotifier<int> _innerRouteDepth = ValueNotifier<int>(0);
43
-
44
- @override
45
- void initState() {
46
- super.initState();
47
- WidgetsBinding.instance.addObserver(this);
48
- _innerRouteDepth.addListener(_scheduleSync);
49
- }
50
-
51
- @override
52
- void dispose() {
53
- _innerRouteDepth.removeListener(_scheduleSync);
54
- _innerRouteDepth.dispose();
55
- WidgetsBinding.instance.removeObserver(this);
56
- super.dispose();
57
- }
58
-
59
- @override
60
- void didChangeAppLifecycleState(AppLifecycleState state) {
61
- super.didChangeAppLifecycleState(state);
62
- if (state == AppLifecycleState.resumed) {
63
- _scheduleSync();
64
- }
65
- }
66
-
67
- void _scheduleSync() {
68
- WidgetsBinding.instance.addPostFrameCallback((_) {
69
- if (!mounted) return;
70
- if (_innerRouteDepth.value > 0) {
71
- hideBottomBar(context);
72
- } else {
73
- showBottomBar(context);
74
- }
75
- });
76
- }
77
-
78
22
  @override
79
23
  Widget build(BuildContext context) {
80
- // Re-assert visibility after every rebuild: the BartScaffold underneath
81
- // may have been freshly instantiated (see class doc).
82
- _scheduleSync();
83
-
84
- final String? resolvedInitialRoute = _resolveInitialRoute(
85
- widget.initialRoute,
86
- );
24
+ final String? resolvedInitialRoute = _resolveInitialRoute(initialRoute);
87
25
  final bool showBottomBarOnStart = _shouldShowBottomBarOnStart(
88
26
  resolvedInitialRoute,
89
27
  );
@@ -91,44 +29,39 @@ class _BottomMenuState extends State<BottomMenu>
91
29
  backgroundColor: context.colors.background,
92
30
  );
93
31
 
94
- return BottomBarVisibilityScope(
95
- innerRouteDepth: _innerRouteDepth,
96
- child: AnnotatedRegion<SystemUiOverlayStyle>(
97
- value: switch (Theme.brightnessOf(context)) {
98
- Brightness.dark => SystemUiOverlayStyle.light,
99
- Brightness.light => SystemUiOverlayStyle.dark,
100
- },
101
- child: ResponsiveLayout(
102
- small: bart.BartScaffold(
103
- routesBuilder: subRoutes,
104
- bottomBar: kasyPaddedSurfaceBottomBar(),
105
- initialRoute: resolvedInitialRoute,
106
- showBottomBarOnStart: showBottomBarOnStart,
107
- scaffoldOptions: scaffoldOptions,
108
- ),
109
- // medium (768–1024 px): KasySidebarPro auto-collapses at this width
110
- medium: bart.BartScaffold(
111
- routesBuilder: subRoutes,
112
- bottomBar: kasyPaddedSurfaceBottomBar(),
113
- initialRoute: resolvedInitialRoute,
114
- showBottomBarOnStart: showBottomBarOnStart,
115
- scaffoldOptions: scaffoldOptions,
116
- sideBarOptions: bart.CustomSideBarOptions(
117
- sideBarBuilder: (routes, onTap, current) =>
118
- const KasySidebarPro(),
119
- ),
32
+ return AnnotatedRegion<SystemUiOverlayStyle>(
33
+ value: switch (Theme.brightnessOf(context)) {
34
+ Brightness.dark => SystemUiOverlayStyle.light,
35
+ Brightness.light => SystemUiOverlayStyle.dark,
36
+ },
37
+ child: ResponsiveLayout(
38
+ small: bart.BartScaffold(
39
+ routesBuilder: subRoutes,
40
+ bottomBar: kasyPaddedSurfaceBottomBar(),
41
+ initialRoute: resolvedInitialRoute,
42
+ showBottomBarOnStart: showBottomBarOnStart,
43
+ scaffoldOptions: scaffoldOptions,
44
+ ),
45
+ // medium (768–1024 px): KasySidebarPro auto-collapses at this width
46
+ medium: bart.BartScaffold(
47
+ routesBuilder: subRoutes,
48
+ bottomBar: kasyPaddedSurfaceBottomBar(),
49
+ initialRoute: resolvedInitialRoute,
50
+ showBottomBarOnStart: showBottomBarOnStart,
51
+ scaffoldOptions: scaffoldOptions,
52
+ sideBarOptions: bart.CustomSideBarOptions(
53
+ sideBarBuilder: (routes, onTap, current) => const KasySidebarPro(),
120
54
  ),
121
- // large (1024 px+): full expanded sidebar
122
- large: bart.BartScaffold(
123
- routesBuilder: subRoutes,
124
- bottomBar: kasyPaddedSurfaceBottomBar(),
125
- initialRoute: resolvedInitialRoute,
126
- showBottomBarOnStart: showBottomBarOnStart,
127
- scaffoldOptions: scaffoldOptions,
128
- sideBarOptions: bart.CustomSideBarOptions(
129
- sideBarBuilder: (routes, onTap, current) =>
130
- const KasySidebarPro(),
131
- ),
55
+ ),
56
+ // large (1024 px+): full expanded sidebar
57
+ large: bart.BartScaffold(
58
+ routesBuilder: subRoutes,
59
+ bottomBar: kasyPaddedSurfaceBottomBar(),
60
+ initialRoute: resolvedInitialRoute,
61
+ showBottomBarOnStart: showBottomBarOnStart,
62
+ scaffoldOptions: scaffoldOptions,
63
+ sideBarOptions: bart.CustomSideBarOptions(
64
+ sideBarBuilder: (routes, onTap, current) => const KasySidebarPro(),
132
65
  ),
133
66
  ),
134
67
  ),
@@ -176,26 +109,3 @@ class _BottomMenuState extends State<BottomMenu>
176
109
  return segments.length < 2;
177
110
  }
178
111
  }
179
-
180
- /// Shared counter that inner home routes increment while mounted. The host
181
- /// [BottomMenu] observes it and toggles Bart's bottom-bar accordingly.
182
- class BottomBarVisibilityScope extends InheritedWidget {
183
- final ValueNotifier<int> innerRouteDepth;
184
-
185
- const BottomBarVisibilityScope({
186
- super.key,
187
- required this.innerRouteDepth,
188
- required super.child,
189
- });
190
-
191
- static ValueNotifier<int>? maybeOf(BuildContext context) {
192
- final scope = context
193
- .getInheritedWidgetOfExactType<BottomBarVisibilityScope>();
194
- return scope?.innerRouteDepth;
195
- }
196
-
197
- @override
198
- bool updateShouldNotify(covariant BottomBarVisibilityScope oldWidget) {
199
- return innerRouteDepth != oldWidget.innerRouteDepth;
200
- }
201
- }
@@ -1,22 +1,20 @@
1
1
  import 'package:bart/bart.dart';
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:kasy_kit/components/kasy_app_bar.dart';
4
- import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
5
- import 'package:kasy_kit/core/bottom_menu/bottom_menu.dart';
6
4
  import 'package:kasy_kit/core/bottom_menu/notification_bottom_item.dart';
7
5
  import 'package:kasy_kit/core/navigation/kasy_navigation_config.dart';
8
6
  import 'package:kasy_kit/core/navigation/kasy_route_transition.dart';
9
7
  import 'package:kasy_kit/core/rating/widgets/rate_banner.dart';
10
8
  import 'package:kasy_kit/core/theme/theme.dart';
11
- import 'package:kasy_kit/features/home/design_system_page.dart';
12
- import 'package:kasy_kit/features/home/home_components_page.dart';
13
- import 'package:kasy_kit/features/home/home_components_preview_page.dart';
14
- import 'package:kasy_kit/features/home/home_features_page.dart';
15
9
  import 'package:kasy_kit/features/home/home_page.dart';
16
10
  import 'package:kasy_kit/features/notifications/ui/notifications_page.dart';
17
11
  import 'package:kasy_kit/features/settings/settings_page.dart';
18
12
  import 'package:kasy_kit/i18n/translations.g.dart';
19
13
 
14
+ /// Bart tab routes (bottom bar). Detail screens reachable from a tab — Features,
15
+ /// Components, Design System, component previews — are NOT inner routes here:
16
+ /// they're pushed on the ROOT navigator (see home_page.dart / home_components_page.dart)
17
+ /// so they open full-screen without the bottom bar and return to the tab intact.
20
18
  List<BartMenuRoute> subRoutes() {
21
19
  return [
22
20
  BartMenuRoute.bottomBar(
@@ -51,101 +49,9 @@ List<BartMenuRoute> subRoutes() {
51
49
  transitionDuration: bottomBarTransitionDuration,
52
50
  transitionsBuilder: bottomBarTransition,
53
51
  ),
54
- BartMenuRoute.innerRoute(
55
- path: bartInnerPath('home', 'features'),
56
- showBottomBar: false,
57
- pageBuilder: (_, _, _) =>
58
- const _HomeInnerRoute(child: HomeFeaturesPage()),
59
- transitionDuration: bottomBarTransitionDuration,
60
- transitionsBuilder: bottomBarTransition,
61
- ),
62
- BartMenuRoute.innerRoute(
63
- path: bartInnerPath('home', 'components'),
64
- showBottomBar: false,
65
- pageBuilder: (_, _, _) =>
66
- const _HomeInnerRoute(child: HomeComponentsPage()),
67
- transitionDuration: bottomBarTransitionDuration,
68
- transitionsBuilder: bottomBarTransition,
69
- ),
70
- BartMenuRoute.innerRoute(
71
- path: bartInnerPath('home', 'design-system'),
72
- showBottomBar: false,
73
- pageBuilder: (_, _, _) =>
74
- const _HomeInnerRoute(child: DesignSystemPage()),
75
- transitionDuration: bottomBarTransitionDuration,
76
- transitionsBuilder: bottomBarTransition,
77
- ),
78
- BartMenuRoute.innerRoute(
79
- path: bartInnerPath('home', 'components-preview'),
80
- showBottomBar: false,
81
- pageBuilder: (_, _, settings) {
82
- final extra = settings?.arguments;
83
- if (extra is! HomeComponentPreviewRouteExtra) {
84
- return const PageFake();
85
- }
86
- return _HomeInnerRoute(
87
- child: HomeComponentsPreviewPage(
88
- title: extra.title,
89
- variants: extra.variants,
90
- ),
91
- );
92
- },
93
- transitionDuration: bottomBarTransitionDuration,
94
- transitionsBuilder: bottomBarTransition,
95
- ),
96
- BartMenuRoute.innerRoute(
97
- path: bartInnerPath('home', 'search'),
98
- showBottomBar: false,
99
- pageBuilder: (_, _, _) => const _HomeInnerRoute(child: PageFake()),
100
- transitionDuration: bottomBarTransitionDuration,
101
- transitionsBuilder: bottomBarTransition,
102
- ),
103
- BartMenuRoute.innerRoute(
104
- path: bartInnerPath('home', 'search/subscribe'),
105
- showBottomBar: false,
106
- pageBuilder: (_, _, _) => const _HomeInnerRoute(child: PageFake()),
107
- transitionDuration: bottomBarTransitionDuration,
108
- transitionsBuilder: bottomBarTransition,
109
- ),
110
52
  ];
111
53
  }
112
54
 
113
- /// Wrapper for inner home routes. While mounted, it bumps a shared counter
114
- /// exposed by [BottomBarVisibilityScope] so [BottomMenu] knows to hide the
115
- /// bottom bar — covering cases Bart's tab-only `onRouteChanged` misses (e.g.
116
- /// pushing inside the same tab) and surviving scaffold rebuilds.
117
- class _HomeInnerRoute extends StatefulWidget {
118
- final Widget child;
119
-
120
- const _HomeInnerRoute({required this.child});
121
-
122
- @override
123
- State<_HomeInnerRoute> createState() => _HomeInnerRouteState();
124
- }
125
-
126
- class _HomeInnerRouteState extends State<_HomeInnerRoute> {
127
- ValueNotifier<int>? _depth;
128
-
129
- @override
130
- void didChangeDependencies() {
131
- super.didChangeDependencies();
132
- final next = BottomBarVisibilityScope.maybeOf(context);
133
- if (identical(_depth, next)) return;
134
- _depth?.value -= 1;
135
- _depth = next;
136
- _depth?.value += 1;
137
- }
138
-
139
- @override
140
- void dispose() {
141
- _depth?.value -= 1;
142
- super.dispose();
143
- }
144
-
145
- @override
146
- Widget build(BuildContext context) => widget.child;
147
- }
148
-
149
55
  /// Placeholder page for the wishlist tab.
150
56
  /// Replace this with your own implementation.
151
57
  class WishlistPage extends StatelessWidget {
@@ -159,8 +65,8 @@ class WishlistPage extends StatelessWidget {
159
65
  crossAxisAlignment: CrossAxisAlignment.stretch,
160
66
  children: [
161
67
  KasyAppBar(
162
- title: context.t.navigation.wishlist,
163
- style: KasyAppBarStyle.rootTab,
68
+ title: context.t.navigation.wishlist,
69
+ style: KasyAppBarStyle.rootTab,
164
70
  ),
165
71
  Expanded(
166
72
  child: Padding(
@@ -224,31 +130,6 @@ class WishlistPage extends StatelessWidget {
224
130
  }
225
131
  }
226
132
 
227
- /// This is a fake page to show how to use Bart
228
- /// You can replace it with your own pages
229
- class PageFake extends StatelessWidget {
230
- final Color? color;
231
-
232
- const PageFake({this.color, super.key});
233
-
234
- @override
235
- Widget build(BuildContext context) {
236
- return SafeArea(
237
- // ignore: use_colored_box
238
- child: Container(
239
- color: color ?? context.colors.background,
240
- child: Column(
241
- children: [
242
- const RateBanner(forceInDebug: true),
243
- const SizedBox(height: 100),
244
- Opacity(opacity: 0.5, child: Text(context.t.bottom_router.fake_page_text)),
245
- ],
246
- ),
247
- ),
248
- );
249
- }
250
- }
251
-
252
133
  Widget bottomBarTransition(
253
134
  BuildContext context,
254
135
  Animation<double> animation,
@@ -267,19 +267,17 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
267
267
  }
268
268
  if (user == null) {
269
269
  // User document not found after login. Possible causes:
270
- // - Firebase: onUserRegistration Cloud Function not deployed or failed
271
- // - Supabase: handle_new_user trigger not run yet (race) or failed
272
- // - User was deleted but session was still cached
273
- // Logout to clear invalid session, then create new anonymous user if applicable.
270
+ // - Cloud Functions not deployed yet (run `kasy deploy` to fix)
271
+ // - onUserRegistration Cloud Function failed
272
+ // - Supabase handle_new_user trigger not run yet (race) or failed
273
+ // Fall back to keeping the user authenticated with their auth ID so
274
+ // the app works locally before `kasy deploy` is run.
274
275
  _logger.e(
275
276
  '⚠️ User profile not found in database after sign-in.\n'
276
- ' Logging out and creating new anonymous user.',
277
+ ' If this is a new project, run: kasy deploy\n'
278
+ ' to deploy the backend and create the user document.',
277
279
  );
278
- await _authenticationRepository.logout();
279
- state = state.copyWith(user: const User.anonymous());
280
- if (mode == AuthenticationMode.anonymous) {
281
- await _loadAnonymousState();
282
- }
280
+ state = state.copyWith(user: User.anonymous(id: credentials.id));
283
281
  return;
284
282
  }
285
283
  // If the Firestore document is anonymous (no email) but Firebase Auth
@@ -44,6 +44,7 @@ class KasyHover extends ConsumerStatefulWidget {
44
44
  this.margin,
45
45
  this.semanticLabel,
46
46
  this.hapticEnabled = true,
47
+ this.hoverEnabled = true,
47
48
  this.hoverColor,
48
49
  this.pressColor,
49
50
  });
@@ -67,6 +68,13 @@ class KasyHover extends ConsumerStatefulWidget {
67
68
  /// Haptic on tap (automatically ignored on web).
68
69
  final bool hapticEnabled;
69
70
 
71
+ /// Whether the pointer-hover overlay is shown on pointer-capable devices.
72
+ ///
73
+ /// When false, the resting/press feedback and click cursor are kept, but no
74
+ /// background fill appears while hovering. Use on plain list rows (e.g.
75
+ /// settings) where a hover highlight is visually unwanted.
76
+ final bool hoverEnabled;
77
+
70
78
  /// Exact background colour used while the pointer hovers over the widget.
71
79
  ///
72
80
  /// When non-null, this solid colour replaces the default semi-transparent
@@ -110,7 +118,7 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
110
118
  final bool isDark = Theme.of(context).brightness == Brightness.dark;
111
119
  final Color base = widget.pressColor ?? context.colors.onSurface;
112
120
  if (_pressed) return base.withValues(alpha: isDark ? 0.04 : 0.10);
113
- if (_hovered) {
121
+ if (_hovered && widget.hoverEnabled) {
114
122
  // Solid hover colour — used for nav items where hover must exactly
115
123
  // match the selected background.
116
124
  if (widget.hoverColor != null) return widget.hoverColor!;
@@ -1,11 +1,10 @@
1
1
  import 'package:flutter/material.dart';
2
+ import 'package:go_router/go_router.dart';
2
3
  import 'package:kasy_kit/components/kasy_app_bar.dart';
3
- import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
4
4
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
5
5
  import 'package:kasy_kit/core/theme/theme.dart';
6
6
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
7
7
  import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
8
- import 'package:kasy_kit/features/home/home_components_preview_page.dart';
9
8
  import 'package:kasy_kit/features/home/home_components_preview_registry.dart';
10
9
 
11
10
  /// UI-kit catalog, reachable from the home dashboard.
@@ -66,22 +65,17 @@ class HomeComponentsPage extends StatelessWidget {
66
65
  onTap: () {
67
66
  KasyHaptics.selection(context);
68
67
  if (row.name == 'Design System') {
69
- Navigator.of(context).pushNamed(
70
- bartInnerPath('home', 'design-system'),
71
- );
68
+ context.push('/design-system');
72
69
  return;
73
70
  }
74
- final ComponentPreviewDefinition? definition =
75
- getComponentPreviewDefinition(row.name);
76
- if (definition == null) {
71
+ // Skip rows without a preview yet (registry returns
72
+ // null); otherwise open /components/:name, which the
73
+ // router resolves back to this component's preview.
74
+ if (getComponentPreviewDefinition(row.name) == null) {
77
75
  return;
78
76
  }
79
- Navigator.of(context).pushNamed(
80
- bartInnerPath('home', 'components-preview'),
81
- arguments: HomeComponentPreviewRouteExtra(
82
- title: definition.title,
83
- variants: definition.variants,
84
- ),
77
+ context.push(
78
+ '/components/${Uri.encodeComponent(row.name)}',
85
79
  );
86
80
  },
87
81
  child: Padding(
@@ -1,7 +1,7 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:go_router/go_router.dart';
3
4
  import 'package:kasy_kit/components/kasy_app_bar.dart';
4
- import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
5
5
  import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
6
6
  import 'package:kasy_kit/core/states/components/maybe_ask_rating.dart';
7
7
  import 'package:kasy_kit/core/states/components/maybe_show_update_bottom_sheet.dart';
@@ -13,7 +13,7 @@ import 'package:kasy_kit/features/notifications/shared/att_permission.dart';
13
13
  import 'package:kasy_kit/features/subscription/shared/maybeshow_premium.dart';
14
14
  import 'package:kasy_kit/i18n/translations.g.dart';
15
15
 
16
- /// Home hub with dashboard cards; features live under [HomeFeaturesPage].
16
+ /// Home hub with dashboard cards linking to the Features and Components screens.
17
17
  class HomePage extends ConsumerWidget {
18
18
  const HomePage({super.key});
19
19
 
@@ -59,9 +59,10 @@ class HomePage extends ConsumerWidget {
59
59
  gradient: _featuresGradient(context, isDark),
60
60
  onOpen: () {
61
61
  KasyHaptics.medium(context);
62
- Navigator.of(context).pushNamed(
63
- bartInnerPath('home', 'features'),
64
- );
62
+ // Top-level go_router route: opens full-screen on the
63
+ // root navigator (above the BottomMenu, no bottom bar)
64
+ // and pops back to home with the menu intact.
65
+ context.push('/features');
65
66
  },
66
67
  );
67
68
  final componentsCard = _DashboardGradientCard(
@@ -74,9 +75,7 @@ class HomePage extends ConsumerWidget {
74
75
  gradient: _componentsGradient(context, isDark),
75
76
  onOpen: () {
76
77
  KasyHaptics.medium(context);
77
- Navigator.of(context).pushNamed(
78
- bartInnerPath('home', 'components'),
79
- );
78
+ context.push('/components');
80
79
  },
81
80
  );
82
81
  if (isWide) {
@@ -155,6 +155,7 @@ class SettingsTile extends StatelessWidget {
155
155
  Widget build(BuildContext context) {
156
156
  return KasyHover(
157
157
  onTap: onTap,
158
+ hoverEnabled: false,
158
159
  semanticLabel: title,
159
160
  padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
160
161
  child: Row(