kasy-cli 1.32.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 (169) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +66 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/apple-web.js +222 -0
  5. package/lib/commands/configure.js +3 -91
  6. package/lib/commands/doctor.js +20 -0
  7. package/lib/commands/facebook.js +189 -0
  8. package/lib/commands/new.js +61 -11
  9. package/lib/commands/release-version.js +234 -0
  10. package/lib/commands/update.js +27 -0
  11. package/lib/scaffold/CHANGELOG.json +27 -0
  12. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  14. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
  17. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  18. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  19. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  20. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  21. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  22. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
  23. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  24. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  25. package/lib/scaffold/generate.js +53 -4
  26. package/lib/scaffold/shared/generator-utils.js +18 -6
  27. package/lib/utils/apple-web.js +147 -0
  28. package/lib/utils/facebook.js +162 -0
  29. package/lib/utils/i18n/messages-en.js +85 -0
  30. package/lib/utils/i18n/messages-es.js +85 -0
  31. package/lib/utils/i18n/messages-pt.js +85 -0
  32. package/package.json +5 -2
  33. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  34. package/templates/firebase/AGENTS.md +170 -0
  35. package/templates/firebase/CLAUDE.md +16 -0
  36. package/templates/firebase/DESIGN_SYSTEM.md +269 -0
  37. package/templates/firebase/docs/auth-setup.en.md +4 -2
  38. package/templates/firebase/docs/auth-setup.es.md +4 -2
  39. package/templates/firebase/docs/auth-setup.pt.md +4 -2
  40. package/templates/firebase/firebase.json +56 -1
  41. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  42. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  43. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  44. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  45. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  46. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  47. package/templates/firebase/lib/components/components.dart +1 -0
  48. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  49. package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
  50. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  51. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  52. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  53. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  54. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  55. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  56. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  57. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  58. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  59. package/templates/firebase/lib/components/kasy_toast.dart +108 -73
  60. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  61. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  62. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  63. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  64. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  65. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  66. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  67. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  68. package/templates/firebase/lib/core/config/features.dart +5 -0
  69. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  70. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  71. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  72. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  73. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
  74. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  75. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  76. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  77. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  78. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  79. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  80. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  81. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  82. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  83. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  84. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  85. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  86. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  87. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  88. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  89. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  90. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  91. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
  92. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  93. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  94. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  95. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  96. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  97. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  98. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
  99. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  100. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  101. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  102. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  103. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  104. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  105. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  106. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  107. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  108. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
  109. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  110. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  111. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  112. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  113. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  114. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  115. package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
  116. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  117. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
  118. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  119. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
  120. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  121. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  122. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  123. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  124. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
  125. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
  126. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  127. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  128. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  129. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  130. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  131. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  132. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  133. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
  134. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  135. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  136. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  137. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  138. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  139. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  140. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  141. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  142. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  143. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  144. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  145. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  146. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  147. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  148. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  149. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  150. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  151. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  152. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  153. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  154. package/templates/firebase/lib/i18n/en.i18n.json +54 -7
  155. package/templates/firebase/lib/i18n/es.i18n.json +54 -7
  156. package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
  157. package/templates/firebase/lib/main.dart +11 -2
  158. package/templates/firebase/lib/router.dart +94 -13
  159. package/templates/firebase/pubspec.yaml +1 -1
  160. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  161. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  162. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  163. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  164. package/templates/firebase/tool/design_check.dart +152 -0
  165. package/templates/firebase/web/index.html +162 -14
  166. package/templates/firebase/assets/images/review.png +0 -0
  167. package/templates/firebase/assets/images/update.png +0 -0
  168. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
  169. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -0,0 +1,89 @@
1
+ import 'package:flutter/gestures.dart';
2
+ import 'package:flutter/services.dart';
3
+ import 'package:flutter/widgets.dart';
4
+
5
+ /// Makes focus rings keyboard-only — the web's `:focus-visible` behaviour.
6
+ ///
7
+ /// Flutter's default [FocusHighlightStrategy.automatic] only hides focus rings
8
+ /// for *touch*; on desktop a mouse click keeps them in "traditional" mode, so a
9
+ /// ring raised by Tab navigation never clears once the user grabs the mouse.
10
+ /// This wraps the app and flips the strategy by input type:
11
+ /// - a navigation key (Tab / arrows / Home / End / Page Up·Down) shows rings;
12
+ /// - any pointer press hides them, and — when the user was mid keyboard
13
+ /// navigation — also drops focus, so the next Tab restarts from the top of
14
+ /// the traversal (skip link → sidebar → content) instead of resuming
15
+ /// wherever the ring happened to be.
16
+ ///
17
+ /// Plain typing does NOT turn rings on (only navigation keys do), so editing a
18
+ /// text field with the mouse stays ring-free. Uses a global pointer route so it
19
+ /// catches clicks anywhere, including empty areas with no hit target.
20
+ class FocusVisibility extends StatefulWidget {
21
+ const FocusVisibility({super.key, required this.child});
22
+
23
+ final Widget child;
24
+
25
+ @override
26
+ State<FocusVisibility> createState() => _FocusVisibilityState();
27
+ }
28
+
29
+ class _FocusVisibilityState extends State<FocusVisibility> {
30
+ static final Set<LogicalKeyboardKey> _navigationKeys = <LogicalKeyboardKey>{
31
+ LogicalKeyboardKey.tab,
32
+ LogicalKeyboardKey.arrowUp,
33
+ LogicalKeyboardKey.arrowDown,
34
+ LogicalKeyboardKey.arrowLeft,
35
+ LogicalKeyboardKey.arrowRight,
36
+ LogicalKeyboardKey.home,
37
+ LogicalKeyboardKey.end,
38
+ LogicalKeyboardKey.pageUp,
39
+ LogicalKeyboardKey.pageDown,
40
+ };
41
+
42
+ @override
43
+ void initState() {
44
+ super.initState();
45
+ // Start ring-free; the first navigation key turns them on.
46
+ FocusManager.instance.highlightStrategy =
47
+ FocusHighlightStrategy.alwaysTouch;
48
+ HardwareKeyboard.instance.addHandler(_handleKey);
49
+ GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointer);
50
+ }
51
+
52
+ @override
53
+ void dispose() {
54
+ GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointer);
55
+ HardwareKeyboard.instance.removeHandler(_handleKey);
56
+ super.dispose();
57
+ }
58
+
59
+ bool _handleKey(KeyEvent event) {
60
+ if (event is KeyDownEvent && _navigationKeys.contains(event.logicalKey)) {
61
+ _setStrategy(FocusHighlightStrategy.alwaysTraditional);
62
+ }
63
+ return false; // observe only — never consume the event
64
+ }
65
+
66
+ void _handlePointer(PointerEvent event) {
67
+ if (event is! PointerDownEvent) {
68
+ return;
69
+ }
70
+ final FocusManager fm = FocusManager.instance;
71
+ final bool wasKeyboardNav =
72
+ fm.highlightMode == FocusHighlightMode.traditional;
73
+ _setStrategy(FocusHighlightStrategy.alwaysTouch);
74
+ // Only reset focus when leaving keyboard navigation; a normal click (e.g.
75
+ // into a text field) then keeps the focus it's about to request.
76
+ if (wasKeyboardNav) {
77
+ fm.primaryFocus?.unfocus();
78
+ }
79
+ }
80
+
81
+ void _setStrategy(FocusHighlightStrategy strategy) {
82
+ if (FocusManager.instance.highlightStrategy != strategy) {
83
+ FocusManager.instance.highlightStrategy = strategy;
84
+ }
85
+ }
86
+
87
+ @override
88
+ Widget build(BuildContext context) => widget.child;
89
+ }
@@ -3,17 +3,19 @@ import 'package:kasy_kit/components/components.dart';
3
3
  import 'package:kasy_kit/core/theme/theme.dart';
4
4
  import 'package:kasy_kit/i18n/translations.g.dart';
5
5
 
6
- /// Shows a modal bottom sheet displaying app update information
6
+ /// Shows the "what's new" bottom sheet, built on [KasyBottomSheet].
7
+ ///
8
+ /// Dismissing it — the Continue button, the drag handle, or tapping the dim
9
+ /// barrier outside — simply closes the sheet and returns the user to the screen
10
+ /// they were on; it never navigates anywhere. The version is already recorded as
11
+ /// seen before this is shown (see `MaybeShowUpdateBottomSheet`), so it won't pop
12
+ /// up again for the same version.
7
13
  Future<void> showUpdateBottomSheet({
8
14
  required BuildContext context,
9
15
  required String version,
10
- bool useRootNavigator = true,
11
16
  }) async {
12
- await showModalBottomSheet(
17
+ await showKasyBottomSheet<void>(
13
18
  context: context,
14
- useSafeArea: true,
15
- useRootNavigator: useRootNavigator,
16
- barrierColor: context.colors.background.withValues(alpha: 0.90),
17
19
  isScrollControlled: true,
18
20
  builder: (context) => _UpdateBottomSheet(version: version),
19
21
  );
@@ -28,130 +30,31 @@ class _UpdateBottomSheet extends StatelessWidget {
28
30
  Widget build(BuildContext context) {
29
31
  final translations = Translations.of(context).update_bottom_sheet;
30
32
 
31
- return Container(
32
- decoration: BoxDecoration(
33
- color: context.colors.surface,
34
- borderRadius: const BorderRadius.only(
35
- topLeft: Radius.circular(KasyRadius.xl),
36
- topRight: Radius.circular(KasyRadius.xl),
33
+ return KasyBottomSheet(
34
+ icon: KasyIcons.star,
35
+ title: translations.title,
36
+ message: 'Version $version',
37
+ body: ConstrainedBox(
38
+ constraints: BoxConstraints(
39
+ maxHeight: MediaQuery.sizeOf(context).height * 0.5,
37
40
  ),
38
- border: Border.all(
39
- color: context.colors.primary.withValues(alpha: 0.2),
40
- strokeAlign: BorderSide.strokeAlignOutside,
41
- width: 2,
41
+ child: ListView.separated(
42
+ shrinkWrap: true,
43
+ padding: EdgeInsets.zero,
44
+ itemBuilder: (context, index) =>
45
+ _UpdateHighlightTile(highlight: translations.highlights[index]),
46
+ separatorBuilder: (context, index) =>
47
+ const SizedBox(height: KasySpacing.sm),
48
+ itemCount: translations.highlights.length,
42
49
  ),
43
50
  ),
44
- child: SafeArea(
45
- minimum: const EdgeInsets.only(bottom: KasySpacing.md),
46
- child: Column(
47
- mainAxisSize: MainAxisSize.min,
48
- crossAxisAlignment: CrossAxisAlignment.start,
49
- children: [
50
- Flexible(
51
- flex: 0,
52
- child: Align(
53
- child: ClipRRect(
54
- borderRadius: const BorderRadius.only(
55
- topLeft: Radius.circular(KasyRadius.xl),
56
- topRight: Radius.circular(KasyRadius.xl),
57
- ),
58
- child: Image.asset(
59
- 'assets/images/update.png',
60
- fit: BoxFit.cover,
61
- ),
62
- ),
63
- ),
64
- ),
65
- Padding(
66
- padding: const EdgeInsets.fromLTRB(
67
- KasySpacing.lg,
68
- KasySpacing.lg,
69
- KasySpacing.md,
70
- 0,
71
- ),
72
- child: Row(
73
- children: [
74
- Container(
75
- width: 48,
76
- height: 48,
77
- decoration: BoxDecoration(
78
- color: context.colors.primary.withValues(alpha: 0.1),
79
- borderRadius: KasyRadius.mdBorderRadius,
80
- ),
81
- child: Icon(
82
- KasyIcons.star,
83
- color: context.colors.primary,
84
- size: KasyIconSize.xl,
85
- ),
86
- ),
87
- const SizedBox(width: KasySpacing.md),
88
- Expanded(
89
- child: Column(
90
- crossAxisAlignment: CrossAxisAlignment.start,
91
- children: [
92
- Text(
93
- translations.title,
94
- style: context.textTheme.headlineSmall?.copyWith(
95
- color: context.colors.onSurface,
96
- fontWeight: FontWeight.w700,
97
- ),
98
- ),
99
- Text(
100
- "Version $version",
101
- style: context.textTheme.bodyMedium?.copyWith(
102
- color: context.colors.muted,
103
- fontWeight: FontWeight.w500,
104
- ),
105
- ),
106
- ],
107
- ),
108
- ),
109
- KasyButton.iconOnly(
110
- icon: KasyIcons.close,
111
- variant: KasyButtonVariant.ghost,
112
- foregroundColor: context.colors.muted,
113
- onPressed: () => Navigator.of(context).pop(),
114
- semanticLabel: translations.title,
115
- ),
116
- ],
117
- ),
118
- ),
119
- const SizedBox(height: KasySpacing.lg),
120
- Flexible(
121
- child: ConstrainedBox(
122
- constraints: BoxConstraints(
123
- maxHeight: MediaQuery.of(context).size.height * 0.6,
124
- ),
125
- child: ListView.separated(
126
- shrinkWrap: true,
127
- padding: const EdgeInsets.symmetric(
128
- horizontal: KasySpacing.lg,
129
- ),
130
- itemBuilder: (context, index) {
131
- final highlight = translations.highlights[index];
132
- return _UpdateHighlightTile(highlight: highlight);
133
- },
134
- separatorBuilder: (context, index) =>
135
- const SizedBox(height: KasySpacing.sm),
136
- itemCount: translations.highlights.length,
137
- ),
138
- ),
139
- ),
140
- const SizedBox(height: KasySpacing.xl),
141
- Padding(
142
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
143
- child: SizedBox(
144
- width: double.infinity,
145
- child: KasyButton(
146
- label: translations.continue_button,
147
- expand: true,
148
- onPressed: () => Navigator.of(context).pop(),
149
- ),
150
- ),
151
- ),
152
- ],
51
+ actions: [
52
+ KasyButton(
53
+ label: translations.continue_button,
54
+ expand: true,
55
+ onPressed: () => Navigator.of(context).pop(),
153
56
  ),
154
- ),
57
+ ],
155
58
  );
156
59
  }
157
60
  }
@@ -115,13 +115,8 @@ class _DetailContainer extends StatelessWidget {
115
115
 
116
116
  @override
117
117
  Widget build(BuildContext context) {
118
- return Container(
119
- clipBehavior: Clip.antiAlias,
120
- decoration: BoxDecoration(
121
- color: context.colors.surface,
122
- borderRadius: BorderRadius.circular(24),
123
- border: Border.all(color: context.colors.border, width: 0.5),
124
- ),
118
+ return KasyCard(
119
+ borderRadius: BorderRadius.circular(KasyRadius.xl),
125
120
  child: child,
126
121
  );
127
122
  }
@@ -143,10 +138,18 @@ class _DetailPlaceholder extends StatelessWidget {
143
138
  showShadow: true,
144
139
  ),
145
140
  const SizedBox(height: KasySpacing.md),
141
+ Text(
142
+ t.home.cards.assistant_title,
143
+ textAlign: TextAlign.center,
144
+ style: context.textTheme.titleMedium?.copyWith(
145
+ color: context.colors.onBackground,
146
+ ),
147
+ ),
148
+ const SizedBox(height: KasySpacing.xs),
146
149
  Text(
147
150
  t.ai_chat.no_conversation_selected,
148
151
  textAlign: TextAlign.center,
149
- style: context.textTheme.bodyLarge?.copyWith(
152
+ style: context.textTheme.bodyMedium?.copyWith(
150
153
  color: context.colors.muted,
151
154
  height: 1.4,
152
155
  ),
@@ -30,6 +30,12 @@ class AiChatComposer extends StatefulWidget {
30
30
  class _AiChatComposerState extends State<AiChatComposer> {
31
31
  bool _canSend = false;
32
32
 
33
+ // Owns the field focus so we can intercept hardware Enter: plain Enter sends,
34
+ // Shift+Enter inserts a newline (the standard chat-composer behaviour). On
35
+ // touch keyboards (no hardware key event) the return key keeps inserting a
36
+ // newline and the send orb is used, so mobile is unchanged.
37
+ late final FocusNode _focusNode = FocusNode(onKeyEvent: _onKeyEvent);
38
+
33
39
  @override
34
40
  void initState() {
35
41
  super.initState();
@@ -40,9 +46,23 @@ class _AiChatComposerState extends State<AiChatComposer> {
40
46
  @override
41
47
  void dispose() {
42
48
  widget.controller.removeListener(_onTextChanged);
49
+ _focusNode.dispose();
43
50
  super.dispose();
44
51
  }
45
52
 
53
+ KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
54
+ if (event is! KeyDownEvent) return KeyEventResult.ignored;
55
+ final bool isEnter = event.logicalKey == LogicalKeyboardKey.enter ||
56
+ event.logicalKey == LogicalKeyboardKey.numpadEnter;
57
+ if (!isEnter) return KeyEventResult.ignored;
58
+ // Shift+Enter falls through to the field and inserts a newline.
59
+ if (HardwareKeyboard.instance.isShiftPressed) {
60
+ return KeyEventResult.ignored;
61
+ }
62
+ _handleSend();
63
+ return KeyEventResult.handled;
64
+ }
65
+
46
66
  bool _hasSendableText(String value) {
47
67
  final String trimmed = value.trim();
48
68
  return trimmed.isNotEmpty && trimmed.length <= kAiChatMaxMessageLength;
@@ -88,6 +108,7 @@ class _AiChatComposerState extends State<AiChatComposer> {
88
108
  Expanded(
89
109
  child: KasyTextField(
90
110
  controller: widget.controller,
111
+ focusNode: _focusNode,
91
112
  enabled: enabled,
92
113
  variant: KasyTextFieldVariant.embedded,
93
114
  hint: t.ai_chat.hint,
@@ -186,7 +186,6 @@ class _EmptyState extends StatelessWidget {
186
186
  t.home.cards.assistant_title,
187
187
  textAlign: TextAlign.center,
188
188
  style: context.textTheme.titleMedium?.copyWith(
189
- fontWeight: FontWeight.w600,
190
189
  color: context.colors.onBackground,
191
190
  ),
192
191
  ),
@@ -194,7 +193,7 @@ class _EmptyState extends StatelessWidget {
194
193
  Text(
195
194
  t.ai_chat.empty_state,
196
195
  textAlign: TextAlign.center,
197
- style: context.textTheme.bodyLarge?.copyWith(
196
+ style: context.textTheme.bodyMedium?.copyWith(
198
197
  color: context.colors.muted,
199
198
  height: 1.4,
200
199
  ),
@@ -207,8 +207,7 @@ class _Header extends StatelessWidget {
207
207
  t.ai_chat.title,
208
208
  maxLines: 1,
209
209
  overflow: TextOverflow.ellipsis,
210
- style: context.textTheme.titleLarge?.copyWith(
211
- fontWeight: FontWeight.w800,
210
+ style: context.kasyTextTheme.pageTitle.copyWith(
212
211
  color: context.colors.onBackground,
213
212
  ),
214
213
  ),
@@ -274,7 +273,7 @@ class _EmptyConversations extends StatelessWidget {
274
273
  child: Text(
275
274
  t.ai_chat.conversations_empty,
276
275
  textAlign: TextAlign.center,
277
- style: context.textTheme.bodyLarge?.copyWith(
276
+ style: context.textTheme.bodyMedium?.copyWith(
278
277
  color: context.colors.muted,
279
278
  height: 1.4,
280
279
  ),
@@ -75,8 +75,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
75
75
  title,
76
76
  maxLines: 1,
77
77
  overflow: TextOverflow.ellipsis,
78
- style: context.textTheme.bodyMedium?.copyWith(
79
- fontWeight: FontWeight.w600,
78
+ style: context.kasyTextTheme.rowTitle.copyWith(
80
79
  color: titleColor,
81
80
  ),
82
81
  ),
@@ -0,0 +1,12 @@
1
+ /// Whether the app is running inside a *mobile* web browser (phone/tablet).
2
+ ///
3
+ /// Drives the OAuth flow choice on web: mobile browsers handle provider popups
4
+ /// unreliably — the provider opens in a new tab and the popup result is
5
+ /// frequently lost when the browser reclaims the backgrounded opener tab, so the
6
+ /// sign-in future never resolves and the app stays stuck "signing in". On mobile
7
+ /// we use a full-page redirect instead (completed at startup by
8
+ /// `getRedirectResult`); desktop web keeps the smoother popup.
9
+ ///
10
+ /// Non-web stub: always false. The real implementation lives in
11
+ /// `auth_web_support_web.dart` and is selected via conditional import on web.
12
+ bool isMobileWebBrowser() => false;
@@ -0,0 +1,25 @@
1
+ import 'package:web/web.dart' as web;
2
+
3
+ /// Web implementation: detects a mobile browser from the user-agent (iOS, Android
4
+ /// and other common phone/tablet agents). iPadOS 13+ reports a desktop Safari
5
+ /// user-agent, so we also treat a touch-capable "Macintosh" as mobile.
6
+ ///
7
+ /// A false positive only costs the (also-working) redirect flow instead of the
8
+ /// popup, so the heuristic deliberately errs toward catching mobiles.
9
+ bool isMobileWebBrowser() {
10
+ final ua = web.window.navigator.userAgent.toLowerCase();
11
+ const needles = [
12
+ 'android',
13
+ 'iphone',
14
+ 'ipad',
15
+ 'ipod',
16
+ 'mobile',
17
+ 'windows phone',
18
+ 'blackberry',
19
+ 'opera mini',
20
+ 'iemobile',
21
+ ];
22
+ if (needles.any(ua.contains)) return true;
23
+ // iPadOS 13+ masquerades as desktop Safari but still exposes touch points.
24
+ return ua.contains('macintosh') && web.window.navigator.maxTouchPoints > 1;
25
+ }