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
|
@@ -63,9 +63,12 @@ async function mergeAuthIntoFirebaseJson(projectDir, providers) {
|
|
|
63
63
|
* @param {string} options.projectId
|
|
64
64
|
* @param {string} options.appName
|
|
65
65
|
* @param {string} [options.supportEmail] - falls back to active gcloud account
|
|
66
|
+
* @param {boolean} [options.googleOnly] - Supabase: enable ONLY Google (the deploy's
|
|
67
|
+
* side effect creates the OAuth Web Client we need). Anonymous/Email/Password in
|
|
68
|
+
* Firebase Auth would be dead config there, since the app uses Supabase Auth.
|
|
66
69
|
* @returns {{ ok: boolean, error?: string, supportEmail?: string }}
|
|
67
70
|
*/
|
|
68
|
-
async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, supportEmail }) {
|
|
71
|
+
async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, supportEmail, googleOnly = false }) {
|
|
69
72
|
// 1. Resolve support email (required by Google's OAuth consent screen)
|
|
70
73
|
let email = (supportEmail || '').trim();
|
|
71
74
|
if (!email) email = await getGcloudAccountEmail();
|
|
@@ -76,15 +79,20 @@ async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, suppor
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
// 2. Merge firebase.json
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// 2. Merge firebase.json. Google is always enabled (it's the whole point — the
|
|
83
|
+
// deploy creates its OAuth Web Client). Anonymous + Email/Password are added only
|
|
84
|
+
// for the Firebase backend, whose app actually signs in through Firebase Auth.
|
|
85
|
+
const providers = {
|
|
83
86
|
googleSignIn: {
|
|
84
87
|
oAuthBrandDisplayName: appName,
|
|
85
88
|
supportEmail: email,
|
|
86
89
|
},
|
|
87
|
-
}
|
|
90
|
+
};
|
|
91
|
+
if (!googleOnly) {
|
|
92
|
+
providers.anonymous = true;
|
|
93
|
+
providers.emailPassword = true;
|
|
94
|
+
}
|
|
95
|
+
const merge = await mergeAuthIntoFirebaseJson(projectDir, providers);
|
|
88
96
|
if (!merge.ok) return merge;
|
|
89
97
|
|
|
90
98
|
// 3. Deploy. --non-interactive prevents the CLI from prompting on edge cases.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Firebase 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
|
-
* postBuild → runDeploy (
|
|
8
|
+
* Specific hook:
|
|
9
|
+
* postBuild → runDeploy (optional, controlled by options.deploy)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { generateProject } = require('../../generate');
|
|
@@ -730,34 +730,24 @@ async function ensureLocalhostAuthorizedDomains(projectId, token) {
|
|
|
730
730
|
}
|
|
731
731
|
|
|
732
732
|
/**
|
|
733
|
-
*
|
|
734
|
-
*
|
|
733
|
+
* Initialize Firebase Auth (Identity Platform) for a project. Brand-new projects
|
|
734
|
+
* have no auth config, so any Admin v2 operation (PATCH /config, or the Firebase
|
|
735
|
+
* CLI `deploy --only auth`) fails with CONFIGURATION_NOT_FOUND until this runs.
|
|
735
736
|
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
* 2. PATCH /config to enable Email/Password and Anonymous.
|
|
740
|
-
* 3. POST defaultSupportedIdpConfigs to enable Google Sign-In.
|
|
741
|
-
* Google Sign-In requires an OAuth client_id that Firebase creates when the user
|
|
742
|
-
* enables it in the Console — if the client doesn't exist yet this step is skipped
|
|
743
|
-
* and googleSignInSkipped=true is returned so the caller can show a targeted hint.
|
|
744
|
-
*
|
|
745
|
-
* Non-fatal: returns { ok: false, error } without throwing if the API call fails.
|
|
737
|
+
* The endpoint is idempotent: it returns {} both on first init and when auth was
|
|
738
|
+
* already initialized. Non-fatal by contract — callers proceed on failure since
|
|
739
|
+
* the config may already exist.
|
|
746
740
|
*
|
|
747
741
|
* @param {string} projectId
|
|
748
|
-
* @returns {{ ok: boolean,
|
|
742
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
749
743
|
*/
|
|
750
|
-
async function
|
|
744
|
+
async function ensureFirebaseAuthInitialized(projectId, { maxRetries = 3, retryDelayMs = 15000 } = {}) {
|
|
751
745
|
let token;
|
|
752
746
|
try {
|
|
753
747
|
token = await getAccessToken();
|
|
754
748
|
} catch (_) {
|
|
755
749
|
return { ok: false, error: 'Could not get access token' };
|
|
756
750
|
}
|
|
757
|
-
|
|
758
|
-
// Step 1: Initialize Firebase Auth for the project.
|
|
759
|
-
// New projects have no auth config — PATCH returns CONFIGURATION_NOT_FOUND without this.
|
|
760
|
-
// The endpoint is idempotent: it returns {} on success or if already initialized.
|
|
761
751
|
const initUrl = `https://identitytoolkit.googleapis.com/v2/projects/${projectId}/identityPlatform:initializeAuth`;
|
|
762
752
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
763
753
|
const initRes = await fetch(initUrl, {
|
|
@@ -769,19 +759,45 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
|
|
|
769
759
|
},
|
|
770
760
|
body: JSON.stringify({}),
|
|
771
761
|
});
|
|
772
|
-
if (initRes.ok)
|
|
773
|
-
await initRes.text(); // consume body to release connection
|
|
762
|
+
if (initRes.ok) return { ok: true };
|
|
763
|
+
const text = await initRes.text(); // consume body to release connection
|
|
774
764
|
if (attempt < maxRetries && (initRes.status === 404 || initRes.status === 503)) {
|
|
775
765
|
await sleep(retryDelayMs);
|
|
776
766
|
try { token = await getAccessToken(); } catch (_) {}
|
|
777
767
|
continue;
|
|
778
768
|
}
|
|
779
|
-
|
|
780
|
-
break;
|
|
769
|
+
return { ok: false, error: `${initRes.status}: ${text}` };
|
|
781
770
|
}
|
|
771
|
+
return { ok: false, error: 'initializeAuth: exhausted retries' };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Enable Firebase Auth sign-in providers: Email/Password, Anonymous and Google.
|
|
776
|
+
* Uses the Identity Toolkit Admin v2 REST API with gcloud credentials.
|
|
777
|
+
*
|
|
778
|
+
* Flow:
|
|
779
|
+
* 1. Call identityPlatform:initializeAuth to create the auth config for the project
|
|
780
|
+
* (required for new projects — PATCH fails with CONFIGURATION_NOT_FOUND without it).
|
|
781
|
+
* 2. PATCH /config to enable Email/Password and Anonymous.
|
|
782
|
+
* 3. POST defaultSupportedIdpConfigs to enable Google Sign-In.
|
|
783
|
+
* Google Sign-In requires an OAuth client_id that Firebase creates when the user
|
|
784
|
+
* enables it in the Console — if the client doesn't exist yet this step is skipped
|
|
785
|
+
* and googleSignInSkipped=true is returned so the caller can show a targeted hint.
|
|
786
|
+
*
|
|
787
|
+
* Non-fatal: returns { ok: false, error } without throwing if the API call fails.
|
|
788
|
+
*
|
|
789
|
+
* @param {string} projectId
|
|
790
|
+
* @returns {{ ok: boolean, googleSignInSkipped?: boolean, error?: string }}
|
|
791
|
+
*/
|
|
792
|
+
async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 15000 } = {}) {
|
|
793
|
+
// Step 1: Initialize Firebase Auth (non-fatal — the PATCH below still runs in
|
|
794
|
+
// case auth was already initialized). The shared helper keeps the init logic in
|
|
795
|
+
// one place so the Supabase flow can reuse it before its `deploy --only auth`.
|
|
796
|
+
await ensureFirebaseAuthInitialized(projectId, { maxRetries, retryDelayMs });
|
|
782
797
|
|
|
783
798
|
// Step 2: Enable Email/Password and Anonymous auth providers.
|
|
784
799
|
const configUrl = `https://identitytoolkit.googleapis.com/admin/v2/projects/${projectId}/config?updateMask=signIn.email,signIn.anonymous`;
|
|
800
|
+
let token;
|
|
785
801
|
let lastError;
|
|
786
802
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
787
803
|
try { token = await getAccessToken(); } catch (_) {
|
|
@@ -996,27 +1012,34 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
996
1012
|
const addResult = await addFirebaseToProject(projectId, { onProgress });
|
|
997
1013
|
if (!addResult.ok) return { ok: false, error: `[addFirebase] ${addResult.error}`, projectId };
|
|
998
1014
|
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1015
|
+
// Firebase Auth providers (Email/Password, Anonymous, Google, Apple) only matter
|
|
1016
|
+
// for the Firebase backend, whose app authenticates against Firebase Auth. In
|
|
1017
|
+
// fcmOnly mode the app is Supabase/API and authenticates elsewhere, so we leave
|
|
1018
|
+
// Firebase Auth untouched — this project exists purely for FCM/push (and, for
|
|
1019
|
+
// Supabase, the Google OAuth client, which new.js creates separately via
|
|
1020
|
+
// `firebase deploy --only auth`). Non-fatal — setup continues even if it fails.
|
|
1021
|
+
let authResult = null;
|
|
1022
|
+
if (!fcmOnly) {
|
|
1023
|
+
authResult = await enableAuthProviders(projectId);
|
|
1024
|
+
if (!authResult.ok) {
|
|
1025
|
+
onProgress('auth-providers-warn', {
|
|
1026
|
+
error: authResult.error,
|
|
1027
|
+
url: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
1028
|
+
});
|
|
1029
|
+
} else if (authResult.googleSignInSkipped) {
|
|
1030
|
+
// Email/Password and Anonymous were enabled. Google Sign-In needs an OAuth client
|
|
1031
|
+
// that Firebase creates when you enable it in the Console for the first time.
|
|
1032
|
+
onProgress('auth-google-warn', {
|
|
1033
|
+
url: `https://console.firebase.google.com/project/${projectId}/authentication/providers`,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
// Providers were enabled but localhost couldn't be authorized — warn so the user
|
|
1037
|
+
// isn't surprised by [firebase_auth/unauthorized-domain] on web sign-in.
|
|
1038
|
+
if (authResult.ok && authResult.localhostAuthorized === false) {
|
|
1039
|
+
onProgress('auth-localhost-warn', {
|
|
1040
|
+
url: `https://console.firebase.google.com/project/${projectId}/authentication/settings`,
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1020
1043
|
}
|
|
1021
1044
|
|
|
1022
1045
|
// Firestore + Storage are Firebase-backend features (and need Blaze). In
|
|
@@ -1089,8 +1112,8 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
1089
1112
|
return {
|
|
1090
1113
|
ok: true,
|
|
1091
1114
|
projectId,
|
|
1092
|
-
authEnabled: authResult.ok,
|
|
1093
|
-
googleSignInSkipped: authResult.ok && !!authResult.googleSignInSkipped,
|
|
1115
|
+
authEnabled: authResult ? authResult.ok : null,
|
|
1116
|
+
googleSignInSkipped: authResult ? (authResult.ok && !!authResult.googleSignInSkipped) : false,
|
|
1094
1117
|
sha1Skipped,
|
|
1095
1118
|
sha1Error,
|
|
1096
1119
|
sha1ManualUrl: `https://console.firebase.google.com/project/${projectId}/settings/general/android:${bundleId}`,
|
|
@@ -1238,6 +1261,7 @@ module.exports = {
|
|
|
1238
1261
|
applyStorageCors,
|
|
1239
1262
|
checkBillingEnabled,
|
|
1240
1263
|
enableAuthProviders,
|
|
1264
|
+
ensureFirebaseAuthInitialized,
|
|
1241
1265
|
ensureLocalhostAuthorizedDomains,
|
|
1242
1266
|
listBillingAccounts,
|
|
1243
1267
|
listGcpOrganizations,
|
|
@@ -21,7 +21,7 @@ const ORIGINAL_SHORT_NAME = 'appfirebase';
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Normalize app name to a valid Dart package name.
|
|
24
|
-
* "
|
|
24
|
+
* "My Café App" → "my_cafe_app"
|
|
25
25
|
*/
|
|
26
26
|
function toPackageName(appName) {
|
|
27
27
|
return (appName || '')
|
|
@@ -52,8 +52,8 @@ function bundleIdToPath(bundleId) {
|
|
|
52
52
|
|
|
53
53
|
const ORIGINAL_BUNDLE_ID_PATH = bundleIdToPath(ORIGINAL_BUNDLE_ID);
|
|
54
54
|
|
|
55
|
-
// Facebook —
|
|
56
|
-
//
|
|
55
|
+
// Facebook — only the characters allowed in Apple's URL scheme (RFC1738).
|
|
56
|
+
// Without credentials in `kasy new`, keep these placeholders (FB login won't work until replaced).
|
|
57
57
|
const FB_APP_ID_PLACEHOLDER = '000000000000000';
|
|
58
58
|
const FB_CLIENT_TOKEN_PLACEHOLDER = '00000000000000000000000000000000';
|
|
59
59
|
|
|
@@ -91,7 +91,7 @@ function buildTokens({ appName, bundleId, fbAppId, fbToken, defaultPaywall = 'ba
|
|
|
91
91
|
[`"${ORIGINAL_SHORT_NAME}"`]: `"${shortName}"`,
|
|
92
92
|
// pubspec.yaml name field
|
|
93
93
|
[`name: ${ORIGINAL_PACKAGE}`]: `name: ${packageName}`,
|
|
94
|
-
// Facebook (placeholders → user values
|
|
94
|
+
// Facebook (placeholders → user values, or keep a valid placeholder for the App Store)
|
|
95
95
|
[FB_APP_ID_PLACEHOLDER]: fbAppId?.trim() || FB_APP_ID_PLACEHOLDER,
|
|
96
96
|
[`fb${FB_APP_ID_PLACEHOLDER}`]: fbAppId?.trim() ? `fb${fbAppId.trim()}` : `fb${FB_APP_ID_PLACEHOLDER}`,
|
|
97
97
|
[FB_CLIENT_TOKEN_PLACEHOLDER]: fbToken?.trim() || FB_CLIENT_TOKEN_PLACEHOLDER,
|
|
@@ -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];
|