kasy-cli 1.21.8 → 1.22.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 (269) hide show
  1. package/lib/commands/add.js +93 -80
  2. package/lib/commands/configure.js +100 -32
  3. package/lib/commands/doctor.js +28 -2
  4. package/lib/commands/new.js +86 -38
  5. package/lib/commands/notifications.js +1 -1
  6. package/lib/commands/remove.js +43 -15
  7. package/lib/commands/run.js +2 -2
  8. package/lib/commands/update.js +2 -2
  9. package/lib/scaffold/CHANGELOG.json +14 -0
  10. package/lib/scaffold/backends/api/generator.js +14 -14
  11. package/lib/scaffold/backends/api/patch/README.md +83 -0
  12. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
  14. package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
  15. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
  16. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
  18. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
  19. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
  20. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
  21. package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
  22. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  23. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  24. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
  25. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  26. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
  27. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  28. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
  29. package/lib/scaffold/backends/firebase/deploy.js +87 -13
  30. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +14 -6
  31. package/lib/scaffold/backends/firebase/generator.js +5 -5
  32. package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
  33. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  34. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  35. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  37. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  40. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  41. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  42. package/lib/scaffold/backends/supabase/generator.js +17 -17
  43. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  44. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  45. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  46. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  48. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  49. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  50. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  51. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  52. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  53. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  55. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  56. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  57. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  59. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  60. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  61. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  62. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  63. package/lib/scaffold/catalog.js +9 -11
  64. package/lib/scaffold/generate.js +45 -31
  65. package/lib/scaffold/shared/generator-utils.js +188 -81
  66. package/lib/scaffold/shared/sort-imports.js +191 -0
  67. package/lib/scaffold/shared/template-strings.js +3 -3
  68. package/lib/utils/checks.js +2 -2
  69. package/lib/utils/i18n/messages-en.js +50 -35
  70. package/lib/utils/i18n/messages-es.js +50 -35
  71. package/lib/utils/i18n/messages-pt.js +52 -37
  72. package/lib/utils/updates.js +15 -15
  73. package/package.json +1 -1
  74. package/templates/firebase/.env.example +2 -2
  75. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  80. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  81. package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
  82. package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
  83. package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
  84. package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
  85. package/templates/firebase/firestore.rules +24 -5
  86. package/templates/firebase/functions/package-lock.json +22 -1
  87. package/templates/firebase/functions/package.json +2 -1
  88. package/templates/firebase/functions/src/admin/functions.ts +113 -0
  89. package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
  90. package/templates/firebase/functions/src/index.ts +8 -2
  91. package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
  92. package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
  93. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
  94. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
  95. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
  96. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  98. package/templates/firebase/lib/components/components.dart +4 -1
  99. package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
  100. package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
  101. package/templates/firebase/lib/components/kasy_button.dart +23 -99
  102. package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
  103. package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
  104. package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
  105. package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
  106. package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
  107. package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
  108. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
  109. package/templates/firebase/lib/components/kasy_toast.dart +2 -2
  110. package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
  111. package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
  112. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
  113. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
  114. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
  115. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
  116. package/templates/firebase/lib/core/config/app_env.dart +5 -11
  117. package/templates/firebase/lib/core/config/features.dart +5 -4
  118. package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
  119. package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
  120. package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
  121. package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
  122. package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
  123. package/templates/firebase/lib/core/data/models/user.dart +11 -0
  124. package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
  125. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
  126. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
  127. package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
  128. package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
  129. package/templates/firebase/lib/core/states/logout_action.dart +25 -0
  130. package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
  131. package/templates/firebase/lib/core/theme/colors.dart +488 -188
  132. package/templates/firebase/lib/core/theme/radius.dart +22 -11
  133. package/templates/firebase/lib/core/theme/shadows.dart +66 -0
  134. package/templates/firebase/lib/core/theme/texts.dart +75 -41
  135. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
  136. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
  137. package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
  138. package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
  139. package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
  140. package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
  141. package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
  142. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
  143. package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
  144. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
  145. package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
  146. package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
  147. package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  148. package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
  149. package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
  150. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
  151. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
  152. package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
  153. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
  154. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
  155. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
  156. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
  157. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
  158. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
  159. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
  160. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
  161. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
  162. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
  163. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
  164. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
  165. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
  166. package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
  167. package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
  168. package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
  169. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
  170. package/templates/firebase/lib/features/home/home_feed.dart +289 -0
  171. package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
  172. package/templates/firebase/lib/features/home/home_page.dart +11 -250
  173. package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
  174. package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
  175. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
  176. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
  177. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
  178. package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
  179. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  180. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
  181. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
  182. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
  183. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
  184. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
  185. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
  186. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
  187. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
  188. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
  189. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
  190. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  191. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
  192. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
  193. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
  194. package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
  195. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
  196. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
  197. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
  198. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
  199. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
  200. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
  201. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
  202. package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
  203. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  204. package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
  205. package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
  206. package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
  207. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
  208. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
  209. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
  210. package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  211. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
  212. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
  213. package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
  214. package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
  215. package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
  216. package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
  217. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
  218. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
  219. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
  220. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
  221. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
  222. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
  223. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
  224. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
  225. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
  226. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
  227. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
  228. package/templates/firebase/lib/i18n/en.i18n.json +171 -46
  229. package/templates/firebase/lib/i18n/es.i18n.json +175 -50
  230. package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
  231. package/templates/firebase/lib/main.dart +6 -3
  232. package/templates/firebase/lib/router.dart +15 -23
  233. package/templates/firebase/pubspec.yaml +4 -5
  234. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
  235. package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
  236. package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
  237. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
  238. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
  239. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
  240. package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
  241. package/templates/firebase/test/test_utils.dart +6 -6
  242. package/templates/firebase/web/index.html +5 -2
  243. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
  244. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
  245. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
  246. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
  247. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  248. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  249. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  250. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  251. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  252. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
  253. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
  254. package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
  255. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
  256. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
  257. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
  258. /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
  259. /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
  260. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
  261. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
  262. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
  263. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
  264. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
  265. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
  266. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
  267. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
  268. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
  269. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
@@ -79,6 +79,89 @@ Após editar qualquer `.i18n.json`, sempre rodar `dart run slang`.
79
79
 
80
80
  ---
81
81
 
82
+ ## Admin (console interno)
83
+
84
+ O app tem um **console de admin** (aba Usuários, Solicitações, etc.) liberado só para quem tem `role == "admin"`. O `role` é um campo de **controle de acesso** que o **seu backend controla** — o app nunca pode escrever nele.
85
+
86
+ ### Campo `role` no usuário
87
+
88
+ O seu endpoint de usuário (`GET /users/{id}`) deve devolver o `role` junto com os outros dados:
89
+
90
+ ```json
91
+ { "id": "...", "email": "ana@b.com", "name": "Ana", "onboarded": true, "role": "admin" }
92
+ ```
93
+
94
+ - `role` ausente / `null` → usuário normal.
95
+ - `role: "admin"` → libera o console de admin.
96
+
97
+ **Regra de segurança (obrigatória):** o `role` só pode ser definido no servidor (banco/painel). O backend deve **rejeitar** qualquer tentativa do cliente de gravar `role` (ex.: num `PATCH /users/{id}`), senão qualquer pessoa vira admin. Defina-o manualmente no seu banco para promover alguém.
98
+
99
+ ### Endpoint: listar usuários
100
+
101
+ ```
102
+ GET /admin/users
103
+ Auth: Authorization: Bearer <token> (enviado automaticamente pelo app)
104
+ O servidor DEVE validar role == "admin" e responder 403 caso contrário.
105
+
106
+ 200 OK:
107
+ {
108
+ "users": [
109
+ {
110
+ "id": "...",
111
+ "email": "ana@b.com" | null,
112
+ "name": "Ana" | null,
113
+ "createdAt": 1700000000000, // epoch em milissegundos | null
114
+ "subscriber": true // tem assinatura ativa?
115
+ }
116
+ ],
117
+ "totalUsers": 142, // tamanho real da coleção
118
+ "truncated": false // true quando há mais usuários do que os retornados
119
+ }
120
+ ```
121
+
122
+ Devolva os usuários **mais recentes** (com um limite, ex.: 1000). O app faz busca, ordenação e paginação localmente — por isso uma chamada só já basta e a experiência fica instantânea.
123
+
124
+ ### Endpoint: moderar solicitações (aba Solicitações)
125
+
126
+ ```
127
+ GET /admin/feature-requests → lista TODAS (ativas + ocultas), mais votadas primeiro
128
+ PATCH /admin/feature-requests/{id} body: {"active": true|false}
129
+ PATCH /admin/feature-requests/{id} body: {"title": {...}, "description": {...}}
130
+ ```
131
+
132
+ Mesma regra: validar `role == "admin"` e responder 403 caso contrário. `title`/`description` são mapas por idioma (`{"en": "...", "pt": "...", "es": "..."}`).
133
+
134
+ ### Endpoints: AI Chat (histórico de conversas)
135
+
136
+ O assistente guarda várias conversas por usuário, cada uma com várias mensagens.
137
+ O usuário é identificado pelo token `Authorization: Bearer`.
138
+
139
+ ```
140
+ GET /ai-conversations → lista as conversas do usuário, mais recente primeiro
141
+ POST /ai-conversations → cria uma conversa vazia e devolve o objeto criado
142
+ DELETE /ai-conversations/{id} → apaga a conversa e todas as mensagens dela
143
+ GET /ai-conversations/{id}/messages → mensagens da conversa, mais antiga primeiro
144
+ POST /ai-conversations/{id}/messages body: {"role": "...", "content": "...", "created_at": "..."}
145
+ ```
146
+
147
+ Formato de uma conversa (o "última mensagem" é desnormalizado para a lista ficar barata):
148
+
149
+ ```json
150
+ {
151
+ "id": "...",
152
+ "created_at": "2026-01-01T12:00:00Z",
153
+ "updated_at": "2026-01-01T12:05:00Z",
154
+ "last_message_role": "user" | "assistant" | null,
155
+ "last_message_content": "..." | null
156
+ }
157
+ ```
158
+
159
+ Ao salvar uma mensagem, o servidor deve atualizar `updated_at`, `last_message_role` e
160
+ `last_message_content` da conversa. O streaming da resposta da IA continua no endpoint
161
+ `AI_CHAT_ENDPOINT` (SSE) — ele só recebe `message` + `history`, não persiste nada.
162
+
163
+ ---
164
+
82
165
  ## Segurança
83
166
 
84
167
  O `.gitignore` já exclui: `.env`, `.env.*`, `*.pem`, `*.keystore`.
@@ -4,7 +4,7 @@ import 'dart:typed_data';
4
4
  import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
5
5
  import 'package:kasy_kit/core/data/api/http_client.dart';
6
6
  import 'package:kasy_kit/core/data/entities/upload_result.dart';
7
- import 'package:kasy_kit/environnements.dart';
7
+ import 'package:kasy_kit/environments.dart';
8
8
  import 'package:dio/dio.dart';
9
9
  import 'package:flutter_riverpod/flutter_riverpod.dart';
10
10
  import 'package:logger/logger.dart';
@@ -16,6 +16,11 @@ sealed class UserEntity with _$UserEntity {
16
16
  String? avatarPath,
17
17
  bool? onboarded,
18
18
  String? locale,
19
+ // Access-control role. null/absent = normal user; "admin" unlocks the admin
20
+ // console's server data. Server-only: your backend must NEVER let a client
21
+ // write this — return it from GET /users/{id} (and in the JWT) but reject any
22
+ // client attempt to set it.
23
+ String? role,
19
24
  }) = UserEntityData;
20
25
 
21
26
  const UserEntity._();
@@ -6,7 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
6
6
  import 'package:kasy_kit/core/config/app_env.dart';
7
7
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
8
8
 
9
- part 'environnements.freezed.dart';
9
+ part 'environments.freezed.dart';
10
10
 
11
11
 
12
12
 
@@ -40,9 +40,6 @@ sealed class Environment with _$Environment {
40
40
  /// (only if you want to use in-app purchases with RevenueCat)
41
41
  String? revenueCatIOSApiKey,
42
42
 
43
- /// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
44
- /// (only if you want to use subscriptions on web)
45
- String? revenueCatWebApiKey,
46
43
 
47
44
  /// this is used to open the app store page of your app for reviews
48
45
  String? appStoreId,
@@ -76,9 +73,6 @@ sealed class Environment with _$Environment {
76
73
  /// (only if you want to use in-app purchases with RevenueCat)
77
74
  String? revenueCatIOSApiKey,
78
75
 
79
- /// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
80
- /// (only if you want to use subscriptions on web)
81
- String? revenueCatWebApiKey,
82
76
 
83
77
  /// only if you want to use ads
84
78
  String? androidInterstitialAdUnitId,
@@ -116,7 +110,6 @@ sealed class Environment with _$Environment {
116
110
  appStoreId: '',
117
111
  revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
118
112
  revenueCatIOSApiKey: AppEnv.rcIosApiKey,
119
- revenueCatWebApiKey: AppEnv.rcWebApiKey,
120
113
  mixpanelToken: AppEnv.mixpanelToken,
121
114
  authenticationMode: AuthenticationMode.authRequired,
122
115
  );
@@ -127,7 +120,6 @@ sealed class Environment with _$Environment {
127
120
  appStoreId: AppEnv.appStoreId,
128
121
  revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
129
122
  revenueCatIOSApiKey: AppEnv.rcIosApiKey,
130
- revenueCatWebApiKey: AppEnv.rcWebApiKey,
131
123
  sentryDsn: AppEnv.sentryDsn,
132
124
  mixpanelToken: AppEnv.mixpanelToken,
133
125
  authenticationMode: AuthenticationMode.authRequired,
@@ -142,8 +134,8 @@ sealed class Environment with _$Environment {
142
134
  revenueCatIOSApiKey != null && revenueCatIOSApiKey!.isNotEmpty,
143
135
  TargetPlatform.android =>
144
136
  revenueCatAndroidApiKey != null && revenueCatAndroidApiKey!.isNotEmpty,
145
- _ => kIsWeb &&
146
- (revenueCatWebApiKey != null && revenueCatWebApiKey!.isNotEmpty),
137
+ // RevenueCat is mobile-only; web/desktop are never RevenueCat-configured.
138
+ _ => false,
147
139
  };
148
140
  }
149
141
 
@@ -0,0 +1,108 @@
1
+ import 'package:dio/dio.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:logger/logger.dart';
4
+ import 'package:kasy_kit/core/data/api/http_client.dart';
5
+ import 'package:kasy_kit/features/ai_chat/api/ai_chat_conversation_entity.dart';
6
+ import 'package:kasy_kit/features/ai_chat/api/ai_chat_message_entity.dart';
7
+
8
+ final aiChatApiProvider = Provider<AiChatApi>(
9
+ (ref) => AiChatApi(client: ref.read(httpClientProvider)),
10
+ );
11
+
12
+ /// REST endpoints expected on your backend (the server identifies the user via
13
+ /// the Authorization: Bearer token injected automatically by HttpClient):
14
+ ///
15
+ /// GET /ai-conversations → `List<AiChatConversationEntity>`
16
+ /// POST /ai-conversations → creates + returns one conversation
17
+ /// DELETE /ai-conversations/{id} → deletes a conversation + its messages
18
+ /// GET /ai-conversations/{id}/messages → `List<AiChatMessageEntity>` (oldest first)
19
+ /// POST /ai-conversations/{id}/messages → saves one message
20
+ ///
21
+ /// Conversations are returned most-recently-updated first, with the last
22
+ /// message denormalized (`last_message_role` / `last_message_content`).
23
+ class AiChatApi {
24
+ final HttpClient _client;
25
+ final Logger _logger = Logger();
26
+
27
+ AiChatApi({required HttpClient client}) : _client = client;
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Conversations
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /// Returns the user's conversations, most recently updated first.
34
+ Future<List<AiChatConversationEntity>> loadConversations(
35
+ String userId,
36
+ ) async {
37
+ try {
38
+ final response = await _client.get('/ai-conversations');
39
+ final list = response.data as List<dynamic>;
40
+ return list
41
+ .map(
42
+ (e) =>
43
+ AiChatConversationEntity.fromJson(e as Map<String, dynamic>),
44
+ )
45
+ .toList();
46
+ } on DioException catch (e) {
47
+ _logger.e('Failed to load conversations: $e');
48
+ return [];
49
+ }
50
+ }
51
+
52
+ /// Creates an empty conversation and returns it (with its generated id).
53
+ Future<AiChatConversationEntity> createConversation(String userId) async {
54
+ final response = await _client.post('/ai-conversations');
55
+ return AiChatConversationEntity.fromJson(
56
+ response.data as Map<String, dynamic>,
57
+ );
58
+ }
59
+
60
+ /// Deletes a conversation (and its messages) on the server.
61
+ Future<void> deleteConversation(String userId, String conversationId) async {
62
+ try {
63
+ await _client.delete('/ai-conversations/$conversationId');
64
+ } catch (e) {
65
+ _logger.e('Failed to delete conversation: $e');
66
+ }
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Messages
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /// Returns all messages in a conversation, oldest first.
74
+ Future<List<AiChatMessageEntity>> loadMessages(
75
+ String userId,
76
+ String conversationId,
77
+ ) async {
78
+ try {
79
+ final response = await _client.get(
80
+ '/ai-conversations/$conversationId/messages',
81
+ );
82
+ final list = response.data as List<dynamic>;
83
+ return list
84
+ .map((e) => AiChatMessageEntity.fromJson(e as Map<String, dynamic>))
85
+ .toList();
86
+ } on DioException catch (e) {
87
+ _logger.e('Failed to load AI messages: $e');
88
+ return [];
89
+ }
90
+ }
91
+
92
+ /// Persists a message in a conversation via POST. The server is expected to
93
+ /// update the conversation's denormalized last-message fields + timestamp.
94
+ Future<void> saveMessage(
95
+ String userId,
96
+ String conversationId,
97
+ AiChatMessageEntity message,
98
+ ) async {
99
+ try {
100
+ await _client.post(
101
+ '/ai-conversations/$conversationId/messages',
102
+ data: message.toJson(),
103
+ );
104
+ } catch (e) {
105
+ _logger.e('Failed to persist AI message: $e');
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,51 @@
1
+ /// REST mapping for a single AI chat conversation.
2
+ ///
3
+ /// The last message is denormalized onto the conversation so the list can be
4
+ /// rendered from one request (no need to fetch each conversation's messages
5
+ /// just to show a preview + timestamp).
6
+ class AiChatConversationEntity {
7
+ final String id;
8
+ final DateTime createdAt;
9
+ final DateTime updatedAt;
10
+
11
+ /// Role of the most recent message ('user' or 'assistant'), or null when the
12
+ /// conversation has no messages yet.
13
+ final String? lastMessageRole;
14
+
15
+ /// Content of the most recent message, or null when empty.
16
+ final String? lastMessageContent;
17
+
18
+ const AiChatConversationEntity({
19
+ required this.id,
20
+ required this.createdAt,
21
+ required this.updatedAt,
22
+ this.lastMessageRole,
23
+ this.lastMessageContent,
24
+ });
25
+
26
+ bool get isEmpty => lastMessageContent == null;
27
+
28
+ AiChatConversationEntity copyWith({
29
+ DateTime? updatedAt,
30
+ String? lastMessageRole,
31
+ String? lastMessageContent,
32
+ }) {
33
+ return AiChatConversationEntity(
34
+ id: id,
35
+ createdAt: createdAt,
36
+ updatedAt: updatedAt ?? this.updatedAt,
37
+ lastMessageRole: lastMessageRole ?? this.lastMessageRole,
38
+ lastMessageContent: lastMessageContent ?? this.lastMessageContent,
39
+ );
40
+ }
41
+
42
+ factory AiChatConversationEntity.fromJson(Map<String, dynamic> json) {
43
+ return AiChatConversationEntity(
44
+ id: json['id'] as String,
45
+ createdAt: DateTime.parse(json['created_at'] as String),
46
+ updatedAt: DateTime.parse(json['updated_at'] as String),
47
+ lastMessageRole: json['last_message_role'] as String?,
48
+ lastMessageContent: json['last_message_content'] as String?,
49
+ );
50
+ }
51
+ }
@@ -1,19 +1,19 @@
1
- /// REST API response mapping for a single LLM chat message.
2
- class LlmChatMessageEntity {
1
+ /// REST API response mapping for a single AI chat message.
2
+ class AiChatMessageEntity {
3
3
  final String? id;
4
4
  final String role; // 'user' or 'assistant'
5
5
  final String content;
6
6
  final DateTime createdAt;
7
7
 
8
- const LlmChatMessageEntity({
8
+ const AiChatMessageEntity({
9
9
  this.id,
10
10
  required this.role,
11
11
  required this.content,
12
12
  required this.createdAt,
13
13
  });
14
14
 
15
- factory LlmChatMessageEntity.fromJson(Map<String, dynamic> json) {
16
- return LlmChatMessageEntity(
15
+ factory AiChatMessageEntity.fromJson(Map<String, dynamic> json) {
16
+ return AiChatMessageEntity(
17
17
  id: json['id'] as String?,
18
18
  role: json['role'] as String,
19
19
  content: json['content'] as String,
@@ -6,8 +6,9 @@ import 'package:kasy_kit/core/config/app_env.dart';
6
6
  import 'package:kasy_kit/core/data/api/http_client.dart';
7
7
  import 'package:kasy_kit/core/states/translations.dart';
8
8
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
9
- import 'package:kasy_kit/features/llm_chat/api/llm_chat_api.dart';
10
- import 'package:kasy_kit/features/llm_chat/api/llm_chat_message_entity.dart';
9
+ import 'package:kasy_kit/features/ai_chat/api/ai_chat_api.dart';
10
+ import 'package:kasy_kit/features/ai_chat/api/ai_chat_message_entity.dart';
11
+ import 'package:kasy_kit/features/ai_chat/providers/ai_conversations_notifier.dart';
11
12
  import 'package:logger/logger.dart';
12
13
 
13
14
  /// Maximum number of recent messages sent to the AI as context.
@@ -34,39 +35,39 @@ class ChatMessage {
34
35
  factory ChatMessage.assistant(String content) =>
35
36
  ChatMessage(role: 'assistant', content: content);
36
37
 
37
- factory ChatMessage.fromEntity(LlmChatMessageEntity entity) =>
38
+ factory ChatMessage.fromEntity(AiChatMessageEntity entity) =>
38
39
  ChatMessage(role: entity.role, content: entity.content);
39
40
 
40
- LlmChatMessageEntity toEntity() => LlmChatMessageEntity(
41
+ AiChatMessageEntity toEntity() => AiChatMessageEntity(
41
42
  role: role,
42
43
  content: content,
43
44
  createdAt: DateTime.now(),
44
45
  );
45
46
  }
46
47
 
47
- /// UI state for the LLM chat screen.
48
- class LlmChatState {
48
+ /// UI state for the AI chat screen.
49
+ class AiChatState {
49
50
  final List<ChatMessage> messages;
50
51
 
51
- /// True while the HTTP request to the LLM backend is in-flight.
52
+ /// True while the HTTP request to the AI backend is in-flight.
52
53
  final bool isReplying;
53
54
 
54
55
  /// True once the first SSE chunk has been received.
55
56
  /// While true the last message in [messages] is the partial assistant reply.
56
57
  final bool streamingStarted;
57
58
 
58
- const LlmChatState({
59
+ const AiChatState({
59
60
  required this.messages,
60
61
  this.isReplying = false,
61
62
  this.streamingStarted = false,
62
63
  });
63
64
 
64
- LlmChatState copyWith({
65
+ AiChatState copyWith({
65
66
  List<ChatMessage>? messages,
66
67
  bool? isReplying,
67
68
  bool? streamingStarted,
68
69
  }) {
69
- return LlmChatState(
70
+ return AiChatState(
70
71
  messages: messages ?? this.messages,
71
72
  isReplying: isReplying ?? this.isReplying,
72
73
  streamingStarted: streamingStarted ?? this.streamingStarted,
@@ -74,39 +75,54 @@ class LlmChatState {
74
75
  }
75
76
  }
76
77
 
77
- /// Manages LLM chat state: loads history from the backend on init,
78
+ /// Manages AI chat state: loads history from the backend on init,
78
79
  /// persists each message, and streams the assistant reply word-by-word via SSE.
79
- final llmChatNotifierProvider =
80
- AsyncNotifierProvider<LlmChatNotifier, LlmChatState>(LlmChatNotifier.new);
80
+ final aiChatNotifierProvider =
81
+ AsyncNotifierProvider<AiChatNotifier, AiChatState>(AiChatNotifier.new);
81
82
 
82
- class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
83
+ class AiChatNotifier extends AsyncNotifier<AiChatState> {
83
84
  final Logger _logger = Logger();
84
85
 
86
+ /// The conversation a reply is currently streaming into. Used to ignore UI
87
+ /// updates if the user switches conversations mid-stream (the reply is still
88
+ /// persisted to the right conversation, just not shown on the wrong one).
89
+ String? _streamingConversationId;
90
+
91
+ bool get _streamStillActive =>
92
+ ref.read(selectedConversationIdProvider) == _streamingConversationId;
93
+
85
94
  @override
86
- Future<LlmChatState> build() async {
95
+ Future<AiChatState> build() async {
96
+ final conversationId = ref.watch(selectedConversationIdProvider);
87
97
  final userId = ref.read(userStateNotifierProvider).user.idOrNull;
88
- if (userId == null) return const LlmChatState(messages: []);
98
+ if (conversationId == null || userId == null) {
99
+ return const AiChatState(messages: []);
100
+ }
89
101
 
90
102
  try {
91
- final entities = await ref.read(llmChatApiProvider).loadMessages(userId);
92
- return LlmChatState(
103
+ final entities = await ref
104
+ .read(aiChatApiProvider)
105
+ .loadMessages(userId, conversationId);
106
+ return AiChatState(
93
107
  messages: entities.map(ChatMessage.fromEntity).toList(),
94
108
  );
95
109
  } catch (e) {
96
- _logger.e('Failed to load LLM chat history: $e');
110
+ _logger.e('Failed to load AI chat history: $e');
97
111
  // Graceful fallback: start fresh if the backend is unavailable.
98
- return const LlmChatState(messages: []);
112
+ return const AiChatState(messages: []);
99
113
  }
100
114
  }
101
115
 
102
116
  /// Adds the user message to the conversation, persists it, then streams
103
117
  /// the assistant reply chunk-by-chunk via SSE, updating state on each token.
104
118
  Future<void> sendMessage(String prompt) async {
119
+ final conversationId = ref.read(selectedConversationIdProvider);
105
120
  final current = switch (state) {
106
121
  AsyncData(:final value) => value,
107
122
  _ => null,
108
123
  };
109
- if (current == null || current.isReplying) return;
124
+ if (conversationId == null || current == null || current.isReplying) return;
125
+ _streamingConversationId = conversationId;
110
126
 
111
127
  final userMsg = ChatMessage.user(prompt);
112
128
  state = AsyncData(
@@ -117,26 +133,29 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
117
133
  ),
118
134
  );
119
135
 
120
- _persistMessage(userMsg);
136
+ _persistMessage(userMsg, conversationId);
121
137
 
122
138
  // Pass only the last _kMaxContextMessages messages to the AI.
123
139
  // The full history is still stored in the DB for the user to read.
124
- final allMessages = (state as AsyncData<LlmChatState>).value.messages;
140
+ final allMessages = (state as AsyncData<AiChatState>).value.messages;
125
141
  final history = allMessages.length > _kMaxContextMessages
126
142
  ? allMessages.sublist(allMessages.length - _kMaxContextMessages)
127
143
  : allMessages;
128
- await _requestReplyStream(history);
144
+ await _requestReplyStream(history, conversationId);
129
145
  }
130
146
 
131
147
  // ---------------------------------------------------------------------------
132
148
  // SSE streaming
133
149
  // ---------------------------------------------------------------------------
134
150
 
135
- Future<void> _requestReplyStream(List<ChatMessage> history) async {
136
- final t = ref.read(translationsProvider).llm_chat;
137
- final String llmChatEndpoint = AppEnv.llmChatEndpoint;
151
+ Future<void> _requestReplyStream(
152
+ List<ChatMessage> history,
153
+ String conversationId,
154
+ ) async {
155
+ final t = ref.read(translationsProvider).ai_chat;
156
+ final String aiChatEndpoint = AppEnv.aiChatEndpoint;
138
157
 
139
- if (llmChatEndpoint.isEmpty) {
158
+ if (aiChatEndpoint.isEmpty) {
140
159
  _finalizeAssistantMessage(t.error_not_configured);
141
160
  return;
142
161
  }
@@ -160,7 +179,7 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
160
179
 
161
180
  try {
162
181
  final response = await dio.post<ResponseBody>(
163
- llmChatEndpoint,
182
+ aiChatEndpoint,
164
183
  data: {
165
184
  'message': history.last.content,
166
185
  'history': history
@@ -201,7 +220,7 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
201
220
  // Persist the complete assistant reply once the stream ends.
202
221
  final fullContent = contentBuffer.toString();
203
222
  if (fullContent.isNotEmpty) {
204
- _persistMessage(ChatMessage.assistant(fullContent));
223
+ _persistMessage(ChatMessage.assistant(fullContent), conversationId);
205
224
  } else {
206
225
  // Stream ended with no content (e.g. error event)
207
226
  _finalizeAssistantMessage(t.error_no_reply);
@@ -213,19 +232,21 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
213
232
  AsyncData(:final value) => value,
214
233
  _ => null,
215
234
  };
216
- if (latest != null) {
235
+ if (latest != null && _streamStillActive) {
217
236
  state = AsyncData(
218
237
  latest.copyWith(isReplying: false, streamingStarted: false),
219
238
  );
220
239
  }
221
240
  } catch (error) {
222
- _logger.e('LLM chat stream failed: $error');
241
+ _logger.e('AI chat stream failed: $error');
223
242
  _finalizeAssistantMessage(t.error_network);
224
243
  }
225
244
  }
226
245
 
227
246
  /// Called on every incoming SSE token to update the last assistant bubble.
228
247
  void _appendStreamingChunk(String partialContent) {
248
+ // Ignore chunks if the user navigated to another conversation mid-stream.
249
+ if (!_streamStillActive) return;
229
250
  final current = switch (state) {
230
251
  AsyncData(:final value) => value,
231
252
  _ => null,
@@ -243,22 +264,24 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
243
264
  : [...msgs, ChatMessage.assistant(partialContent)];
244
265
 
245
266
  state = AsyncData(
246
- LlmChatState(messages: newMsgs, isReplying: true, streamingStarted: true),
267
+ AiChatState(messages: newMsgs, isReplying: true, streamingStarted: true),
247
268
  );
248
269
  }
249
270
 
250
271
  /// Replaces (or appends) the assistant bubble with [content] and marks
251
272
  /// the reply as finished. Used for error messages and empty-stream fallback.
252
273
  void _finalizeAssistantMessage(String content) {
274
+ // Ignore if the user navigated to another conversation mid-stream.
275
+ if (!_streamStillActive) return;
253
276
  final current = switch (state) {
254
277
  AsyncData(:final value) => value,
255
- _ => const LlmChatState(messages: []),
278
+ _ => const AiChatState(messages: []),
256
279
  };
257
280
  final msgs = current.messages;
258
281
  final newMsgs = current.streamingStarted
259
282
  ? [...msgs.sublist(0, msgs.length - 1), ChatMessage.assistant(content)]
260
283
  : [...msgs, ChatMessage.assistant(content)];
261
- state = AsyncData(LlmChatState(messages: newMsgs));
284
+ state = AsyncData(AiChatState(messages: newMsgs));
262
285
  }
263
286
 
264
287
  // ---------------------------------------------------------------------------
@@ -304,14 +327,24 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
304
327
  // Persistence
305
328
  // ---------------------------------------------------------------------------
306
329
 
307
- void _persistMessage(ChatMessage message) {
330
+ void _persistMessage(ChatMessage message, String conversationId) {
308
331
  final userId = ref.read(userStateNotifierProvider).user.idOrNull;
309
332
  if (userId == null) return;
333
+ final entity = message.toEntity();
310
334
  ref
311
- .read(llmChatApiProvider)
312
- .saveMessage(userId, message.toEntity())
335
+ .read(aiChatApiProvider)
336
+ .saveMessage(userId, conversationId, entity)
313
337
  .catchError((e) {
314
338
  _logger.e('Failed to persist message: $e');
315
339
  });
340
+ // Keep the conversation list preview + ordering in sync.
341
+ ref
342
+ .read(aiConversationsNotifierProvider.notifier)
343
+ .touch(
344
+ conversationId,
345
+ role: message.role,
346
+ content: message.content,
347
+ at: entity.createdAt,
348
+ );
316
349
  }
317
350
  }
@@ -135,7 +135,7 @@ class HttpAuthenticationApi implements AuthenticationApi {
135
135
  }
136
136
  rethrow;
137
137
  }
138
- final googleAuth = await googleUser.authentication;
138
+ final googleAuth = googleUser.authentication;
139
139
  throw UnimplementedError('''
140
140
  ❌ You must edit lib/features/authentication/api/authentication_api.dart
141
141
  to send the Oauth2 token result to your backend.
@@ -228,7 +228,7 @@ class HttpAuthenticationApi implements AuthenticationApi {
228
228
  }
229
229
  rethrow;
230
230
  }
231
- final googleAuth = await googleUser.authentication;
231
+ final googleAuth = googleUser.authentication;
232
232
  throw UnimplementedError(
233
233
  '❌ Send the tokens to your backend to merge the guest account.\n'
234
234
  ' idToken: ${googleAuth.idToken}\n'