kasy-cli 1.21.9 → 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 (267) 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 +80 -37
  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/generator.js +5 -5
  31. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  32. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  33. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  34. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  35. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  37. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  40. package/lib/scaffold/backends/supabase/generator.js +17 -17
  41. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  42. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  43. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  44. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  45. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  46. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  48. 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
  49. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  50. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  51. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  52. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  53. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  55. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  56. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  57. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  59. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  60. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  61. package/lib/scaffold/catalog.js +9 -11
  62. package/lib/scaffold/generate.js +45 -31
  63. package/lib/scaffold/shared/generator-utils.js +188 -81
  64. package/lib/scaffold/shared/sort-imports.js +191 -0
  65. package/lib/scaffold/shared/template-strings.js +3 -3
  66. package/lib/utils/checks.js +2 -2
  67. package/lib/utils/i18n/messages-en.js +50 -35
  68. package/lib/utils/i18n/messages-es.js +50 -35
  69. package/lib/utils/i18n/messages-pt.js +52 -37
  70. package/lib/utils/updates.js +15 -15
  71. package/package.json +1 -1
  72. package/templates/firebase/.env.example +2 -2
  73. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  74. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  75. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  78. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  79. package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
  80. package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
  81. package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
  82. package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
  83. package/templates/firebase/firestore.rules +24 -5
  84. package/templates/firebase/functions/package-lock.json +22 -1
  85. package/templates/firebase/functions/package.json +2 -1
  86. package/templates/firebase/functions/src/admin/functions.ts +113 -0
  87. package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
  88. package/templates/firebase/functions/src/index.ts +8 -2
  89. package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
  90. package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
  91. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
  92. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
  93. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
  94. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  95. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  96. package/templates/firebase/lib/components/components.dart +4 -1
  97. package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
  98. package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
  99. package/templates/firebase/lib/components/kasy_button.dart +23 -99
  100. package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
  101. package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
  102. package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
  103. package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
  104. package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
  105. package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
  106. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
  107. package/templates/firebase/lib/components/kasy_toast.dart +2 -2
  108. package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
  109. package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
  110. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
  111. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
  112. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
  113. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
  114. package/templates/firebase/lib/core/config/app_env.dart +5 -11
  115. package/templates/firebase/lib/core/config/features.dart +5 -4
  116. package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
  117. package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
  118. package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
  119. package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
  120. package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
  121. package/templates/firebase/lib/core/data/models/user.dart +11 -0
  122. package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
  123. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
  124. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
  125. package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
  126. package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
  127. package/templates/firebase/lib/core/states/logout_action.dart +25 -0
  128. package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
  129. package/templates/firebase/lib/core/theme/colors.dart +488 -188
  130. package/templates/firebase/lib/core/theme/radius.dart +22 -11
  131. package/templates/firebase/lib/core/theme/shadows.dart +66 -0
  132. package/templates/firebase/lib/core/theme/texts.dart +75 -41
  133. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
  134. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
  135. package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
  136. package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
  137. package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
  138. package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
  139. package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
  140. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
  141. package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
  142. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
  143. package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
  144. package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
  145. package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  146. package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
  147. package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
  148. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
  149. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
  150. package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
  151. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
  152. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
  153. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
  154. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
  155. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
  156. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
  157. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
  158. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
  159. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
  160. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
  161. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
  162. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
  163. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
  164. package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
  165. package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
  166. package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
  167. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
  168. package/templates/firebase/lib/features/home/home_feed.dart +289 -0
  169. package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
  170. package/templates/firebase/lib/features/home/home_page.dart +11 -250
  171. package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
  172. package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
  173. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
  174. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
  175. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
  176. package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
  177. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  178. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
  179. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
  180. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
  181. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
  182. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
  183. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
  184. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
  185. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
  186. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
  187. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
  188. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  189. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
  190. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
  191. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
  192. package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
  193. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
  194. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
  195. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
  196. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
  197. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
  198. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
  199. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
  200. package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
  201. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  202. package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
  203. package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
  204. package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
  205. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
  206. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
  207. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
  208. package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  209. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
  210. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
  211. package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
  212. package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
  213. package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
  214. package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
  215. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
  216. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
  217. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
  218. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
  219. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
  220. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
  221. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
  222. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
  223. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
  224. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
  225. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
  226. package/templates/firebase/lib/i18n/en.i18n.json +171 -46
  227. package/templates/firebase/lib/i18n/es.i18n.json +175 -50
  228. package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
  229. package/templates/firebase/lib/main.dart +6 -3
  230. package/templates/firebase/lib/router.dart +15 -23
  231. package/templates/firebase/pubspec.yaml +4 -5
  232. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
  233. package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
  234. package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
  235. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
  236. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
  237. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
  238. package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
  239. package/templates/firebase/test/test_utils.dart +6 -6
  240. package/templates/firebase/web/index.html +5 -2
  241. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
  242. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
  243. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
  244. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
  245. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  246. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  247. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  248. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  249. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  250. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
  251. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
  252. package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
  253. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
  254. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
  255. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
  256. /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
  257. /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
  258. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
  259. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
  260. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
  261. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
  262. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
  263. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
  264. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
  265. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
  266. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
  267. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
@@ -5,8 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
5
5
  import 'package:kasy_kit/core/config/app_env.dart';
6
6
  import 'package:kasy_kit/core/states/translations.dart';
7
7
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
8
- import 'package:kasy_kit/features/llm_chat/api/llm_chat_api.dart';
9
- import 'package:kasy_kit/features/llm_chat/api/llm_chat_message_entity.dart';
8
+ import 'package:kasy_kit/features/ai_chat/api/ai_chat_api.dart';
9
+ import 'package:kasy_kit/features/ai_chat/api/ai_chat_message_entity.dart';
10
+ import 'package:kasy_kit/features/ai_chat/providers/ai_conversations_notifier.dart';
10
11
  import 'package:logger/logger.dart';
11
12
  import 'package:supabase_flutter/supabase_flutter.dart';
12
13
 
@@ -24,33 +25,33 @@ class ChatMessage {
24
25
  factory ChatMessage.assistant(String content) =>
25
26
  ChatMessage(role: 'assistant', content: content);
26
27
 
27
- factory ChatMessage.fromEntity(LlmChatMessageEntity entity) =>
28
+ factory ChatMessage.fromEntity(AiChatMessageEntity entity) =>
28
29
  ChatMessage(role: entity.role, content: entity.content);
29
30
 
30
- LlmChatMessageEntity toEntity() => LlmChatMessageEntity(
31
+ AiChatMessageEntity toEntity() => AiChatMessageEntity(
31
32
  role: role,
32
33
  content: content,
33
34
  createdAt: DateTime.now(),
34
35
  );
35
36
  }
36
37
 
37
- class LlmChatState {
38
+ class AiChatState {
38
39
  final List<ChatMessage> messages;
39
40
  final bool isReplying;
40
41
  final bool streamingStarted;
41
42
 
42
- const LlmChatState({
43
+ const AiChatState({
43
44
  required this.messages,
44
45
  this.isReplying = false,
45
46
  this.streamingStarted = false,
46
47
  });
47
48
 
48
- LlmChatState copyWith({
49
+ AiChatState copyWith({
49
50
  List<ChatMessage>? messages,
50
51
  bool? isReplying,
51
52
  bool? streamingStarted,
52
53
  }) {
53
- return LlmChatState(
54
+ return AiChatState(
54
55
  messages: messages ?? this.messages,
55
56
  isReplying: isReplying ?? this.isReplying,
56
57
  streamingStarted: streamingStarted ?? this.streamingStarted,
@@ -58,34 +59,46 @@ class LlmChatState {
58
59
  }
59
60
  }
60
61
 
61
- final llmChatNotifierProvider =
62
- AsyncNotifierProvider<LlmChatNotifier, LlmChatState>(LlmChatNotifier.new);
62
+ final aiChatNotifierProvider =
63
+ AsyncNotifierProvider<AiChatNotifier, AiChatState>(AiChatNotifier.new);
63
64
 
64
- class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
65
+ class AiChatNotifier extends AsyncNotifier<AiChatState> {
65
66
  final Logger _logger = Logger();
66
67
 
68
+ String? _streamingConversationId;
69
+
70
+ bool get _streamStillActive =>
71
+ ref.read(selectedConversationIdProvider) == _streamingConversationId;
72
+
67
73
  @override
68
- Future<LlmChatState> build() async {
74
+ Future<AiChatState> build() async {
75
+ final conversationId = ref.watch(selectedConversationIdProvider);
69
76
  final userId = ref.read(userStateNotifierProvider).user.idOrNull;
70
- if (userId == null) return const LlmChatState(messages: []);
77
+ if (conversationId == null || userId == null) {
78
+ return const AiChatState(messages: []);
79
+ }
71
80
 
72
81
  try {
73
- final entities = await ref.read(llmChatApiProvider).loadMessages(userId);
74
- return LlmChatState(
82
+ final entities = await ref
83
+ .read(aiChatApiProvider)
84
+ .loadMessages(userId, conversationId);
85
+ return AiChatState(
75
86
  messages: entities.map(ChatMessage.fromEntity).toList(),
76
87
  );
77
88
  } catch (e) {
78
- _logger.e('Failed to load LLM chat history: $e');
79
- return const LlmChatState(messages: []);
89
+ _logger.e('Failed to load AI chat history: $e');
90
+ return const AiChatState(messages: []);
80
91
  }
81
92
  }
82
93
 
83
94
  Future<void> sendMessage(String prompt) async {
95
+ final conversationId = ref.read(selectedConversationIdProvider);
84
96
  final current = switch (state) {
85
97
  AsyncData(:final value) => value,
86
98
  _ => null,
87
99
  };
88
- if (current == null || current.isReplying) return;
100
+ if (conversationId == null || current == null || current.isReplying) return;
101
+ _streamingConversationId = conversationId;
89
102
 
90
103
  final userMsg = ChatMessage.user(prompt);
91
104
  state = AsyncData(
@@ -96,20 +109,23 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
96
109
  ),
97
110
  );
98
111
 
99
- _persistMessage(userMsg);
112
+ _persistMessage(userMsg, conversationId);
100
113
 
101
- final allMessages = (state as AsyncData<LlmChatState>).value.messages;
114
+ final allMessages = (state as AsyncData<AiChatState>).value.messages;
102
115
  final history = allMessages.length > _kMaxContextMessages
103
116
  ? allMessages.sublist(allMessages.length - _kMaxContextMessages)
104
117
  : allMessages;
105
- await _requestReplyStream(history);
118
+ await _requestReplyStream(history, conversationId);
106
119
  }
107
120
 
108
- Future<void> _requestReplyStream(List<ChatMessage> history) async {
109
- final t = ref.read(translationsProvider).llm_chat;
110
- final String llmChatEndpoint = AppEnv.llmChatEndpoint;
121
+ Future<void> _requestReplyStream(
122
+ List<ChatMessage> history,
123
+ String conversationId,
124
+ ) async {
125
+ final t = ref.read(translationsProvider).ai_chat;
126
+ final String aiChatEndpoint = AppEnv.aiChatEndpoint;
111
127
 
112
- if (llmChatEndpoint.isEmpty) {
128
+ if (aiChatEndpoint.isEmpty) {
113
129
  _finalizeAssistantMessage(t.error_not_configured);
114
130
  return;
115
131
  }
@@ -132,7 +148,7 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
132
148
 
133
149
  try {
134
150
  final response = await dio.post<ResponseBody>(
135
- llmChatEndpoint,
151
+ aiChatEndpoint,
136
152
  data: {
137
153
  'message': history.last.content,
138
154
  'history': history
@@ -170,7 +186,7 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
170
186
 
171
187
  final fullContent = contentBuffer.toString();
172
188
  if (fullContent.isNotEmpty) {
173
- _persistMessage(ChatMessage.assistant(fullContent));
189
+ _persistMessage(ChatMessage.assistant(fullContent), conversationId);
174
190
  } else {
175
191
  _finalizeAssistantMessage(t.error_no_reply);
176
192
  return;
@@ -180,18 +196,19 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
180
196
  AsyncData(:final value) => value,
181
197
  _ => null,
182
198
  };
183
- if (latest != null) {
199
+ if (latest != null && _streamStillActive) {
184
200
  state = AsyncData(
185
201
  latest.copyWith(isReplying: false, streamingStarted: false),
186
202
  );
187
203
  }
188
204
  } catch (error) {
189
- _logger.e('LLM chat stream failed: $error');
205
+ _logger.e('AI chat stream failed: $error');
190
206
  _finalizeAssistantMessage(t.error_network);
191
207
  }
192
208
  }
193
209
 
194
210
  void _appendStreamingChunk(String partialContent) {
211
+ if (!_streamStillActive) return;
195
212
  final current = switch (state) {
196
213
  AsyncData(:final value) => value,
197
214
  _ => null,
@@ -207,20 +224,21 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
207
224
  : [...msgs, ChatMessage.assistant(partialContent)];
208
225
 
209
226
  state = AsyncData(
210
- LlmChatState(messages: newMsgs, isReplying: true, streamingStarted: true),
227
+ AiChatState(messages: newMsgs, isReplying: true, streamingStarted: true),
211
228
  );
212
229
  }
213
230
 
214
231
  void _finalizeAssistantMessage(String content) {
232
+ if (!_streamStillActive) return;
215
233
  final current = switch (state) {
216
234
  AsyncData(:final value) => value,
217
- _ => const LlmChatState(messages: []),
235
+ _ => const AiChatState(messages: []),
218
236
  };
219
237
  final msgs = current.messages;
220
238
  final newMsgs = current.streamingStarted
221
239
  ? [...msgs.sublist(0, msgs.length - 1), ChatMessage.assistant(content)]
222
240
  : [...msgs, ChatMessage.assistant(content)];
223
- state = AsyncData(LlmChatState(messages: newMsgs));
241
+ state = AsyncData(AiChatState(messages: newMsgs));
224
242
  }
225
243
 
226
244
  String _extractSSEDelta(String line) {
@@ -253,14 +271,24 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
253
271
  return '';
254
272
  }
255
273
 
256
- void _persistMessage(ChatMessage message) {
274
+ void _persistMessage(ChatMessage message, String conversationId) {
257
275
  final userId = ref.read(userStateNotifierProvider).user.idOrNull;
258
276
  if (userId == null) return;
277
+ final entity = message.toEntity();
259
278
  ref
260
- .read(llmChatApiProvider)
261
- .saveMessage(userId, message.toEntity())
279
+ .read(aiChatApiProvider)
280
+ .saveMessage(userId, conversationId, entity)
262
281
  .catchError((e) {
263
282
  _logger.e('Failed to persist message: $e');
264
283
  });
284
+ // Keep the conversation list preview + ordering in sync.
285
+ ref
286
+ .read(aiConversationsNotifierProvider.notifier)
287
+ .touch(
288
+ conversationId,
289
+ role: message.role,
290
+ content: message.content,
291
+ at: entity.createdAt,
292
+ );
265
293
  }
266
294
  }
@@ -8,7 +8,7 @@ import 'package:google_sign_in/google_sign_in.dart';
8
8
  import 'package:logger/logger.dart';
9
9
  import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
10
10
  import 'package:kasy_kit/core/data/entities/user_entity.dart';
11
- import 'package:kasy_kit/environnements.dart';
11
+ import 'package:kasy_kit/environments.dart';
12
12
  import 'package:kasy_kit/google_auth_options.dart';
13
13
  import 'package:kasy_kit/features/authentication/api/authentication_api_interface.dart';
14
14
  import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
@@ -56,4 +56,50 @@ class FeatureRequestApi {
56
56
  throw ApiError(code: 0, message: '$e: $stacktrace');
57
57
  }
58
58
  }
59
+
60
+ /// Admin: every request (active + hidden), highest-voted first.
61
+ Future<List<FeatureRequestEntity>> getAll() async {
62
+ try {
63
+ final res = await _client
64
+ .from(_kFeatureRequestTable) //
65
+ .select()
66
+ .order('votes', ascending: false);
67
+ return res.map((e) => FeatureRequestEntity.fromJson(e)).toList();
68
+ } catch (e, stacktrace) {
69
+ Logger().e('$e: $stacktrace');
70
+ throw ApiError(code: 0, message: '$e: $stacktrace');
71
+ }
72
+ }
73
+
74
+ /// Admin: toggle whether a request is visible (and votable) to users.
75
+ /// Gated server-side by the "Feature requests admin update" RLS policy.
76
+ Future<void> setActive(String id, bool active) async {
77
+ try {
78
+ await _client
79
+ .from(_kFeatureRequestTable) //
80
+ .update({'active': active})
81
+ .eq('id', id);
82
+ } catch (e, stacktrace) {
83
+ Logger().e('$e: $stacktrace');
84
+ throw ApiError(code: 0, message: '$e: $stacktrace');
85
+ }
86
+ }
87
+
88
+ /// Admin: update the localized title/description maps (en/pt/es).
89
+ Future<void> updateTexts({
90
+ required String id,
91
+ required Map<String, String> title,
92
+ required Map<String, String> description,
93
+ }) async {
94
+ try {
95
+ await _client.from(_kFeatureRequestTable).update({
96
+ 'title': title,
97
+ 'description': description,
98
+ 'last_update_date': DateTime.now().toUtc().toIso8601String(),
99
+ }).eq('id', id);
100
+ } catch (e, stacktrace) {
101
+ Logger().e('$e: $stacktrace');
102
+ throw ApiError(code: 0, message: '$e: $stacktrace');
103
+ }
104
+ }
59
105
  }
@@ -1,11 +1,11 @@
1
1
  import 'package:kasy_kit/features/onboarding/api/entities/user_info_entity.dart';
2
2
 
3
3
  enum UserInfoKeys {
4
- genre,
4
+ gender,
5
5
  age,
6
6
  }
7
7
 
8
- enum Genre {
8
+ enum Gender {
9
9
  male,
10
10
  female,
11
11
  none,
@@ -44,11 +44,11 @@ class UserAgeInfo extends UserInfoDetail<AgeRange> {
44
44
  }
45
45
 
46
46
  factory UserAgeInfo.fromString(String value) {
47
- final sexe = AgeRange.values.firstWhere(
47
+ final range = AgeRange.values.firstWhere(
48
48
  (element) => element.name == value,
49
49
  orElse: () => AgeRange.none,
50
50
  );
51
- return UserAgeInfo(sexe);
51
+ return UserAgeInfo(range);
52
52
  }
53
53
 
54
54
 
@@ -62,32 +62,32 @@ class UserAgeInfo extends UserInfoDetail<AgeRange> {
62
62
  }
63
63
 
64
64
  /// ======================================
65
- /// user genre info
65
+ /// user gender info
66
66
  /// ======================================
67
- class UserSexeInfo extends UserInfoDetail<Genre> {
68
- UserSexeInfo(super.value);
67
+ class UserGenderInfo extends UserInfoDetail<Gender> {
68
+ UserGenderInfo(super.value);
69
69
 
70
- factory UserSexeInfo.fromEntity(UserInfoEntity entity) {
71
- final value = Genre.values.firstWhere(
70
+ factory UserGenderInfo.fromEntity(UserInfoEntity entity) {
71
+ final value = Gender.values.firstWhere(
72
72
  (element) => element.name == entity.value,
73
- orElse: () => Genre.none,
73
+ orElse: () => Gender.none,
74
74
  );
75
- return UserSexeInfo(value);
75
+ return UserGenderInfo(value);
76
76
  }
77
77
 
78
- factory UserSexeInfo.fromString(String value) {
79
- final genre = Genre.values.firstWhere(
78
+ factory UserGenderInfo.fromString(String value) {
79
+ final gender = Gender.values.firstWhere(
80
80
  (element) => element.name == value,
81
- orElse: () => Genre.none,
81
+ orElse: () => Gender.none,
82
82
  );
83
- return UserSexeInfo(genre);
83
+ return UserGenderInfo(gender);
84
84
  }
85
85
 
86
86
 
87
87
 
88
88
  @override
89
89
  UserInfoEntity toEntity(String userId) => UserInfoEntity(
90
- key: UserInfoKeys.genre.name,
90
+ key: UserInfoKeys.gender.name,
91
91
  value: value.name,
92
92
  userId: userId,
93
93
  );
@@ -6,7 +6,8 @@ import 'package:kasy_kit/core/data/api/tracking_api.dart';
6
6
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
7
7
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
8
8
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart';
9
- import 'package:kasy_kit/features/subscription/repositories/subscription_repository.dart';
9
+ import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_module_mockups.dart';
10
+ import 'package:kasy_kit/features/subscriptions/repositories/subscription_repository.dart';
10
11
  import 'package:kasy_kit/i18n/translations.g.dart';
11
12
  import 'package:permission_handler/permission_handler.dart';
12
13
 
@@ -29,10 +30,10 @@ class AttPermissionStep extends ConsumerWidget {
29
30
 
30
31
  return OnboardingBackground(
31
32
  child: OnboardingIllustrationScaffold(
32
- progress: 0.9,
33
+ step: 6,
33
34
  title: translations.title,
34
35
  description: translations.description,
35
- imageAsset: 'assets/images/onboarding/img3.jpg',
36
+ image: const TrackingPermissionMockup(),
36
37
  footerActions: [
37
38
  KasyButton(
38
39
  label: translations.continue_button,
@@ -7,6 +7,7 @@ import 'package:kasy_kit/core/theme/theme.dart';
7
7
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
8
8
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
9
9
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_progress.dart';
10
+ import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_step_header.dart';
10
11
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_sticky_footer.dart';
11
12
  import 'package:kasy_kit/features/onboarding/ui/widgets/selectable_row_tile.dart';
12
13
 
@@ -18,9 +19,11 @@ typedef OnOptionIdSelected = void Function(String id);
18
19
 
19
20
  typedef OnValidate = void Function(String? key);
20
21
 
21
- /// Single choice question with radio buttons
22
+ /// Single choice question with selectable tiles, matching the clean‑premium
23
+ /// onboarding layout (shared header, left‑aligned title, sticky footer).
22
24
  class OnboardingRadioQuestion extends ConsumerStatefulWidget {
23
- final double? progress;
25
+ final int step;
26
+ final int totalSteps;
24
27
  final String title;
25
28
  final String description;
26
29
  final String btnText;
@@ -37,7 +40,8 @@ class OnboardingRadioQuestion extends ConsumerStatefulWidget {
37
40
  required this.btnText,
38
41
  required this.optionIds,
39
42
  required this.optionBuilder,
40
- this.progress,
43
+ required this.step,
44
+ this.totalSteps = kOnboardingSteps,
41
45
  this.onOptionIdSelected,
42
46
  this.onValidate,
43
47
  this.reassuranceBuilder,
@@ -54,61 +58,68 @@ class _OnboardingRadioQuestionState
54
58
 
55
59
  @override
56
60
  Widget build(BuildContext context) {
61
+ const gutter = KasySpacing.lg;
62
+
57
63
  final scrollBody = Column(
58
- crossAxisAlignment: CrossAxisAlignment.stretch,
64
+ crossAxisAlignment: CrossAxisAlignment.start,
59
65
  children: [
60
- if (widget.progress != null)
61
- OnboardingProgress(value: widget.progress!),
66
+ OnboardingStepHeader(step: widget.step, totalSteps: widget.totalSteps),
62
67
  Padding(
63
- padding: const EdgeInsets.fromLTRB(
64
- KasySpacing.md,
65
- KasySpacing.xl,
66
- KasySpacing.md,
67
- 0,
68
- ),
68
+ padding: const EdgeInsets.fromLTRB(gutter, KasySpacing.lg, gutter, 0),
69
69
  child: MoveFadeAnim(
70
- delayInMs: 100,
70
+ delayInMs: 120,
71
71
  child: Text(
72
72
  widget.title,
73
- textAlign: TextAlign.center,
74
- style: context.textTheme.headlineLarge?.copyWith(
73
+ textAlign: TextAlign.start,
74
+ style: context.textTheme.headlineMedium?.copyWith(
75
75
  color: context.colors.onBackground,
76
+ fontSize: 26,
77
+ fontWeight: FontWeight.w700,
78
+ letterSpacing: -0.2,
79
+ height: 1.2,
76
80
  ),
77
81
  ),
78
82
  ),
79
83
  ),
80
84
  Padding(
81
85
  padding: const EdgeInsets.fromLTRB(
86
+ gutter,
87
+ KasySpacing.smd,
88
+ gutter,
82
89
  KasySpacing.lg,
83
- KasySpacing.md,
84
- KasySpacing.lg,
85
- KasySpacing.xl,
86
90
  ),
87
91
  child: MoveFadeAnim(
88
92
  delayInMs: 200,
89
93
  child: Text(
90
94
  widget.description,
91
- textAlign: TextAlign.center,
95
+ textAlign: TextAlign.start,
92
96
  style: context.textTheme.bodyLarge?.copyWith(
93
- color: context.colors.onBackground.withValues(alpha: 0.6),
97
+ color: context.colors.muted,
98
+ height: 1.45,
94
99
  ),
95
100
  ),
96
101
  ),
97
102
  ),
98
103
  Padding(
99
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
104
+ padding: const EdgeInsets.symmetric(horizontal: gutter),
100
105
  child: OnboardingSelectableRowGroup(
101
106
  physics: const NeverScrollableScrollPhysics(),
102
- options: widget.optionIds
103
- .map(
107
+ options: widget.optionIds.map(
104
108
  (e) {
105
109
  final index = widget.optionIds.indexOf(e);
106
110
  return Animate(
107
111
  effects: [
108
112
  FadeEffect(
109
- delay: Duration(milliseconds: 300 + index * 100),
110
- duration: const Duration(milliseconds: 550),
111
- curve: Curves.easeInOut,
113
+ delay: Duration(milliseconds: 280 + index * 80),
114
+ duration: const Duration(milliseconds: 450),
115
+ curve: Curves.easeOut,
116
+ ),
117
+ MoveEffect(
118
+ delay: Duration(milliseconds: 280 + index * 80),
119
+ duration: const Duration(milliseconds: 450),
120
+ curve: Curves.easeOut,
121
+ begin: const Offset(0, 24),
122
+ end: Offset.zero,
112
123
  ),
113
124
  ],
114
125
  child: widget.optionBuilder(e, e == selectedChoiceId),
@@ -153,8 +164,7 @@ class _OnboardingRadioQuestionState
153
164
  ),
154
165
  DeviceSizeBuilder(
155
166
  builder: (device) => OnboardingStickyFooter(
156
- maxContentWidth:
157
- device == DeviceType.small ? null : 600,
167
+ maxContentWidth: device == DeviceType.small ? null : 600,
158
168
  children: [
159
169
  KasyButton(
160
170
  label: widget.btnText,
@@ -0,0 +1,93 @@
1
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+ import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
3
+ import 'package:supabase_flutter/supabase_flutter.dart';
4
+
5
+ /// One row of the admin Users table, as returned by the `admin-list-users` Edge
6
+ /// Function. The function verifies the caller is `role == "admin"` and only then
7
+ /// reads across all users with the service role, so this data is never readable
8
+ /// by a non-admin client.
9
+ ///
10
+ /// This file mirrors the shared admin data-layer contract (see the Firebase
11
+ /// version): the public surface — [AdminUser], [AdminUsersResult] and
12
+ /// [adminUsersApiProvider] — is identical across backends so the Users tab UI
13
+ /// stays the same; only the body of [AdminUsersApi.fetch] differs.
14
+ class AdminUser {
15
+ final String id;
16
+ final String? email;
17
+ final String? name;
18
+ final DateTime? createdAt;
19
+ final bool subscriber;
20
+
21
+ const AdminUser({
22
+ required this.id,
23
+ required this.subscriber,
24
+ this.email,
25
+ this.name,
26
+ this.createdAt,
27
+ });
28
+
29
+ factory AdminUser.fromMap(Map<String, dynamic> m) {
30
+ final createdMillis = m['createdAt'];
31
+ return AdminUser(
32
+ id: (m['id'] as String?) ?? '',
33
+ email: m['email'] as String?,
34
+ name: m['name'] as String?,
35
+ createdAt: createdMillis is num
36
+ ? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
37
+ : null,
38
+ subscriber: m['subscriber'] == true,
39
+ );
40
+ }
41
+ }
42
+
43
+ /// The full (bounded) set of users plus collection metadata. The UI searches,
44
+ /// sorts and paginates this list locally, so those interactions are instant.
45
+ class AdminUsersResult {
46
+ final List<AdminUser> users;
47
+ final int totalUsers;
48
+ final bool truncated;
49
+
50
+ const AdminUsersResult({
51
+ required this.users,
52
+ required this.totalUsers,
53
+ required this.truncated,
54
+ });
55
+ }
56
+
57
+ final adminUsersApiProvider = Provider<AdminUsersApi>(
58
+ (ref) => AdminUsersApi(client: Supabase.instance.client),
59
+ );
60
+
61
+ class AdminUsersApi {
62
+ final SupabaseClient _client;
63
+
64
+ AdminUsersApi({required SupabaseClient client}) : _client = client;
65
+
66
+ /// Loads the users for the admin console via the `admin-list-users` Edge
67
+ /// Function. Returns the whole bounded set in one call; the UI handles
68
+ /// search/sort/pagination on the client.
69
+ Future<AdminUsersResult> fetch() async {
70
+ try {
71
+ final res = await _client.functions.invoke('admin-list-users');
72
+ if (res.status != 200) {
73
+ final msg = res.data is Map && res.data != null
74
+ ? (res.data as Map)['error'] as String? ?? 'Error listing users'
75
+ : 'Error listing users';
76
+ throw ApiError(code: res.status, message: msg);
77
+ }
78
+ final data = Map<String, dynamic>.from(res.data as Map);
79
+ final rawUsers = (data['users'] as List?) ?? const [];
80
+ return AdminUsersResult(
81
+ users: rawUsers
82
+ .map((e) => AdminUser.fromMap(Map<String, dynamic>.from(e as Map)))
83
+ .toList(),
84
+ totalUsers: (data['totalUsers'] as num?)?.toInt() ?? 0,
85
+ truncated: data['truncated'] == true,
86
+ );
87
+ } on ApiError {
88
+ rethrow;
89
+ } catch (e, stacktrace) {
90
+ throw ApiError(code: 0, message: '$e: $stacktrace');
91
+ }
92
+ }
93
+ }
@@ -15,6 +15,17 @@ enum SubscriptionStatus {
15
15
  CANCELLED,
16
16
  }
17
17
 
18
+ /// Where the subscription was purchased (its origin). `STRIPE` means it was
19
+ /// bought on the web via Stripe. Used to route the user to the correct
20
+ /// management flow regardless of the device they are currently on.
21
+ enum SubscriptionStore {
22
+ PLAY_STORE,
23
+ APPLE_STORE,
24
+ EARLY_BIRD,
25
+ STRIPE,
26
+ unknown,
27
+ }
28
+
18
29
 
19
30
  @freezed
20
31
  sealed class SubscriptionEntity with _$SubscriptionEntity {
@@ -27,6 +38,8 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
27
38
  @JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
28
39
  @JsonKey(name: 'period_end_date') DateTime? periodEndDate,
29
40
  @JsonKey(name: 'status') required SubscriptionStatus status,
41
+ @JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
42
+ SubscriptionStore? store,
30
43
  }) = SubscriptionEntityData;
31
44
 
32
45
  factory SubscriptionEntity.fromJson(Map<String, Object?> json) =>