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
@@ -2,8 +2,22 @@ rules_version = '2';
2
2
  service cloud.firestore {
3
3
  match /databases/{database}/documents {
4
4
 
5
+ // True when the signed-in user has role == "admin" on their OWN user doc.
6
+ // `role` is writable only server-side (see the users rules below), so a
7
+ // client cannot forge it — this check is trustworthy and server-evaluated.
8
+ function isAdmin() {
9
+ return request.auth != null
10
+ && exists(/databases/$(database)/documents/users/$(request.auth.uid))
11
+ && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.get('role', '') == 'admin';
12
+ }
13
+
5
14
  match /users/{id} {
6
- allow read, update, delete: if request.auth.uid != null && request.auth.uid == id;
15
+ allow read, delete: if request.auth.uid != null && request.auth.uid == id;
16
+ // Owners update their own profile, but NEVER their own `role`
17
+ // (no self-promotion to admin). `role` is set only server-side
18
+ // (Cloud Function / console), never from the client.
19
+ allow update: if request.auth.uid != null && request.auth.uid == id
20
+ && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']);
7
21
  allow create: if false;
8
22
  }
9
23
  match /users/{id}/{document=**} {
@@ -23,10 +37,15 @@ service cloud.firestore {
23
37
  allow create: if request.auth.uid != null
24
38
  && request.resource.data.active == false
25
39
  && request.resource.data.votes == 0;
40
+ // Either a normal ±1 vote by any signed-in user, OR full moderation
41
+ // (toggle visibility, edit translations) by an admin.
26
42
  allow update: if request.auth.uid != null
27
- && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['votes', 'last_update_date'])
28
- && (request.resource.data.votes == resource.data.votes + 1
29
- || request.resource.data.votes == resource.data.votes - 1);
43
+ && (
44
+ (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['votes', 'last_update_date'])
45
+ && (request.resource.data.votes == resource.data.votes + 1
46
+ || request.resource.data.votes == resource.data.votes - 1))
47
+ || isAdmin()
48
+ );
30
49
  allow delete: if false;
31
50
  }
32
51
  match /feature_requests/{featureId}/votes/{id} {
@@ -48,4 +67,4 @@ service cloud.firestore {
48
67
  allow read, write: if false;
49
68
  }
50
69
  }
51
- }
70
+ }
@@ -7,7 +7,8 @@
7
7
  "name": "functions",
8
8
  "dependencies": {
9
9
  "firebase-admin": "^13.6.0",
10
- "firebase-functions": "^7.1.1"
10
+ "firebase-functions": "^7.1.1",
11
+ "stripe": "^18.5.0"
11
12
  },
12
13
  "devDependencies": {
13
14
  "@types/jest": "^29.5.14",
@@ -6239,6 +6240,26 @@
6239
6240
  "url": "https://github.com/sponsors/sindresorhus"
6240
6241
  }
6241
6242
  },
6243
+ "node_modules/stripe": {
6244
+ "version": "18.5.0",
6245
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.5.0.tgz",
6246
+ "integrity": "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA==",
6247
+ "license": "MIT",
6248
+ "dependencies": {
6249
+ "qs": "^6.11.0"
6250
+ },
6251
+ "engines": {
6252
+ "node": ">=12.*"
6253
+ },
6254
+ "peerDependencies": {
6255
+ "@types/node": ">=12.x.x"
6256
+ },
6257
+ "peerDependenciesMeta": {
6258
+ "@types/node": {
6259
+ "optional": true
6260
+ }
6261
+ }
6262
+ },
6242
6263
  "node_modules/strnum": {
6243
6264
  "version": "2.2.0",
6244
6265
  "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
@@ -19,7 +19,8 @@
19
19
  "main": "lib/index.js",
20
20
  "dependencies": {
21
21
  "firebase-admin": "^13.6.0",
22
- "firebase-functions": "^7.1.1"
22
+ "firebase-functions": "^7.1.1",
23
+ "stripe": "^18.5.0"
23
24
  },
24
25
  "devDependencies": {
25
26
  "@types/jest": "^29.5.14",
@@ -0,0 +1,113 @@
1
+ import {onCall, HttpsError} from "firebase-functions/v2/https";
2
+ import * as admin from "firebase-admin";
3
+ import {Logger} from "../core/logger/logger";
4
+
5
+ // In-memory cap: the admin console loads up to this many of the most recent
6
+ // users in ONE call, then searches / sorts / paginates entirely on the client
7
+ // for an instant experience (no extra reads when typing or paging). Apps that
8
+ // outgrow this should add a dedicated search index (Algolia/Typesense) — see
9
+ // README. Reads are admin-only and bounded, so the cost stays predictable.
10
+ const MAX_SCAN = 1000;
11
+
12
+ // getAll() batch size for the subscription lookups (Firestore caps a single
13
+ // getAll at a few hundred refs comfortably; chunking keeps it safe).
14
+ const SUB_CHUNK = 200;
15
+
16
+ // A user counts as a subscriber when their subscription is currently usable.
17
+ const ACTIVE_SUBSCRIPTION_STATUSES = ["ACTIVE", "LIFETIME"];
18
+
19
+ interface AdminUser {
20
+ id: string;
21
+ email: string | null;
22
+ name: string | null;
23
+ createdAt: number | null; // epoch millis
24
+ subscriber: boolean;
25
+ }
26
+
27
+ /**
28
+ * Lists app users for the admin console.
29
+ *
30
+ * Security: the caller must be authenticated AND have `role == "admin"` on their
31
+ * own `users/{uid}` document. Clients cannot read the whole users collection
32
+ * (Firestore rules), so this runs with Admin privileges and gates on the role.
33
+ *
34
+ * Returns the most-recent users (bounded to MAX_SCAN) with their subscriber
35
+ * status already resolved. The client (admin_users_tab.dart) does the search,
36
+ * sort and pagination locally so those interactions are instant. Shape:
37
+ * { users: AdminUser[], totalUsers: number, truncated: boolean }
38
+ * - totalUsers: the true size of the collection (for an honest count).
39
+ * - truncated: true when the collection is larger than what we returned, so
40
+ * the UI can warn that search only covers the loaded set.
41
+ */
42
+ export const listUsers = onCall(async (request) => {
43
+ if (!request.auth) {
44
+ throw new HttpsError("unauthenticated", "Authentication required");
45
+ }
46
+ const callerUid = request.auth.uid;
47
+ const db = admin.firestore();
48
+ const logger = new Logger("listUsers");
49
+
50
+ // Admin gate: the caller's own user doc must carry role == "admin".
51
+ const callerDoc = await db.collection("users").doc(callerUid).get();
52
+ if (!callerDoc.exists || callerDoc.get("role") !== "admin") {
53
+ throw new HttpsError("permission-denied", "Admin role required");
54
+ }
55
+
56
+ try {
57
+ const usersRef = db.collection("users");
58
+
59
+ // Bounded fetch — the client does the search/sort/paginate.
60
+ //
61
+ // IMPORTANT: do NOT order this query by "creation_date". A Firestore
62
+ // orderBy silently DROPS every document that lacks the ordered field, and
63
+ // some user docs have no creation_date (e.g. accounts whose doc was created
64
+ // client-side during anonymous→social linking; the auth trigger then only
65
+ // patches the email and never backfills creation_date). Ordering by it
66
+ // would hide exactly those real, active users. We fetch unordered and sort
67
+ // in memory below (newest first, undated last) so every user is returned.
68
+ const snapshot = await usersRef.limit(MAX_SCAN).get();
69
+ const docs = snapshot.docs;
70
+
71
+ // Subscriber status for the whole batch, in chunked getAll() reads.
72
+ const activeSubscribers = new Set<string>();
73
+ for (let i = 0; i < docs.length; i += SUB_CHUNK) {
74
+ const chunk = docs.slice(i, i + SUB_CHUNK);
75
+ const refs = chunk.map((d) => db.collection("subscriptions").doc(d.id));
76
+ const snaps = await db.getAll(...refs);
77
+ for (const s of snaps) {
78
+ if (
79
+ s.exists &&
80
+ ACTIVE_SUBSCRIPTION_STATUSES.includes(s.get("status"))
81
+ ) {
82
+ activeSubscribers.add(s.id);
83
+ }
84
+ }
85
+ }
86
+
87
+ const users: AdminUser[] = docs.map((d) => {
88
+ const createdAt = d.get("creation_date");
89
+ return {
90
+ id: d.id,
91
+ email: (d.get("email") as string) || null,
92
+ name: (d.get("name") as string) || null,
93
+ createdAt: createdAt ? createdAt.toMillis() : null,
94
+ subscriber: activeSubscribers.has(d.id),
95
+ };
96
+ });
97
+
98
+ // Newest first; users without a creation_date sort to the end.
99
+ users.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
100
+
101
+ const countSnap = await usersRef.count().get();
102
+ const totalUsers = countSnap.data().count;
103
+
104
+ return {
105
+ users,
106
+ totalUsers,
107
+ truncated: totalUsers > users.length,
108
+ };
109
+ } catch (e) {
110
+ logger.error(`listUsers error: ${e}`);
111
+ throw new HttpsError("internal", "Failed to list users");
112
+ }
113
+ });
@@ -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';