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
@@ -1,11 +1,11 @@
1
1
  import 'package:kasy_kit/features/onboarding/api/entities/user_info_entity.dart';
2
2
 
3
3
  enum UserInfoKeys {
4
- genre,
4
+ gender,
5
5
  age,
6
6
  }
7
7
 
8
- enum Genre {
8
+ enum Gender {
9
9
  male,
10
10
  female,
11
11
  none,
@@ -44,11 +44,11 @@ class UserAgeInfo extends UserInfoDetail<AgeRange> {
44
44
  }
45
45
 
46
46
  factory UserAgeInfo.fromString(String value) {
47
- final sexe = AgeRange.values.firstWhere(
47
+ final range = AgeRange.values.firstWhere(
48
48
  (element) => element.name == value,
49
49
  orElse: () => AgeRange.none,
50
50
  );
51
- return UserAgeInfo(sexe);
51
+ return UserAgeInfo(range);
52
52
  }
53
53
 
54
54
 
@@ -62,32 +62,32 @@ class UserAgeInfo extends UserInfoDetail<AgeRange> {
62
62
  }
63
63
 
64
64
  /// ======================================
65
- /// user genre info
65
+ /// user gender info
66
66
  /// ======================================
67
- class UserSexeInfo extends UserInfoDetail<Genre> {
68
- UserSexeInfo(super.value);
67
+ class UserGenderInfo extends UserInfoDetail<Gender> {
68
+ UserGenderInfo(super.value);
69
69
 
70
- factory UserSexeInfo.fromEntity(UserInfoEntity entity) {
71
- final value = Genre.values.firstWhere(
70
+ factory UserGenderInfo.fromEntity(UserInfoEntity entity) {
71
+ final value = Gender.values.firstWhere(
72
72
  (element) => element.name == entity.value,
73
- orElse: () => Genre.none,
73
+ orElse: () => Gender.none,
74
74
  );
75
- return UserSexeInfo(value);
75
+ return UserGenderInfo(value);
76
76
  }
77
77
 
78
- factory UserSexeInfo.fromString(String value) {
79
- final genre = Genre.values.firstWhere(
78
+ factory UserGenderInfo.fromString(String value) {
79
+ final gender = Gender.values.firstWhere(
80
80
  (element) => element.name == value,
81
- orElse: () => Genre.none,
81
+ orElse: () => Gender.none,
82
82
  );
83
- return UserSexeInfo(genre);
83
+ return UserGenderInfo(gender);
84
84
  }
85
85
 
86
86
 
87
87
 
88
88
  @override
89
89
  UserInfoEntity toEntity(String userId) => UserInfoEntity(
90
- key: UserInfoKeys.genre.name,
90
+ key: UserInfoKeys.gender.name,
91
91
  value: value.name,
92
92
  userId: userId,
93
93
  );
@@ -6,7 +6,8 @@ import 'package:kasy_kit/core/data/api/tracking_api.dart';
6
6
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
7
7
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
8
8
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart';
9
- import 'package:kasy_kit/features/subscription/repositories/subscription_repository.dart';
9
+ import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_module_mockups.dart';
10
+ import 'package:kasy_kit/features/subscriptions/repositories/subscription_repository.dart';
10
11
  import 'package:kasy_kit/i18n/translations.g.dart';
11
12
  import 'package:permission_handler/permission_handler.dart';
12
13
 
@@ -29,10 +30,10 @@ class AttPermissionStep extends ConsumerWidget {
29
30
 
30
31
  return OnboardingBackground(
31
32
  child: OnboardingIllustrationScaffold(
32
- progress: 0.9,
33
+ step: 6,
33
34
  title: translations.title,
34
35
  description: translations.description,
35
- imageAsset: 'assets/images/onboarding/img3.jpg',
36
+ image: const TrackingPermissionMockup(),
36
37
  footerActions: [
37
38
  KasyButton(
38
39
  label: translations.continue_button,
@@ -7,6 +7,7 @@ import 'package:kasy_kit/core/theme/theme.dart';
7
7
  import 'package:kasy_kit/core/widgets/responsive_layout.dart';
8
8
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
9
9
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_progress.dart';
10
+ import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_step_header.dart';
10
11
  import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_sticky_footer.dart';
11
12
  import 'package:kasy_kit/features/onboarding/ui/widgets/selectable_row_tile.dart';
12
13
 
@@ -18,9 +19,11 @@ typedef OnOptionIdSelected = void Function(String id);
18
19
 
19
20
  typedef OnValidate = void Function(String? key);
20
21
 
21
- /// Single choice question with radio buttons
22
+ /// Single choice question with selectable tiles, matching the clean‑premium
23
+ /// onboarding layout (shared header, left‑aligned title, sticky footer).
22
24
  class OnboardingRadioQuestion extends ConsumerStatefulWidget {
23
- final double? progress;
25
+ final int step;
26
+ final int totalSteps;
24
27
  final String title;
25
28
  final String description;
26
29
  final String btnText;
@@ -37,7 +40,8 @@ class OnboardingRadioQuestion extends ConsumerStatefulWidget {
37
40
  required this.btnText,
38
41
  required this.optionIds,
39
42
  required this.optionBuilder,
40
- this.progress,
43
+ required this.step,
44
+ this.totalSteps = kOnboardingSteps,
41
45
  this.onOptionIdSelected,
42
46
  this.onValidate,
43
47
  this.reassuranceBuilder,
@@ -54,61 +58,68 @@ class _OnboardingRadioQuestionState
54
58
 
55
59
  @override
56
60
  Widget build(BuildContext context) {
61
+ const gutter = KasySpacing.lg;
62
+
57
63
  final scrollBody = Column(
58
- crossAxisAlignment: CrossAxisAlignment.stretch,
64
+ crossAxisAlignment: CrossAxisAlignment.start,
59
65
  children: [
60
- if (widget.progress != null)
61
- OnboardingProgress(value: widget.progress!),
66
+ OnboardingStepHeader(step: widget.step, totalSteps: widget.totalSteps),
62
67
  Padding(
63
- padding: const EdgeInsets.fromLTRB(
64
- KasySpacing.md,
65
- KasySpacing.xl,
66
- KasySpacing.md,
67
- 0,
68
- ),
68
+ padding: const EdgeInsets.fromLTRB(gutter, KasySpacing.lg, gutter, 0),
69
69
  child: MoveFadeAnim(
70
- delayInMs: 100,
70
+ delayInMs: 120,
71
71
  child: Text(
72
72
  widget.title,
73
- textAlign: TextAlign.center,
74
- style: context.textTheme.headlineLarge?.copyWith(
73
+ textAlign: TextAlign.start,
74
+ style: context.textTheme.headlineMedium?.copyWith(
75
75
  color: context.colors.onBackground,
76
+ fontSize: 26,
77
+ fontWeight: FontWeight.w700,
78
+ letterSpacing: -0.2,
79
+ height: 1.2,
76
80
  ),
77
81
  ),
78
82
  ),
79
83
  ),
80
84
  Padding(
81
85
  padding: const EdgeInsets.fromLTRB(
86
+ gutter,
87
+ KasySpacing.smd,
88
+ gutter,
82
89
  KasySpacing.lg,
83
- KasySpacing.md,
84
- KasySpacing.lg,
85
- KasySpacing.xl,
86
90
  ),
87
91
  child: MoveFadeAnim(
88
92
  delayInMs: 200,
89
93
  child: Text(
90
94
  widget.description,
91
- textAlign: TextAlign.center,
95
+ textAlign: TextAlign.start,
92
96
  style: context.textTheme.bodyLarge?.copyWith(
93
- color: context.colors.onBackground.withValues(alpha: 0.6),
97
+ color: context.colors.muted,
98
+ height: 1.45,
94
99
  ),
95
100
  ),
96
101
  ),
97
102
  ),
98
103
  Padding(
99
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
104
+ padding: const EdgeInsets.symmetric(horizontal: gutter),
100
105
  child: OnboardingSelectableRowGroup(
101
106
  physics: const NeverScrollableScrollPhysics(),
102
- options: widget.optionIds
103
- .map(
107
+ options: widget.optionIds.map(
104
108
  (e) {
105
109
  final index = widget.optionIds.indexOf(e);
106
110
  return Animate(
107
111
  effects: [
108
112
  FadeEffect(
109
- delay: Duration(milliseconds: 300 + index * 100),
110
- duration: const Duration(milliseconds: 550),
111
- curve: Curves.easeInOut,
113
+ delay: Duration(milliseconds: 280 + index * 80),
114
+ duration: const Duration(milliseconds: 450),
115
+ curve: Curves.easeOut,
116
+ ),
117
+ MoveEffect(
118
+ delay: Duration(milliseconds: 280 + index * 80),
119
+ duration: const Duration(milliseconds: 450),
120
+ curve: Curves.easeOut,
121
+ begin: const Offset(0, 24),
122
+ end: Offset.zero,
112
123
  ),
113
124
  ],
114
125
  child: widget.optionBuilder(e, e == selectedChoiceId),
@@ -153,8 +164,7 @@ class _OnboardingRadioQuestionState
153
164
  ),
154
165
  DeviceSizeBuilder(
155
166
  builder: (device) => OnboardingStickyFooter(
156
- maxContentWidth:
157
- device == DeviceType.small ? null : 600,
167
+ maxContentWidth: device == DeviceType.small ? null : 600,
158
168
  children: [
159
169
  KasyButton(
160
170
  label: widget.btnText,
@@ -0,0 +1,100 @@
1
+ import 'package:dio/dio.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
4
+ import 'package:kasy_kit/core/data/api/http_client.dart';
5
+
6
+ /// One row of the admin Users table, as returned by `GET /admin/users`. Your
7
+ /// backend MUST verify the caller's `role == "admin"` (and return 403 otherwise)
8
+ /// before serving this — it is never readable by a non-admin client.
9
+ ///
10
+ /// This file mirrors the shared admin data-layer contract (see the Firebase
11
+ /// version): the public surface — [AdminUser], [AdminUsersResult] and
12
+ /// [adminUsersApiProvider] — is identical across backends so the Users tab UI
13
+ /// stays the same; only the body of [AdminUsersApi.fetch] differs.
14
+ class AdminUser {
15
+ final String id;
16
+ final String? email;
17
+ final String? name;
18
+ final DateTime? createdAt;
19
+ final bool subscriber;
20
+
21
+ const AdminUser({
22
+ required this.id,
23
+ required this.subscriber,
24
+ this.email,
25
+ this.name,
26
+ this.createdAt,
27
+ });
28
+
29
+ factory AdminUser.fromMap(Map<String, dynamic> m) {
30
+ final createdMillis = m['createdAt'];
31
+ return AdminUser(
32
+ id: (m['id'] as String?) ?? '',
33
+ email: m['email'] as String?,
34
+ name: m['name'] as String?,
35
+ createdAt: createdMillis is num
36
+ ? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
37
+ : null,
38
+ subscriber: m['subscriber'] == true,
39
+ );
40
+ }
41
+ }
42
+
43
+ /// The full (bounded) set of users plus collection metadata. The UI searches,
44
+ /// sorts and paginates this list locally, so those interactions are instant.
45
+ class AdminUsersResult {
46
+ final List<AdminUser> users;
47
+ final int totalUsers;
48
+ final bool truncated;
49
+
50
+ const AdminUsersResult({
51
+ required this.users,
52
+ required this.totalUsers,
53
+ required this.truncated,
54
+ });
55
+ }
56
+
57
+ final adminUsersApiProvider = Provider<AdminUsersApi>(
58
+ (ref) => AdminUsersApi(client: ref.read(httpClientProvider)),
59
+ );
60
+
61
+ /// REST endpoint expected:
62
+ /// GET /admin/users
63
+ /// Auth: Authorization Bearer token (attached automatically). The server
64
+ /// MUST verify the caller's role == "admin" and return 403 otherwise.
65
+ /// Response 200: {
66
+ /// "users": [
67
+ /// {"id": "...", "email": "a@b.com"|null, "name": "Ana"|null,
68
+ /// "createdAt": 1700000000000 (epoch millis)|null, "subscriber": true}
69
+ /// ],
70
+ /// "totalUsers": 142, // true size of the collection
71
+ /// "truncated": false // true when more users exist than returned
72
+ /// }
73
+ /// Return the most-recent users (bounded, e.g. 1000); the app does the
74
+ /// search / sort / pagination locally so those interactions are instant.
75
+ class AdminUsersApi {
76
+ final HttpClient _client;
77
+
78
+ AdminUsersApi({required HttpClient client}) : _client = client;
79
+
80
+ /// Loads the users for the admin console. Returns the whole bounded set in one
81
+ /// call; the UI handles search/sort/pagination on the client.
82
+ Future<AdminUsersResult> fetch() async {
83
+ try {
84
+ final response = await _client.get('/admin/users');
85
+ final data = Map<String, dynamic>.from(response.data as Map);
86
+ final rawUsers = (data['users'] as List?) ?? const [];
87
+ return AdminUsersResult(
88
+ users: rawUsers
89
+ .map((e) => AdminUser.fromMap(Map<String, dynamic>.from(e as Map)))
90
+ .toList(),
91
+ totalUsers: (data['totalUsers'] as num?)?.toInt() ?? 0,
92
+ truncated: data['truncated'] == true,
93
+ );
94
+ } on DioException catch (e) {
95
+ throw ApiError.fromDioException(e);
96
+ } catch (e, s) {
97
+ throw ApiError(code: 0, message: '$e: $s');
98
+ }
99
+ }
100
+ }
@@ -15,6 +15,17 @@ enum SubscriptionStatus {
15
15
  CANCELLED,
16
16
  }
17
17
 
18
+ /// Where the subscription was purchased (its origin). `STRIPE` means it was
19
+ /// bought on the web via Stripe. Used to route the user to the correct
20
+ /// management flow regardless of the device they are currently on.
21
+ enum SubscriptionStore {
22
+ PLAY_STORE,
23
+ APPLE_STORE,
24
+ EARLY_BIRD,
25
+ STRIPE,
26
+ unknown,
27
+ }
28
+
18
29
 
19
30
  @freezed
20
31
  sealed class SubscriptionEntity with _$SubscriptionEntity {
@@ -26,6 +37,8 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
26
37
  @JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
27
38
  @JsonKey(name: 'period_end_date') DateTime? periodEndDate,
28
39
  @JsonKey(name: 'status') required SubscriptionStatus status,
40
+ @JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
41
+ SubscriptionStore? store,
29
42
  }) = SubscriptionEntityData;
30
43
 
31
44
  factory SubscriptionEntity.fromJson(Map<String, Object?> json) =>
@@ -0,0 +1,60 @@
1
+ import 'package:dio/dio.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:kasy_kit/core/data/api/http_client.dart';
4
+ import 'package:kasy_kit/features/subscriptions/api/stripe_product.dart';
5
+
6
+ final stripeBackendApiProvider = Provider<StripeBackendApi>(
7
+ (ref) => StripeBackendApi(client: ref.read(httpClientProvider)),
8
+ );
9
+
10
+ /// Talks to the Stripe backend (your own server). The user identity must be
11
+ /// taken server-side from the authenticated request (the Bearer token attached
12
+ /// by [HttpClient]) — never trusted from the request body. This file replaces
13
+ /// the Firebase version so the rest of the Stripe client code stays
14
+ /// backend-agnostic.
15
+ ///
16
+ /// Your backend must expose these endpoints (see Stripe-Guia.md):
17
+ /// GET /stripe/list-prices -> active recurring prices (offer list)
18
+ /// POST /stripe/checkout-session -> { url } hosted Checkout session
19
+ /// POST /stripe/portal-session -> { url } Customer Portal session
20
+ /// and a Stripe webhook that writes the `subscriptions` record (store=STRIPE).
21
+ class StripeBackendApi {
22
+ final HttpClient _client;
23
+
24
+ StripeBackendApi({required HttpClient client}) : _client = client;
25
+
26
+ /// Active recurring prices, mapped to paywall offers.
27
+ Future<List<StripeProduct>> listPrices() async {
28
+ final Response res = await _client.get('/stripe/list-prices');
29
+ final list = (res.data as List).cast<dynamic>();
30
+ return list
31
+ .map((e) => StripeProduct.fromJson(Map<String, dynamic>.from(e as Map)))
32
+ .toList();
33
+ }
34
+
35
+ /// Create a hosted Checkout session for [priceId] and return its URL.
36
+ Future<String> createCheckoutSession({
37
+ required String priceId,
38
+ String? successUrl,
39
+ String? cancelUrl,
40
+ }) async {
41
+ final Response res = await _client.post(
42
+ '/stripe/checkout-session',
43
+ data: {
44
+ 'priceId': priceId,
45
+ if (successUrl != null) 'successUrl': successUrl,
46
+ if (cancelUrl != null) 'cancelUrl': cancelUrl,
47
+ },
48
+ );
49
+ return (res.data as Map)['url'] as String;
50
+ }
51
+
52
+ /// Create a Customer Portal session (manage / cancel) and return its URL.
53
+ Future<String> createPortalSession({String? returnUrl}) async {
54
+ final Response res = await _client.post(
55
+ '/stripe/portal-session',
56
+ data: {if (returnUrl != null) 'returnUrl': returnUrl},
57
+ );
58
+ return (res.data as Map)['url'] as String;
59
+ }
60
+ }
@@ -1,6 +1,6 @@
1
1
  import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
2
2
  import 'package:kasy_kit/core/data/api/http_client.dart';
3
- import 'package:kasy_kit/features/subscription/api/entities/subscription_entity.dart';
3
+ import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
4
4
  import 'package:dio/dio.dart';
5
5
  import 'package:flutter_riverpod/flutter_riverpod.dart';
6
6
  import 'package:logger/logger.dart';
@@ -93,7 +93,6 @@ flutter:
93
93
  assets:
94
94
  - .env
95
95
  - assets/images/
96
- - assets/images/onboarding/
97
96
  - assets/icons/
98
97
  flutter_launcher_icons:
99
98
  image_path: assets/images/icon.png
@@ -108,15 +107,15 @@ flutter_launcher_icons:
108
107
  background_color: "#01171f"
109
108
  theme_color: "#01171f"
110
109
  flutter_native_splash:
111
- color: "#FFFFFF"
112
- color_dark: "#000000"
110
+ color: "#FAF9FC"
111
+ color_dark: "#0C0A14"
113
112
  fullscreen: true
114
113
  ios: true
115
114
  android: true
116
115
  image: assets/images/splash_logo_light.png
117
116
  image_dark: assets/images/splash_logo_dark.png
118
117
  android_12:
119
- color: "#FFFFFF"
120
- color_dark: "#000000"
118
+ color: "#FAF9FC"
119
+ color_dark: "#0C0A14"
121
120
  image: assets/images/splash_logo_light_android12.png
122
121
  image_dark: assets/images/splash_logo_dark_android12.png
@@ -256,11 +256,17 @@ async function writeFirebaseRc(projectDir, firebaseProjectId) {
256
256
  * These must exist in Secret Manager before `firebase deploy` runs,
257
257
  * otherwise firebase-tools prompts interactively and hangs in non-TTY environments.
258
258
  */
259
- const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WEBHOOK_KEY', 'LLM_API_KEY'];
259
+ const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WEBHOOK_KEY', 'AI_API_KEY', 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'];
260
260
 
261
261
  /**
262
- * Create each required secret if it doesn't exist yet.
263
- * Uses the real value when provided, otherwise falls back to 'placeholder'.
262
+ * Ensure every required secret exists AND holds a value, so `firebase deploy` never
263
+ * blocks waiting for one. A secret has two layers: the "container" and its versions.
264
+ * A container can exist with zero enabled versions (e.g. created by hand but never
265
+ * filled) — in that state `firebase deploy` still aborts with
266
+ * "Secret Manager has no latest version of ...". Checking only the container (describe)
267
+ * misses this, so we also verify there is an enabled version and add a placeholder when
268
+ * there isn't. The user replaces placeholders with real values later via `kasy configure`.
269
+ *
264
270
  * Uses gcloud to avoid firebase-tools interactive prompts.
265
271
  * Non-fatal: if a secret can't be created (e.g. API not enabled yet) we continue anyway.
266
272
  *
@@ -268,22 +274,87 @@ const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WE
268
274
  * @param {Record<string,string>} [secretValues] - map of secret name → real value (optional)
269
275
  */
270
276
  async function ensureSecretsExist(firebaseProjectId, secretValues = {}) {
277
+ const escape = (v) => v.replace(/'/g, "'\\''");
271
278
  for (const secret of REQUIRED_SECRETS) {
272
- const check = await run(`gcloud secrets describe ${secret} --project=${firebaseProjectId}`);
273
- if (!check.ok) {
274
- const value = secretValues[secret]?.trim() || 'placeholder';
279
+ const realValue = secretValues[secret]?.trim();
280
+ const exists = (await run(`gcloud secrets describe ${secret} --project=${firebaseProjectId}`)).ok;
281
+
282
+ if (!exists) {
283
+ // No container yet — create it with the real value, or a placeholder so deploy never blocks.
284
+ const value = realValue || 'placeholder';
285
+ await run(
286
+ `printf '${escape(value)}' | gcloud secrets create ${secret} --data-file=- --replication-policy=automatic --project=${firebaseProjectId}`
287
+ );
288
+ continue;
289
+ }
290
+
291
+ // Container exists. Does it hold an enabled version (an actual value)?
292
+ const versions = await run(`gcloud secrets versions list ${secret} --format='value(state)' --project=${firebaseProjectId}`);
293
+ const hasEnabledVersion = versions.ok && /enabled/i.test(versions.stdout);
294
+
295
+ if (realValue) {
296
+ // User provided a real value — always add it as the latest version.
275
297
  await run(
276
- `printf '${value.replace(/'/g, "'\\''")}' | gcloud secrets create ${secret} --data-file=- --replication-policy=automatic --project=${firebaseProjectId}`
298
+ `printf '${escape(realValue)}' | gcloud secrets versions add ${secret} --data-file=- --project=${firebaseProjectId}`
277
299
  );
278
- } else if (secretValues[secret]?.trim()) {
279
- // Secret exists but user provided a real value update it.
300
+ } else if (!hasEnabledVersion) {
301
+ // Container exists but is empty — add a placeholder so deploy never blocks.
280
302
  await run(
281
- `printf '${secretValues[secret].trim().replace(/'/g, "'\\''")}' | gcloud secrets versions add ${secret} --data-file=- --project=${firebaseProjectId}`
303
+ `printf 'placeholder' | gcloud secrets versions add ${secret} --data-file=- --project=${firebaseProjectId}`
282
304
  );
283
305
  }
284
306
  }
285
307
  }
286
308
 
309
+ /**
310
+ * Cloud Functions declare non-secret config via defineString()/defineInt()/etc. In
311
+ * non-interactive mode `firebase deploy` aborts if such a param has no value AND no
312
+ * usable default (an empty-string default isn't reliably honored across firebase-tools
313
+ * versions). To make deploy never block on missing config, we ensure functions/.env has
314
+ * an (empty) entry for every declared param. The user fills real values later via
315
+ * `kasy configure`; an empty value keeps the documented "unset" behavior.
316
+ *
317
+ * Non-fatal: any failure here is swallowed so it never breaks a deploy.
318
+ */
319
+ async function ensureEnvParamsExist(projectDir) {
320
+ try {
321
+ const srcDir = path.join(projectDir, 'functions', 'src');
322
+ if (!(await fs.pathExists(srcDir))) return;
323
+
324
+ // Collect every defineString/Int/Boolean/List param name from the functions source.
325
+ const params = new Set();
326
+ const walk = async (dir) => {
327
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
328
+ const full = path.join(dir, entry.name);
329
+ if (entry.isDirectory()) { await walk(full); continue; }
330
+ if (!/\.(ts|js)$/.test(entry.name)) continue;
331
+ const code = await fs.readFile(full, 'utf8');
332
+ const re = /define(?:String|Int|Boolean|List)\(\s*["']([A-Z0-9_]+)["']/g;
333
+ let m;
334
+ while ((m = re.exec(code)) !== null) params.add(m[1]);
335
+ }
336
+ };
337
+ await walk(srcDir);
338
+ if (params.size === 0) return;
339
+
340
+ const envPath = path.join(projectDir, 'functions', '.env');
341
+ let content = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf8') : '';
342
+ const existing = new Set(
343
+ content.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
344
+ .map((l) => (l.includes('=') ? l.slice(0, l.indexOf('=')).trim() : ''))
345
+ .filter(Boolean)
346
+ );
347
+
348
+ const missing = [...params].filter((p) => !existing.has(p));
349
+ if (missing.length === 0) return;
350
+ if (content && !content.endsWith('\n')) content += '\n';
351
+ content += missing.map((p) => `${p}=`).join('\n') + '\n';
352
+ await fs.outputFile(envPath, content, 'utf8');
353
+ } catch (_) {
354
+ // Never let env-bootstrap break a deploy.
355
+ }
356
+ }
357
+
287
358
  /**
288
359
  * Get the numeric project number for a GCP project ID.
289
360
  */
@@ -867,6 +938,9 @@ async function runDeploy(projectDir, serviceAccountPath, firebaseProjectId, { on
867
938
  // Running this before waitForApis caused silent failures when the API was still
868
939
  // propagating — the deploy would then fail because the secrets didn't exist yet.
869
940
  await ensureSecretsExist(firebaseProjectId, secretValues);
941
+ // Bootstrap empty entries for any defineString/etc. config param so deploy never
942
+ // blocks asking for one (e.g. STRIPE_PRODUCT_ID). Real values come later via configure.
943
+ await ensureEnvParamsExist(projectDir);
870
944
 
871
945
  // Quick IAM readiness check: if the two critical grants (Eventarc + Storage) are
872
946
  // already confirmed in IAM, skip the entire grant block below. On re-deploy this
@@ -938,10 +1012,10 @@ async function runIamSetup(firebaseProjectId, projectNumber, functionsRegion = '
938
1012
  waitForServiceAccount(gcfSa, firebaseProjectId),
939
1013
  waitForServiceAccount(cloudBuildAgentSa, firebaseProjectId),
940
1014
  waitForServiceAccount(cloudRunSa, firebaseProjectId),
941
- // eventarcSa recebe role abaixoprecisa existir antes do grant.
942
- // Em projetos novos pode demorar até 30s após identity create.
1015
+ // eventarcSa gets its role belowit must exist before the grant.
1016
+ // On new projects it can take up to 30s after identity create.
943
1017
  waitForServiceAccount(eventarcSa, firebaseProjectId),
944
- // pubsubSa é necessário para triggers Firestore v2 (Eventarc usa Pub/Sub para entregar eventos).
1018
+ // pubsubSa is required for Firestore v2 triggers (Eventarc uses Pub/Sub to deliver events).
945
1019
  waitForServiceAccount(pubsubSa, firebaseProjectId),
946
1020
  ]);
947
1021
 
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * Firebase project generator.
3
3
  *
4
- * Wrapper fino sobre generateProject().
5
- * A lógica de geração comum (cópia, pub get, slang, build_runner, flutterfire)
6
- * vive em cli/lib/scaffold/generate.js.
4
+ * Thin wrapper over generateProject().
5
+ * The common generation logic (copy, pub get, slang, build_runner, flutterfire)
6
+ * lives in cli/lib/scaffold/generate.js.
7
7
  *
8
- * Hook específico:
9
- * postBuild → runDeploy (opcional, controlado por options.deploy)
8
+ * Specific hook:
9
+ * postBuild → runDeploy (optional, controlled by options.deploy)
10
10
  */
11
11
 
12
12
  const { generateProject } = require('../../generate');
@@ -21,7 +21,7 @@ const ORIGINAL_SHORT_NAME = 'appfirebase';
21
21
 
22
22
  /**
23
23
  * Normalize app name to a valid Dart package name.
24
- * "Meu App Incrível" → "meu_app_incrivel"
24
+ * "My Café App" → "my_cafe_app"
25
25
  */
26
26
  function toPackageName(appName) {
27
27
  return (appName || '')
@@ -52,8 +52,8 @@ function bundleIdToPath(bundleId) {
52
52
 
53
53
  const ORIGINAL_BUNDLE_ID_PATH = bundleIdToPath(ORIGINAL_BUNDLE_ID);
54
54
 
55
- // Facebook — caracteres permitidos no URL scheme da Apple (RFC1738).
56
- // Sem credenciais no `kasy new`, mantém estes placeholders (login FB não funciona até trocar).
55
+ // Facebook — only the characters allowed in Apple's URL scheme (RFC1738).
56
+ // Without credentials in `kasy new`, keep these placeholders (FB login won't work until replaced).
57
57
  const FB_APP_ID_PLACEHOLDER = '000000000000000';
58
58
  const FB_CLIENT_TOKEN_PLACEHOLDER = '00000000000000000000000000000000';
59
59
 
@@ -91,7 +91,7 @@ function buildTokens({ appName, bundleId, fbAppId, fbToken, defaultPaywall = 'ba
91
91
  [`"${ORIGINAL_SHORT_NAME}"`]: `"${shortName}"`,
92
92
  // pubspec.yaml name field
93
93
  [`name: ${ORIGINAL_PACKAGE}`]: `name: ${packageName}`,
94
- // Facebook (placeholders → user values ou mantém placeholder válido p/ App Store)
94
+ // Facebook (placeholders → user values, or keep a valid placeholder for the App Store)
95
95
  [FB_APP_ID_PLACEHOLDER]: fbAppId?.trim() || FB_APP_ID_PLACEHOLDER,
96
96
  [`fb${FB_APP_ID_PLACEHOLDER}`]: fbAppId?.trim() ? `fb${fbAppId.trim()}` : `fb${FB_APP_ID_PLACEHOLDER}`,
97
97
  [FB_CLIENT_TOKEN_PLACEHOLDER]: fbToken?.trim() || FB_CLIENT_TOKEN_PLACEHOLDER,