kasy-cli 1.21.8 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +86 -38
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- 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
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +14 -6
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- 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
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +2 -2
- package/lib/utils/i18n/messages-en.js +50 -35
- package/lib/utils/i18n/messages-es.js +50 -35
- package/lib/utils/i18n/messages-pt.js +52 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +4 -5
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Edge Function: Stripe Webhook
|
|
3
|
+
*
|
|
4
|
+
* The ONLY writer of the `subscriptions` table for Stripe (web) subscriptions.
|
|
5
|
+
* Verifies the Stripe signature, then upserts the user's subscription with
|
|
6
|
+
* store='STRIPE'. The user id is read from the subscription metadata
|
|
7
|
+
* (supabaseUID), which the checkout function set server-side.
|
|
8
|
+
*
|
|
9
|
+
* Deployed WITHOUT JWT verification (Stripe calls it with a stripe-signature,
|
|
10
|
+
* not a Supabase JWT) — authenticity is enforced by the signature check.
|
|
11
|
+
*
|
|
12
|
+
* Secrets required (set via `supabase secrets set`):
|
|
13
|
+
* - STRIPE_SECRET_KEY
|
|
14
|
+
* - STRIPE_WEBHOOK_SECRET: whsec_... (from the Stripe webhook endpoint config)
|
|
15
|
+
* - SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY (auto-provided)
|
|
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 SubscriptionStatus = {
|
|
22
|
+
ACTIVE: "ACTIVE",
|
|
23
|
+
EXPIRED: "EXPIRED",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
function statusFromStripe(sub: Stripe.Subscription): string {
|
|
27
|
+
switch (sub.status) {
|
|
28
|
+
case "active":
|
|
29
|
+
case "trialing":
|
|
30
|
+
return SubscriptionStatus.ACTIVE;
|
|
31
|
+
default:
|
|
32
|
+
// canceled, unpaid, past_due, incomplete, incomplete_expired, paused
|
|
33
|
+
return SubscriptionStatus.EXPIRED;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// In Stripe API v18 the billing period lives on each subscription item; older
|
|
38
|
+
// versions exposed it on the subscription. Read whichever is present.
|
|
39
|
+
function periodEndMs(sub: Stripe.Subscription): number | null {
|
|
40
|
+
const item = sub.items?.data?.[0] as { current_period_end?: number } | undefined;
|
|
41
|
+
const fromItem = item?.current_period_end;
|
|
42
|
+
const fromSub = (sub as unknown as { current_period_end?: number }).current_period_end;
|
|
43
|
+
const sec = fromItem ?? fromSub;
|
|
44
|
+
return sec ? sec * 1000 : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Deno.serve(async (req: Request) => {
|
|
48
|
+
const signature = req.headers.get("stripe-signature");
|
|
49
|
+
if (!signature) return new Response("Missing signature", { status: 400 });
|
|
50
|
+
|
|
51
|
+
const secretKey = Deno.env.get("STRIPE_SECRET_KEY");
|
|
52
|
+
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
|
|
53
|
+
const supabaseUrl = Deno.env.get("SUPABASE_URL");
|
|
54
|
+
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
55
|
+
if (!secretKey || !webhookSecret || !supabaseUrl || !serviceRoleKey) {
|
|
56
|
+
console.error("[stripe-webhook] missing secrets");
|
|
57
|
+
return new Response("Server configuration error", { status: 500 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const stripe = new Stripe(secretKey);
|
|
61
|
+
const bodyText = await req.text();
|
|
62
|
+
|
|
63
|
+
let event: Stripe.Event;
|
|
64
|
+
try {
|
|
65
|
+
event = await stripe.webhooks.constructEventAsync(bodyText, signature, webhookSecret);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.log(`[stripe-webhook] signature verification failed: ${e}`);
|
|
68
|
+
return new Response("Invalid signature", { status: 400 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handled = [
|
|
72
|
+
"customer.subscription.created",
|
|
73
|
+
"customer.subscription.updated",
|
|
74
|
+
"customer.subscription.deleted",
|
|
75
|
+
];
|
|
76
|
+
if (!handled.includes(event.type)) {
|
|
77
|
+
return new Response("ok");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sub = event.data.object as Stripe.Subscription;
|
|
81
|
+
const uid = sub.metadata?.supabaseUID;
|
|
82
|
+
if (!uid) {
|
|
83
|
+
console.log("[stripe-webhook] subscription without supabaseUID metadata, skipping");
|
|
84
|
+
return new Response("ok");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const supabase = createClient(supabaseUrl, serviceRoleKey);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// The subscriptions row references public.users(id). Skip unknown users.
|
|
91
|
+
const { data: userRows } = await supabase
|
|
92
|
+
.from("users")
|
|
93
|
+
.select("id")
|
|
94
|
+
.eq("id", uid)
|
|
95
|
+
.limit(1);
|
|
96
|
+
if (!userRows?.length) {
|
|
97
|
+
console.log("[stripe-webhook] user not found:", uid);
|
|
98
|
+
return new Response("ok");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const item = sub.items?.data?.[0];
|
|
102
|
+
const priceId = item?.price?.id ?? "";
|
|
103
|
+
const ms = periodEndMs(sub);
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
const payload = {
|
|
106
|
+
status: statusFromStripe(sub),
|
|
107
|
+
last_update_date: now,
|
|
108
|
+
period_end_date: ms ? new Date(ms).toISOString() : null,
|
|
109
|
+
sku_id: priceId,
|
|
110
|
+
offer_id: priceId,
|
|
111
|
+
store: "STRIPE",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const { data: existing } = await supabase
|
|
115
|
+
.from("subscriptions")
|
|
116
|
+
.select("user_id")
|
|
117
|
+
.eq("user_id", uid)
|
|
118
|
+
.maybeSingle();
|
|
119
|
+
|
|
120
|
+
if (existing) {
|
|
121
|
+
const { error: updateErr } = await supabase
|
|
122
|
+
.from("subscriptions")
|
|
123
|
+
.update(payload)
|
|
124
|
+
.eq("user_id", uid);
|
|
125
|
+
if (updateErr) console.error("[stripe-webhook] update error:", updateErr);
|
|
126
|
+
} else {
|
|
127
|
+
const { error: insertErr } = await supabase
|
|
128
|
+
.from("subscriptions")
|
|
129
|
+
.insert({ user_id: uid, creation_date: now, ...payload });
|
|
130
|
+
if (insertErr) console.error("[stripe-webhook] insert error:", insertErr);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Response("ok");
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error("[stripe-webhook] error:", err);
|
|
136
|
+
return new Response(err instanceof Error ? err.message : "Internal error", { status: 500 });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Supabase project generator.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Thin wrapper over generateProject().
|
|
5
|
+
* The common generation logic (copy, pub get, slang, build_runner, flutterfire)
|
|
6
|
+
* lives in cli/lib/scaffold/generate.js.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
* 5.
|
|
8
|
+
* Specific hook (applyBackendSetup):
|
|
9
|
+
* 1. Applies supabase/patch/ over the copied Firebase template
|
|
10
|
+
* 2. Replaces pubspec.yaml with the Supabase template (pubspec.yaml.tpl)
|
|
11
|
+
* 3. Removes Firebase-only artifacts (e.g. GoogleService-Info.plist paths)
|
|
12
|
+
* 4. Writes environment overrides (supabaseUrl, supabaseAnonKey)
|
|
13
|
+
* 5. Copies config.toml, migrations and edge-functions for the Supabase backend
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
const fs = require('fs-extra');
|
|
18
18
|
const { applyPatch, copyWithTokens } = require('../../engine');
|
|
19
19
|
const { generateProject } = require('../../generate');
|
|
20
|
-
const {
|
|
20
|
+
const { writeEnvironmentsOverrides } = require('../../shared/generator-utils');
|
|
21
21
|
const { removeBackendSpecificArtifacts } = require('../../shared/backend-config');
|
|
22
22
|
|
|
23
23
|
const SUPABASE_PATCH_DIR = path.join(__dirname, 'patch');
|
|
@@ -40,7 +40,7 @@ async function generateSupabaseProject(targetDir, options) {
|
|
|
40
40
|
// 1. Patch Supabase (auth, storage, notifications, etc.)
|
|
41
41
|
const { filesApplied } = await applyPatch(SUPABASE_PATCH_DIR, dir, tokens, pathReplacements);
|
|
42
42
|
|
|
43
|
-
// 1b. README
|
|
43
|
+
// 1b. Backend README (excluded from applyPatch) — copy it manually
|
|
44
44
|
const language = opts.language ?? 'pt';
|
|
45
45
|
const readmeLang = ['en', 'pt', 'es'].includes(language) ? language : 'en';
|
|
46
46
|
const candidates = [
|
|
@@ -54,7 +54,7 @@ async function generateSupabaseProject(targetDir, options) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// 2.
|
|
57
|
+
// 2. Replace pubspec.yaml with the Supabase deps
|
|
58
58
|
const pubspecContent = await fs.readFile(SUPABASE_PUBSPEC, 'utf8');
|
|
59
59
|
let pubspecReplaced = pubspecContent;
|
|
60
60
|
for (const [from, to] of Object.entries(tokens)) {
|
|
@@ -62,20 +62,20 @@ async function generateSupabaseProject(targetDir, options) {
|
|
|
62
62
|
}
|
|
63
63
|
await fs.outputFile(path.join(dir, 'pubspec.yaml'), pubspecReplaced, 'utf8');
|
|
64
64
|
|
|
65
|
-
// 3.
|
|
65
|
+
// 3. Remove Firebase-only artifacts
|
|
66
66
|
await removeBackendSpecificArtifacts(dir, 'supabase', opts.modules ?? []);
|
|
67
67
|
|
|
68
|
-
// 4.
|
|
69
|
-
await
|
|
68
|
+
// 4. Write environment overrides with the Supabase credentials
|
|
69
|
+
await writeEnvironmentsOverrides(dir, 'supabase', tokens, {
|
|
70
70
|
supabaseUrl: opts.supabaseUrl,
|
|
71
71
|
supabaseAnonKey: opts.supabaseAnonKey,
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
// 5.
|
|
74
|
+
// 5. Copy the Supabase infrastructure files
|
|
75
75
|
const supabaseDir = path.join(dir, 'supabase');
|
|
76
76
|
await fs.ensureDir(supabaseDir);
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Extra tokens for substitution in the SQL migrations
|
|
79
79
|
const supabaseTokens = {
|
|
80
80
|
...tokens,
|
|
81
81
|
'supabaseplaceholder.supabase.co': (opts.supabaseUrl || '').replace(/^https?:\/\//, ''),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- AI chat history (one user has many conversations, each with many messages).
|
|
2
|
+
|
|
3
|
+
-- Conversations. The last message is denormalized onto the row so the list can
|
|
4
|
+
-- be rendered with a single query (no need to read each conversation's messages
|
|
5
|
+
-- just to show a preview + timestamp).
|
|
6
|
+
CREATE TABLE IF NOT EXISTS public.ai_conversations (
|
|
7
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
+
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
9
|
+
last_message_role TEXT CHECK (last_message_role IN ('user', 'assistant')),
|
|
10
|
+
last_message_content TEXT,
|
|
11
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- Row Level Security: users can only access their own conversations.
|
|
16
|
+
ALTER TABLE public.ai_conversations ENABLE ROW LEVEL SECURITY;
|
|
17
|
+
|
|
18
|
+
CREATE POLICY "Users can manage their own ai conversations"
|
|
19
|
+
ON public.ai_conversations
|
|
20
|
+
FOR ALL
|
|
21
|
+
USING (auth.uid() = user_id)
|
|
22
|
+
WITH CHECK (auth.uid() = user_id);
|
|
23
|
+
|
|
24
|
+
-- Index for listing a user's conversations, most recently updated first.
|
|
25
|
+
CREATE INDEX IF NOT EXISTS ai_conversations_user_updated_at_idx
|
|
26
|
+
ON public.ai_conversations (user_id, updated_at DESC);
|
|
27
|
+
|
|
28
|
+
-- Messages. Each row stores one message (user or assistant) inside a
|
|
29
|
+
-- conversation. user_id is kept for a simple RLS policy.
|
|
30
|
+
CREATE TABLE IF NOT EXISTS public.ai_messages (
|
|
31
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
32
|
+
conversation_id UUID NOT NULL REFERENCES public.ai_conversations(id) ON DELETE CASCADE,
|
|
33
|
+
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
34
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
35
|
+
content TEXT NOT NULL,
|
|
36
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- Row Level Security: users can only access their own messages.
|
|
40
|
+
ALTER TABLE public.ai_messages ENABLE ROW LEVEL SECURITY;
|
|
41
|
+
|
|
42
|
+
CREATE POLICY "Users can manage their own ai messages"
|
|
43
|
+
ON public.ai_messages
|
|
44
|
+
FOR ALL
|
|
45
|
+
USING (auth.uid() = user_id)
|
|
46
|
+
WITH CHECK (auth.uid() = user_id);
|
|
47
|
+
|
|
48
|
+
-- Index for fast ordered queries within a conversation.
|
|
49
|
+
CREATE INDEX IF NOT EXISTS ai_messages_conversation_created_at_idx
|
|
50
|
+
ON public.ai_messages (conversation_id, created_at ASC);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Stripe (web) subscriptions support + subscription write hardening.
|
|
2
|
+
--
|
|
3
|
+
-- 1. `stripe_customers` maps a Supabase auth user to its single Stripe customer
|
|
4
|
+
-- id, so the checkout/portal Edge Functions can find-or-create one customer
|
|
5
|
+
-- per user.
|
|
6
|
+
-- 2. Hardens `subscriptions` so ONLY the server (service role, used by the
|
|
7
|
+
-- Stripe and RevenueCat webhooks) can write it. The client can only read its
|
|
8
|
+
-- own row. This prevents a client from forging a premium subscription.
|
|
9
|
+
-- Previously a `FOR ALL` policy let a client insert/update its own row.
|
|
10
|
+
|
|
11
|
+
-- Map auth user -> Stripe customer id. Written only by the server (service role,
|
|
12
|
+
-- which bypasses RLS). Clients may read their own mapping but never write it.
|
|
13
|
+
CREATE TABLE IF NOT EXISTS public.stripe_customers (
|
|
14
|
+
user_id UUID PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE,
|
|
15
|
+
customer_id TEXT NOT NULL,
|
|
16
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_stripe_customers_customer_id
|
|
20
|
+
ON public.stripe_customers(customer_id);
|
|
21
|
+
|
|
22
|
+
ALTER TABLE public.stripe_customers ENABLE ROW LEVEL SECURITY;
|
|
23
|
+
|
|
24
|
+
-- Read-only for the owning user; no insert/update/delete policy => clients
|
|
25
|
+
-- cannot write (only the service role, which bypasses RLS, can).
|
|
26
|
+
DROP POLICY IF EXISTS "Stripe customers read own" ON public.stripe_customers;
|
|
27
|
+
CREATE POLICY "Stripe customers read own" ON public.stripe_customers
|
|
28
|
+
FOR SELECT USING (auth.uid() = user_id);
|
|
29
|
+
|
|
30
|
+
-- Harden subscriptions: clients read their own row only; all writes come from
|
|
31
|
+
-- the webhook via the service role. Replaces the previous FOR ALL policy that
|
|
32
|
+
-- allowed a client to insert/update (forge) its own subscription.
|
|
33
|
+
DROP POLICY IF EXISTS "Subscriptions own" ON public.subscriptions;
|
|
34
|
+
DROP POLICY IF EXISTS "Subscriptions read own" ON public.subscriptions;
|
|
35
|
+
CREATE POLICY "Subscriptions read own" ON public.subscriptions
|
|
36
|
+
FOR SELECT USING (auth.uid() = user_id);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- Admin role support
|
|
2
|
+
--
|
|
3
|
+
-- Adds a server-only `role` column to public.users. null/absent = normal user,
|
|
4
|
+
-- 'admin' = administrator (unlocks the admin console's server data). The column
|
|
5
|
+
-- can be written ONLY by the service role (Edge Functions) or the dashboard /
|
|
6
|
+
-- SQL editor — never by the app client, so a user can never promote themselves
|
|
7
|
+
-- to admin. This mirrors the Firebase rule that blocks `role` writes.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS role TEXT;
|
|
10
|
+
|
|
11
|
+
-- Guard trigger: the app client (anon / authenticated) can update its own row
|
|
12
|
+
-- but never the `role` column. Privileged roles pass straight through:
|
|
13
|
+
-- - service_role : used by Edge Functions and the dashboard
|
|
14
|
+
-- - postgres / supabase_admin : migrations and the SQL editor
|
|
15
|
+
-- so an admin can still assign roles by hand.
|
|
16
|
+
CREATE OR REPLACE FUNCTION public.enforce_role_immutable()
|
|
17
|
+
RETURNS TRIGGER
|
|
18
|
+
LANGUAGE plpgsql
|
|
19
|
+
AS $$
|
|
20
|
+
BEGIN
|
|
21
|
+
IF current_user IN ('service_role', 'postgres', 'supabase_admin') THEN
|
|
22
|
+
RETURN NEW; -- privileged caller: allow the change
|
|
23
|
+
END IF;
|
|
24
|
+
|
|
25
|
+
IF TG_OP = 'INSERT' THEN
|
|
26
|
+
NEW.role := NULL; -- clients never set a role on signup
|
|
27
|
+
ELSIF NEW.role IS DISTINCT FROM OLD.role THEN
|
|
28
|
+
RAISE EXCEPTION 'The role field can only be changed by the server';
|
|
29
|
+
END IF;
|
|
30
|
+
|
|
31
|
+
RETURN NEW;
|
|
32
|
+
END;
|
|
33
|
+
$$;
|
|
34
|
+
|
|
35
|
+
DROP TRIGGER IF EXISTS users_enforce_role_immutable ON public.users;
|
|
36
|
+
CREATE TRIGGER users_enforce_role_immutable
|
|
37
|
+
BEFORE INSERT OR UPDATE ON public.users
|
|
38
|
+
FOR EACH ROW EXECUTE FUNCTION public.enforce_role_immutable();
|
|
39
|
+
|
|
40
|
+
-- Admin helper: true when the current caller is an administrator. SECURITY
|
|
41
|
+
-- DEFINER so it reads the role regardless of the caller's own RLS (and avoids
|
|
42
|
+
-- policy recursion). Used by admin-only policies below.
|
|
43
|
+
CREATE OR REPLACE FUNCTION public.is_admin()
|
|
44
|
+
RETURNS BOOLEAN
|
|
45
|
+
LANGUAGE sql
|
|
46
|
+
SECURITY DEFINER
|
|
47
|
+
SET search_path = public
|
|
48
|
+
STABLE
|
|
49
|
+
AS $$
|
|
50
|
+
SELECT EXISTS (
|
|
51
|
+
SELECT 1 FROM public.users
|
|
52
|
+
WHERE id = auth.uid() AND role = 'admin'
|
|
53
|
+
);
|
|
54
|
+
$$;
|
|
55
|
+
|
|
56
|
+
-- Feature requests moderation (admin console "Requests" tab): only admins can
|
|
57
|
+
-- toggle visibility (active) or edit the localized texts. Voting still flows
|
|
58
|
+
-- through the feature_votes triggers, so this admin policy is the ONLY way a
|
|
59
|
+
-- client can UPDATE feature_requests directly.
|
|
60
|
+
DROP POLICY IF EXISTS "Feature requests admin update" ON public.feature_requests;
|
|
61
|
+
CREATE POLICY "Feature requests admin update" ON public.feature_requests
|
|
62
|
+
FOR UPDATE USING (public.is_admin()) WITH CHECK (public.is_admin());
|