kasy-cli 1.21.9 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +80 -37
- 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/generator.js +5 -5
- 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 +22 -6
- package/lib/utils/env-tools.js +7 -0
- package/lib/utils/flutter-install.js +114 -0
- package/lib/utils/i18n/messages-en.js +52 -35
- package/lib/utils/i18n/messages-es.js +52 -35
- package/lib/utils/i18n/messages-pt.js +54 -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} +702 -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 +136 -23
- 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 +54 -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 +53 -14
- 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 +128 -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 +5 -6
- 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
|
@@ -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,
|
|
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 (
|
|
377
|
-
const r = await setSecret(projectDir, '
|
|
378
|
-
steps.push({ name: 'secret
|
|
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 (
|
|
382
|
-
const r = await setSecret(projectDir, '
|
|
383
|
-
steps.push({ name: 'secret
|
|
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 (
|
|
387
|
-
const r = await setSecret(projectDir, '
|
|
388
|
-
steps.push({ name: 'secret
|
|
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 === '
|
|
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:
|
|
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
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
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
|
-
* -
|
|
13
|
+
* - AI_CHAT_ENDPOINT: https://<project-ref>.supabase.co/functions/v1/ai-chat
|
|
14
14
|
*
|
|
15
|
-
* Deploy: supabase functions deploy
|
|
15
|
+
* Deploy: supabase functions deploy ai-chat --no-verify-jwt
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
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 (
|
|
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 ${
|
|
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 (
|
|
78
|
-
body.systemInstruction = { parts: [{ text:
|
|
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=${
|
|
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 (!
|
|
139
|
+
if (!AI_API_KEY) {
|
|
140
140
|
return Response.json(
|
|
141
|
-
{ error: "
|
|
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
|
|
147
|
+
return AI_PROVIDER === "gemini"
|
|
148
148
|
? await streamGemini(message, history)
|
|
149
149
|
: await streamOpenAI(message, history);
|
|
150
150
|
} catch (err) {
|
|
151
|
-
console.error("[
|
|
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: "
|
|
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
|
+
});
|