kasy-cli 1.21.9 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/lib/commands/add.js +93 -80
  2. package/lib/commands/configure.js +100 -32
  3. package/lib/commands/doctor.js +28 -2
  4. package/lib/commands/new.js +80 -37
  5. package/lib/commands/notifications.js +1 -1
  6. package/lib/commands/remove.js +43 -15
  7. package/lib/commands/run.js +2 -2
  8. package/lib/commands/update.js +2 -2
  9. package/lib/scaffold/CHANGELOG.json +14 -0
  10. package/lib/scaffold/backends/api/generator.js +14 -14
  11. package/lib/scaffold/backends/api/patch/README.md +83 -0
  12. package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
  13. package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
  14. package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
  15. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
  16. package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
  17. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
  18. package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
  19. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
  20. package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
  21. package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
  22. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  23. package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  24. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
  25. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  26. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
  27. package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  28. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
  29. package/lib/scaffold/backends/firebase/deploy.js +87 -13
  30. package/lib/scaffold/backends/firebase/generator.js +5 -5
  31. package/lib/scaffold/backends/firebase/tokens.js +4 -4
  32. package/lib/scaffold/backends/supabase/deploy.js +63 -11
  33. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
  34. package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
  35. package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
  36. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
  37. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
  38. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
  39. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
  40. package/lib/scaffold/backends/supabase/generator.js +17 -17
  41. package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
  42. package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
  43. package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
  44. package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
  45. package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
  46. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
  47. package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
  48. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  49. package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
  50. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
  51. package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
  52. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
  53. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  54. package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  55. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
  56. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
  57. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
  58. package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  59. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
  60. package/lib/scaffold/backends/supabase/tokens.js +3 -3
  61. package/lib/scaffold/catalog.js +9 -11
  62. package/lib/scaffold/generate.js +45 -31
  63. package/lib/scaffold/shared/generator-utils.js +188 -81
  64. package/lib/scaffold/shared/sort-imports.js +191 -0
  65. package/lib/scaffold/shared/template-strings.js +3 -3
  66. package/lib/utils/checks.js +2 -2
  67. package/lib/utils/i18n/messages-en.js +50 -35
  68. package/lib/utils/i18n/messages-es.js +50 -35
  69. package/lib/utils/i18n/messages-pt.js +52 -37
  70. package/lib/utils/updates.js +15 -15
  71. package/package.json +1 -1
  72. package/templates/firebase/.env.example +2 -2
  73. package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
  74. package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
  75. package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
  78. package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
  79. package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
  80. package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
  81. package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
  82. package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
  83. package/templates/firebase/firestore.rules +24 -5
  84. package/templates/firebase/functions/package-lock.json +22 -1
  85. package/templates/firebase/functions/package.json +2 -1
  86. package/templates/firebase/functions/src/admin/functions.ts +113 -0
  87. package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
  88. package/templates/firebase/functions/src/index.ts +8 -2
  89. package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
  90. package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
  91. package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
  92. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
  93. package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
  94. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
  95. package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
  96. package/templates/firebase/lib/components/components.dart +4 -1
  97. package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
  98. package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
  99. package/templates/firebase/lib/components/kasy_button.dart +23 -99
  100. package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
  101. package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
  102. package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
  103. package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
  104. package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
  105. package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
  106. package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
  107. package/templates/firebase/lib/components/kasy_toast.dart +2 -2
  108. package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
  109. package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
  110. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
  111. package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
  112. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
  113. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
  114. package/templates/firebase/lib/core/config/app_env.dart +5 -11
  115. package/templates/firebase/lib/core/config/features.dart +5 -4
  116. package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
  117. package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
  118. package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
  119. package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
  120. package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
  121. package/templates/firebase/lib/core/data/models/user.dart +11 -0
  122. package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
  123. package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
  124. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
  125. package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
  126. package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
  127. package/templates/firebase/lib/core/states/logout_action.dart +25 -0
  128. package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
  129. package/templates/firebase/lib/core/theme/colors.dart +488 -188
  130. package/templates/firebase/lib/core/theme/radius.dart +22 -11
  131. package/templates/firebase/lib/core/theme/shadows.dart +66 -0
  132. package/templates/firebase/lib/core/theme/texts.dart +75 -41
  133. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
  134. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
  135. package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
  136. package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
  137. package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
  138. package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
  139. package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
  140. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
  141. package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
  142. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
  143. package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
  144. package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
  145. package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
  146. package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
  147. package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
  148. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
  149. package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
  150. package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
  151. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
  152. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
  153. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
  154. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
  155. package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
  156. package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
  157. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
  158. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
  159. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
  160. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
  161. package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
  162. package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
  163. package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
  164. package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
  165. package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
  166. package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
  167. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
  168. package/templates/firebase/lib/features/home/home_feed.dart +289 -0
  169. package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
  170. package/templates/firebase/lib/features/home/home_page.dart +11 -250
  171. package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
  172. package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
  173. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
  174. package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
  175. package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
  176. package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
  177. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
  178. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
  179. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
  180. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
  181. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
  182. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
  183. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
  184. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
  185. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
  186. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
  187. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
  188. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
  189. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
  190. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
  191. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
  192. package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
  193. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
  194. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
  195. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
  196. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
  197. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
  198. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
  199. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
  200. package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
  201. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
  202. package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
  203. package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
  204. package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
  205. package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
  206. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
  207. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
  208. package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
  209. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
  210. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
  211. package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
  212. package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
  213. package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
  214. package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
  215. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
  216. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
  217. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
  218. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
  219. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
  220. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
  221. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
  222. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
  223. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
  224. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
  225. package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
  226. package/templates/firebase/lib/i18n/en.i18n.json +171 -46
  227. package/templates/firebase/lib/i18n/es.i18n.json +175 -50
  228. package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
  229. package/templates/firebase/lib/main.dart +6 -3
  230. package/templates/firebase/lib/router.dart +15 -23
  231. package/templates/firebase/pubspec.yaml +4 -5
  232. package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
  233. package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
  234. package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
  235. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
  236. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
  237. package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
  238. package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
  239. package/templates/firebase/test/test_utils.dart +6 -6
  240. package/templates/firebase/web/index.html +5 -2
  241. package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
  242. package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
  243. package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
  244. package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
  245. package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
  246. package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
  247. package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
  248. package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
  249. package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
  250. package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
  251. package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
  252. package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
  253. package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
  254. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
  255. package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
  256. /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
  257. /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
  258. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
  259. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
  260. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
  261. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
  262. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
  263. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
  264. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
  265. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
  266. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
  267. /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
@@ -88,6 +88,42 @@ async function getOrgsList() {
88
88
  return { ok: true, orgs: orgs.map((o) => ({ id: o.id, name: o.name || o.id })) };
89
89
  }
90
90
 
91
+ /**
92
+ * Classify a failed `supabase projects create` (or login) error so the caller
93
+ * can show targeted guidance instead of a raw API string:
94
+ * - 'login' → not authenticated (token missing/expired)
95
+ * - 'free_limit' → Free plan active-project cap reached for this organization
96
+ * - 'unknown' → anything else (caller shows the raw message)
97
+ *
98
+ * Matching is intentionally loose: the Supabase Management API wording for the
99
+ * Free-plan cap has shifted over time ("maximum number of projects", "free plan",
100
+ * "project limit", "upgrade to a paid plan"…), so we key off stable keywords
101
+ * rather than an exact string that would silently stop matching after a change.
102
+ *
103
+ * @param {string} error
104
+ * @returns {'login'|'free_limit'|'unknown'}
105
+ */
106
+ function classifyCreateError(error) {
107
+ const msg = (error || '').toLowerCase();
108
+ if (!msg) return 'unknown';
109
+ if (
110
+ msg.includes('login required') ||
111
+ msg.includes('not logged in') ||
112
+ msg.includes('access token') ||
113
+ msg.includes('unauthorized') ||
114
+ msg.includes('401')
115
+ ) {
116
+ return 'login';
117
+ }
118
+ const freeLimit =
119
+ /maximum number of (active )?projects/.test(msg) ||
120
+ /project limit/.test(msg) ||
121
+ (/free/.test(msg) && /(plan|tier)/.test(msg) && /(limit|maximum|reached|exceed)/.test(msg)) ||
122
+ (/reached/.test(msg) && /limit/.test(msg)) ||
123
+ /upgrade .*(plan|pro|paid)/.test(msg);
124
+ return freeLimit ? 'free_limit' : 'unknown';
125
+ }
126
+
91
127
  /**
92
128
  * Create a new Supabase project.
93
129
  * @param {string} projectName
@@ -338,7 +374,7 @@ async function setSecret(projectDir, key, value) {
338
374
  * Uses execFile to avoid shell injection on user-supplied values.
339
375
  */
340
376
  async function setSupabaseSecrets(projectDir, secrets = {}) {
341
- const { rcWebhookKey, metaAccessToken, metaDatasetId, firebaseProjectId, firebaseServiceAccountJson, llmApiKey, llmProvider, llmSystemPrompt } = secrets;
377
+ const { rcWebhookKey, metaAccessToken, metaDatasetId, firebaseProjectId, firebaseServiceAccountJson, aiApiKey, aiProvider, aiSystemPrompt, stripeSecretKey, stripeWebhookSecret, stripeProductId } = secrets;
342
378
  const steps = [];
343
379
 
344
380
  if (firebaseProjectId && String(firebaseProjectId).trim()) {
@@ -373,19 +409,34 @@ async function setSupabaseSecrets(projectDir, secrets = {}) {
373
409
  steps.push({ name: 'secret META_DATASET_ID', ok: r.ok, error: r.error });
374
410
  }
375
411
 
376
- if (llmApiKey && String(llmApiKey).trim()) {
377
- const r = await setSecret(projectDir, 'LLM_API_KEY', String(llmApiKey).trim());
378
- steps.push({ name: 'secret LLM_API_KEY', ok: r.ok, error: r.error });
412
+ if (aiApiKey && String(aiApiKey).trim()) {
413
+ const r = await setSecret(projectDir, 'AI_API_KEY', String(aiApiKey).trim());
414
+ steps.push({ name: 'secret AI_API_KEY', ok: r.ok, error: r.error });
415
+ }
416
+
417
+ if (aiProvider && String(aiProvider).trim()) {
418
+ const r = await setSecret(projectDir, 'AI_PROVIDER', String(aiProvider).trim());
419
+ steps.push({ name: 'secret AI_PROVIDER', ok: r.ok, error: r.error });
420
+ }
421
+
422
+ if (aiSystemPrompt && String(aiSystemPrompt).trim()) {
423
+ const r = await setSecret(projectDir, 'AI_SYSTEM_PROMPT', String(aiSystemPrompt).trim());
424
+ steps.push({ name: 'secret AI_SYSTEM_PROMPT', ok: r.ok, error: r.error });
425
+ }
426
+
427
+ if (stripeSecretKey && String(stripeSecretKey).trim()) {
428
+ const r = await setSecret(projectDir, 'STRIPE_SECRET_KEY', String(stripeSecretKey).trim());
429
+ steps.push({ name: 'secret STRIPE_SECRET_KEY', ok: r.ok, error: r.error });
379
430
  }
380
431
 
381
- if (llmProvider && String(llmProvider).trim()) {
382
- const r = await setSecret(projectDir, 'LLM_PROVIDER', String(llmProvider).trim());
383
- steps.push({ name: 'secret LLM_PROVIDER', ok: r.ok, error: r.error });
432
+ if (stripeWebhookSecret && String(stripeWebhookSecret).trim()) {
433
+ const r = await setSecret(projectDir, 'STRIPE_WEBHOOK_SECRET', String(stripeWebhookSecret).trim());
434
+ steps.push({ name: 'secret STRIPE_WEBHOOK_SECRET', ok: r.ok, error: r.error });
384
435
  }
385
436
 
386
- if (llmSystemPrompt && String(llmSystemPrompt).trim()) {
387
- const r = await setSecret(projectDir, 'LLM_SYSTEM_PROMPT', String(llmSystemPrompt).trim());
388
- steps.push({ name: 'secret LLM_SYSTEM_PROMPT', ok: r.ok, error: r.error });
437
+ if (stripeProductId && String(stripeProductId).trim()) {
438
+ const r = await setSecret(projectDir, 'STRIPE_PRODUCT_ID', String(stripeProductId).trim());
439
+ steps.push({ name: 'secret STRIPE_PRODUCT_ID', ok: r.ok, error: r.error });
389
440
  }
390
441
 
391
442
  return steps;
@@ -410,7 +461,7 @@ async function deployFunctions(projectDir, functionNames = []) {
410
461
  for (const name of toDeploy) {
411
462
  const fnPath = path.join(functionsDir, name);
412
463
  if (!(await fs.pathExists(fnPath))) continue;
413
- const noVerifyJwt = (name === 'llm-chat' || name === 'revenuecat-webhook' || name === 'send-push-notification') ? ' --no-verify-jwt' : '';
464
+ const noVerifyJwt = (name === 'ai-chat' || name === 'revenuecat-webhook' || name === 'send-push-notification' || name === 'stripe-webhook') ? ' --no-verify-jwt' : '';
414
465
  const result = await run(`supabase functions deploy ${name}${noVerifyJwt}`, projectDir);
415
466
  steps.push({ name: `deploy ${name}`, ok: result.ok, error: result.error });
416
467
  }
@@ -540,6 +591,7 @@ module.exports = {
540
591
  getOrgsList,
541
592
  getProjectsByOrg,
542
593
  createProject,
594
+ classifyCreateError,
543
595
  getProjectKeys,
544
596
  linkProject,
545
597
  dbPush,
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Supabase Edge Function: Admin — List Users
3
+ *
4
+ * Lists app users for the admin console. Returns the most-recent users (bounded
5
+ * to MAX_SCAN) with their subscriber status already resolved; the Flutter client
6
+ * (admin_users_tab.dart) does search / sort / pagination locally so those
7
+ * interactions are instant. Shape: { users, totalUsers, truncated } — identical
8
+ * to the Firebase Cloud Function so the shared UI stays the same.
9
+ *
10
+ * Security (two layers):
11
+ * 1. The caller is verified with THEIR OWN JWT (getUser) and must carry
12
+ * role == "admin" on their own public.users row (read under their RLS).
13
+ * 2. Only AFTER that check do we use the service role to read across all
14
+ * users — the users/subscriptions RLS exposes only a user's own row, so a
15
+ * non-admin can never reach other people's data.
16
+ *
17
+ * Deployed WITH JWT verification (the default) — see deploy.js.
18
+ */
19
+
20
+ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.47.10";
21
+
22
+ const corsHeaders = {
23
+ "Access-Control-Allow-Origin": "*",
24
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
25
+ "Access-Control-Allow-Headers": "Authorization, Content-Type",
26
+ };
27
+
28
+ // In-memory cap: load up to this many of the most recent users in one call.
29
+ // Larger apps should add a dedicated search index (see README).
30
+ const MAX_SCAN = 1000;
31
+
32
+ // A user counts as a subscriber when their subscription is currently usable.
33
+ const ACTIVE_SUBSCRIPTION_STATUSES = ["ACTIVE", "LIFETIME"];
34
+
35
+ interface AdminUser {
36
+ id: string;
37
+ email: string | null;
38
+ name: string | null;
39
+ createdAt: number | null; // epoch millis
40
+ subscriber: boolean;
41
+ }
42
+
43
+ function json(body: unknown, status: number): Response {
44
+ return new Response(JSON.stringify(body), {
45
+ status,
46
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
47
+ });
48
+ }
49
+
50
+ Deno.serve(async (req: Request) => {
51
+ if (req.method === "OPTIONS") {
52
+ return new Response(null, { status: 204, headers: corsHeaders });
53
+ }
54
+ if (req.method !== "POST") {
55
+ return json({ error: "Method not allowed" }, 405);
56
+ }
57
+
58
+ const authHeader = req.headers.get("Authorization");
59
+ if (!authHeader?.startsWith("Bearer ")) {
60
+ return json({ error: "Missing or invalid Authorization header" }, 401);
61
+ }
62
+
63
+ const supabaseUrl = Deno.env.get("SUPABASE_URL");
64
+ const anonKey = Deno.env.get("SUPABASE_ANON_KEY");
65
+ const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
66
+ if (!supabaseUrl || !anonKey || !serviceRoleKey) {
67
+ console.error("[admin-list-users] Missing SUPABASE_URL / ANON / SERVICE key");
68
+ return json({ error: "Server configuration error" }, 500);
69
+ }
70
+
71
+ // 1. Verify the caller with their own JWT.
72
+ const token = authHeader.replace("Bearer ", "");
73
+ const supabaseAuth = createClient(supabaseUrl, anonKey, {
74
+ global: { headers: { Authorization: authHeader } },
75
+ });
76
+ const {
77
+ data: { user },
78
+ error: authError,
79
+ } = await supabaseAuth.auth.getUser(token);
80
+ if (authError || !user) {
81
+ console.warn("[admin-list-users] Unauthenticated request:", authError?.message);
82
+ return json({ error: "You must be authenticated" }, 401);
83
+ }
84
+
85
+ try {
86
+ // 2. Admin gate: the caller's own row must carry role == "admin".
87
+ // This read runs under the caller's RLS (own row only) — no privilege.
88
+ const { data: caller, error: callerError } = await supabaseAuth
89
+ .from("users")
90
+ .select("role")
91
+ .eq("id", user.id)
92
+ .maybeSingle();
93
+ if (callerError || !caller || caller.role !== "admin") {
94
+ console.warn(`[admin-list-users] Non-admin request from ${user.id}`);
95
+ return json({ error: "Admin role required" }, 403);
96
+ }
97
+
98
+ // 3. Verified admin → service role can read across all users/subscriptions.
99
+ const admin = createClient(supabaseUrl, serviceRoleKey);
100
+
101
+ // Newest first; users without a creation_date sort to the end (Postgres,
102
+ // unlike Firestore, keeps NULL-date rows instead of dropping them). The
103
+ // client re-sorts anyway, so this only decides which rows survive the cap.
104
+ const { data: rows, error: rowsError } = await admin
105
+ .from("users")
106
+ .select("id, email, name, creation_date")
107
+ .order("creation_date", { ascending: false, nullsFirst: false })
108
+ .limit(MAX_SCAN);
109
+ if (rowsError) throw rowsError;
110
+
111
+ const docs = rows ?? [];
112
+ const ids = docs.map((d) => d.id);
113
+
114
+ const activeSubscribers = new Set<string>();
115
+ if (ids.length > 0) {
116
+ const { data: subs, error: subError } = await admin
117
+ .from("subscriptions")
118
+ .select("user_id, status")
119
+ .in("user_id", ids);
120
+ if (subError) throw subError;
121
+ for (const s of subs ?? []) {
122
+ if (ACTIVE_SUBSCRIPTION_STATUSES.includes(s.status)) {
123
+ activeSubscribers.add(s.user_id);
124
+ }
125
+ }
126
+ }
127
+
128
+ const users: AdminUser[] = docs.map((d) => ({
129
+ id: d.id,
130
+ email: (d.email as string) || null,
131
+ name: (d.name as string) || null,
132
+ createdAt: d.creation_date ? new Date(d.creation_date).getTime() : null,
133
+ subscriber: activeSubscribers.has(d.id),
134
+ }));
135
+
136
+ const { count } = await admin
137
+ .from("users")
138
+ .select("*", { count: "exact", head: true });
139
+ const totalUsers = count ?? users.length;
140
+
141
+ return json(
142
+ { users, totalUsers, truncated: totalUsers > users.length },
143
+ 200,
144
+ );
145
+ } catch (e) {
146
+ console.error("[admin-list-users] Error:", e);
147
+ return json({ error: "Failed to list users" }, 500);
148
+ }
149
+ });
@@ -1,23 +1,23 @@
1
1
  /**
2
- * Supabase Edge Function: LLM Chat Proxy (streaming)
2
+ * Supabase Edge 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 `supabase secrets set`):
8
- * - LLM_API_KEY: API key for OpenAI or Gemini
9
- * - LLM_PROVIDER: "openai" (default) or "gemini"
10
- * - LLM_SYSTEM_PROMPT: System prompt for the agent (optional)
8
+ * - AI_API_KEY: API key for OpenAI or Gemini
9
+ * - AI_PROVIDER: "openai" (default) or "gemini"
10
+ * - AI_SYSTEM_PROMPT: System prompt for the agent (optional)
11
11
  *
12
12
  * App dart-define:
13
- * - LLM_CHAT_ENDPOINT: https://<project-ref>.supabase.co/functions/v1/llm-chat
13
+ * - AI_CHAT_ENDPOINT: https://<project-ref>.supabase.co/functions/v1/ai-chat
14
14
  *
15
- * Deploy: supabase functions deploy llm-chat --no-verify-jwt
15
+ * Deploy: supabase functions deploy ai-chat --no-verify-jwt
16
16
  */
17
17
 
18
- const LLM_API_KEY = Deno.env.get("LLM_API_KEY") ?? "";
19
- const LLM_PROVIDER = Deno.env.get("LLM_PROVIDER") ?? "openai";
20
- const LLM_SYSTEM_PROMPT = Deno.env.get("LLM_SYSTEM_PROMPT") ?? "";
18
+ const AI_API_KEY = Deno.env.get("AI_API_KEY") ?? "";
19
+ const AI_PROVIDER = Deno.env.get("AI_PROVIDER") ?? "openai";
20
+ const AI_SYSTEM_PROMPT = Deno.env.get("AI_SYSTEM_PROMPT") ?? "";
21
21
  const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
22
22
  const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
23
23
 
@@ -41,14 +41,14 @@ interface ChatMessage {
41
41
  // Pipes the OpenAI SSE stream directly to the Deno response.
42
42
  function streamOpenAI(message: string, history: ChatMessage[]): Promise<Response> {
43
43
  const messages: { role: string; content: string }[] = [];
44
- if (LLM_SYSTEM_PROMPT) messages.push({ role: "system", content: LLM_SYSTEM_PROMPT });
44
+ if (AI_SYSTEM_PROMPT) messages.push({ role: "system", content: AI_SYSTEM_PROMPT });
45
45
  messages.push(...history);
46
46
  messages.push({ role: "user", content: message });
47
47
 
48
48
  return fetch("https://api.openai.com/v1/chat/completions", {
49
49
  method: "POST",
50
50
  headers: {
51
- Authorization: `Bearer ${LLM_API_KEY}`,
51
+ Authorization: `Bearer ${AI_API_KEY}`,
52
52
  "Content-Type": "application/json",
53
53
  },
54
54
  body: JSON.stringify({ model: "gpt-4o-mini", messages, stream: true }),
@@ -74,13 +74,13 @@ function streamGemini(message: string, history: ChatMessage[]): Promise<Response
74
74
  ];
75
75
 
76
76
  const body: Record<string, unknown> = { contents };
77
- if (LLM_SYSTEM_PROMPT) {
78
- body.systemInstruction = { parts: [{ text: LLM_SYSTEM_PROMPT }] };
77
+ if (AI_SYSTEM_PROMPT) {
78
+ body.systemInstruction = { parts: [{ text: AI_SYSTEM_PROMPT }] };
79
79
  }
80
80
 
81
81
  // alt=sse makes Gemini return the same SSE format as OpenAI
82
82
  return fetch(
83
- `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:streamGenerateContent?key=${LLM_API_KEY}&alt=sse`,
83
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:streamGenerateContent?key=${AI_API_KEY}&alt=sse`,
84
84
  {
85
85
  method: "POST",
86
86
  headers: { "Content-Type": "application/json" },
@@ -136,21 +136,21 @@ Deno.serve(async (req: Request) => {
136
136
  return Response.json({ error: "Missing message" }, { status: 400 });
137
137
  }
138
138
 
139
- if (!LLM_API_KEY) {
139
+ if (!AI_API_KEY) {
140
140
  return Response.json(
141
- { error: "LLM_API_KEY not configured. Run: supabase secrets set LLM_API_KEY=..." },
141
+ { error: "AI_API_KEY not configured. Run: supabase secrets set AI_API_KEY=..." },
142
142
  { status: 500 }
143
143
  );
144
144
  }
145
145
 
146
146
  try {
147
- return LLM_PROVIDER === "gemini"
147
+ return AI_PROVIDER === "gemini"
148
148
  ? await streamGemini(message, history)
149
149
  : await streamOpenAI(message, history);
150
150
  } catch (err) {
151
- console.error("[llm-chat]", err);
151
+ console.error("[ai-chat]", err);
152
152
  // Send error as SSE event so the Flutter client can surface it
153
- const errorEvent = `data: ${JSON.stringify({ error: "LLM request failed" })}\n\n`;
153
+ const errorEvent = `data: ${JSON.stringify({ error: "AI request failed" })}\n\n`;
154
154
  return new Response(errorEvent, { headers: SSE_HEADERS });
155
155
  }
156
156
  });
@@ -36,6 +36,8 @@ const Stores = {
36
36
  PLAY_STORE: "PLAY_STORE",
37
37
  APPLE_STORE: "APPLE_STORE",
38
38
  EARLY_BIRD: "EARLY_BIRD",
39
+ // Subscription purchased on the web via Stripe (written by stripe-webhook).
40
+ STRIPE: "STRIPE",
39
41
  } as const;
40
42
 
41
43
  type SubscriptionStatusType = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus];
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Supabase Edge Function: Stripe — Create Checkout Session
3
+ *
4
+ * Creates a hosted Stripe Checkout session (mode=subscription) for the
5
+ * authenticated user and returns its URL. The user identity is taken from the
6
+ * verified JWT, never trusted from the request body, so a client cannot check
7
+ * out on behalf of someone else.
8
+ *
9
+ * Deployed WITH JWT verification.
10
+ *
11
+ * Secrets required:
12
+ * - STRIPE_SECRET_KEY: sk_test_... / sk_live_...
13
+ * - SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY (auto-provided)
14
+ *
15
+ * Body: { priceId: string, successUrl?: string, cancelUrl?: string }
16
+ */
17
+
18
+ import Stripe from "npm:stripe@18";
19
+ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.47.10";
20
+
21
+ const corsHeaders = {
22
+ "Access-Control-Allow-Origin": "*",
23
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
24
+ "Access-Control-Allow-Headers": "Authorization, Content-Type",
25
+ };
26
+
27
+ // Maps a Supabase auth user -> its Stripe customer id.
28
+ const CUSTOMERS_TABLE = "stripe_customers";
29
+
30
+ function trialDaysFor(price: Stripe.Price, product: Stripe.Product): number | null {
31
+ const meta = price.metadata?.trial_days ?? product.metadata?.trial_days;
32
+ return meta ? Number(meta) : null;
33
+ }
34
+
35
+ async function getUid(req: Request, supabaseUrl: string, anonKey: string): Promise<string | null> {
36
+ const authHeader = req.headers.get("Authorization");
37
+ if (!authHeader?.startsWith("Bearer ")) return null;
38
+ const token = authHeader.replace("Bearer ", "");
39
+ const client = createClient(supabaseUrl, anonKey, {
40
+ global: { headers: { Authorization: authHeader } },
41
+ });
42
+ const { data: { user }, error } = await client.auth.getUser(token);
43
+ if (error || !user) return null;
44
+ return user.id;
45
+ }
46
+
47
+ // deno-lint-ignore no-explicit-any
48
+ async function getOrCreateCustomer(stripe: Stripe, admin: any, uid: string): Promise<string> {
49
+ const { data } = await admin
50
+ .from(CUSTOMERS_TABLE)
51
+ .select("customer_id")
52
+ .eq("user_id", uid)
53
+ .maybeSingle();
54
+ const existing = data?.customer_id as string | undefined;
55
+ if (existing) return existing;
56
+ const customer = await stripe.customers.create({ metadata: { supabaseUID: uid } });
57
+ await admin.from(CUSTOMERS_TABLE).upsert({ user_id: uid, customer_id: customer.id });
58
+ return customer.id;
59
+ }
60
+
61
+ Deno.serve(async (req: Request) => {
62
+ if (req.method === "OPTIONS") {
63
+ return new Response(null, { status: 204, headers: corsHeaders });
64
+ }
65
+ if (req.method !== "POST") {
66
+ return Response.json({ error: "Method not allowed" }, { status: 405, headers: corsHeaders });
67
+ }
68
+
69
+ const secretKey = Deno.env.get("STRIPE_SECRET_KEY");
70
+ const supabaseUrl = Deno.env.get("SUPABASE_URL");
71
+ const anonKey = Deno.env.get("SUPABASE_ANON_KEY");
72
+ const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
73
+ if (!secretKey || !supabaseUrl || !anonKey || !serviceRoleKey) {
74
+ return Response.json({ error: "Server configuration error" }, { status: 500, headers: corsHeaders });
75
+ }
76
+
77
+ const uid = await getUid(req, supabaseUrl, anonKey);
78
+ if (!uid) {
79
+ return Response.json({ error: "Sign in required" }, { status: 401, headers: corsHeaders });
80
+ }
81
+
82
+ let body: { priceId?: string; successUrl?: string; cancelUrl?: string };
83
+ try {
84
+ body = await req.json();
85
+ } catch {
86
+ return Response.json({ error: "Invalid JSON" }, { status: 400, headers: corsHeaders });
87
+ }
88
+ const priceId = body.priceId;
89
+ if (!priceId) {
90
+ return Response.json({ error: "priceId is required" }, { status: 400, headers: corsHeaders });
91
+ }
92
+ const successUrl = body.successUrl ?? "";
93
+ const cancelUrl = body.cancelUrl ?? successUrl;
94
+
95
+ try {
96
+ const stripe = new Stripe(secretKey);
97
+ const admin = createClient(supabaseUrl, serviceRoleKey);
98
+ const customerId = await getOrCreateCustomer(stripe, admin, uid);
99
+
100
+ const price = await stripe.prices.retrieve(priceId, { expand: ["product"] });
101
+ const trialDays = trialDaysFor(price, price.product as Stripe.Product);
102
+
103
+ const session = await stripe.checkout.sessions.create({
104
+ mode: "subscription",
105
+ customer: customerId,
106
+ client_reference_id: uid,
107
+ line_items: [{ price: priceId, quantity: 1 }],
108
+ success_url: successUrl,
109
+ cancel_url: cancelUrl,
110
+ subscription_data: {
111
+ metadata: { supabaseUID: uid },
112
+ ...(trialDays ? { trial_period_days: trialDays } : {}),
113
+ },
114
+ });
115
+ return Response.json({ url: session.url }, { headers: corsHeaders });
116
+ } catch (err) {
117
+ console.error("[stripe-create-checkout-session]", err);
118
+ return Response.json(
119
+ { error: err instanceof Error ? err.message : "Internal error" },
120
+ { status: 500, headers: corsHeaders },
121
+ );
122
+ }
123
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Supabase Edge Function: Stripe — Create Customer Portal Session
3
+ *
4
+ * Creates a Stripe Customer Portal session (manage / cancel) for the
5
+ * authenticated user and returns its URL. The user is identified by the
6
+ * verified JWT and the Stripe customer is looked up server-side.
7
+ *
8
+ * Deployed WITH JWT verification.
9
+ *
10
+ * Secrets required:
11
+ * - STRIPE_SECRET_KEY
12
+ * - SUPABASE_URL / SUPABASE_ANON_KEY / SUPABASE_SERVICE_ROLE_KEY (auto-provided)
13
+ *
14
+ * Body: { returnUrl?: string }
15
+ */
16
+
17
+ import Stripe from "npm:stripe@18";
18
+ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.47.10";
19
+
20
+ const corsHeaders = {
21
+ "Access-Control-Allow-Origin": "*",
22
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
23
+ "Access-Control-Allow-Headers": "Authorization, Content-Type",
24
+ };
25
+
26
+ const CUSTOMERS_TABLE = "stripe_customers";
27
+
28
+ async function getUid(req: Request, supabaseUrl: string, anonKey: string): Promise<string | null> {
29
+ const authHeader = req.headers.get("Authorization");
30
+ if (!authHeader?.startsWith("Bearer ")) return null;
31
+ const token = authHeader.replace("Bearer ", "");
32
+ const client = createClient(supabaseUrl, anonKey, {
33
+ global: { headers: { Authorization: authHeader } },
34
+ });
35
+ const { data: { user }, error } = await client.auth.getUser(token);
36
+ if (error || !user) return null;
37
+ return user.id;
38
+ }
39
+
40
+ Deno.serve(async (req: Request) => {
41
+ if (req.method === "OPTIONS") {
42
+ return new Response(null, { status: 204, headers: corsHeaders });
43
+ }
44
+ if (req.method !== "POST") {
45
+ return Response.json({ error: "Method not allowed" }, { status: 405, headers: corsHeaders });
46
+ }
47
+
48
+ const secretKey = Deno.env.get("STRIPE_SECRET_KEY");
49
+ const supabaseUrl = Deno.env.get("SUPABASE_URL");
50
+ const anonKey = Deno.env.get("SUPABASE_ANON_KEY");
51
+ const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
52
+ if (!secretKey || !supabaseUrl || !anonKey || !serviceRoleKey) {
53
+ return Response.json({ error: "Server configuration error" }, { status: 500, headers: corsHeaders });
54
+ }
55
+
56
+ const uid = await getUid(req, supabaseUrl, anonKey);
57
+ if (!uid) {
58
+ return Response.json({ error: "Sign in required" }, { status: 401, headers: corsHeaders });
59
+ }
60
+
61
+ let returnUrl = "";
62
+ try {
63
+ const body = await req.json();
64
+ returnUrl = (body?.returnUrl as string | undefined) ?? "";
65
+ } catch {
66
+ // body is optional
67
+ }
68
+
69
+ try {
70
+ const admin = createClient(supabaseUrl, serviceRoleKey);
71
+ const { data } = await admin
72
+ .from(CUSTOMERS_TABLE)
73
+ .select("customer_id")
74
+ .eq("user_id", uid)
75
+ .maybeSingle();
76
+ const customerId = data?.customer_id as string | undefined;
77
+ if (!customerId) {
78
+ return Response.json(
79
+ { error: "No Stripe customer for user" },
80
+ { status: 400, headers: corsHeaders },
81
+ );
82
+ }
83
+
84
+ const stripe = new Stripe(secretKey);
85
+ const session = await stripe.billingPortal.sessions.create({
86
+ customer: customerId,
87
+ return_url: returnUrl,
88
+ });
89
+ return Response.json({ url: session.url }, { headers: corsHeaders });
90
+ } catch (err) {
91
+ console.error("[stripe-create-portal-session]", err);
92
+ return Response.json(
93
+ { error: err instanceof Error ? err.message : "Internal error" },
94
+ { status: 500, headers: corsHeaders },
95
+ );
96
+ }
97
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Supabase Edge Function: Stripe — List Prices
3
+ *
4
+ * Returns the active recurring Stripe prices mapped to the paywall offer
5
+ * contract used by the Flutter client (mirrors the Firebase `listPrices`
6
+ * callable). The Stripe secret key never leaves the server.
7
+ *
8
+ * Deployed WITH JWT verification (the platform checks the caller's token), so
9
+ * only an authenticated app user can reach it.
10
+ *
11
+ * Secrets required (set via `supabase secrets set`):
12
+ * - STRIPE_SECRET_KEY: sk_test_... / sk_live_...
13
+ * - STRIPE_PRODUCT_ID (optional): restrict prices to a single Stripe product.
14
+ */
15
+
16
+ import Stripe from "npm:stripe@18";
17
+
18
+ const corsHeaders = {
19
+ "Access-Control-Allow-Origin": "*",
20
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
21
+ "Access-Control-Allow-Headers": "Authorization, Content-Type",
22
+ };
23
+
24
+ function trialDaysFor(price: Stripe.Price, product: Stripe.Product): number | null {
25
+ const meta = price.metadata?.trial_days ?? product.metadata?.trial_days;
26
+ return meta ? Number(meta) : null;
27
+ }
28
+
29
+ Deno.serve(async (req: Request) => {
30
+ if (req.method === "OPTIONS") {
31
+ return new Response(null, { status: 204, headers: corsHeaders });
32
+ }
33
+
34
+ const secretKey = Deno.env.get("STRIPE_SECRET_KEY");
35
+ if (!secretKey) {
36
+ return Response.json(
37
+ { error: "STRIPE_SECRET_KEY not configured. Run: supabase secrets set STRIPE_SECRET_KEY=..." },
38
+ { status: 500, headers: corsHeaders },
39
+ );
40
+ }
41
+
42
+ const stripe = new Stripe(secretKey);
43
+ try {
44
+ const params: Stripe.PriceListParams = {
45
+ active: true,
46
+ type: "recurring",
47
+ expand: ["data.product"],
48
+ limit: 100,
49
+ };
50
+ const productFilter = Deno.env.get("STRIPE_PRODUCT_ID");
51
+ if (productFilter) params.product = productFilter;
52
+
53
+ const prices = await stripe.prices.list(params);
54
+ const offers = prices.data
55
+ .filter((p) => Boolean(p.recurring))
56
+ .map((p) => {
57
+ const product = p.product as Stripe.Product;
58
+ const features = (product.marketing_features ?? [])
59
+ .map((f) => f.name)
60
+ .filter((n): n is string => Boolean(n));
61
+ return {
62
+ priceId: p.id,
63
+ productId: typeof p.product === "string" ? p.product : product.id,
64
+ productName: product.name ?? "",
65
+ description: product.description ?? "",
66
+ unitAmount: p.unit_amount ?? 0,
67
+ currency: p.currency,
68
+ interval: p.recurring?.interval ?? "month",
69
+ intervalCount: p.recurring?.interval_count ?? 1,
70
+ trialDays: trialDaysFor(p, product),
71
+ features,
72
+ };
73
+ });
74
+
75
+ return Response.json(offers, { headers: corsHeaders });
76
+ } catch (err) {
77
+ console.error("[stripe-list-prices]", err);
78
+ return Response.json(
79
+ { error: err instanceof Error ? err.message : "Internal error" },
80
+ { status: 500, headers: corsHeaders },
81
+ );
82
+ }
83
+ });