kasy-cli 1.21.8 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/lib/commands/add.js +93 -80
  2. package/lib/commands/configure.js +100 -32
  3. package/lib/commands/doctor.js +28 -2
  4. package/lib/commands/new.js +86 -38
  5. package/lib/commands/notifications.js +1 -1
  6. package/lib/commands/remove.js +43 -15
  7. package/lib/commands/run.js +2 -2
  8. package/lib/commands/update.js +2 -2
  9. package/lib/scaffold/CHANGELOG.json +14 -0
  10. package/lib/scaffold/backends/api/generator.js +14 -14
  11. package/lib/scaffold/backends/api/patch/README.md +83 -0
  12. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
  14. package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
  15. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
  16. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
  18. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
  19. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
  20. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
  21. package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
  22. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  23. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  24. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
  25. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  26. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
  27. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  28. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
  29. package/lib/scaffold/backends/firebase/deploy.js +87 -13
  30. package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +14 -6
  31. package/lib/scaffold/backends/firebase/generator.js +5 -5
  32. package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
  33. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  34. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  35. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  37. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  40. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  41. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  42. package/lib/scaffold/backends/supabase/generator.js +17 -17
  43. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  44. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  45. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  46. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  48. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  49. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  50. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  51. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  52. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  53. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  55. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  56. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  57. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  59. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  60. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  61. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  62. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  63. package/lib/scaffold/catalog.js +9 -11
  64. package/lib/scaffold/generate.js +45 -31
  65. package/lib/scaffold/shared/generator-utils.js +188 -81
  66. package/lib/scaffold/shared/sort-imports.js +191 -0
  67. package/lib/scaffold/shared/template-strings.js +3 -3
  68. package/lib/utils/checks.js +2 -2
  69. package/lib/utils/i18n/messages-en.js +50 -35
  70. package/lib/utils/i18n/messages-es.js +50 -35
  71. package/lib/utils/i18n/messages-pt.js +52 -37
  72. package/lib/utils/updates.js +15 -15
  73. package/package.json +1 -1
  74. package/templates/firebase/.env.example +2 -2
  75. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  80. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  81. package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
  82. package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
  83. package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
  84. package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
  85. package/templates/firebase/firestore.rules +24 -5
  86. package/templates/firebase/functions/package-lock.json +22 -1
  87. package/templates/firebase/functions/package.json +2 -1
  88. package/templates/firebase/functions/src/admin/functions.ts +113 -0
  89. package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
  90. package/templates/firebase/functions/src/index.ts +8 -2
  91. package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
  92. package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
  93. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
  94. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
  95. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
  96. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  98. package/templates/firebase/lib/components/components.dart +4 -1
  99. package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
  100. package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
  101. package/templates/firebase/lib/components/kasy_button.dart +23 -99
  102. package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
  103. package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
  104. package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
  105. package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
  106. package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
  107. package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
  108. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
  109. package/templates/firebase/lib/components/kasy_toast.dart +2 -2
  110. package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
  111. package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
  112. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
  113. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
  114. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
  115. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
  116. package/templates/firebase/lib/core/config/app_env.dart +5 -11
  117. package/templates/firebase/lib/core/config/features.dart +5 -4
  118. package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
  119. package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
  120. package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
  121. package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
  122. package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
  123. package/templates/firebase/lib/core/data/models/user.dart +11 -0
  124. package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
  125. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
  126. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
  127. package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
  128. package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
  129. package/templates/firebase/lib/core/states/logout_action.dart +25 -0
  130. package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
  131. package/templates/firebase/lib/core/theme/colors.dart +488 -188
  132. package/templates/firebase/lib/core/theme/radius.dart +22 -11
  133. package/templates/firebase/lib/core/theme/shadows.dart +66 -0
  134. package/templates/firebase/lib/core/theme/texts.dart +75 -41
  135. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
  136. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
  137. package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
  138. package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
  139. package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
  140. package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
  141. package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
  142. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
  143. package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
  144. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
  145. package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
  146. package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
  147. package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  148. package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
  149. package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
  150. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
  151. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
  152. package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
  153. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
  154. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
  155. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
  156. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
  157. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
  158. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
  159. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
  160. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
  161. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
  162. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
  163. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
  164. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
  165. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
  166. package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
  167. package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
  168. package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
  169. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
  170. package/templates/firebase/lib/features/home/home_feed.dart +289 -0
  171. package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
  172. package/templates/firebase/lib/features/home/home_page.dart +11 -250
  173. package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
  174. package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
  175. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
  176. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
  177. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
  178. package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
  179. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  180. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
  181. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
  182. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
  183. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
  184. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
  185. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
  186. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
  187. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
  188. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
  189. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
  190. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  191. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
  192. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
  193. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
  194. package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
  195. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
  196. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
  197. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
  198. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
  199. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
  200. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
  201. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
  202. package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
  203. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  204. package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
  205. package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
  206. package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
  207. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
  208. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
  209. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
  210. package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  211. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
  212. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
  213. package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
  214. package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
  215. package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
  216. package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
  217. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
  218. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
  219. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
  220. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
  221. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
  222. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
  223. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
  224. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
  225. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
  226. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
  227. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
  228. package/templates/firebase/lib/i18n/en.i18n.json +171 -46
  229. package/templates/firebase/lib/i18n/es.i18n.json +175 -50
  230. package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
  231. package/templates/firebase/lib/main.dart +6 -3
  232. package/templates/firebase/lib/router.dart +15 -23
  233. package/templates/firebase/pubspec.yaml +4 -5
  234. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
  235. package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
  236. package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
  237. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
  238. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
  239. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
  240. package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
  241. package/templates/firebase/test/test_utils.dart +6 -6
  242. package/templates/firebase/web/index.html +5 -2
  243. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
  244. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
  245. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
  246. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
  247. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  248. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  249. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  250. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  251. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  252. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
  253. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
  254. package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
  255. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
  256. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
  257. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
  258. /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
  259. /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
  260. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
  261. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
  262. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
  263. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
  264. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
  265. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
  266. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
  267. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
  268. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
  269. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
@@ -1,21 +1,21 @@
1
1
  /**
2
- * Firebase Cloud Function: LLM Chat Proxy (streaming)
2
+ * Firebase Cloud Function: AI Chat Proxy (streaming)
3
3
  *
4
4
  * Receives {message, history} from the Flutter app and streams the response
5
5
  * back as Server-Sent Events (SSE). The API key never leaves the server.
6
6
  *
7
7
  * Secrets required (set via `firebase functions:secrets:set`):
8
- * - LLM_API_KEY: API key for OpenAI or Gemini
8
+ * - AI_API_KEY: API key for OpenAI or Gemini
9
9
  *
10
10
  * Environment variables (set in functions/.env):
11
- * - LLM_PROVIDER: "openai" (default) or "gemini"
12
- * - LLM_SYSTEM_PROMPT: System prompt for the agent (optional)
11
+ * - AI_PROVIDER: "openai" (default) or "gemini"
12
+ * - AI_SYSTEM_PROMPT: System prompt for the agent (optional)
13
13
  *
14
14
  * App dart-define:
15
- * - LLM_CHAT_ENDPOINT: URL of this function after deploy
16
- * Example: https://europe-west1-<project-id>.cloudfunctions.net/llmChat
15
+ * - AI_CHAT_ENDPOINT: URL of this function after deploy
16
+ * Example: https://europe-west1-<project-id>.cloudfunctions.net/aiChat
17
17
  *
18
- * Deploy: kasy deploy (or: firebase deploy --only functions:llmChat)
18
+ * Deploy: kasy deploy (or: firebase deploy --only functions:aiChat)
19
19
  */
20
20
 
21
21
  import { onRequest } from "firebase-functions/v2/https";
@@ -23,7 +23,7 @@ import { defineSecret } from "firebase-functions/params";
23
23
  import { getAuth } from "firebase-admin/auth";
24
24
  import type { Request, Response } from "express";
25
25
 
26
- const llmApiKey = defineSecret("LLM_API_KEY");
26
+ const aiApiKey = defineSecret("AI_API_KEY");
27
27
 
28
28
  interface ChatMessage {
29
29
  role: "user" | "assistant";
@@ -130,8 +130,8 @@ async function verifyFirebaseToken(req: Request, res: Response): Promise<string
130
130
  }
131
131
  }
132
132
 
133
- export const llmChat = onRequest(
134
- { cors: true, secrets: [llmApiKey] },
133
+ export const aiChat = onRequest(
134
+ { cors: true, secrets: [aiApiKey] },
135
135
  async (req, res) => {
136
136
  if (req.method !== "POST") {
137
137
  res.status(405).json({ error: "Method not allowed" });
@@ -147,16 +147,16 @@ export const llmChat = onRequest(
147
147
  return;
148
148
  }
149
149
 
150
- const apiKey = llmApiKey.value();
150
+ const apiKey = aiApiKey.value();
151
151
  if (!apiKey) {
152
152
  res
153
153
  .status(500)
154
- .json({ error: "LLM_API_KEY not configured. Run: firebase functions:secrets:set LLM_API_KEY" });
154
+ .json({ error: "AI_API_KEY not configured. Run: firebase functions:secrets:set AI_API_KEY" });
155
155
  return;
156
156
  }
157
157
 
158
- const provider = process.env.LLM_PROVIDER ?? "openai";
159
- const systemPrompt = process.env.LLM_SYSTEM_PROMPT ?? "";
158
+ const provider = process.env.AI_PROVIDER ?? "openai";
159
+ const systemPrompt = process.env.AI_SYSTEM_PROMPT ?? "";
160
160
 
161
161
  // SSE headers — must be set before any write
162
162
  res.setHeader("Content-Type", "text/event-stream");
@@ -172,9 +172,9 @@ export const llmChat = onRequest(
172
172
  }
173
173
  res.end();
174
174
  } catch (err) {
175
- console.error("[llm-chat]", err);
175
+ console.error("[ai-chat]", err);
176
176
  // Send error as SSE event so the Flutter client can surface it
177
- res.write(`data: ${JSON.stringify({ error: "LLM request failed" })}\n\n`);
177
+ res.write(`data: ${JSON.stringify({ error: "AI request failed" })}\n\n`);
178
178
  res.end();
179
179
  }
180
180
  }
@@ -19,7 +19,13 @@ exports.deviceTriggers = require("./notifications/device_triggers");
19
19
  exports.subscriptions = require("./subscriptions/subscriptions_functions");
20
20
  exports.subscriptionTriggers = require("./subscriptions/triggers");
21
21
 
22
+ // stripe web subscriptions (activated when the Stripe module is enabled)
23
+ exports.stripeFunctions = require("./subscriptions/stripe_functions");
24
+
25
+ // admin console (listUsers — gated on users/{uid}.role == "admin")
26
+ exports.adminFunctions = require("./admin/functions");
27
+
22
28
  // feature requests: vote counter updated atomically by client (WriteBatch)
23
29
 
24
- // llm chat proxy (activated when withLlmChat = true)
25
- exports.llmChat = require("./llm_chat").llmChat;
30
+ // llm chat proxy (activated when withAiChat = true)
31
+ exports.aiChat = require("./ai_chat").aiChat;
@@ -14,7 +14,7 @@ import {Logger} from "../core/logger/logger";
14
14
  * any time. Without it, sending a push to user A could deliver to a phone now
15
15
  * signed in as user B.
16
16
  */
17
- export const onDeviceWritten = onDocumentWritten(
17
+ export const dedupeDeviceTokens = onDocumentWritten(
18
18
  "users/{userId}/devices/{deviceId}",
19
19
  async (event) => {
20
20
  const after = event.data?.after?.data();
@@ -25,7 +25,7 @@ export const onDeviceWritten = onDocumentWritten(
25
25
 
26
26
  const currentUserId = event.params.userId;
27
27
  const currentDeviceId = event.params.deviceId;
28
- const logger = new Logger("onDeviceWritten");
28
+ const logger = new Logger("dedupeDeviceTokens");
29
29
 
30
30
  try {
31
31
  const duplicates = await admin
@@ -7,13 +7,13 @@ import {onDocumentCreated} from "firebase-functions/v2/firestore";
7
7
 
8
8
  const kChannelId = "appfirebase";
9
9
 
10
- export const onNewNotificationRequest = onDocumentCreated(
10
+ export const onNotificationCreated = onDocumentCreated(
11
11
  "users/{userId}/notifications/{notificationId}",
12
12
  async (event) => {
13
13
  if (!event.data) {
14
14
  return;
15
15
  }
16
- const logger = new Logger("onNewNotificationRequest");
16
+ const logger = new Logger("onNotificationCreated");
17
17
  try {
18
18
  const notificationEntity = NotificationEntity.fromDocument(event.data);
19
19
  const userId = event.params.userId;
@@ -97,6 +97,6 @@ export const onNewNotificationRequest = onDocumentCreated(
97
97
  }
98
98
  }
99
99
  } catch (e) {
100
- logger.error(`Error onNewNotificationRequest users/${event.params.userId}/notifications/${event.id} : ${e}`);
100
+ logger.error(`Error onNotificationCreated users/${event.params.userId}/notifications/${event.id} : ${e}`);
101
101
  }
102
102
  });
@@ -13,6 +13,8 @@ export const Stores = {
13
13
  PLAY_STORE: "PLAY_STORE",
14
14
  APPLE_STORE: "APPLE_STORE",
15
15
  EARLY_BIRD: "EARLY_BIRD",
16
+ // Subscription purchased on the web via Stripe.
17
+ STRIPE: "STRIPE",
16
18
  } as const;
17
19
 
18
20
  export type Stores = ObjectValues<typeof Stores>;
@@ -0,0 +1,222 @@
1
+ import {error} from "firebase-functions/logger";
2
+ import {onCall, onRequest, HttpsError} from "firebase-functions/v2/https";
3
+ import {defineSecret, defineString} from "firebase-functions/params";
4
+ import * as admin from "firebase-admin";
5
+ import {Timestamp} from "firebase-admin/firestore";
6
+ import Stripe from "stripe";
7
+ import {Subscription} from "./models/subscriptions";
8
+ import {subscriptionsRepository} from "../core/data/repositories/repositories";
9
+ import {Stores, SubscriptionStatus} from "./models/subscription_status";
10
+
11
+ // Server-side only. Never exposed to the client.
12
+ const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY");
13
+ const stripeWebhookSecret = defineSecret("STRIPE_WEBHOOK_SECRET");
14
+ // Optional: restrict the listed prices to a single Stripe product.
15
+ const stripeProductId = defineString("STRIPE_PRODUCT_ID", {default: ""});
16
+
17
+ // Firestore collection mapping a Firebase uid -> its Stripe customer id.
18
+ const CUSTOMERS_COLLECTION = "stripe_customers";
19
+
20
+ function stripeClient(): Stripe {
21
+ return new Stripe(stripeSecretKey.value());
22
+ }
23
+
24
+ async function getOrCreateCustomer(stripe: Stripe, uid: string): Promise<string> {
25
+ const db = admin.firestore();
26
+ const ref = db.collection(CUSTOMERS_COLLECTION).doc(uid);
27
+ const snap = await ref.get();
28
+ const existing = snap.data()?.customerId as string | undefined;
29
+ if (existing) return existing;
30
+ const customer = await stripe.customers.create({metadata: {firebaseUID: uid}});
31
+ await ref.set({customerId: customer.id, created_at: Timestamp.now()});
32
+ return customer.id;
33
+ }
34
+
35
+ function trialDaysFor(price: Stripe.Price, product: Stripe.Product): number | null {
36
+ const meta = price.metadata?.trial_days ?? product.metadata?.trial_days;
37
+ return meta ? Number(meta) : null;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // listPrices — active recurring prices, mapped to the paywall offer contract.
42
+ // ---------------------------------------------------------------------------
43
+ export const listPrices = onCall({secrets: [stripeSecretKey]}, async () => {
44
+ const stripe = stripeClient();
45
+ const params: Stripe.PriceListParams = {
46
+ active: true,
47
+ type: "recurring",
48
+ expand: ["data.product"],
49
+ limit: 100,
50
+ };
51
+ const productFilter = stripeProductId.value();
52
+ if (productFilter) params.product = productFilter;
53
+
54
+ const prices = await stripe.prices.list(params);
55
+ return prices.data
56
+ .filter((p) => Boolean(p.recurring))
57
+ .map((p) => {
58
+ const product = p.product as Stripe.Product;
59
+ const features = (product.marketing_features ?? [])
60
+ .map((f) => f.name)
61
+ .filter((n): n is string => Boolean(n));
62
+ return {
63
+ priceId: p.id,
64
+ productId: typeof p.product === "string" ? p.product : product.id,
65
+ productName: product.name ?? "",
66
+ description: product.description ?? "",
67
+ unitAmount: p.unit_amount ?? 0,
68
+ currency: p.currency,
69
+ interval: p.recurring?.interval ?? "month",
70
+ intervalCount: p.recurring?.interval_count ?? 1,
71
+ trialDays: trialDaysFor(p, product),
72
+ features,
73
+ };
74
+ });
75
+ });
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // createCheckoutSession — hosted Stripe Checkout (mode=subscription).
79
+ // ---------------------------------------------------------------------------
80
+ export const createCheckoutSession = onCall(
81
+ {secrets: [stripeSecretKey]},
82
+ async (request) => {
83
+ const uid = request.auth?.uid;
84
+ if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
85
+ const priceId = request.data?.priceId as string | undefined;
86
+ if (!priceId) throw new HttpsError("invalid-argument", "priceId is required");
87
+ const successUrl = (request.data?.successUrl as string | undefined) ?? "";
88
+ const cancelUrl = (request.data?.cancelUrl as string | undefined) ?? successUrl;
89
+
90
+ const stripe = stripeClient();
91
+ const customerId = await getOrCreateCustomer(stripe, uid);
92
+
93
+ const price = await stripe.prices.retrieve(priceId, {expand: ["product"]});
94
+ const trialDays = trialDaysFor(price, price.product as Stripe.Product);
95
+
96
+ const session = await stripe.checkout.sessions.create({
97
+ mode: "subscription",
98
+ customer: customerId,
99
+ client_reference_id: uid,
100
+ line_items: [{price: priceId, quantity: 1}],
101
+ success_url: successUrl,
102
+ cancel_url: cancelUrl,
103
+ subscription_data: {
104
+ metadata: {firebaseUID: uid},
105
+ ...(trialDays ? {trial_period_days: trialDays} : {}),
106
+ },
107
+ });
108
+ return {url: session.url};
109
+ },
110
+ );
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // createPortalSession — Stripe Customer Portal (manage / cancel).
114
+ // ---------------------------------------------------------------------------
115
+ export const createPortalSession = onCall(
116
+ {secrets: [stripeSecretKey]},
117
+ async (request) => {
118
+ const uid = request.auth?.uid;
119
+ if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
120
+ const returnUrl = (request.data?.returnUrl as string | undefined) ?? "";
121
+
122
+ const stripe = stripeClient();
123
+ const snap = await admin
124
+ .firestore()
125
+ .collection(CUSTOMERS_COLLECTION)
126
+ .doc(uid)
127
+ .get();
128
+ const customerId = snap.data()?.customerId as string | undefined;
129
+ if (!customerId) {
130
+ throw new HttpsError("failed-precondition", "No Stripe customer for user");
131
+ }
132
+ const session = await stripe.billingPortal.sessions.create({
133
+ customer: customerId,
134
+ return_url: returnUrl,
135
+ });
136
+ return {url: session.url};
137
+ },
138
+ );
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // stripeWebhook — webhook. The ONLY writer of `subscriptions` for Stripe.
142
+ // ---------------------------------------------------------------------------
143
+ function statusFromStripe(sub: Stripe.Subscription): SubscriptionStatus {
144
+ switch (sub.status) {
145
+ case "active":
146
+ case "trialing":
147
+ return SubscriptionStatus.ACTIVE;
148
+ default:
149
+ // canceled, unpaid, past_due, incomplete, incomplete_expired, paused
150
+ return SubscriptionStatus.EXPIRED;
151
+ }
152
+ }
153
+
154
+ async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<void> {
155
+ const uid = sub.metadata?.firebaseUID;
156
+ if (!uid) {
157
+ console.log("[stripe-webhook] subscription without firebaseUID metadata, skipping");
158
+ return;
159
+ }
160
+ const now = Timestamp.now();
161
+ const existing = await subscriptionsRepository.getFromUserId(uid);
162
+ // In Stripe API v18 the billing period lives on each subscription item.
163
+ const item = sub.items.data[0];
164
+ const priceId = item?.price?.id ?? "";
165
+ const periodEnd = item?.current_period_end;
166
+ const expiration = periodEnd
167
+ ? Timestamp.fromMillis(periodEnd * 1000)
168
+ : undefined;
169
+
170
+ const subscription = new Subscription(
171
+ {
172
+ userId: uid,
173
+ status: statusFromStripe(sub),
174
+ creationDate: existing?.creationDate ?? now,
175
+ lastUpdate: now,
176
+ expirationDate: expiration,
177
+ store: Stores.STRIPE,
178
+ productId: priceId,
179
+ },
180
+ subscriptionsRepository,
181
+ );
182
+ await subscription.save();
183
+ }
184
+
185
+ export const stripeWebhook = onRequest(
186
+ {cors: false, secrets: [stripeSecretKey, stripeWebhookSecret]},
187
+ async (req, res) => {
188
+ const signature = req.header("stripe-signature");
189
+ if (!signature) {
190
+ res.status(400).send("Missing signature");
191
+ return;
192
+ }
193
+ const stripe = stripeClient();
194
+ let event: Stripe.Event;
195
+ try {
196
+ event = stripe.webhooks.constructEvent(
197
+ req.rawBody,
198
+ signature,
199
+ stripeWebhookSecret.value(),
200
+ );
201
+ } catch (e) {
202
+ console.log(`[stripe-webhook] signature verification failed: ${e}`);
203
+ res.status(400).send("Invalid signature");
204
+ return;
205
+ }
206
+ try {
207
+ switch (event.type) {
208
+ case "customer.subscription.created":
209
+ case "customer.subscription.updated":
210
+ case "customer.subscription.deleted":
211
+ await upsertFromStripeSubscription(event.data.object as Stripe.Subscription);
212
+ break;
213
+ default:
214
+ break;
215
+ }
216
+ res.status(200).send("ok");
217
+ } catch (e) {
218
+ error(e);
219
+ res.status(500).send(e instanceof Error ? e.message : String(e));
220
+ }
221
+ },
222
+ );
@@ -84,11 +84,11 @@ async function sendMetaEventsForSubscription(
84
84
  }
85
85
  }
86
86
 
87
- export const onRcPremiumUpdate = onRequest({cors: true, secrets: [revenuecatToken, metaAccessToken, metaDatasetId]}, async (
87
+ export const revenuecatWebhook = onRequest({cors: true, secrets: [revenuecatToken, metaAccessToken, metaDatasetId]}, async (
88
88
  req: https.Request,
89
89
  res: express.Response,
90
90
  ) => {
91
- console.log("[onPremiumUpdate]");
91
+ console.log("[revenuecatWebhook]");
92
92
  const authorization = req.header("Authorization");
93
93
  if (!authorization) {
94
94
  console.log("Unauthorized - no token provided");
@@ -6,7 +6,6 @@
6
6
  /// import 'package:kasy_kit/components/components.dart';
7
7
  library;
8
8
 
9
- export '../core/sidebar/kasy_sidebar.dart';
10
9
  export 'kasy_accordion.dart';
11
10
  export 'kasy_alert.dart';
12
11
  export 'kasy_app_bar.dart';
@@ -20,11 +19,15 @@ export 'kasy_checkbox.dart';
20
19
  export 'kasy_chip.dart';
21
20
  export 'kasy_date_picker.dart';
22
21
  export 'kasy_dialog.dart';
22
+ export 'kasy_image_viewer.dart';
23
23
  export 'kasy_otp_verification_bottom_sheet.dart';
24
+ export 'kasy_sidebar.dart';
24
25
  export 'kasy_skeleton.dart';
26
+ export 'kasy_status_tag.dart';
25
27
  export 'kasy_swipe_action.dart';
26
28
  export 'kasy_tabs.dart';
27
29
  export 'kasy_text_area.dart';
28
30
  export 'kasy_text_field.dart';
29
31
  export 'kasy_text_field_otp.dart';
30
32
  export 'kasy_toast.dart';
33
+ export 'kasy_web_header.dart';
@@ -29,9 +29,17 @@ import 'package:flutter/foundation.dart' show kIsWeb;
29
29
  import 'package:flutter/material.dart';
30
30
  import 'package:flutter/services.dart' show SystemUiOverlayStyle;
31
31
  import 'package:kasy_kit/core/theme/theme.dart';
32
+ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
32
33
 
33
34
  /// Inner toolbar band height (orbit hit targets, title baseline).
34
35
  const double kasyAppBarToolbarRowHeight = 44;
36
+
37
+ /// Effective toolbar band height. [KasyAppBar] serves phone and tablet only — on
38
+ /// desktop the richer web header takes over — so the band keeps a single
39
+ /// compact height across every viewport where the app bar appears.
40
+ double kasyAppBarToolbarRowHeightOf(BuildContext context) =>
41
+ kasyAppBarToolbarRowHeight;
42
+
35
43
  const double kasyAppBarTitleFontScale = 0.92;
36
44
 
37
45
  /// Tight vertical padding inside the frost, above/below [kasyAppBarToolbarRowHeight].
@@ -48,9 +56,13 @@ enum KasyAppBarScrollTreatment { overlay, intrinsicScroll }
48
56
 
49
57
  /// Vertical inset for scroll/viewport when using [KasyAppBarScrollTreatment.overlay].
50
58
  double kasyAppBarBodyTopOverlap(BuildContext context) {
59
+ // On desktop the app bar hides (the web header owns the top chrome).
60
+ if (MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint) {
61
+ return 0;
62
+ }
51
63
  return MediaQuery.paddingOf(context).top +
52
64
  kasyAppBarChromePaddingTop +
53
- kasyAppBarToolbarRowHeight +
65
+ kasyAppBarToolbarRowHeightOf(context) +
54
66
  kasyAppBarChromePaddingBottom;
55
67
  }
56
68
 
@@ -112,13 +124,11 @@ class KasyFrostedChromeBackground extends StatelessWidget {
112
124
  });
113
125
 
114
126
  /// Translucent tint so blurred content shows through; tuned per brightness.
127
+ /// Derived from the global `surface` token so the bar lifts off the canvas
128
+ /// and follows light/dark automatically.
115
129
  Color _tint(BuildContext context) {
116
130
  final bool dark = Theme.of(context).brightness == Brightness.dark;
117
- if (dark) {
118
- return const Color(0xFF161618).withValues(alpha: 0.88);
119
- }
120
- // White (surface) instead of background grey so the bar lifts off the canvas.
121
- return const Color(0xFFFFFFFF).withValues(alpha: 0.82);
131
+ return context.colors.surface.withValues(alpha: dark ? 0.88 : 0.82);
122
132
  }
123
133
 
124
134
  @override
@@ -227,11 +237,16 @@ class KasyAppBar extends StatelessWidget {
227
237
 
228
238
  @override
229
239
  Widget build(BuildContext context) {
240
+ // On desktop the web header owns the top chrome, so the page app bar hides.
241
+ if (MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint) {
242
+ return const SizedBox.shrink();
243
+ }
230
244
  final Color orbFg = context.colors.onSurface;
231
245
  final Color orbFill = kasyChromeOrbFillColor(context);
232
246
  final VoidCallback handleBack = onBack ?? () => Navigator.maybePop(context);
233
247
 
234
- final double rowHeight = toolbarHeight ?? kasyAppBarToolbarRowHeight;
248
+ final double rowHeight =
249
+ toolbarHeight ?? kasyAppBarToolbarRowHeightOf(context);
235
250
  final Widget leading = switch (style) {
236
251
  KasyAppBarStyle.rootTab => SizedBox(
237
252
  width: 44,
@@ -153,9 +153,9 @@ class KasyAvatar extends StatelessWidget {
153
153
  width: _d,
154
154
  height: _d,
155
155
  fit: BoxFit.cover,
156
- // Evita o blur de decodificação progressiva de JPEG:
157
- // enquanto a imagem carrega, mostra o fallback; quando o primeiro
158
- // frame está pronto, faz crossfade suave.
156
+ // Avoids the blur from progressive JPEG decoding:
157
+ // while the image loads, show the fallback; when the first
158
+ // frame is ready, do a smooth crossfade.
159
159
  frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
160
160
  if (wasSynchronouslyLoaded) return child;
161
161
  return SizedBox(
@@ -301,7 +301,7 @@ class KasyAvatar extends StatelessWidget {
301
301
  width: badge,
302
302
  height: badge,
303
303
  decoration: BoxDecoration(
304
- color: const Color(0xFF81C784),
304
+ color: context.colors.success,
305
305
  shape: BoxShape.circle,
306
306
  border: Border.all(color: context.colors.surface, width: 2),
307
307
  ),
@@ -451,8 +451,9 @@ _KasyAvatarColors _colorsForTone(
451
451
  final KasyColors k = context.colors;
452
452
  final bool dark = context.isDark;
453
453
 
454
- // Accent color per tone (Figma: accent=#0485F7, success=#17C964,
455
- // warning=#F5A524, danger=#FF383C, neutral uses neutral fg).
454
+ // Accent color per tone, from the global theme tokens: the "blue" tone maps
455
+ // to the primary accent, success/warning/danger to their semantic tokens,
456
+ // neutral to the foreground.
456
457
  Color accentColor() => switch (tone) {
457
458
  KasyAvatarTone.blue => k.primary,
458
459
  KasyAvatarTone.neutral => k.grey3,
@@ -1,7 +1,7 @@
1
1
  import 'package:flutter/foundation.dart' show kIsWeb;
2
2
  import 'package:flutter/material.dart';
3
- import 'package:flutter/services.dart';
4
3
  import 'package:kasy_kit/core/theme/theme.dart';
4
+ import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
5
5
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
6
6
  import 'package:kasy_kit/core/widgets/kasy_pressable_depth.dart';
7
7
 
@@ -496,8 +496,8 @@ class _KasyButtonShell extends StatelessWidget {
496
496
 
497
497
  Widget _wrapFocus(Widget child, BorderRadius borderRadius) {
498
498
  if (!enabled || onPressed == null) return child;
499
- return _KeyboardFocus(
500
- onActivate: onPressed!,
499
+ return KasyFocusRing(
500
+ onActivate: onPressed,
501
501
  borderRadius: borderRadius,
502
502
  child: child,
503
503
  );
@@ -625,30 +625,28 @@ class _KasyButtonShell extends StatelessWidget {
625
625
 
626
626
  if (pressEffect == KasyButtonPressEffect.both) {
627
627
  final bool isDark = Theme.of(context).brightness == Brightness.dark;
628
- return _wrapFocus(
629
- KasyPressableDepth(
630
- semanticLabel: _accessibilityLabel,
631
- onPressed: onPressed!,
632
- clipBorderRadius: resolvedRadius,
633
- hapticFeedbackEnabled: hapticFeedbackEnabled,
634
- pressOverlayColor: palette.foreground.withValues(
635
- alpha: isDark ? 0.04 : 0.10,
636
- ),
637
- child: painted,
638
- ),
639
- resolvedRadius,
640
- );
641
- }
642
-
643
- return _wrapFocus(
644
- KasyPressableDepth(
628
+ return KasyPressableDepth(
645
629
  semanticLabel: _accessibilityLabel,
646
630
  onPressed: onPressed!,
647
631
  clipBorderRadius: resolvedRadius,
648
632
  hapticFeedbackEnabled: hapticFeedbackEnabled,
649
- child: painted,
650
- ),
651
- resolvedRadius,
633
+ pressOverlayColor: palette.foreground.withValues(
634
+ alpha: isDark ? 0.04 : 0.10,
635
+ ),
636
+ // Wrap the focus ring around the painted visual, not the pressable: the
637
+ // pressable enforces a 44px min tap target and stretches to its parent,
638
+ // so ringing it would draw an oversized oval around a small round orb.
639
+ child: _wrapFocus(painted, resolvedRadius),
640
+ );
641
+ }
642
+
643
+ return KasyPressableDepth(
644
+ semanticLabel: _accessibilityLabel,
645
+ onPressed: onPressed!,
646
+ clipBorderRadius: resolvedRadius,
647
+ hapticFeedbackEnabled: hapticFeedbackEnabled,
648
+ // Ring hugs the visual, not the (larger) tap target. See note above.
649
+ child: _wrapFocus(painted, resolvedRadius),
652
650
  );
653
651
  }
654
652
 
@@ -840,10 +838,10 @@ class _KasyButtonMetrics {
840
838
  final EdgeInsets horizontalPadding;
841
839
  final double labelFontSize;
842
840
 
843
- /// Ícone ao lado do rótulo (botões com texto).
841
+ /// Icon next to the label (buttons with text).
844
842
  final double iconSize;
845
843
 
846
- /// Tamanho do glifo dentro do botão apenas-ícone (pode diferir de [iconSize]).
844
+ /// Glyph size inside the icon-only button (may differ from [iconSize]).
847
845
  final double iconOnlyGlyphSize;
848
846
  final double loadingSpinnerExtent;
849
847
 
@@ -858,77 +856,3 @@ class _KasyButtonMetrics {
858
856
  });
859
857
  }
860
858
 
861
- /// Makes a button keyboard-navigable (Tab) and activates it on Enter/Space.
862
- /// Shows a focus ring when focused. Inert on touch-only platforms.
863
- class _KeyboardFocus extends StatefulWidget {
864
- final VoidCallback onActivate;
865
- final Widget child;
866
- final BorderRadius borderRadius;
867
-
868
- const _KeyboardFocus({
869
- required this.onActivate,
870
- required this.child,
871
- required this.borderRadius,
872
- });
873
-
874
- @override
875
- State<_KeyboardFocus> createState() => _KeyboardFocusState();
876
- }
877
-
878
- class _KeyboardFocusState extends State<_KeyboardFocus> {
879
- late final FocusNode _node;
880
- bool _focused = false;
881
-
882
- @override
883
- void initState() {
884
- super.initState();
885
- _node = FocusNode();
886
- _node.addListener(_onFocusChange);
887
- }
888
-
889
- @override
890
- void dispose() {
891
- _node.removeListener(_onFocusChange);
892
- _node.dispose();
893
- super.dispose();
894
- }
895
-
896
- void _onFocusChange() {
897
- if (mounted) setState(() => _focused = _node.hasFocus);
898
- }
899
-
900
- KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
901
- if (event is KeyDownEvent &&
902
- (event.logicalKey == LogicalKeyboardKey.enter ||
903
- event.logicalKey == LogicalKeyboardKey.space ||
904
- event.logicalKey == LogicalKeyboardKey.numpadEnter)) {
905
- widget.onActivate();
906
- return KeyEventResult.handled;
907
- }
908
- return KeyEventResult.ignored;
909
- }
910
-
911
- @override
912
- Widget build(BuildContext context) {
913
- final Color focusColor = Theme.of(context).colorScheme.primary;
914
- return Focus(
915
- focusNode: _node,
916
- onKeyEvent: _onKeyEvent,
917
- child: AnimatedContainer(
918
- duration: const Duration(milliseconds: 120),
919
- decoration: _focused
920
- ? BoxDecoration(
921
- borderRadius: widget.borderRadius,
922
- boxShadow: [
923
- BoxShadow(
924
- color: focusColor.withValues(alpha: 0.5),
925
- spreadRadius: 2,
926
- ),
927
- ],
928
- )
929
- : const BoxDecoration(),
930
- child: widget.child,
931
- ),
932
- );
933
- }
934
- }