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
@@ -9,8 +9,13 @@ import 'package:kasy_kit/features/notifications/providers/models/notification.da
9
9
  typedef OnTapNotification = void Function(app.Notification notification);
10
10
 
11
11
  /// A tile for the [NotificationsPage] widget.
12
- class NotificationTile extends StatefulWidget {
13
- final double bgOpacity;
12
+ ///
13
+ /// Renders only the row content (icon + title / body / date + unread dot). The
14
+ /// card surface (elevated [KasyCard]) and the tap/hover feedback live in
15
+ /// [NotificationTileComponent], so a notification reads exactly like a Settings
16
+ /// card. Unread vs read is signalled by the trailing dot and text strength, not
17
+ /// by a tinted background.
18
+ class NotificationTile extends StatelessWidget {
14
19
  final double titleOpacity;
15
20
  final Color titleColor;
16
21
  final Color descriptionColor;
@@ -24,7 +29,6 @@ class NotificationTile extends StatefulWidget {
24
29
 
25
30
  const NotificationTile({
26
31
  super.key,
27
- required this.bgOpacity,
28
32
  required this.titleOpacity,
29
33
  required this.icon,
30
34
  required this.date,
@@ -47,7 +51,6 @@ class NotificationTile extends StatefulWidget {
47
51
  Key? key,
48
52
  }) => NotificationTile(
49
53
  key: key,
50
- bgOpacity: .08,
51
54
  titleOpacity: 1,
52
55
  showUnreadDot: true,
53
56
  icon: imageUrl != null
@@ -56,7 +59,9 @@ class NotificationTile extends StatefulWidget {
56
59
  date: date,
57
60
  title: title,
58
61
  description: description,
59
- 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,
60
65
  descriptionColor: context.colors.onSurface,
61
66
  dateColor: context.colors.muted,
62
67
  onTap: onTap,
@@ -72,7 +77,6 @@ class NotificationTile extends StatefulWidget {
72
77
  Key? key,
73
78
  }) => NotificationTile(
74
79
  key: key,
75
- bgOpacity: 0,
76
80
  titleOpacity: .6,
77
81
  icon: imageUrl != null
78
82
  ? TileNotificationImage(url: imageUrl)
@@ -80,7 +84,10 @@ class NotificationTile extends StatefulWidget {
80
84
  date: date,
81
85
  title: title,
82
86
  description: description,
83
- 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,
84
91
  descriptionColor: context.colors.onSurface.withValues(alpha: 0.45),
85
92
  dateColor: context.colors.muted,
86
93
  onTap: onTap,
@@ -111,124 +118,66 @@ class NotificationTile extends StatefulWidget {
111
118
  onTap: onTap,
112
119
  );
113
120
 
114
- @override
115
- State<NotificationTile> createState() => _NotificationTileState();
116
- }
117
-
118
- class _NotificationTileState extends State<NotificationTile>
119
- with SingleTickerProviderStateMixin {
120
- late final AnimationController _controller;
121
- late Animation<double> _opacityAnimation;
122
-
123
- @override
124
- void initState() {
125
- super.initState();
126
- _controller = AnimationController(
127
- duration: const Duration(milliseconds: 500),
128
- vsync: this,
129
- );
130
- _opacityAnimation = Tween(
131
- begin: 0.0,
132
- end: widget.bgOpacity,
133
- ).animate(CurvedAnimation(parent: _controller, curve: Curves.decelerate));
134
- _controller.forward();
135
- }
136
-
137
- @override
138
- void didUpdateWidget(covariant NotificationTile oldWidget) {
139
- super.didUpdateWidget(oldWidget);
140
- if (oldWidget.bgOpacity != widget.bgOpacity) {
141
- _opacityAnimation = Tween(
142
- begin: oldWidget.bgOpacity,
143
- end: widget.bgOpacity,
144
- ).animate(CurvedAnimation(parent: _controller, curve: Curves.ease));
145
- _controller.forward(from: 0);
146
- }
147
- }
148
-
149
- @override
150
- void dispose() {
151
- _controller.dispose();
152
- super.dispose();
153
- }
154
-
155
121
  @override
156
122
  Widget build(BuildContext context) {
157
- return AnimatedBuilder(
158
- animation: _controller,
159
- builder: (context, child) {
160
- return Container(
161
- decoration: BoxDecoration(
162
- color: context.colors.primary.withValues(
163
- alpha: _opacityAnimation.value,
164
- ),
165
- borderRadius: KasyRadius.smBorderRadius,
166
- ),
167
- padding: const EdgeInsets.all(KasySpacing.md),
168
- child: child,
169
- );
170
- },
171
- child: Row(
172
- crossAxisAlignment: CrossAxisAlignment.start,
173
- children: [
174
- widget.icon,
175
- const SizedBox(width: KasySpacing.smd),
176
- Expanded(
177
- child: Column(
178
- crossAxisAlignment: CrossAxisAlignment.start,
179
- children: [
180
- Flexible(
181
- flex: 0,
182
- child: Text(
183
- widget.title,
184
- style: context.textTheme.labelLarge?.copyWith(
185
- color: widget.titleColor,
186
- ),
187
- overflow: TextOverflow.clip,
188
- maxLines: 2,
189
- ),
190
- ),
191
- const SizedBox(height: KasySpacing.xs),
192
- Flexible(
193
- flex: 0,
194
- child: Text(
195
- widget.description,
196
- style: context.textTheme.bodyMedium?.copyWith(
197
- color: widget.descriptionColor,
123
+ return Row(
124
+ crossAxisAlignment: CrossAxisAlignment.start,
125
+ children: [
126
+ icon,
127
+ const SizedBox(width: KasySpacing.sm),
128
+ Expanded(
129
+ child: Column(
130
+ crossAxisAlignment: CrossAxisAlignment.start,
131
+ children: [
132
+ // Headline row: bold title on the left, timestamp (and the
133
+ // unread dot) pinned to the right — the compact, professional
134
+ // notification layout (Linear / Gmail).
135
+ Row(
136
+ children: [
137
+ Expanded(
138
+ child: Text(
139
+ title,
140
+ style: context.textTheme.titleSmall?.copyWith(
141
+ color: titleColor,
142
+ fontWeight: FontWeight.w600,
143
+ ),
144
+ overflow: TextOverflow.ellipsis,
145
+ maxLines: 1,
198
146
  ),
199
- overflow: TextOverflow.ellipsis,
200
- maxLines: 3,
201
147
  ),
202
- ),
203
- const SizedBox(height: KasySpacing.smd),
204
- Flexible(
205
- flex: 0,
206
- child: Text(
207
- Jiffy.parseFromDateTime(widget.date).fromNow(),
148
+ const SizedBox(width: KasySpacing.sm),
149
+ Text(
150
+ Jiffy.parseFromDateTime(date).fromNow(),
208
151
  style: context.textTheme.bodySmall?.copyWith(
209
- color: widget.dateColor,
152
+ color: dateColor,
210
153
  ),
211
154
  ),
155
+ if (showUnreadDot) ...[
156
+ const SizedBox(width: KasySpacing.sm),
157
+ Container(
158
+ width: 6,
159
+ height: 6,
160
+ decoration: BoxDecoration(
161
+ shape: BoxShape.circle,
162
+ color: context.colors.primary,
163
+ ),
164
+ ),
165
+ ],
166
+ ],
167
+ ),
168
+ const SizedBox(height: KasySpacing.xs),
169
+ Text(
170
+ description,
171
+ style: context.textTheme.bodyMedium?.copyWith(
172
+ color: descriptionColor,
212
173
  ),
213
- ],
214
- ),
215
- ),
216
- if (widget.showUnreadDot) ...[
217
- const SizedBox(width: KasySpacing.sm),
218
- Padding(
219
- padding: const EdgeInsets.only(top: 3),
220
- child: Container(
221
- width: 8,
222
- height: 8,
223
- decoration: BoxDecoration(
224
- shape: BoxShape.circle,
225
- color: context.colors.primary,
226
- ),
174
+ overflow: TextOverflow.ellipsis,
175
+ maxLines: 2,
227
176
  ),
228
- ),
229
- ],
230
- ],
231
- ),
177
+ ],
178
+ ),
179
+ ),
180
+ ],
232
181
  );
233
182
  }
234
183
  }
@@ -269,8 +218,8 @@ class TileNotificationImage extends StatelessWidget {
269
218
  borderRadius: BorderRadius.circular(8),
270
219
  child: Image.network(
271
220
  url,
272
- width: 48,
273
- height: 48,
221
+ width: 36,
222
+ height: 36,
274
223
  fit: BoxFit.cover,
275
224
  errorBuilder: (context, error, stack) =>
276
225
  const TileNotificationIcon(active: false),
@@ -297,8 +246,8 @@ class TileNotificationIcon extends StatelessWidget {
297
246
  ? context.colors.primary
298
247
  : context.colors.onSurface.withValues(alpha: 0.35);
299
248
  return Container(
300
- width: 40,
301
- height: 40,
249
+ width: 36,
250
+ height: 36,
302
251
  decoration: BoxDecoration(
303
252
  color: active
304
253
  ? context.colors.primary.withValues(alpha: 0.12)
@@ -307,7 +256,7 @@ class TileNotificationIcon extends StatelessWidget {
307
256
  ),
308
257
  child: Icon(
309
258
  active ? KasyIcons.notificationActive : KasyIcons.notification,
310
- size: KasyIconSize.lg,
259
+ size: KasyIconSize.sm,
311
260
  color: color,
312
261
  ),
313
262
  );
@@ -326,27 +275,34 @@ class NotificationSkeletonTile extends StatelessWidget {
326
275
  padding: EdgeInsets.only(top: KasySpacing.sm),
327
276
  child: KasySkeletonGroup(
328
277
  child: Padding(
329
- padding: EdgeInsets.all(KasySpacing.md),
278
+ padding: EdgeInsets.symmetric(
279
+ horizontal: KasySpacing.md,
280
+ vertical: KasySpacing.smd,
281
+ ),
330
282
  child: Row(
331
283
  crossAxisAlignment: CrossAxisAlignment.start,
332
284
  children: [
333
285
  KasySkeleton(
334
- width: 40,
335
- height: 40,
286
+ width: 36,
287
+ height: 36,
336
288
  borderRadius: BorderRadius.all(Radius.circular(10)),
337
289
  ),
338
- SizedBox(width: KasySpacing.smd),
290
+ SizedBox(width: KasySpacing.sm),
339
291
  Expanded(
340
292
  child: Column(
341
293
  crossAxisAlignment: CrossAxisAlignment.start,
342
294
  children: [
343
- KasySkeleton(width: 150, height: 13),
295
+ Row(
296
+ children: [
297
+ Expanded(child: KasySkeleton(width: 150, height: 13)),
298
+ SizedBox(width: KasySpacing.sm),
299
+ KasySkeleton(width: 44, height: 10),
300
+ ],
301
+ ),
344
302
  SizedBox(height: KasySpacing.sm),
345
303
  KasySkeleton(width: double.infinity, height: 11),
346
304
  SizedBox(height: KasySpacing.xs),
347
305
  KasySkeleton(width: 210, height: 11),
348
- SizedBox(height: KasySpacing.smd),
349
- KasySkeleton(width: 70, height: 10),
350
306
  ],
351
307
  ),
352
308
  ),
@@ -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
@@ -93,12 +93,11 @@ class OnboardingIllustrationScaffold extends StatelessWidget {
93
93
  child: Text(
94
94
  title,
95
95
  textAlign: TextAlign.start,
96
+ // Design-system page-title scale (headlineMedium = 24/w700).
97
+ // Previously bumped to 28 with custom tracking, which read as
98
+ // oversized next to the rest of the app.
96
99
  style: context.textTheme.headlineMedium?.copyWith(
97
100
  color: colors.onBackground,
98
- fontSize: 28,
99
- fontWeight: FontWeight.w700,
100
- letterSpacing: -0.3,
101
- height: 1.2,
102
101
  ),
103
102
  ),
104
103
  ),
@@ -71,12 +71,10 @@ class _OnboardingRadioQuestionState
71
71
  child: Text(
72
72
  widget.title,
73
73
  textAlign: TextAlign.start,
74
+ // Design-system page-title scale (headlineMedium = 24/w700),
75
+ // matching the illustration steps instead of a bespoke 26px size.
74
76
  style: context.textTheme.headlineMedium?.copyWith(
75
77
  color: context.colors.onBackground,
76
- fontSize: 26,
77
- fontWeight: FontWeight.w700,
78
- letterSpacing: -0.2,
79
- height: 1.2,
80
78
  ),
81
79
  ),
82
80
  ),
@@ -283,7 +283,8 @@ class _SelectableRowTileState extends State<SelectableRowTile>
283
283
  padding: const EdgeInsets.only(right: KasySpacing.md),
284
284
  child: Text(
285
285
  widget.emoj!,
286
- style: context.textTheme.titleMedium?.copyWith(fontSize: 28),
286
+ style: context.textTheme.titleMedium
287
+ ?.copyWith(fontSize: 28), // design-check: ignore — emoji glyph
287
288
  ),
288
289
  ),
289
290
  ),