kasy-cli 1.21.9 → 1.23.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 +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 +22 -6
  67. package/lib/utils/env-tools.js +7 -0
  68. package/lib/utils/flutter-install.js +114 -0
  69. package/lib/utils/i18n/messages-en.js +52 -35
  70. package/lib/utils/i18n/messages-es.js +52 -35
  71. package/lib/utils/i18n/messages-pt.js +54 -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} +702 -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 +136 -23
  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 +54 -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 +53 -14
  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 +128 -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 +5 -6
  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
@@ -0,0 +1,1193 @@
1
+ import 'package:firebase_messaging/firebase_messaging.dart';
2
+ import 'package:flutter/foundation.dart';
3
+ import 'package:flutter/material.dart';
4
+ import 'package:flutter/services.dart';
5
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
6
+ import 'package:go_router/go_router.dart';
7
+ import 'package:kasy_kit/components/kasy_app_bar.dart';
8
+ import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
9
+ import 'package:kasy_kit/components/kasy_button.dart';
10
+ import 'package:kasy_kit/components/kasy_status_tag.dart';
11
+ import 'package:kasy_kit/components/kasy_tabs.dart';
12
+ import 'package:kasy_kit/components/kasy_text_field.dart';
13
+ import 'package:kasy_kit/core/config/features.dart';
14
+ import 'package:kasy_kit/core/data/models/user.dart';
15
+ import 'package:kasy_kit/core/dev_inspector/dev_inspector.dart';
16
+ import 'package:kasy_kit/core/rating/widgets/review_popup.dart';
17
+ import 'package:kasy_kit/core/states/user_state_notifier.dart';
18
+ import 'package:kasy_kit/core/theme/theme.dart';
19
+ import 'package:kasy_kit/core/toast/toast_service.dart';
20
+ import 'package:kasy_kit/core/web_device_preview/web_device_preview.dart';
21
+ import 'package:kasy_kit/core/widgets/update_bottom_sheet.dart';
22
+ import 'package:kasy_kit/features/feedbacks/api/entities/feature_request_entity.dart';
23
+ import 'package:kasy_kit/features/feedbacks/api/feature_request_api.dart';
24
+ import 'package:kasy_kit/features/notifications/api/local_notifier.dart';
25
+ import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
26
+ as kasy_kit;
27
+ import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
28
+ import 'package:kasy_kit/features/settings/ui/components/admin/admin_users_tab.dart';
29
+ import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
30
+ import 'package:kasy_kit/i18n/translations.g.dart';
31
+ import 'package:kasy_kit/router.dart';
32
+ import 'package:package_info_plus/package_info_plus.dart';
33
+ import 'package:shared_preferences/shared_preferences.dart';
34
+
35
+ /// Full-screen developer admin console (debug only).
36
+ ///
37
+ /// Vercel-style layout: frosted "Admin" app bar + horizontally-scrollable
38
+ /// underline tabs over a content column that is centred and width-capped so the
39
+ /// console reads as a real dashboard on web/desktop instead of a stretched
40
+ /// phone screen. Replaces the old admin bottom sheet — reached via the
41
+ /// double-tap on the settings version label; popping returns to Settings.
42
+ ///
43
+ /// TOP-LEVEL route (sibling of '/', outside the BottomMenu shell) so it never
44
+ /// renders the web sidebar — the admin is a dev tool, not product chrome.
45
+ class AdminPage extends ConsumerStatefulWidget {
46
+ const AdminPage({super.key});
47
+
48
+ @override
49
+ ConsumerState<AdminPage> createState() => _AdminPageState();
50
+ }
51
+
52
+ class _AdminPageState extends ConsumerState<AdminPage> {
53
+ int _tab = 0;
54
+
55
+ @override
56
+ Widget build(BuildContext context) {
57
+ final bool isAdmin = ref.watch(userStateNotifierProvider).user.isAdmin;
58
+ final ac = t.admin_console;
59
+
60
+ // The console itself opens for admins (even in release) or anyone in debug.
61
+ if (!isAdmin && !kDebugMode) {
62
+ return Scaffold(
63
+ backgroundColor: context.colors.background,
64
+ body: Column(
65
+ crossAxisAlignment: CrossAxisAlignment.stretch,
66
+ children: [
67
+ const KasyAppBar(title: 'Admin'),
68
+ Expanded(
69
+ child: _EmptyState(
70
+ icon: Icons.lock_outline_rounded,
71
+ title: ac.tabs.overview,
72
+ message: ac.requires_admin,
73
+ ),
74
+ ),
75
+ ],
76
+ ),
77
+ );
78
+ }
79
+
80
+ // Server-data tabs always present (gated by role inside). Dev tabs (Kit,
81
+ // Tools) only in debug — they are developer conveniences, not admin data.
82
+ final List<({String label, Widget view})> entries = [
83
+ (label: ac.tabs.overview, view: const _OverviewTab()),
84
+ (label: ac.tabs.users, view: const _UsersTab()),
85
+ (label: ac.tabs.requests, view: const _RequestsTab()),
86
+ if (kDebugMode) (label: ac.tabs.kit, view: const _KitTab()),
87
+ if (kDebugMode) (label: ac.tabs.tools, view: const _ToolsTab()),
88
+ ];
89
+ final int tab = _tab.clamp(0, entries.length - 1);
90
+
91
+ return Scaffold(
92
+ backgroundColor: context.colors.background,
93
+ body: Column(
94
+ crossAxisAlignment: CrossAxisAlignment.stretch,
95
+ children: [
96
+ const KasyAppBar(title: 'Admin'),
97
+ const SizedBox(height: KasySpacing.sm),
98
+ _MaxWidth(
99
+ child: Padding(
100
+ padding: const EdgeInsets.symmetric(
101
+ horizontal: KasySpacing.pageHorizontalGutter,
102
+ ),
103
+ child: KasyTabs(
104
+ tabs: [for (final e in entries) e.label],
105
+ selectedIndex: tab,
106
+ onTabSelected: (i) => setState(() => _tab = i),
107
+ variant: KasyTabsVariant.secondary,
108
+ ),
109
+ ),
110
+ ),
111
+ Expanded(
112
+ child: IndexedStack(
113
+ index: tab,
114
+ children: [for (final e in entries) e.view],
115
+ ),
116
+ ),
117
+ ],
118
+ ),
119
+ );
120
+ }
121
+ }
122
+
123
+ /// Live feature requests (active + hidden), highest-voted first — real data for
124
+ /// the Requests tab and the Overview count. Invalidate to refresh after a change.
125
+ final _adminRequestsProvider =
126
+ FutureProvider.autoDispose<List<FeatureRequestEntity>>((ref) {
127
+ return ref.read(featureRequestApiProvider).getAll();
128
+ });
129
+
130
+ // ─────────────────────────────────────────────────────────────────────────────
131
+ // Layout primitives
132
+ // ─────────────────────────────────────────────────────────────────────────────
133
+
134
+ const double _cardRadius = 18;
135
+ const double _contentMaxWidth = 1080;
136
+
137
+ /// Centres content and caps its width so the console looks deliberate on wide
138
+ /// web/desktop viewports (mobile fills the screen normally).
139
+ class _MaxWidth extends StatelessWidget {
140
+ final Widget child;
141
+ const _MaxWidth({required this.child});
142
+
143
+ @override
144
+ Widget build(BuildContext context) {
145
+ return Center(
146
+ child: ConstrainedBox(
147
+ constraints: const BoxConstraints(maxWidth: _contentMaxWidth),
148
+ child: child,
149
+ ),
150
+ );
151
+ }
152
+ }
153
+
154
+ /// Scrollable, gutter-padded, width-capped body shared by every tab.
155
+ class _TabScroll extends StatelessWidget {
156
+ final List<Widget> children;
157
+ const _TabScroll({required this.children});
158
+
159
+ @override
160
+ Widget build(BuildContext context) {
161
+ return SingleChildScrollView(
162
+ child: _MaxWidth(
163
+ child: Padding(
164
+ padding: EdgeInsets.fromLTRB(
165
+ KasySpacing.pageHorizontalGutter,
166
+ KasySpacing.lg,
167
+ KasySpacing.pageHorizontalGutter,
168
+ MediaQuery.paddingOf(context).bottom + KasySpacing.xl,
169
+ ),
170
+ child: Column(
171
+ crossAxisAlignment: CrossAxisAlignment.stretch,
172
+ children: children,
173
+ ),
174
+ ),
175
+ ),
176
+ );
177
+ }
178
+ }
179
+
180
+ /// Small uppercase label that opens a group (Vercel-style section head).
181
+ class _GroupLabel extends StatelessWidget {
182
+ final String label;
183
+ const _GroupLabel(this.label);
184
+
185
+ @override
186
+ Widget build(BuildContext context) {
187
+ return Padding(
188
+ padding: const EdgeInsets.only(
189
+ left: KasySpacing.xs,
190
+ bottom: KasySpacing.smd,
191
+ ),
192
+ child: Text(
193
+ label.toUpperCase(),
194
+ style: context.textTheme.labelMedium?.copyWith(
195
+ fontSize: 12,
196
+ color: context.colors.muted,
197
+ letterSpacing: 1.2,
198
+ fontWeight: FontWeight.w700,
199
+ ),
200
+ ),
201
+ );
202
+ }
203
+ }
204
+
205
+ /// Rounded surface card with a hairline border and the design-system shadow.
206
+ class _CardShell extends StatelessWidget {
207
+ final Widget child;
208
+ final EdgeInsetsGeometry padding;
209
+ const _CardShell({
210
+ required this.child,
211
+ this.padding = const EdgeInsets.all(KasySpacing.md),
212
+ });
213
+
214
+ @override
215
+ Widget build(BuildContext context) {
216
+ return DecoratedBox(
217
+ decoration: BoxDecoration(
218
+ color: context.colors.surface,
219
+ borderRadius: BorderRadius.circular(_cardRadius),
220
+ border: Border.all(
221
+ color: context.colors.outline.withValues(
222
+ alpha: context.isDark ? 0.45 : 0.6,
223
+ ),
224
+ ),
225
+ boxShadow: [KasyShadows.component(context)],
226
+ ),
227
+ child: Padding(padding: padding, child: child),
228
+ );
229
+ }
230
+ }
231
+
232
+ /// Soft-tinted rounded-square icon container.
233
+ class _IconBubble extends StatelessWidget {
234
+ final IconData icon;
235
+ final Color tone;
236
+ final double size;
237
+ const _IconBubble({required this.icon, required this.tone, this.size = 40});
238
+
239
+ @override
240
+ Widget build(BuildContext context) {
241
+ return Container(
242
+ width: size,
243
+ height: size,
244
+ decoration: BoxDecoration(
245
+ color: tone.withValues(alpha: context.isDark ? 0.22 : 0.12),
246
+ borderRadius: BorderRadius.circular(size * 0.3),
247
+ ),
248
+ child: Icon(icon, size: size * 0.5, color: tone),
249
+ );
250
+ }
251
+ }
252
+
253
+ /// Metric card: an icon bubble, a big value, and a muted label.
254
+ class _StatCard extends StatelessWidget {
255
+ final IconData icon;
256
+ final Color tone;
257
+ final String value;
258
+ final String label;
259
+ const _StatCard({
260
+ required this.icon,
261
+ required this.tone,
262
+ required this.value,
263
+ required this.label,
264
+ });
265
+
266
+ @override
267
+ Widget build(BuildContext context) {
268
+ return _CardShell(
269
+ child: Column(
270
+ crossAxisAlignment: CrossAxisAlignment.start,
271
+ mainAxisSize: MainAxisSize.min,
272
+ children: [
273
+ _IconBubble(icon: icon, tone: tone),
274
+ const SizedBox(height: KasySpacing.smd),
275
+ Text(
276
+ value,
277
+ maxLines: 1,
278
+ overflow: TextOverflow.ellipsis,
279
+ style: context.textTheme.headlineSmall?.copyWith(
280
+ color: context.colors.onSurface,
281
+ fontWeight: FontWeight.w800,
282
+ letterSpacing: -0.5,
283
+ ),
284
+ ),
285
+ const SizedBox(height: 2),
286
+ Text(
287
+ label,
288
+ style: context.textTheme.bodySmall?.copyWith(
289
+ color: context.colors.muted,
290
+ ),
291
+ ),
292
+ ],
293
+ ),
294
+ );
295
+ }
296
+ }
297
+
298
+ /// Tappable action card: icon bubble + title/subtitle + chevron, with ripple.
299
+ class _ActionCard extends StatelessWidget {
300
+ final IconData icon;
301
+ final String title;
302
+ final String? subtitle;
303
+ final VoidCallback onTap;
304
+ final Color? tone;
305
+ const _ActionCard({
306
+ required this.icon,
307
+ required this.title,
308
+ required this.onTap,
309
+ this.subtitle,
310
+ this.tone,
311
+ });
312
+
313
+ @override
314
+ Widget build(BuildContext context) {
315
+ final BorderRadius radius = BorderRadius.circular(_cardRadius);
316
+ final Color accent = tone ?? context.colors.primary;
317
+ return DecoratedBox(
318
+ decoration: BoxDecoration(
319
+ borderRadius: radius,
320
+ boxShadow: [KasyShadows.component(context)],
321
+ ),
322
+ child: Material(
323
+ color: context.colors.surface,
324
+ borderRadius: radius,
325
+ clipBehavior: Clip.antiAlias,
326
+ child: InkWell(
327
+ onTap: onTap,
328
+ child: Container(
329
+ decoration: BoxDecoration(
330
+ borderRadius: radius,
331
+ border: Border.all(
332
+ color: context.colors.outline.withValues(
333
+ alpha: context.isDark ? 0.45 : 0.6,
334
+ ),
335
+ ),
336
+ ),
337
+ padding: const EdgeInsets.all(KasySpacing.md),
338
+ child: Row(
339
+ children: [
340
+ _IconBubble(icon: icon, tone: accent),
341
+ const SizedBox(width: KasySpacing.smd),
342
+ Expanded(
343
+ child: Column(
344
+ crossAxisAlignment: CrossAxisAlignment.start,
345
+ mainAxisSize: MainAxisSize.min,
346
+ children: [
347
+ Text(
348
+ title,
349
+ maxLines: 1,
350
+ overflow: TextOverflow.ellipsis,
351
+ style: context.textTheme.titleSmall?.copyWith(
352
+ color: context.colors.onSurface,
353
+ fontWeight: FontWeight.w700,
354
+ ),
355
+ ),
356
+ if (subtitle != null) ...[
357
+ const SizedBox(height: 2),
358
+ Text(
359
+ subtitle!,
360
+ maxLines: 2,
361
+ overflow: TextOverflow.ellipsis,
362
+ style: context.textTheme.bodySmall?.copyWith(
363
+ color: context.colors.muted,
364
+ height: 1.3,
365
+ ),
366
+ ),
367
+ ],
368
+ ],
369
+ ),
370
+ ),
371
+ const SizedBox(width: KasySpacing.sm),
372
+ Icon(
373
+ KasyIcons.chevronRight,
374
+ size: 18,
375
+ color: context.colors.muted,
376
+ ),
377
+ ],
378
+ ),
379
+ ),
380
+ ),
381
+ ),
382
+ );
383
+ }
384
+ }
385
+
386
+ /// Lays children out in as many equal columns as fit [minItemWidth], capped at
387
+ /// [maxCols]. Collapses to a single full-width column on narrow screens.
388
+ class _ResponsiveGrid extends StatelessWidget {
389
+ final List<Widget> children;
390
+ final double minItemWidth;
391
+ final int maxCols;
392
+ const _ResponsiveGrid({
393
+ required this.children,
394
+ this.minItemWidth = 240,
395
+ this.maxCols = 4,
396
+ });
397
+
398
+ @override
399
+ Widget build(BuildContext context) {
400
+ if (children.isEmpty) return const SizedBox.shrink();
401
+ const double gap = KasySpacing.md;
402
+ return LayoutBuilder(
403
+ builder: (context, constraints) {
404
+ final double maxW = constraints.maxWidth;
405
+ int cols = ((maxW + gap) / (minItemWidth + gap)).floor();
406
+ cols = cols.clamp(1, maxCols);
407
+ if (cols > children.length) cols = children.length;
408
+ final double itemW =
409
+ cols <= 1 ? maxW : (maxW - gap * (cols - 1)) / cols;
410
+ return Wrap(
411
+ spacing: gap,
412
+ runSpacing: gap,
413
+ children: [
414
+ for (final child in children) SizedBox(width: itemW, child: child),
415
+ ],
416
+ );
417
+ },
418
+ );
419
+ }
420
+ }
421
+
422
+ /// Pops the admin page back to Settings — used by actions that historically
423
+ /// closed the bottom sheet so their result is visible over the app.
424
+ void _backToApp(BuildContext context) {
425
+ if (context.canPop()) context.pop();
426
+ }
427
+
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+ // Overview — real project + session data (no vanity metrics)
430
+ // ─────────────────────────────────────────────────────────────────────────────
431
+
432
+ class _OverviewTab extends ConsumerWidget {
433
+ const _OverviewTab();
434
+
435
+ @override
436
+ Widget build(BuildContext context, WidgetRef ref) {
437
+ final userState = ref.watch(userStateNotifierProvider);
438
+ final user = userState.user;
439
+ final bool isAuth = user is AuthenticatedUserData;
440
+ final bool isAdmin = user.isAdmin;
441
+ final ov = t.admin_console.overview;
442
+ final String account = isAuth ? user.email : ov.guest;
443
+ final String uid = userState.user.idOrNull ?? '—';
444
+
445
+ return _TabScroll(
446
+ children: [
447
+ _GroupLabel(ov.section),
448
+ _ResponsiveGrid(
449
+ minItemWidth: 200,
450
+ children: [
451
+ _StatCard(
452
+ icon: Icons.cloud_done_rounded,
453
+ tone: context.colors.primary,
454
+ value: 'Firebase',
455
+ label: ov.backend,
456
+ ),
457
+ // Feature-request count reads the server — admins only.
458
+ if (isAdmin)
459
+ _StatCard(
460
+ icon: Icons.how_to_vote_rounded,
461
+ tone: context.colors.primary,
462
+ value: ref.watch(_adminRequestsProvider).maybeWhen(
463
+ data: (l) => '${l.length}',
464
+ orElse: () => '…',
465
+ ),
466
+ label: ov.requests_metric,
467
+ ),
468
+ ],
469
+ ),
470
+ const SizedBox(height: KasySpacing.lg),
471
+ _GroupLabel(ov.session_title),
472
+ _CardShell(
473
+ padding: const EdgeInsets.symmetric(
474
+ horizontal: KasySpacing.md,
475
+ vertical: KasySpacing.xs,
476
+ ),
477
+ child: Column(
478
+ children: [
479
+ _InfoRow(
480
+ label: ov.account,
481
+ value: account,
482
+ valueColor: isAuth ? null : context.colors.muted,
483
+ ),
484
+ const SettingsDivider(),
485
+ _InfoRow(
486
+ label: ov.user_id,
487
+ value: uid,
488
+ trailing: _CopyButton(
489
+ onTap: () {
490
+ Clipboard.setData(ClipboardData(text: uid));
491
+ ref.read(toastProvider).alert(
492
+ title: '',
493
+ text: t.settings.admin.user_id_copied,
494
+ );
495
+ },
496
+ ),
497
+ ),
498
+ const SettingsDivider(),
499
+ FutureBuilder<PackageInfo>(
500
+ future: PackageInfo.fromPlatform(),
501
+ builder: (context, snap) => _InfoRow(
502
+ label: ov.build,
503
+ value: snap.hasData
504
+ ? 'v${snap.data!.version} (${snap.data!.buildNumber})'
505
+ : '…',
506
+ ),
507
+ ),
508
+ ],
509
+ ),
510
+ ),
511
+ const SizedBox(height: KasySpacing.md),
512
+ Text(
513
+ ov.users_hint,
514
+ style: context.textTheme.bodySmall?.copyWith(
515
+ color: context.colors.muted,
516
+ ),
517
+ ),
518
+ const SizedBox(height: KasySpacing.xs),
519
+ Text(
520
+ ov.debug_note,
521
+ style: context.textTheme.bodySmall?.copyWith(
522
+ color: context.colors.muted,
523
+ ),
524
+ ),
525
+ ],
526
+ );
527
+ }
528
+ }
529
+
530
+ class _InfoRow extends StatelessWidget {
531
+ final String label;
532
+ final String value;
533
+ final Color? valueColor;
534
+ final Widget? trailing;
535
+ const _InfoRow({
536
+ required this.label,
537
+ required this.value,
538
+ this.valueColor,
539
+ this.trailing,
540
+ });
541
+
542
+ @override
543
+ Widget build(BuildContext context) {
544
+ return Padding(
545
+ padding: const EdgeInsets.symmetric(vertical: KasySpacing.smd),
546
+ child: Row(
547
+ children: [
548
+ SizedBox(
549
+ width: 110,
550
+ child: Text(
551
+ label,
552
+ style: context.textTheme.bodyMedium?.copyWith(
553
+ color: context.colors.muted,
554
+ ),
555
+ ),
556
+ ),
557
+ const SizedBox(width: KasySpacing.sm),
558
+ Expanded(
559
+ child: Text(
560
+ value,
561
+ style: context.textTheme.bodyMedium?.copyWith(
562
+ color: valueColor ?? context.colors.onSurface,
563
+ fontWeight: FontWeight.w600,
564
+ ),
565
+ ),
566
+ ),
567
+ if (trailing != null) trailing!,
568
+ ],
569
+ ),
570
+ );
571
+ }
572
+ }
573
+
574
+ class _CopyButton extends StatelessWidget {
575
+ final VoidCallback onTap;
576
+ const _CopyButton({required this.onTap});
577
+
578
+ @override
579
+ Widget build(BuildContext context) {
580
+ return InkWell(
581
+ onTap: onTap,
582
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
583
+ child: Padding(
584
+ padding: const EdgeInsets.all(6),
585
+ child: Icon(
586
+ Icons.content_copy_rounded,
587
+ size: 16,
588
+ color: context.colors.primary,
589
+ ),
590
+ ),
591
+ );
592
+ }
593
+ }
594
+
595
+ // ─────────────────────────────────────────────────────────────────────────────
596
+ // Empty-state tabs (real content lands next: Users via a server function,
597
+ // Requests reads the live feature_requests collection)
598
+ // ─────────────────────────────────────────────────────────────────────────────
599
+
600
+ class _EmptyState extends StatelessWidget {
601
+ final IconData icon;
602
+ final String title;
603
+ final String message;
604
+ const _EmptyState({
605
+ required this.icon,
606
+ required this.title,
607
+ required this.message,
608
+ });
609
+
610
+ @override
611
+ Widget build(BuildContext context) {
612
+ return _TabScroll(
613
+ children: [
614
+ _CardShell(
615
+ padding: const EdgeInsets.symmetric(
616
+ horizontal: KasySpacing.lg,
617
+ vertical: KasySpacing.xxl,
618
+ ),
619
+ child: Column(
620
+ mainAxisSize: MainAxisSize.min,
621
+ children: [
622
+ _IconBubble(icon: icon, tone: context.colors.primary, size: 64),
623
+ const SizedBox(height: KasySpacing.md),
624
+ Text(
625
+ title,
626
+ textAlign: TextAlign.center,
627
+ style: context.textTheme.titleMedium?.copyWith(
628
+ fontWeight: FontWeight.w700,
629
+ color: context.colors.onSurface,
630
+ ),
631
+ ),
632
+ const SizedBox(height: KasySpacing.xs),
633
+ Text(
634
+ message,
635
+ textAlign: TextAlign.center,
636
+ style: context.textTheme.bodyMedium?.copyWith(
637
+ color: context.colors.muted,
638
+ height: 1.45,
639
+ ),
640
+ ),
641
+ ],
642
+ ),
643
+ ),
644
+ ],
645
+ );
646
+ }
647
+ }
648
+
649
+ class _UsersTab extends ConsumerWidget {
650
+ const _UsersTab();
651
+
652
+ @override
653
+ Widget build(BuildContext context, WidgetRef ref) {
654
+ final bool isAdmin = ref.watch(userStateNotifierProvider).user.isAdmin;
655
+ final u = t.admin_console.users;
656
+ if (!isAdmin) {
657
+ return _EmptyState(
658
+ icon: Icons.lock_outline_rounded,
659
+ title: u.title,
660
+ message: t.admin_console.requires_admin,
661
+ );
662
+ }
663
+ return const AdminUsersTab();
664
+ }
665
+ }
666
+
667
+ class _RequestsTab extends ConsumerWidget {
668
+ const _RequestsTab();
669
+
670
+ @override
671
+ Widget build(BuildContext context, WidgetRef ref) {
672
+ final bool isAdmin = ref.watch(userStateNotifierProvider).user.isAdmin;
673
+ final r = t.admin_console.requests;
674
+ if (!isAdmin) {
675
+ return _EmptyState(
676
+ icon: Icons.lock_outline_rounded,
677
+ title: r.title,
678
+ message: t.admin_console.requires_admin,
679
+ );
680
+ }
681
+ final AsyncValue<List<FeatureRequestEntity>> async =
682
+ ref.watch(_adminRequestsProvider);
683
+ return async.when(
684
+ loading: () => const Center(child: CircularProgressIndicator.adaptive()),
685
+ error: (_, _) => _EmptyState(
686
+ icon: KasyIcons.message,
687
+ title: r.title,
688
+ message: r.error,
689
+ ),
690
+ data: (list) {
691
+ if (list.isEmpty) {
692
+ return _EmptyState(
693
+ icon: KasyIcons.message,
694
+ title: r.title,
695
+ message: r.empty,
696
+ );
697
+ }
698
+ return _TabScroll(
699
+ children: [
700
+ for (final req in list) ...[
701
+ _RequestCard(req),
702
+ const SizedBox(height: KasySpacing.md),
703
+ ],
704
+ ],
705
+ );
706
+ },
707
+ );
708
+ }
709
+ }
710
+
711
+ /// Picks the value for [lang], falling back to English then anything present.
712
+ String _pickLocale(Map<String, String> m, String lang) =>
713
+ m[lang] ?? m['en'] ?? (m.isNotEmpty ? m.values.first : '');
714
+
715
+ class _RequestCard extends ConsumerWidget {
716
+ final FeatureRequestEntity req;
717
+ const _RequestCard(this.req);
718
+
719
+ @override
720
+ Widget build(BuildContext context, WidgetRef ref) {
721
+ final r = t.admin_console.requests;
722
+ final String lang = Localizations.localeOf(context).languageCode;
723
+ final String title = _pickLocale(req.title, lang);
724
+ final String desc = _pickLocale(req.description, lang);
725
+ final bool active = req.active;
726
+
727
+ return _CardShell(
728
+ child: Column(
729
+ crossAxisAlignment: CrossAxisAlignment.start,
730
+ children: [
731
+ Row(
732
+ crossAxisAlignment: CrossAxisAlignment.start,
733
+ children: [
734
+ _VotesChip(req.votes),
735
+ const SizedBox(width: KasySpacing.md),
736
+ Expanded(
737
+ child: Column(
738
+ crossAxisAlignment: CrossAxisAlignment.start,
739
+ children: [
740
+ Text(
741
+ title,
742
+ maxLines: 2,
743
+ overflow: TextOverflow.ellipsis,
744
+ style: context.textTheme.titleSmall?.copyWith(
745
+ color: context.colors.onSurface,
746
+ fontWeight: FontWeight.w700,
747
+ ),
748
+ ),
749
+ const SizedBox(height: 2),
750
+ Text(
751
+ desc,
752
+ maxLines: 3,
753
+ overflow: TextOverflow.ellipsis,
754
+ style: context.textTheme.bodySmall?.copyWith(
755
+ color: context.colors.muted,
756
+ height: 1.35,
757
+ ),
758
+ ),
759
+ ],
760
+ ),
761
+ ),
762
+ ],
763
+ ),
764
+ const SizedBox(height: KasySpacing.smd),
765
+ Divider(
766
+ height: 1,
767
+ color: context.colors.outline.withValues(alpha: 0.35),
768
+ ),
769
+ const SizedBox(height: KasySpacing.sm),
770
+ Row(
771
+ children: [
772
+ KasyStatusTag(
773
+ label: active ? r.visible : r.hidden,
774
+ tone: active
775
+ ? KasyStatusTagTone.success
776
+ : KasyStatusTagTone.neutral,
777
+ ),
778
+ const SizedBox(width: KasySpacing.sm),
779
+ Transform.scale(
780
+ scale: 0.8,
781
+ child: Switch.adaptive(
782
+ value: active,
783
+ materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
784
+ onChanged: (v) async {
785
+ await ref
786
+ .read(featureRequestApiProvider)
787
+ .setActive(req.id!, v);
788
+ ref.invalidate(_adminRequestsProvider);
789
+ if (context.mounted) {
790
+ ref.read(toastProvider).alert(title: '', text: r.saved);
791
+ }
792
+ },
793
+ ),
794
+ ),
795
+ const Spacer(),
796
+ KasyButton(
797
+ label: r.edit,
798
+ variant: KasyButtonVariant.soft,
799
+ onPressed: () => _openRequestEditor(context, req),
800
+ ),
801
+ ],
802
+ ),
803
+ ],
804
+ ),
805
+ );
806
+ }
807
+ }
808
+
809
+ class _VotesChip extends StatelessWidget {
810
+ final int votes;
811
+ const _VotesChip(this.votes);
812
+
813
+ @override
814
+ Widget build(BuildContext context) {
815
+ return Container(
816
+ width: 48,
817
+ padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
818
+ decoration: BoxDecoration(
819
+ color: context.colors.primary.withValues(
820
+ alpha: context.isDark ? 0.16 : 0.08,
821
+ ),
822
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
823
+ border: Border.all(
824
+ color: context.colors.primary.withValues(alpha: 0.25),
825
+ ),
826
+ ),
827
+ child: Column(
828
+ mainAxisSize: MainAxisSize.min,
829
+ children: [
830
+ Icon(KasyIcons.voteUp, size: 18, color: context.colors.primary),
831
+ const SizedBox(height: 2),
832
+ Text(
833
+ '$votes',
834
+ style: context.textTheme.labelLarge?.copyWith(
835
+ color: context.colors.primary,
836
+ fontWeight: FontWeight.w800,
837
+ height: 1,
838
+ ),
839
+ ),
840
+ ],
841
+ ),
842
+ );
843
+ }
844
+ }
845
+
846
+ Future<void> _openRequestEditor(BuildContext context, FeatureRequestEntity req) {
847
+ return showKasyBlurBottomSheet<void>(
848
+ context: context,
849
+ builder: (_) => _RequestEditorSheet(req: req),
850
+ );
851
+ }
852
+
853
+ class _RequestEditorSheet extends ConsumerStatefulWidget {
854
+ final FeatureRequestEntity req;
855
+ const _RequestEditorSheet({required this.req});
856
+
857
+ @override
858
+ ConsumerState<_RequestEditorSheet> createState() =>
859
+ _RequestEditorSheetState();
860
+ }
861
+
862
+ class _RequestEditorSheetState extends ConsumerState<_RequestEditorSheet> {
863
+ static const List<String> _langs = ['en', 'pt', 'es'];
864
+ late final Map<String, TextEditingController> _title;
865
+ late final Map<String, TextEditingController> _desc;
866
+ bool _saving = false;
867
+
868
+ @override
869
+ void initState() {
870
+ super.initState();
871
+ _title = {
872
+ for (final l in _langs)
873
+ l: TextEditingController(text: widget.req.title[l] ?? ''),
874
+ };
875
+ _desc = {
876
+ for (final l in _langs)
877
+ l: TextEditingController(text: widget.req.description[l] ?? ''),
878
+ };
879
+ }
880
+
881
+ @override
882
+ void dispose() {
883
+ for (final c in _title.values) {
884
+ c.dispose();
885
+ }
886
+ for (final c in _desc.values) {
887
+ c.dispose();
888
+ }
889
+ super.dispose();
890
+ }
891
+
892
+ String _langLabel(String l) {
893
+ final r = t.admin_console.requests;
894
+ return switch (l) {
895
+ 'en' => r.lang_en,
896
+ 'pt' => r.lang_pt,
897
+ _ => r.lang_es,
898
+ };
899
+ }
900
+
901
+ Future<void> _save() async {
902
+ final r = t.admin_console.requests;
903
+ setState(() => _saving = true);
904
+ try {
905
+ await ref.read(featureRequestApiProvider).updateTexts(
906
+ id: widget.req.id!,
907
+ title: {for (final l in _langs) l: _title[l]!.text.trim()},
908
+ description: {for (final l in _langs) l: _desc[l]!.text.trim()},
909
+ );
910
+ ref.invalidate(_adminRequestsProvider);
911
+ if (!mounted) return;
912
+ context.pop();
913
+ ref.read(toastProvider).alert(title: '', text: r.saved);
914
+ } catch (_) {
915
+ if (!mounted) return;
916
+ setState(() => _saving = false);
917
+ ref.read(toastProvider).alert(title: '', text: r.error);
918
+ }
919
+ }
920
+
921
+ @override
922
+ Widget build(BuildContext context) {
923
+ final r = t.admin_console.requests;
924
+ return KasyBottomSheet(
925
+ title: r.editor_title,
926
+ addKeyboardInset: true,
927
+ body: SizedBox(
928
+ height: MediaQuery.sizeOf(context).height * 0.5,
929
+ child: SingleChildScrollView(
930
+ child: Column(
931
+ crossAxisAlignment: CrossAxisAlignment.stretch,
932
+ children: [
933
+ for (final l in _langs) ...[
934
+ _GroupLabel(_langLabel(l)),
935
+ KasyTextField(
936
+ controller: _title[l],
937
+ label: r.field_title,
938
+ maxLength: 60,
939
+ textInputAction: TextInputAction.next,
940
+ ),
941
+ const SizedBox(height: KasySpacing.sm),
942
+ KasyTextField(
943
+ controller: _desc[l],
944
+ label: r.field_description,
945
+ minLines: 2,
946
+ maxLines: 4,
947
+ maxLength: 1000,
948
+ ),
949
+ const SizedBox(height: KasySpacing.md),
950
+ ],
951
+ ],
952
+ ),
953
+ ),
954
+ ),
955
+ actions: [
956
+ KasyButton(
957
+ label: r.save,
958
+ expand: true,
959
+ isLoading: _saving,
960
+ onPressed: _saving ? null : _save,
961
+ ),
962
+ ],
963
+ );
964
+ }
965
+ }
966
+
967
+ // ─────────────────────────────────────────────────────────────────────────────
968
+ // Kit — feature demos + the component gallery
969
+ // ─────────────────────────────────────────────────────────────────────────────
970
+
971
+ class _KitTab extends ConsumerWidget {
972
+ const _KitTab();
973
+
974
+ @override
975
+ Widget build(BuildContext context, WidgetRef ref) {
976
+ final page = t.home.features_page;
977
+ final dash = t.home.dashboard;
978
+ final groups = t.admin_console.groups;
979
+
980
+ final List<Widget> features = <Widget>[
981
+ if (withFeedback)
982
+ _ActionCard(
983
+ icon: KasyIcons.message,
984
+ title: page.feedback_title,
985
+ subtitle: page.feedback_description,
986
+ onTap: () => context.push('/feedback'),
987
+ ),
988
+ _ActionCard(
989
+ icon: KasyIcons.notification,
990
+ title: page.notification_title,
991
+ subtitle: page.notification_description,
992
+ onTap: () {
993
+ final settings = ref.read(notificationsSettingsProvider);
994
+ final localNotifier = ref.read(localNotifierProvider);
995
+ kasy_kit.Notification.withData(
996
+ id: 'fake-id',
997
+ title: page.notification_demo_title,
998
+ body: page.notification_demo_body,
999
+ createdAt: DateTime.now(),
1000
+ notifier: localNotifier,
1001
+ notifierSettings: settings,
1002
+ ).show();
1003
+ },
1004
+ ),
1005
+ _ActionCard(
1006
+ icon: KasyIcons.notificationActive,
1007
+ title: page.send_push_title,
1008
+ subtitle: page.send_push_description,
1009
+ onTap: () => context.push(adminRouteSendPush),
1010
+ ),
1011
+ if (withRevenuecat)
1012
+ _ActionCard(
1013
+ icon: KasyIcons.payment,
1014
+ title: page.paywall_title,
1015
+ subtitle: page.paywall_description,
1016
+ onTap: () => context.push('/premium'),
1017
+ ),
1018
+ ];
1019
+
1020
+ return _TabScroll(
1021
+ children: [
1022
+ _GroupLabel(groups.features),
1023
+ _ResponsiveGrid(minItemWidth: 280, maxCols: 2, children: features),
1024
+ const SizedBox(height: KasySpacing.lg),
1025
+ _GroupLabel(groups.showcase),
1026
+ _ResponsiveGrid(
1027
+ minItemWidth: 280,
1028
+ maxCols: 2,
1029
+ children: [
1030
+ _ActionCard(
1031
+ icon: KasyIcons.widgets,
1032
+ title: dash.components_title,
1033
+ subtitle: dash.components_subtitle,
1034
+ tone: context.colors.success,
1035
+ onTap: () => context.push('/components'),
1036
+ ),
1037
+ ],
1038
+ ),
1039
+ ],
1040
+ );
1041
+ }
1042
+ }
1043
+
1044
+ // ─────────────────────────────────────────────────────────────────────────────
1045
+ // Tools — every developer toggle/action migrated from the old admin sheet
1046
+ // ─────────────────────────────────────────────────────────────────────────────
1047
+
1048
+ class _ToolsTab extends ConsumerWidget {
1049
+ const _ToolsTab();
1050
+
1051
+ @override
1052
+ Widget build(BuildContext context, WidgetRef ref) {
1053
+ final userState = ref.watch(userStateNotifierProvider);
1054
+ final admin = t.settings.admin;
1055
+ final groups = t.admin_console.groups;
1056
+
1057
+ final List<Widget> previews = <Widget>[
1058
+ ValueListenableBuilder<bool>(
1059
+ valueListenable: devInspectorEnabledNotifier,
1060
+ builder: (context, enabled, _) {
1061
+ return SettingsSwitchTile(
1062
+ icon: KasyIcons.widgets,
1063
+ title: admin.inspector_fab_title,
1064
+ subtitle:
1065
+ '${admin.inspector_fab_subtitle_prefix} ${devInspectorShortcutLabel()}',
1066
+ value: enabled,
1067
+ onChanged: (v) async {
1068
+ final p = await SharedPreferences.getInstance();
1069
+ await p.setBool(devInspectorEnabledPrefKey, v);
1070
+ devInspectorEnabledNotifier.value = v;
1071
+ if (v && context.mounted) _backToApp(context);
1072
+ },
1073
+ );
1074
+ },
1075
+ ),
1076
+ if (kIsWeb)
1077
+ ValueListenableBuilder<bool>(
1078
+ valueListenable: webDevicePreviewEnabledNotifier,
1079
+ builder: (context, enabled, _) {
1080
+ return SettingsSwitchTile(
1081
+ icon: KasyIcons.phoneAndroid,
1082
+ title: admin.device_preview_title,
1083
+ subtitle:
1084
+ '${admin.inspector_fab_subtitle_prefix} ${webDevicePreviewShortcutLabel()}',
1085
+ value: enabled,
1086
+ onChanged: (v) async {
1087
+ final p = await SharedPreferences.getInstance();
1088
+ await p.setBool(webDevicePreviewEnabledPrefKey, v);
1089
+ webDevicePreviewEnabledNotifier.value = v;
1090
+ if (v && context.mounted) _backToApp(context);
1091
+ },
1092
+ );
1093
+ },
1094
+ ),
1095
+ ];
1096
+
1097
+ final List<Widget> tools = <Widget>[
1098
+ _ActionCard(
1099
+ icon: KasyIcons.note,
1100
+ title: admin.update_bottom_sheet,
1101
+ onTap: () {
1102
+ _backToApp(context);
1103
+ showUpdateBottomSheet(
1104
+ context: navigatorKey.currentContext!,
1105
+ version: '0.0.0',
1106
+ );
1107
+ },
1108
+ ),
1109
+ _ActionCard(
1110
+ icon: KasyIcons.payment,
1111
+ title: admin.paywalls,
1112
+ onTap: () => context.push(adminRoutePaywalls),
1113
+ ),
1114
+ _ActionCard(
1115
+ icon: KasyIcons.check,
1116
+ title: admin.test_onboarding,
1117
+ onTap: () => ref.read(goRouterProvider).go('/onboarding'),
1118
+ ),
1119
+ _ActionCard(
1120
+ icon: KasyIcons.notificationActive,
1121
+ title: admin.send_push_title,
1122
+ onTap: () => context.push(adminRouteSendPush),
1123
+ ),
1124
+ _ActionCard(
1125
+ icon: KasyIcons.star,
1126
+ title: admin.ask_review,
1127
+ onTap: () => showReviewDialog(context, ref, force: true),
1128
+ ),
1129
+ _ActionCard(
1130
+ icon: KasyIcons.message,
1131
+ title: admin.home_widgets_panel,
1132
+ onTap: () => context.push(adminRouteHomeWidgets),
1133
+ ),
1134
+ ];
1135
+
1136
+ final List<Widget> identity = <Widget>[
1137
+ _ActionCard(
1138
+ icon: KasyIcons.person,
1139
+ title: admin.copy_user_id,
1140
+ onTap: () {
1141
+ Clipboard.setData(
1142
+ ClipboardData(text: userState.user.idOrNull ?? 'no-id (guest)'),
1143
+ );
1144
+ ref.read(toastProvider).alert(title: '', text: admin.user_id_copied);
1145
+ },
1146
+ ),
1147
+ _ActionCard(
1148
+ icon: KasyIcons.notification,
1149
+ title: admin.copy_fcm_token,
1150
+ onTap: () async {
1151
+ final token = await FirebaseMessaging.instance.getToken();
1152
+ if (token == null) {
1153
+ ref.read(toastProvider).alert(
1154
+ title: '',
1155
+ text: admin.fcm_token_unavailable,
1156
+ );
1157
+ return;
1158
+ }
1159
+ await Clipboard.setData(ClipboardData(text: token));
1160
+ ref.read(toastProvider).alert(title: '', text: admin.fcm_token_copied);
1161
+ },
1162
+ ),
1163
+ _ActionCard(
1164
+ icon: KasyIcons.notificationActive,
1165
+ title: admin.ask_notification,
1166
+ onTap: () => ref.read(notificationsSettingsProvider).askPermission(),
1167
+ ),
1168
+ ];
1169
+
1170
+ return _TabScroll(
1171
+ children: [
1172
+ _GroupLabel(groups.preview),
1173
+ _CardShell(
1174
+ child: Column(
1175
+ mainAxisSize: MainAxisSize.min,
1176
+ children: [
1177
+ for (int i = 0; i < previews.length; i++) ...[
1178
+ if (i > 0) const SettingsDivider(),
1179
+ previews[i],
1180
+ ],
1181
+ ],
1182
+ ),
1183
+ ),
1184
+ const SizedBox(height: KasySpacing.lg),
1185
+ _GroupLabel(groups.debug_actions),
1186
+ _ResponsiveGrid(minItemWidth: 280, maxCols: 2, children: tools),
1187
+ const SizedBox(height: KasySpacing.lg),
1188
+ _GroupLabel(groups.identity),
1189
+ _ResponsiveGrid(minItemWidth: 280, maxCols: 2, children: identity),
1190
+ ],
1191
+ );
1192
+ }
1193
+ }