kasy-cli 1.2.1
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/README.md +56 -0
- package/bin/kasy.js +529 -0
- package/docs/cli-reference.md +251 -0
- package/lib/commands/add.js +777 -0
- package/lib/commands/check.js +240 -0
- package/lib/commands/codemagic.js +161 -0
- package/lib/commands/deploy.js +257 -0
- package/lib/commands/docs.js +47 -0
- package/lib/commands/doctor.js +227 -0
- package/lib/commands/features.js +36 -0
- package/lib/commands/ios.js +206 -0
- package/lib/commands/new.js +1870 -0
- package/lib/commands/notifications.js +207 -0
- package/lib/commands/remove.js +466 -0
- package/lib/commands/run.js +75 -0
- package/lib/commands/update.js +382 -0
- package/lib/commands/validate.js +131 -0
- package/lib/scaffold/CHANGELOG.json +6 -0
- package/lib/scaffold/backends/api/.flutter-plugins-dependencies +1 -0
- package/lib/scaffold/backends/api/analysis_options.yaml +5 -0
- package/lib/scaffold/backends/api/generator.js +76 -0
- package/lib/scaffold/backends/api/patch/README.md +85 -0
- package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +87 -0
- package/lib/scaffold/backends/api/patch/ios/Runner/AppDelegate.swift +61 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +80 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +117 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +148 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/json_converters.dart +35 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +48 -0
- package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +211 -0
- package/lib/scaffold/backends/api/patch/lib/environnements.dart +151 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +303 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +80 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +406 -0
- package/lib/scaffold/backends/api/patch/lib/features/camera/xfile_extension.dart +6 -0
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/entities/feature_request_entity.dart +24 -0
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/entities/feature_vote_entity.dart +21 -0
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +52 -0
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +58 -0
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_message_entity.dart +29 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +398 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/device_entity.dart +39 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +23 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/notifications_api.dart +333 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +185 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +26 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/user_infos_api.dart +107 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +94 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +34 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +73 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/widgets/avatar_utils.dart +33 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +34 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscription/api/subscription_api.dart +49 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +88 -0
- package/lib/scaffold/backends/api/patch/lib/main.dart +256 -0
- package/lib/scaffold/backends/api/patch/lib/router.dart +133 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +108 -0
- package/lib/scaffold/backends/firebase/deploy.js +1006 -0
- package/lib/scaffold/backends/firebase/generator.js +24 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +1008 -0
- package/lib/scaffold/backends/firebase/tokens.js +113 -0
- package/lib/scaffold/backends/supabase/.flutter-plugins-dependencies +1 -0
- package/lib/scaffold/backends/supabase/analysis_options.yaml +5 -0
- package/lib/scaffold/backends/supabase/config.toml +34 -0
- package/lib/scaffold/backends/supabase/deploy.js +485 -0
- package/lib/scaffold/backends/supabase/edge-functions/delete-user-account/README.md +15 -0
- package/lib/scaffold/backends/supabase/edge-functions/delete-user-account/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/llm-chat/index.ts +156 -0
- package/lib/scaffold/backends/supabase/edge-functions/meta-track-event/index.ts +292 -0
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/README.md +30 -0
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +488 -0
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +35 -0
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +314 -0
- package/lib/scaffold/backends/supabase/generator.js +108 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000001_initial_schema.sql +149 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000002_storage_bucket.sql +40 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000005_vote_triggers.sql +34 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000006_notification_webhook.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +64 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000008_user_feature_requests.sql +2 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +22 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000010_notification_image.sql +2 -0
- package/lib/scaffold/backends/supabase/patch/README.md +121 -0
- package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +87 -0
- package/lib/scaffold/backends/supabase/patch/ios/Runner/AppDelegate.swift +61 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/meta_ads_api.dart +75 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +83 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +143 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/json_converters.dart +35 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +48 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +211 -0
- package/lib/scaffold/backends/supabase/patch/lib/environnements.dart +149 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +546 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +410 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/camera/xfile_extension.dart +6 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/entities/feature_request_entity.dart +25 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/entities/feature_vote_entity.dart +21 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +59 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_vote_api.dart +85 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +199 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +47 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_message_entity.dart +30 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +266 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +410 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/entities/device_entity.dart +51 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/entities/notifications_entity.dart +26 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/notifications_api.dart +317 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +174 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +26 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/user_infos_api.dart +90 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +94 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +34 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +73 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/widgets/avatar_utils.dart +35 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +35 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/subscription_api.dart +44 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/shared/maybeshow_premium.dart +85 -0
- package/lib/scaffold/backends/supabase/patch/lib/google_auth_options.dart +23 -0
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +288 -0
- package/lib/scaffold/backends/supabase/patch/lib/router.dart +133 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +110 -0
- package/lib/scaffold/backends/supabase/tokens.js +9 -0
- package/lib/scaffold/catalog.js +177 -0
- package/lib/scaffold/engine.js +230 -0
- package/lib/scaffold/features/README.md +152 -0
- package/lib/scaffold/features/analytics/lib/core/data/api/analytics_api.dart +124 -0
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +35 -0
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +35 -0
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +35 -0
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +12 -0
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +12 -0
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +12 -0
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +17 -0
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +17 -0
- package/lib/scaffold/features/ci/.github/dependabot.yml +16 -0
- package/lib/scaffold/features/ci/.github/workflows/app.yml +20 -0
- package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +14 -0
- package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +19 -0
- package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +163 -0
- package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +28 -0
- package/lib/scaffold/features/ci/.gitlab-ci.yml +37 -0
- package/lib/scaffold/features/ci/codemagic.yaml +157 -0
- package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +111 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +27 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +27 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +50 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +79 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +48 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +42 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +147 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +95 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +199 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +175 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +76 -0
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +279 -0
- package/lib/scaffold/features/ios-release/.kasy/apple.env.example +8 -0
- package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +7 -0
- package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +50 -0
- package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +50 -0
- package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +50 -0
- package/lib/scaffold/features/ios-release/docs/ios-release.en.md +41 -0
- package/lib/scaffold/features/ios-release/docs/ios-release.es.md +41 -0
- package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +41 -0
- package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +38 -0
- package/lib/scaffold/features/ios-release/scripts/release-ios.sh +137 -0
- package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +301 -0
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +81 -0
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +76 -0
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +282 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +24 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +71 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +92 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +15 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +78 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +30 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +72 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +92 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +73 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +89 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +94 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +80 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +139 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +110 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +84 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +45 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +77 -0
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +392 -0
- package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +116 -0
- package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +322 -0
- package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +41 -0
- package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +305 -0
- package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_background_task.dart +41 -0
- package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_mywidget_service.dart +57 -0
- package/lib/scaffold/features/widget/lib/core/home_widgets/home_widget_service.dart +54 -0
- package/lib/scaffold/features/widget/lib/features/settings/ui/components/admin/admin_home_widgets.dart +32 -0
- package/lib/scaffold/generate.js +370 -0
- package/lib/scaffold/patch.js +395 -0
- package/lib/scaffold/shared/backend-config.js +74 -0
- package/lib/scaffold/shared/fcm-service-account.js +126 -0
- package/lib/scaffold/shared/generator-utils.js +1664 -0
- package/lib/scaffold/shared/post-build.js +809 -0
- package/lib/scaffold/shared/template-strings.js +112 -0
- package/lib/utils/apple-release.js +273 -0
- package/lib/utils/checks.js +319 -0
- package/lib/utils/codemagic-release.js +127 -0
- package/lib/utils/fs.js +122 -0
- package/lib/utils/i18n.js +2123 -0
- package/lib/utils/license-gate.js +72 -0
- package/lib/utils/license.js +64 -0
- package/lib/utils/prompts.js +213 -0
- package/lib/utils/updates.js +97 -0
- package/package.json +55 -0
- package/templates/firebase/.env.example +24 -0
- package/templates/firebase/.firebaserc +5 -0
- package/templates/firebase/.kasy/apple.env.example +8 -0
- package/templates/firebase/.kasy/codemagic.env.example +7 -0
- package/templates/firebase/.metadata +30 -0
- package/templates/firebase/.windsurfrules +95 -0
- package/templates/firebase/Makefile +30 -0
- package/templates/firebase/README.en.md +180 -0
- package/templates/firebase/README.es.md +180 -0
- package/templates/firebase/README.md +180 -0
- package/templates/firebase/analysis_options.yaml +37 -0
- package/templates/firebase/android/app/build.gradle.kts +80 -0
- package/templates/firebase/android/app/src/debug/AndroidManifest.xml +7 -0
- package/templates/firebase/android/app/src/main/AndroidManifest.xml +93 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +36 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +50 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +7 -0
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable/launch_background.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.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/drawable-v21/launch_background.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/values/strings.xml +6 -0
- package/templates/firebase/android/app/src/main/res/values/styles.xml +22 -0
- package/templates/firebase/android/app/src/main/res/values-night/styles.xml +22 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +20 -0
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +20 -0
- package/templates/firebase/android/app/src/main/res/xml/locales_config.xml +7 -0
- package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +11 -0
- package/templates/firebase/android/app/src/profile/AndroidManifest.xml +7 -0
- package/templates/firebase/android/build.gradle.kts +28 -0
- package/templates/firebase/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/templates/firebase/android/gradle.properties +2 -0
- package/templates/firebase/android/settings.gradle.kts +29 -0
- package/templates/firebase/assets/icons/apple.png +0 -0
- package/templates/firebase/assets/icons/facebook.png +0 -0
- package/templates/firebase/assets/icons/google.png +0 -0
- package/templates/firebase/assets/icons/google_play_games.png +0 -0
- package/templates/firebase/assets/images/app_icon.png +0 -0
- package/templates/firebase/assets/images/empty_notifications.jpg +0 -0
- package/templates/firebase/assets/images/icon.png +0 -0
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img1.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/onboarding.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/assets/images/premium-bg.jpg +0 -0
- package/templates/firebase/assets/images/premium-switch-header.png +0 -0
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/splashscreen.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/build.yaml +13 -0
- package/templates/firebase/devtools_options.yaml +3 -0
- package/templates/firebase/docs/auth-setup.en.md +145 -0
- package/templates/firebase/docs/auth-setup.es.md +145 -0
- package/templates/firebase/docs/auth-setup.pt.md +145 -0
- package/templates/firebase/docs/codemagic-release.en.md +50 -0
- package/templates/firebase/docs/codemagic-release.es.md +50 -0
- package/templates/firebase/docs/codemagic-release.pt.md +50 -0
- package/templates/firebase/docs/ios-release.en.md +41 -0
- package/templates/firebase/docs/ios-release.es.md +41 -0
- package/templates/firebase/docs/ios-release.pt.md +54 -0
- package/templates/firebase/docs/navigation-transitions.md +80 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +341 -0
- package/templates/firebase/docs/revenuecat-setup.pt.md +341 -0
- package/templates/firebase/firebase.json +1 -0
- package/templates/firebase/firestore.indexes.json +25 -0
- package/templates/firebase/firestore.rules +51 -0
- package/templates/firebase/functions/.eslintrc.js +45 -0
- package/templates/firebase/functions/jest.config.js +16 -0
- package/templates/firebase/functions/package-lock.json +6876 -0
- package/templates/firebase/functions/package.json +33 -0
- package/templates/firebase/functions/src/authentication/functions.ts +30 -0
- package/templates/firebase/functions/src/authentication/triggers.ts +103 -0
- package/templates/firebase/functions/src/core/api/meta_ads_api.ts +390 -0
- package/templates/firebase/functions/src/core/data/entities/entity_utils.ts +87 -0
- package/templates/firebase/functions/src/core/data/entities/notification_entity.ts +74 -0
- package/templates/firebase/functions/src/core/data/entities/page.ts +4 -0
- package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +76 -0
- package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +64 -0
- package/templates/firebase/functions/src/core/data/entities/user_entity.ts +43 -0
- package/templates/firebase/functions/src/core/data/repositories/repositories.ts +16 -0
- package/templates/firebase/functions/src/core/data/repositories/subscription_repository.ts +42 -0
- package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +31 -0
- package/templates/firebase/functions/src/core/data/repositories/user_notifications_repository.ts +58 -0
- package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +84 -0
- package/templates/firebase/functions/src/core/logger/logger.ts +13 -0
- package/templates/firebase/functions/src/core/storage/storage_manager.ts +52 -0
- package/templates/firebase/functions/src/core/utils/async_utils.ts +5 -0
- package/templates/firebase/functions/src/feature_requests/triggers.ts +3 -0
- package/templates/firebase/functions/src/index.ts +24 -0
- package/templates/firebase/functions/src/llm_chat/index.ts +181 -0
- package/templates/firebase/functions/src/notifications/admin_functions.ts +91 -0
- package/templates/firebase/functions/src/notifications/models/notification.ts +61 -0
- package/templates/firebase/functions/src/notifications/notifications_api.ts +147 -0
- package/templates/firebase/functions/src/notifications/triggers.ts +102 -0
- package/templates/firebase/functions/src/subscriptions/models/revenuecat_events.ts +162 -0
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +18 -0
- package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +173 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +135 -0
- package/templates/firebase/functions/src/subscriptions/triggers.ts +35 -0
- package/templates/firebase/functions/src/test/core/data/entities/entity_utils.spec.ts +44 -0
- package/templates/firebase/functions/tsconfig.dev.json +5 -0
- package/templates/firebase/functions/tsconfig.json +18 -0
- package/templates/firebase/home_widget_config.json +30 -0
- package/templates/firebase/ios/Flutter/AppFrameworkInfo.plist +24 -0
- package/templates/firebase/ios/Flutter/Debug.xcconfig +2 -0
- package/templates/firebase/ios/Flutter/Profile.xcconfig +2 -0
- package/templates/firebase/ios/Flutter/Release.xcconfig +2 -0
- package/templates/firebase/ios/HomeWidgetExtension/HomeWidgetBundle.swift +13 -0
- package/templates/firebase/ios/HomeWidgetExtension/Info.plist +29 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +86 -0
- package/templates/firebase/ios/HomeWidgetExtension.entitlements +10 -0
- package/templates/firebase/ios/NotificationService/Info.plist +31 -0
- package/templates/firebase/ios/NotificationService/NotificationService.swift +76 -0
- package/templates/firebase/ios/Podfile +111 -0
- package/templates/firebase/ios/Runner/AppDelegate.swift +75 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +1 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +21 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
- package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +44 -0
- package/templates/firebase/ios/Runner/Base.lproj/Main.storyboard +26 -0
- package/templates/firebase/ios/Runner/Info.plist +132 -0
- package/templates/firebase/ios/Runner/Runner-Bridging-Header.h +1 -0
- package/templates/firebase/ios/Runner/Runner.entitlements +14 -0
- package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +10 -0
- package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +10 -0
- package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +10 -0
- package/templates/firebase/ios/Runner.xcodeproj/project.pbxproj +1108 -0
- package/templates/firebase/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/templates/firebase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/templates/firebase/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- package/templates/firebase/ios/Runner.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme +58 -0
- package/templates/firebase/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
- package/templates/firebase/ios/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/templates/firebase/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/templates/firebase/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- package/templates/firebase/ios/RunnerTests/RunnerTests.swift +12 -0
- package/templates/firebase/lib/components/components.dart +27 -0
- package/templates/firebase/lib/components/kasy_accordion.dart +384 -0
- package/templates/firebase/lib/components/kasy_alert.dart +266 -0
- package/templates/firebase/lib/components/kasy_app_bar.dart +536 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +549 -0
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +104 -0
- package/templates/firebase/lib/components/kasy_badge.dart +278 -0
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +338 -0
- package/templates/firebase/lib/components/kasy_button.dart +926 -0
- package/templates/firebase/lib/components/kasy_card.dart +128 -0
- package/templates/firebase/lib/components/kasy_checkbox.dart +429 -0
- package/templates/firebase/lib/components/kasy_chip.dart +83 -0
- package/templates/firebase/lib/components/kasy_dialog.dart +789 -0
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +233 -0
- package/templates/firebase/lib/components/kasy_skeleton.dart +225 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +392 -0
- package/templates/firebase/lib/components/kasy_text_field.dart +591 -0
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +532 -0
- package/templates/firebase/lib/components/kasy_toast.dart +803 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +136 -0
- package/templates/firebase/lib/core/ads/ads_state.dart +15 -0
- package/templates/firebase/lib/core/animations/bottomfade_anim.dart +37 -0
- package/templates/firebase/lib/core/animations/movefade_anim.dart +34 -0
- package/templates/firebase/lib/core/animations/slideright_anim.dart +38 -0
- package/templates/firebase/lib/core/bottom_menu/bart_inner_paths.dart +5 -0
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +127 -0
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +281 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +22 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +127 -0
- package/templates/firebase/lib/core/bottom_menu/notification_bottom_item.dart +48 -0
- package/templates/firebase/lib/core/config/app_env.dart +100 -0
- package/templates/firebase/lib/core/config/features.dart +15 -0
- package/templates/firebase/lib/core/data/api/analytics_api.dart +124 -0
- package/templates/firebase/lib/core/data/api/base_api_exceptions.dart +28 -0
- package/templates/firebase/lib/core/data/api/firestore_retry.dart +29 -0
- package/templates/firebase/lib/core/data/api/http_client.dart +38 -0
- package/templates/firebase/lib/core/data/api/image.dart +52 -0
- package/templates/firebase/lib/core/data/api/remote_config_api.dart +170 -0
- package/templates/firebase/lib/core/data/api/storage_api.dart +115 -0
- package/templates/firebase/lib/core/data/api/tracking_api.dart +119 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +115 -0
- package/templates/firebase/lib/core/data/entities/json_converters.dart +39 -0
- package/templates/firebase/lib/core/data/entities/upload_result.dart +48 -0
- package/templates/firebase/lib/core/data/entities/user_entity.dart +54 -0
- package/templates/firebase/lib/core/data/models/pageable.dart +12 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +322 -0
- package/templates/firebase/lib/core/data/models/user.dart +107 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +130 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +464 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_highlight.dart +88 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +70 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_panel.dart +204 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +321 -0
- package/templates/firebase/lib/core/extensions/date.ext.dart +32 -0
- package/templates/firebase/lib/core/guards/authenticated_guard.dart +37 -0
- package/templates/firebase/lib/core/guards/guard.dart +68 -0
- package/templates/firebase/lib/core/guards/user_info_guard.dart +55 -0
- package/templates/firebase/lib/core/haptics/haptic_feedback_notifier.dart +19 -0
- package/templates/firebase/lib/core/haptics/kasy_haptics.dart +27 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +41 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +57 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +54 -0
- package/templates/firebase/lib/core/icons/kasy_icons.dart +86 -0
- package/templates/firebase/lib/core/initializer/models/run_state.dart +14 -0
- package/templates/firebase/lib/core/initializer/onstart_service.dart +38 -0
- package/templates/firebase/lib/core/initializer/onstart_widget.dart +83 -0
- package/templates/firebase/lib/core/initializer/pending_notification_handler.dart +48 -0
- package/templates/firebase/lib/core/keyboard_fix/keyboard_flicker_fix.dart +164 -0
- package/templates/firebase/lib/core/navigation/kasy_fade_page_transitions_builder.dart +34 -0
- package/templates/firebase/lib/core/navigation/kasy_material_page_route.dart +41 -0
- package/templates/firebase/lib/core/navigation/kasy_navigation_config.dart +29 -0
- package/templates/firebase/lib/core/navigation/kasy_page_transition.dart +40 -0
- package/templates/firebase/lib/core/navigation/kasy_route_transition.dart +68 -0
- package/templates/firebase/lib/core/navigation/kasy_transition_kind.dart +17 -0
- package/templates/firebase/lib/core/navigation/navigation.dart +6 -0
- package/templates/firebase/lib/core/rating/api/rating_api.dart +99 -0
- package/templates/firebase/lib/core/rating/models/rating.dart +52 -0
- package/templates/firebase/lib/core/rating/models/review.dart +70 -0
- package/templates/firebase/lib/core/rating/providers/rating_repository.dart +75 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +190 -0
- package/templates/firebase/lib/core/rating/widgets/rate_popup.dart +54 -0
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +175 -0
- package/templates/firebase/lib/core/security/biometric_copy_slot.dart +20 -0
- package/templates/firebase/lib/core/security/biometric_guard.dart +114 -0
- package/templates/firebase/lib/core/security/biometric_preference_notifier.dart +17 -0
- package/templates/firebase/lib/core/security/biometric_service.dart +79 -0
- package/templates/firebase/lib/core/security/biometric_ui_bundle.dart +136 -0
- package/templates/firebase/lib/core/security/secured_storage.dart +46 -0
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +70 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +2021 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +88 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_rating.dart +49 -0
- package/templates/firebase/lib/core/states/components/maybe_show_update_bottom_sheet.dart +86 -0
- package/templates/firebase/lib/core/states/components/maybeshow_component.dart +110 -0
- package/templates/firebase/lib/core/states/events_dispatcher.dart +103 -0
- package/templates/firebase/lib/core/states/models/event_model.dart +118 -0
- package/templates/firebase/lib/core/states/models/user_state.dart +22 -0
- package/templates/firebase/lib/core/states/notifications_dispatcher.dart +81 -0
- package/templates/firebase/lib/core/states/translations.dart +11 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +321 -0
- package/templates/firebase/lib/core/theme/colors.dart +262 -0
- package/templates/firebase/lib/core/theme/extensions/theme_extension.dart +48 -0
- package/templates/firebase/lib/core/theme/providers/kasy_theme.dart +76 -0
- package/templates/firebase/lib/core/theme/providers/theme_provider.dart +212 -0
- package/templates/firebase/lib/core/theme/radius.dart +33 -0
- package/templates/firebase/lib/core/theme/shadows.dart +64 -0
- package/templates/firebase/lib/core/theme/spacing.dart +44 -0
- package/templates/firebase/lib/core/theme/texts.dart +176 -0
- package/templates/firebase/lib/core/theme/theme.dart +24 -0
- package/templates/firebase/lib/core/theme/theme_data/theme_data.dart +32 -0
- package/templates/firebase/lib/core/theme/theme_data/theme_data_factory.dart +15 -0
- package/templates/firebase/lib/core/theme/universal_theme.dart +210 -0
- package/templates/firebase/lib/core/toast/toast_service.dart +55 -0
- package/templates/firebase/lib/core/ui/app_dialog.dart +26 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +1126 -0
- package/templates/firebase/lib/core/widgets/debouncer.dart +31 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +166 -0
- package/templates/firebase/lib/core/widgets/kasy_pressable.dart +2 -0
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +161 -0
- package/templates/firebase/lib/core/widgets/kasy_scroll_behavior.dart +17 -0
- package/templates/firebase/lib/core/widgets/keyboard_visibility.dart +75 -0
- package/templates/firebase/lib/core/widgets/page_background.dart +71 -0
- package/templates/firebase/lib/core/widgets/page_not_found.dart +15 -0
- package/templates/firebase/lib/core/widgets/responsive_layout.dart +215 -0
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +192 -0
- package/templates/firebase/lib/environnements.dart +160 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +528 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +84 -0
- package/templates/firebase/lib/features/authentication/providers/models/email.dart +36 -0
- package/templates/firebase/lib/features/authentication/providers/models/password.dart +37 -0
- package/templates/firebase/lib/features/authentication/providers/models/phone_signin_state.dart +21 -0
- package/templates/firebase/lib/features/authentication/providers/models/recover_state.dart +21 -0
- package/templates/firebase/lib/features/authentication/providers/models/signin_state.dart +20 -0
- package/templates/firebase/lib/features/authentication/providers/models/signup_state.dart +20 -0
- package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +132 -0
- package/templates/firebase/lib/features/authentication/providers/recover_provider.dart +51 -0
- package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +166 -0
- package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +67 -0
- package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +399 -0
- package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +85 -0
- package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +19 -0
- package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +19 -0
- package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +32 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +100 -0
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +109 -0
- package/templates/firebase/lib/features/authentication/ui/phone_auth_page.dart +60 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +101 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +321 -0
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +310 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_page_back_button.dart +149 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/otp_input.dart +194 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +62 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +73 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/social_separator.dart +26 -0
- package/templates/firebase/lib/features/dev/keyboard_test_page.dart +93 -0
- package/templates/firebase/lib/features/feedbacks/api/entities/feature_request_entity.dart +27 -0
- package/templates/firebase/lib/features/feedbacks/api/entities/feature_vote_entity.dart +27 -0
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +51 -0
- package/templates/firebase/lib/features/feedbacks/api/feature_vote_api.dart +79 -0
- package/templates/firebase/lib/features/feedbacks/models/feature_requests.dart +48 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +43 -0
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +147 -0
- package/templates/firebase/lib/features/feedbacks/repositories/feature_request_repository.dart +92 -0
- package/templates/firebase/lib/features/feedbacks/ui/component/add_feature_form.dart +112 -0
- package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +184 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +66 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +266 -0
- package/templates/firebase/lib/features/home/design_system_page.dart +552 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +314 -0
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +791 -0
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +7712 -0
- package/templates/firebase/lib/features/home/home_features_page.dart +207 -0
- package/templates/firebase/lib/features/home/home_page.dart +348 -0
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +50 -0
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_message_entity.dart +35 -0
- package/templates/firebase/lib/features/llm_chat/llm_chat_page.dart +352 -0
- package/templates/firebase/lib/features/llm_chat/providers/llm_chat_notifier.dart +315 -0
- package/templates/firebase/lib/features/llm_chat/ui/widgets/llm_chat_avatars.dart +148 -0
- package/templates/firebase/lib/features/llm_chat/ui/widgets/llm_chat_composer.dart +124 -0
- package/templates/firebase/lib/features/local_reminder/providers/reminder_notifier.dart +81 -0
- package/templates/firebase/lib/features/local_reminder/repositories/reminder_preferences.dart +76 -0
- package/templates/firebase/lib/features/local_reminder/ui/reminder_page.dart +288 -0
- package/templates/firebase/lib/features/notifications/api/device_api.dart +433 -0
- package/templates/firebase/lib/features/notifications/api/entities/device_entity.dart +62 -0
- package/templates/firebase/lib/features/notifications/api/entities/notifications_entity.dart +35 -0
- package/templates/firebase/lib/features/notifications/api/local_notifier.dart +409 -0
- package/templates/firebase/lib/features/notifications/api/notifications_api.dart +319 -0
- package/templates/firebase/lib/features/notifications/providers/models/device.dart +27 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification.dart +196 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification_list.dart +38 -0
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +120 -0
- package/templates/firebase/lib/features/notifications/repositories/background_notification_handler.dart +13 -0
- package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +137 -0
- package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +214 -0
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +75 -0
- package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +144 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +169 -0
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +46 -0
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +32 -0
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +106 -0
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +202 -0
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +84 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +73 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +362 -0
- package/templates/firebase/lib/features/onboarding/api/entities/user_info_entity.dart +24 -0
- package/templates/firebase/lib/features/onboarding/api/user_infos_api.dart +71 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +92 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +15 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +78 -0
- package/templates/firebase/lib/features/onboarding/repositories/user_infos_repository.dart +29 -0
- package/templates/firebase/lib/features/onboarding/ui/animations/page_transitions.dart +33 -0
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +66 -0
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +76 -0
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +92 -0
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +76 -0
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +89 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +108 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +80 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +145 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +125 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +84 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +173 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +44 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +77 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +392 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +460 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +301 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +38 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +53 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +22 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +366 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notifier.dart +40 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +430 -0
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +28 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +163 -0
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +59 -0
- package/templates/firebase/lib/features/settings/ui/widgets/avatar_utils.dart +51 -0
- package/templates/firebase/lib/features/settings/ui/widgets/round_progress.dart +151 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +52 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +163 -0
- package/templates/firebase/lib/features/subscription/api/entities/subscription_entity.dart +40 -0
- package/templates/firebase/lib/features/subscription/api/inapp_subscription_api.dart +389 -0
- package/templates/firebase/lib/features/subscription/api/subscription_api.dart +39 -0
- package/templates/firebase/lib/features/subscription/providers/models/premium_state.dart +35 -0
- package/templates/firebase/lib/features/subscription/providers/premium_page_provider.dart +259 -0
- package/templates/firebase/lib/features/subscription/repositories/subscription_repository.dart +177 -0
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +85 -0
- package/templates/firebase/lib/features/subscription/ui/component/active_premium_content.dart +171 -0
- package/templates/firebase/lib/features/subscription/ui/component/paywall_minimal.dart +146 -0
- package/templates/firebase/lib/features/subscription/ui/component/paywall_row.dart +332 -0
- package/templates/firebase/lib/features/subscription/ui/component/paywall_with_switch.dart +207 -0
- package/templates/firebase/lib/features/subscription/ui/component/premium_content.dart +157 -0
- package/templates/firebase/lib/features/subscription/ui/component/premium_page_factory.dart +161 -0
- package/templates/firebase/lib/features/subscription/ui/premium_page.dart +139 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/comparison_table.dart +346 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/feature_line.dart +131 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/paywall_empty_state.dart +68 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_background.dart +63 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_background_gradient.dart +53 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_banner.dart +80 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_bottom_menu.dart +102 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_card.dart +26 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_close_button.dart +61 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/premium_feature.dart +39 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/selectable_col.dart +377 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/selectable_row.dart +436 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/trial_switcher.dart +85 -0
- package/templates/firebase/lib/features/subscription/ui/widgets/unsubscribe_feedback_popup.dart +123 -0
- package/templates/firebase/lib/firebase_options.dart +75 -0
- package/templates/firebase/lib/google_auth_options.dart +10 -0
- package/templates/firebase/lib/i18n/app_locale_display.dart +17 -0
- package/templates/firebase/lib/i18n/en.i18n.json +514 -0
- package/templates/firebase/lib/i18n/es.i18n.json +514 -0
- package/templates/firebase/lib/i18n/pt.i18n.json +514 -0
- package/templates/firebase/lib/i18n/widgets/locale_code_badge.dart +47 -0
- package/templates/firebase/lib/main.dart +263 -0
- package/templates/firebase/lib/router.dart +226 -0
- package/templates/firebase/login-redesign-preview.png +0 -0
- package/templates/firebase/pubspec.yaml +177 -0
- package/templates/firebase/scripts/bump-ios-version.js +38 -0
- package/templates/firebase/scripts/release-ios.sh +137 -0
- package/templates/firebase/slang.yaml +7 -0
- package/templates/firebase/storage.rules +12 -0
- package/templates/firebase/test/core/data/api/analytics_api_fake.dart +25 -0
- package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +45 -0
- package/templates/firebase/test/core/data/api/storage_api_fake.dart +28 -0
- package/templates/firebase/test/core/data/models/subscription_test.dart +42 -0
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +56 -0
- package/templates/firebase/test/core/guards/guard_test.dart +78 -0
- package/templates/firebase/test/core/navigation/kasy_page_transition_test.dart +30 -0
- package/templates/firebase/test/core/onstart_service_test.dart +18 -0
- package/templates/firebase/test/core/rating/rating_api_fake.dart +58 -0
- package/templates/firebase/test/core/rating/rating_banner_test.dart +117 -0
- package/templates/firebase/test/core/rating/rating_test.dart +60 -0
- package/templates/firebase/test/core/security/secured_storage_fake.dart +33 -0
- package/templates/firebase/test/core/states/event_dispatcher_test.dart +269 -0
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +244 -0
- package/templates/firebase/test/core/widgets/responsive_layout_test.dart +50 -0
- package/templates/firebase/test/device_test_utils.dart +98 -0
- package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +169 -0
- package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +86 -0
- package/templates/firebase/test/features/authentication/lost_password_page_test.dart +59 -0
- package/templates/firebase/test/features/authentication/repositories/authentication_repository_test.dart +88 -0
- package/templates/firebase/test/features/authentication/signin_page_test.dart +133 -0
- package/templates/firebase/test/features/authentication/signup_page_test.dart +136 -0
- package/templates/firebase/test/features/notifications/data/device_api_fake.dart +66 -0
- package/templates/firebase/test/features/notifications/data/fake_facebook_api.dart +40 -0
- package/templates/firebase/test/features/notifications/data/local_notifier_fake.dart +59 -0
- package/templates/firebase/test/features/notifications/data/notifications_api_fake.dart +120 -0
- package/templates/firebase/test/features/notifications/data/notifications_settings_fake.dart +34 -0
- package/templates/firebase/test/features/notifications/providers/device_repository_test.dart +68 -0
- package/templates/firebase/test/features/notifications/providers/notifications_repository_test.dart +74 -0
- package/templates/firebase/test/features/notifications/ui/notifications_page_test.dart +69 -0
- package/templates/firebase/test/features/subscription/api/fake_inapp_subscription_api.dart +85 -0
- package/templates/firebase/test/features/subscription/api/fake_revenuecat_product.dart +186 -0
- package/templates/firebase/test/features/subscription/api/fake_subscription_api.dart +11 -0
- package/templates/firebase/test/features/subscription/subscription_page_test.dart +119 -0
- package/templates/firebase/test/firebase_test_utils.dart +25 -0
- package/templates/firebase/test/test_utils.dart +272 -0
- package/templates/firebase/web/favicon.png +0 -0
- package/templates/firebase/web/firebase-messaging-sw.js +27 -0
- package/templates/firebase/web/icons/Icon-192.png +0 -0
- package/templates/firebase/web/icons/Icon-512.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
- package/templates/firebase/web/index.html +110 -0
- package/templates/firebase/web/local_notifications.js +31 -0
- package/templates/firebase/web/manifest.json +35 -0
- package/templates/firebase/web/splash/img/dark-1x.png +0 -0
- package/templates/firebase/web/splash/img/dark-2x.png +0 -0
- package/templates/firebase/web/splash/img/dark-3x.png +0 -0
- package/templates/firebase/web/splash/img/dark-4x.png +0 -0
- package/templates/firebase/web/splash/img/light-1x.png +0 -0
- package/templates/firebase/web/splash/img/light-2x.png +0 -0
- package/templates/firebase/web/splash/img/light-3x.png +0 -0
- package/templates/firebase/web/splash/img/light-4x.png +0 -0
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase backend deployment:
|
|
3
|
+
* - Deploy Cloud Functions
|
|
4
|
+
* - Deploy Firestore security rules
|
|
5
|
+
* - Deploy Storage rules
|
|
6
|
+
*
|
|
7
|
+
* Requires: firebase-tools installed globally, user logged in.
|
|
8
|
+
* Prerequisites (client must enable manually): Firebase Auth (Email/Password),
|
|
9
|
+
* Secret Manager API, Firebase Storage. See PREREQUISITES.md.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { exec } = require('node:child_process');
|
|
13
|
+
const { promisify } = require('node:util');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const fs = require('fs-extra');
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Poll until all required GCP APIs are confirmed active.
|
|
26
|
+
* APIs enabled via `gcloud services enable` can take 1–3 min to propagate,
|
|
27
|
+
* especially in organization projects. This prevents deploy failures caused
|
|
28
|
+
* by APIs that are "enabling" but not yet ready.
|
|
29
|
+
*
|
|
30
|
+
* Throws a descriptive error if any API is still not active after the timeout.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} projectId
|
|
33
|
+
* @param {string[]} apis - list of API service names to wait for
|
|
34
|
+
* @param {object} [opts]
|
|
35
|
+
* @param {number} [opts.timeoutMs] - max total wait time across all APIs (default 10 min)
|
|
36
|
+
* @param {Function} [opts.onProgress] - called on each poll with (key, data)
|
|
37
|
+
*/
|
|
38
|
+
async function waitForApis(projectId, apis, { timeoutMs = 10 * 60 * 1000, onProgress } = {}) {
|
|
39
|
+
const deadline = Date.now() + timeoutMs;
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
for (const api of apis) {
|
|
42
|
+
while (true) {
|
|
43
|
+
const result = await run(
|
|
44
|
+
`gcloud services list --enabled --filter="name:${api}" --project=${projectId} --format='value(name)'`
|
|
45
|
+
);
|
|
46
|
+
if (result.ok && result.stdout.includes(api)) break;
|
|
47
|
+
if (Date.now() >= deadline) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`A API ${api} não ficou pronta em ${Math.round(timeoutMs / 60000)} minutos.\n` +
|
|
50
|
+
`Isso é incomum — tente rodar o deploy manualmente em alguns minutos:\n` +
|
|
51
|
+
`firebase deploy --project ${projectId} --only functions,firestore:rules,storage`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
55
|
+
if (onProgress) onProgress('wait-apis', { api, elapsed });
|
|
56
|
+
await sleep(5000);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Poll until a GCP service account exists (is visible via `gcloud iam`).
|
|
63
|
+
* On brand-new projects, service accounts created by enabling APIs may take
|
|
64
|
+
* a minute to propagate before IAM policy bindings can be applied to them.
|
|
65
|
+
* Non-fatal: returns false if not found within the timeout.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} serviceAccountEmail
|
|
68
|
+
* @param {string} projectId
|
|
69
|
+
* @param {number} timeoutMs - max wait time (default 2 min)
|
|
70
|
+
*/
|
|
71
|
+
async function waitForServiceAccount(serviceAccountEmail, projectId, timeoutMs = 2 * 60 * 1000) {
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
while (true) {
|
|
74
|
+
const result = await run(
|
|
75
|
+
`gcloud iam service-accounts describe ${serviceAccountEmail} --project=${projectId}`
|
|
76
|
+
);
|
|
77
|
+
if (result.ok) return true;
|
|
78
|
+
if (Date.now() >= deadline) return false;
|
|
79
|
+
await sleep(5000);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Poll until the Eventarc Service Agent has its required IAM role on the project.
|
|
85
|
+
* Returns as soon as the binding is confirmed — no unnecessary waiting on re-deploys
|
|
86
|
+
* where the role was already granted in a previous run.
|
|
87
|
+
*
|
|
88
|
+
* On first deploy: waits up to timeoutMs (typically 60s) for propagation.
|
|
89
|
+
* On re-deploy: returns in one poll cycle (~1s) because the binding already exists.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} projectId
|
|
92
|
+
* @param {string} projectNumber
|
|
93
|
+
* @param {number} timeoutMs
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* Poll until the Eventarc Service Agent has its required IAM role on the project.
|
|
97
|
+
* Returns true when confirmed, false if the binding never appeared within timeoutMs.
|
|
98
|
+
* A false return means the IAM grant is being blocked (org policy or VPC SC).
|
|
99
|
+
*
|
|
100
|
+
* On re-deploy: returns true in one poll cycle (~1s) because the binding already exists.
|
|
101
|
+
* On first deploy: polls up to timeoutMs for the binding to propagate.
|
|
102
|
+
*/
|
|
103
|
+
async function waitForEventarcIam(projectId, projectNumber, timeoutMs = 60 * 1000) {
|
|
104
|
+
const eventarcSa = `service-${projectNumber}@gcp-sa-eventarc.iam.gserviceaccount.com`;
|
|
105
|
+
const deadline = Date.now() + timeoutMs;
|
|
106
|
+
while (true) {
|
|
107
|
+
const result = await run(
|
|
108
|
+
`gcloud projects get-iam-policy ${projectId} ` +
|
|
109
|
+
`--flatten="bindings[].members" ` +
|
|
110
|
+
`--filter="bindings.role=roles/eventarc.serviceAgent AND bindings.members=serviceAccount:${eventarcSa}" ` +
|
|
111
|
+
`--format="value(bindings.members)" --limit=1`
|
|
112
|
+
);
|
|
113
|
+
if (result.ok && result.stdout.trim().length > 0) return true; // binding confirmed
|
|
114
|
+
if (Date.now() >= deadline) return false; // timed out — grant likely blocked
|
|
115
|
+
await sleep(5000);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Verify that the App Engine default SA has roles/datastore.user on the project.
|
|
121
|
+
* Cloud Functions v1 (auth triggers) run as this SA and need Firestore write access.
|
|
122
|
+
* Returns true if the binding exists, false otherwise.
|
|
123
|
+
*/
|
|
124
|
+
async function verifyAppEngineSaGrant(projectId) {
|
|
125
|
+
const appEngineSa = `${projectId}@appspot.gserviceaccount.com`;
|
|
126
|
+
const result = await run(
|
|
127
|
+
`gcloud projects get-iam-policy ${projectId} ` +
|
|
128
|
+
`--flatten="bindings[].members" ` +
|
|
129
|
+
`--filter="bindings.role=roles/datastore.user AND bindings.members=serviceAccount:${appEngineSa}" ` +
|
|
130
|
+
`--format="value(bindings.members)" --limit=1`
|
|
131
|
+
);
|
|
132
|
+
return result.ok && result.stdout.trim().length > 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Verify that the compute SA has read access to a GCF source bucket.
|
|
137
|
+
* Returns true if the SA appears in the bucket's IAM policy, false otherwise.
|
|
138
|
+
* A false return indicates the bucket-level grant was blocked (org policy or VPC SC).
|
|
139
|
+
*/
|
|
140
|
+
async function verifyStorageGrant(projectNumber, region) {
|
|
141
|
+
const computeSa = `${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
142
|
+
const bucket = `gs://gcf-sources-${projectNumber}-${region}`;
|
|
143
|
+
// gcloud storage buckets get-iam-policy does not support --flatten/--filter/--limit.
|
|
144
|
+
// Parse JSON output instead and search for the member manually.
|
|
145
|
+
const result = await run(`gcloud storage buckets get-iam-policy ${bucket} --format=json`);
|
|
146
|
+
if (!result.ok) return false;
|
|
147
|
+
try {
|
|
148
|
+
const policy = JSON.parse(result.stdout);
|
|
149
|
+
for (const binding of (policy.bindings || [])) {
|
|
150
|
+
if (binding.members && binding.members.includes(`serviceAccount:${computeSa}`)) return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
} catch (_) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function run(cmd, cwd) {
|
|
159
|
+
try {
|
|
160
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
161
|
+
cwd,
|
|
162
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
163
|
+
});
|
|
164
|
+
return { ok: true, stdout, stderr };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
error: err.message,
|
|
169
|
+
stdout: err.stdout || '',
|
|
170
|
+
stderr: err.stderr || '',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Apply multiple IAM role bindings to a GCP project in a single atomic operation.
|
|
177
|
+
* Uses get-iam-policy + set-iam-policy to avoid the race condition that occurs
|
|
178
|
+
* when many parallel add-iam-policy-binding calls all read and write the same policy.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} projectId - GCP project ID
|
|
181
|
+
* @param {Array<{role: string, member: string}>} bindings - list of {role, member} to add
|
|
182
|
+
*/
|
|
183
|
+
async function applyProjectIamBindings(projectId, bindings) {
|
|
184
|
+
const policyResult = await run(`gcloud projects get-iam-policy ${projectId} --format=json`);
|
|
185
|
+
if (!policyResult.ok) return;
|
|
186
|
+
let policy;
|
|
187
|
+
try { policy = JSON.parse(policyResult.stdout); } catch (_) { return; }
|
|
188
|
+
|
|
189
|
+
for (const { role, member } of bindings) {
|
|
190
|
+
let binding = (policy.bindings || []).find(b => b.role === role);
|
|
191
|
+
if (!binding) {
|
|
192
|
+
binding = { role, members: [] };
|
|
193
|
+
policy.bindings.push(binding);
|
|
194
|
+
}
|
|
195
|
+
if (!binding.members.includes(member)) {
|
|
196
|
+
binding.members.push(member);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const tmpFile = path.join(os.tmpdir(), `iam-policy-${projectId}.json`);
|
|
201
|
+
await fs.writeJson(tmpFile, policy);
|
|
202
|
+
await run(`gcloud projects set-iam-policy ${projectId} ${tmpFile} --quiet`);
|
|
203
|
+
await fs.remove(tmpFile);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Copy the service account key to the project directory if it's not already there.
|
|
208
|
+
*/
|
|
209
|
+
async function ensureServiceAccountKey(projectDir, serviceAccountPath) {
|
|
210
|
+
if (!serviceAccountPath) return { ok: false, error: 'No service account path provided.' };
|
|
211
|
+
|
|
212
|
+
const destPath = path.join(projectDir, 'firebase_key.json');
|
|
213
|
+
const srcPath = path.resolve(serviceAccountPath);
|
|
214
|
+
|
|
215
|
+
if (!(await fs.pathExists(srcPath))) {
|
|
216
|
+
return { ok: false, error: `Service account file not found: ${srcPath}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (srcPath !== destPath) {
|
|
220
|
+
await fs.copy(srcPath, destPath);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { ok: true };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Install dependencies in the functions/ folder.
|
|
228
|
+
*/
|
|
229
|
+
async function installFunctionsDeps(projectDir) {
|
|
230
|
+
const functionsDir = path.join(projectDir, 'functions');
|
|
231
|
+
if (!(await fs.pathExists(functionsDir))) {
|
|
232
|
+
return { ok: true, skipped: true };
|
|
233
|
+
}
|
|
234
|
+
return run('npm install', functionsDir);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Write .firebaserc so firebase-tools knows which project to target.
|
|
239
|
+
* Without this file, `firebase deploy` asks interactively or fails.
|
|
240
|
+
*/
|
|
241
|
+
async function writeFirebaseRc(projectDir, firebaseProjectId) {
|
|
242
|
+
const rc = {
|
|
243
|
+
projects: {
|
|
244
|
+
default: firebaseProjectId,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
await fs.outputFile(
|
|
248
|
+
path.join(projectDir, '.firebaserc'),
|
|
249
|
+
JSON.stringify(rc, null, 2) + '\n',
|
|
250
|
+
'utf8'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Secrets that Cloud Functions define via defineSecret().
|
|
256
|
+
* These must exist in Secret Manager before `firebase deploy` runs,
|
|
257
|
+
* otherwise firebase-tools prompts interactively and hangs in non-TTY environments.
|
|
258
|
+
*/
|
|
259
|
+
const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WEBHOOK_KEY', 'LLM_API_KEY'];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create each required secret if it doesn't exist yet.
|
|
263
|
+
* Uses the real value when provided, otherwise falls back to 'placeholder'.
|
|
264
|
+
* Uses gcloud to avoid firebase-tools interactive prompts.
|
|
265
|
+
* Non-fatal: if a secret can't be created (e.g. API not enabled yet) we continue anyway.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} firebaseProjectId
|
|
268
|
+
* @param {Record<string,string>} [secretValues] - map of secret name → real value (optional)
|
|
269
|
+
*/
|
|
270
|
+
async function ensureSecretsExist(firebaseProjectId, secretValues = {}) {
|
|
271
|
+
for (const secret of REQUIRED_SECRETS) {
|
|
272
|
+
const check = await run(`gcloud secrets describe ${secret} --project=${firebaseProjectId}`);
|
|
273
|
+
if (!check.ok) {
|
|
274
|
+
const value = secretValues[secret]?.trim() || 'placeholder';
|
|
275
|
+
await run(
|
|
276
|
+
`printf '${value.replace(/'/g, "'\\''")}' | gcloud secrets create ${secret} --data-file=- --replication-policy=automatic --project=${firebaseProjectId}`
|
|
277
|
+
);
|
|
278
|
+
} else if (secretValues[secret]?.trim()) {
|
|
279
|
+
// Secret exists but user provided a real value — update it.
|
|
280
|
+
await run(
|
|
281
|
+
`printf '${secretValues[secret].trim().replace(/'/g, "'\\''")}' | gcloud secrets versions add ${secret} --data-file=- --project=${firebaseProjectId}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the numeric project number for a GCP project ID.
|
|
289
|
+
*/
|
|
290
|
+
async function getProjectNumber(firebaseProjectId) {
|
|
291
|
+
const result = await run(
|
|
292
|
+
`gcloud projects describe ${firebaseProjectId} --format='value(projectNumber)'`
|
|
293
|
+
);
|
|
294
|
+
if (!result.ok) return null;
|
|
295
|
+
return result.stdout.trim();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Pre-create Cloud Functions source buckets and grant compute SA access at bucket level.
|
|
300
|
+
*
|
|
301
|
+
* WHY: Firebase creates gcf-sources-* and gcf-v2-sources-* buckets during the first deploy.
|
|
302
|
+
* Cloud Build immediately tries to read source code from those buckets. Even though we grant
|
|
303
|
+
* roles/storage.objectAdmin at the project level, GCP IAM inheritance to newly-created
|
|
304
|
+
* resources in org projects can take 60-120s to propagate. Cloud Build fails in that window.
|
|
305
|
+
*
|
|
306
|
+
* By pre-creating the buckets ourselves and granting bucket-level permissions directly,
|
|
307
|
+
* the permissions are in place before Cloud Build ever touches the bucket.
|
|
308
|
+
*
|
|
309
|
+
* Non-fatal: if the bucket already exists (repeated deploy) we skip creation and just
|
|
310
|
+
* re-apply the permission grant (idempotent).
|
|
311
|
+
*/
|
|
312
|
+
async function ensureGcfSourceBuckets(firebaseProjectId, projectNumber, functionsRegion = 'us-central1') {
|
|
313
|
+
const regions = [...new Set(['us-central1', functionsRegion])];
|
|
314
|
+
const computeSa = `${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
315
|
+
const cloudBuildSa = `${projectNumber}@cloudbuild.gserviceaccount.com`;
|
|
316
|
+
|
|
317
|
+
for (const region of regions) {
|
|
318
|
+
for (const bucketPrefix of ['gcf-sources', 'gcf-v2-sources']) {
|
|
319
|
+
const bucket = `gs://${bucketPrefix}-${projectNumber}-${region}`;
|
|
320
|
+
// Create the bucket if it doesn't exist yet.
|
|
321
|
+
// Non-fatal: "409 already exists" is fine — we just proceed to the IAM grant below.
|
|
322
|
+
await run(
|
|
323
|
+
`gcloud storage buckets create ${bucket} --location=${region} --project=${firebaseProjectId} --uniform-bucket-level-access`
|
|
324
|
+
);
|
|
325
|
+
// Grant objectAdmin at bucket level — covers the window where project-level IAM
|
|
326
|
+
// hasn't yet propagated to this specific bucket in org projects.
|
|
327
|
+
for (const sa of [computeSa, cloudBuildSa]) {
|
|
328
|
+
await run(
|
|
329
|
+
`gcloud storage buckets add-iam-policy-binding ${bucket} --member=serviceAccount:${sa} --role=roles/storage.objectAdmin --project=${firebaseProjectId}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Grant all permissions required for Cloud Functions to build and deploy.
|
|
338
|
+
* Safe to call multiple times — all operations are idempotent.
|
|
339
|
+
* Non-fatal per operation — some resources may not exist yet in all regions.
|
|
340
|
+
*
|
|
341
|
+
* Service accounts that need access:
|
|
342
|
+
* - compute SA (PROJECT_NUMBER-compute@developer.gserviceaccount.com)
|
|
343
|
+
* → Storage Object Viewer on gcf-sources buckets (v1 functions)
|
|
344
|
+
* → roles/cloudbuild.builds.builder at project level (new GCP projects post Apr 2024
|
|
345
|
+
* use compute SA as the default Cloud Build SA instead of the legacy cloudBuild SA)
|
|
346
|
+
* → roles/artifactregistry.writer on gcf-artifacts (v2 functions)
|
|
347
|
+
* - Cloud Build SA (PROJECT_NUMBER@cloudbuild.gserviceaccount.com)
|
|
348
|
+
* → roles/cloudbuild.builds.builder at project level (legacy GCP projects)
|
|
349
|
+
* → roles/artifactregistry.writer on gcf-artifacts (legacy GCP projects)
|
|
350
|
+
*/
|
|
351
|
+
async function grantBuildPermissions(firebaseProjectId, projectNumber, functionsRegion = 'us-central1') {
|
|
352
|
+
const computeSa = `${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
353
|
+
const cloudBuildSa = `${projectNumber}@cloudbuild.gserviceaccount.com`;
|
|
354
|
+
const gcfSa = `service-${projectNumber}@gcf-admin-robot.iam.gserviceaccount.com`;
|
|
355
|
+
const cloudBuildAgentSa = `service-${projectNumber}@gcp-sa-cloudbuild.iam.gserviceaccount.com`;
|
|
356
|
+
// us-central1 is always needed (auth trigger v1 always deploys there).
|
|
357
|
+
// Add the chosen functions region if different.
|
|
358
|
+
const regions = [...new Set(['us-central1', functionsRegion])];
|
|
359
|
+
|
|
360
|
+
// Storage Object Viewer on GCF source buckets:
|
|
361
|
+
// - gcf-sources-* → Gen 1 functions (us-central1 auth trigger)
|
|
362
|
+
// - gcf-v2-sources-* → Gen 2 functions (Cloud Build fetches source from here)
|
|
363
|
+
// Both naming patterns need the compute SA to have read access.
|
|
364
|
+
for (const region of regions) {
|
|
365
|
+
for (const bucketPrefix of ['gcf-sources', 'gcf-v2-sources']) {
|
|
366
|
+
const bucket = `gs://${bucketPrefix}-${projectNumber}-${region}`;
|
|
367
|
+
await run(
|
|
368
|
+
`gcloud storage buckets add-iam-policy-binding ${bucket} --member=serviceAccount:${computeSa} --role=roles/storage.objectViewer --project=${firebaseProjectId}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Cloud Build builder role — grant to BOTH SAs.
|
|
374
|
+
// New GCP projects (post Apr 2024) use the compute SA as the default Cloud Build SA
|
|
375
|
+
// for v2 Cloud Functions builds. Older projects use the legacy cloudBuild SA.
|
|
376
|
+
// Granting to both ensures compatibility across all project ages.
|
|
377
|
+
for (const sa of [computeSa, cloudBuildSa]) {
|
|
378
|
+
await run(
|
|
379
|
+
`gcloud projects add-iam-policy-binding ${firebaseProjectId} --member=serviceAccount:${sa} --role=roles/cloudbuild.builds.builder`
|
|
380
|
+
);
|
|
381
|
+
await run(
|
|
382
|
+
`gcloud projects add-iam-policy-binding ${firebaseProjectId} --member=serviceAccount:${sa} --role=roles/editor`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Cloud Functions Service Agent role — required for Cloud Build to build Cloud Functions.
|
|
387
|
+
// This is the most common cause of "missing permission on the build service account".
|
|
388
|
+
// See: https://cloud.google.com/functions/docs/troubleshooting#build-service-account
|
|
389
|
+
for (const sa of [computeSa, cloudBuildSa]) {
|
|
390
|
+
await run(
|
|
391
|
+
`gcloud projects add-iam-policy-binding ${firebaseProjectId} --member=serviceAccount:${sa} --role=roles/cloudfunctions.serviceAgent`
|
|
392
|
+
);
|
|
393
|
+
await run(
|
|
394
|
+
`gcloud projects add-iam-policy-binding ${firebaseProjectId} --member=serviceAccount:${sa} --role=roles/logging.logWriter`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Artifact Registry writer — grant to BOTH SAs for the same reason.
|
|
399
|
+
// The gcf-artifacts repo is created during the first deploy.
|
|
400
|
+
for (const region of regions) {
|
|
401
|
+
for (const sa of [computeSa, cloudBuildSa]) {
|
|
402
|
+
await run(
|
|
403
|
+
`gcloud artifacts repositories add-iam-policy-binding gcf-artifacts --location=${region} --member=serviceAccount:${sa} --role=roles/artifactregistry.writer --project=${firebaseProjectId}`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Eventarc Service Agent — required for 2nd gen functions event triggers.
|
|
409
|
+
const eventarcSa = `service-${projectNumber}@gcp-sa-eventarc.iam.gserviceaccount.com`;
|
|
410
|
+
await run(
|
|
411
|
+
`gcloud projects add-iam-policy-binding ${firebaseProjectId} --member=serviceAccount:${eventarcSa} --role=roles/eventarc.serviceAgent`
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Pub/Sub SA — needed for Eventarc Firestore triggers (v2 functions).
|
|
415
|
+
// Pub/Sub delivers Firestore events to Cloud Run via HTTP push, and must be able to
|
|
416
|
+
// create OIDC tokens for the compute SA to authenticate those push requests.
|
|
417
|
+
const pubsubSa = `service-${projectNumber}@gcp-sa-pubsub.iam.gserviceaccount.com`;
|
|
418
|
+
// Wait for the SA to exist before granting — on brand-new projects the SA may not have
|
|
419
|
+
// propagated yet even after identity create. If the grant runs before the SA exists
|
|
420
|
+
// it fails silently, leaving Eventarc without the token-creator permission, which
|
|
421
|
+
// causes Cloud Run to be created as an HTTPS function (no Eventarc trigger attached).
|
|
422
|
+
await waitForServiceAccount(pubsubSa, firebaseProjectId);
|
|
423
|
+
await run(
|
|
424
|
+
`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${pubsubSa} --role=roles/iam.serviceAccountTokenCreator --project=${firebaseProjectId}`
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Service-account-level bindings: allow service agents to impersonate the compute SA.
|
|
428
|
+
// Required for Cloud Functions v2 builds — the GCF and Cloud Build agents must be able
|
|
429
|
+
// to create tokens for the compute SA (which runs Cloud Run services).
|
|
430
|
+
await Promise.all([
|
|
431
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${gcfSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
432
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${cloudBuildAgentSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
433
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${cloudBuildSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
434
|
+
]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Deploy Cloud Functions + Firestore rules + Storage rules.
|
|
439
|
+
*
|
|
440
|
+
* On a brand-new GCP project the first deploy may fail because:
|
|
441
|
+
* - gcf-sources-PROJECT_NUMBER-REGION (Gen 1) and gcf-v2-sources-* (Gen 2) are created
|
|
442
|
+
* during the deploy but the compute SA lacks Storage Object Viewer on them.
|
|
443
|
+
* (Project-level roles — Cloud Build builder, Eventarc service agent — are pre-granted
|
|
444
|
+
* by runDeploy before this function is called.)
|
|
445
|
+
*
|
|
446
|
+
* Strategy: attempt deploy → if it fails with bucket permission errors, grant bucket
|
|
447
|
+
* roles (buckets now exist after the failed attempt), wait 4 min, retry.
|
|
448
|
+
*/
|
|
449
|
+
async function deployFirebase(projectDir, firebaseProjectId, { onProgress, functionsRegion = 'us-central1' } = {}) {
|
|
450
|
+
const cmd = `firebase deploy --only functions,firestore:rules,firestore:indexes,storage --project=${firebaseProjectId} --force`;
|
|
451
|
+
|
|
452
|
+
// Normalise: firebase-tools sometimes exits 0 but prints errors in stdout.
|
|
453
|
+
// Treat those as failures so the retry logic can act on them.
|
|
454
|
+
function normalise(r) {
|
|
455
|
+
if (!r.ok) return r;
|
|
456
|
+
const outLow = (r.stdout + r.stderr).toLowerCase();
|
|
457
|
+
const hasError =
|
|
458
|
+
outLow.includes('changing from an https function to a background triggered') ||
|
|
459
|
+
outLow.includes('changing from a background triggered function to an https') ||
|
|
460
|
+
outLow.includes('missing permission on the build service account') ||
|
|
461
|
+
outLow.includes('build failed with status: failure') ||
|
|
462
|
+
outLow.includes('functions deploy had errors');
|
|
463
|
+
if (!hasError) return r;
|
|
464
|
+
return { ok: false, error: 'Deploy errors in output (exit code 0)', stderr: r.stderr, stdout: r.stdout };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function hasTriggerConflict(r) {
|
|
468
|
+
const t = (r.error + r.stderr + r.stdout).toLowerCase();
|
|
469
|
+
return (
|
|
470
|
+
t.includes('changing from an https function to a background triggered') ||
|
|
471
|
+
t.includes('changing from a background triggered function to an https')
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function hasPermissionError(r) {
|
|
476
|
+
const t = (r.error + r.stderr + r.stdout).toLowerCase();
|
|
477
|
+
return (
|
|
478
|
+
t.includes('storage object viewer') ||
|
|
479
|
+
t.includes('eventarc service agent') ||
|
|
480
|
+
t.includes('first time using 2nd gen') ||
|
|
481
|
+
t.includes('retry the deployment in a few minutes') ||
|
|
482
|
+
t.includes('missing permission on the build service account') ||
|
|
483
|
+
t.includes('build service account') ||
|
|
484
|
+
t.includes('gcf-sources') ||
|
|
485
|
+
t.includes('gcf-v2-sources') ||
|
|
486
|
+
t.includes('permission_denied') ||
|
|
487
|
+
t.includes('caller does not have permission') ||
|
|
488
|
+
t.includes('serviceaccounts.actas') ||
|
|
489
|
+
t.includes('iam.serviceaccounts') ||
|
|
490
|
+
t.includes('cloud run admin') ||
|
|
491
|
+
t.includes('roles/run') ||
|
|
492
|
+
t.includes('artifact registry') ||
|
|
493
|
+
t.includes('"code":403') || t.includes('"code": 403') ||
|
|
494
|
+
t.includes('failed to build') ||
|
|
495
|
+
t.includes('build failed') ||
|
|
496
|
+
t.includes('api is not enabled') ||
|
|
497
|
+
t.includes('not enabled on project') ||
|
|
498
|
+
t.includes('has not been used in project') ||
|
|
499
|
+
t.includes('api_not_activated') ||
|
|
500
|
+
t.includes('enable it by visiting') ||
|
|
501
|
+
t.includes('cloudfunctions.googleapis.com') ||
|
|
502
|
+
t.includes('cloudbuild.googleapis.com') ||
|
|
503
|
+
t.includes('run.googleapis.com') ||
|
|
504
|
+
// GCP org-policy violations block IAM grants and Storage access in org projects.
|
|
505
|
+
// These errors appear when the org has constraints like iam.allowedPolicyMemberDomains.
|
|
506
|
+
t.includes('policy_violated') ||
|
|
507
|
+
t.includes('violates the constraint') ||
|
|
508
|
+
t.includes('constraints/iam.allowed') ||
|
|
509
|
+
t.includes('orgpolicy') ||
|
|
510
|
+
t.includes('org policy') ||
|
|
511
|
+
t.includes('organization policy') ||
|
|
512
|
+
t.includes('denied by organization policy')
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function deleteBrokenFunctions(r) {
|
|
517
|
+
const text = r.error + r.stderr + r.stdout;
|
|
518
|
+
// Delete the explicitly mentioned broken function(s) from the error output.
|
|
519
|
+
const pattern = /(?:Error:\s*)?\[([^\]]+)\(([^)]+)\)\][^\n]*changing from/gi;
|
|
520
|
+
let m;
|
|
521
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
522
|
+
const funcName = m[1].trim();
|
|
523
|
+
const region = m[2].trim();
|
|
524
|
+
const fbDelete = await run(
|
|
525
|
+
`firebase functions:delete ${funcName} --region=${region} --project=${firebaseProjectId} --force`,
|
|
526
|
+
projectDir
|
|
527
|
+
);
|
|
528
|
+
if (!fbDelete.ok) {
|
|
529
|
+
// Fallback: delete the underlying Cloud Run service directly.
|
|
530
|
+
// Cloud Run service IDs are the Firebase function name in lowercase.
|
|
531
|
+
const serviceId = funcName.toLowerCase();
|
|
532
|
+
await run(
|
|
533
|
+
`gcloud run services delete ${serviceId} --region=${region} --project=${firebaseProjectId} --quiet`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Full sweep: find ALL Firebase Cloud Run services that have no Eventarc trigger.
|
|
539
|
+
//
|
|
540
|
+
// When Eventarc IAM isn't ready on the first deploy, EVERY Eventarc-triggered function
|
|
541
|
+
// gets created as a plain HTTPS Cloud Run service (no trigger attached). firebase deploy
|
|
542
|
+
// reports only ONE conflict per run and aborts — so without this sweep each retry can
|
|
543
|
+
// only fix one function, needing N retries for N broken functions and exhausting
|
|
544
|
+
// MAX_ATTEMPTS before all functions are fixed.
|
|
545
|
+
//
|
|
546
|
+
// Strategy: list all Cloud Run services deployed by Firebase in each region, list all
|
|
547
|
+
// Eventarc triggers in that region, then delete any Firebase service that has NO trigger
|
|
548
|
+
// pointing to it. HTTP / callable functions that are correctly deployed as HTTPS will
|
|
549
|
+
// also be deleted here, but firebase deploy will recreate them correctly on the next
|
|
550
|
+
// attempt without conflict (they don't have a trigger type mismatch).
|
|
551
|
+
const sweepRegions = [...new Set(['us-central1', functionsRegion])];
|
|
552
|
+
for (const region of sweepRegions) {
|
|
553
|
+
// Find which Cloud Run services already have Eventarc triggers.
|
|
554
|
+
const triggersResult = await run(
|
|
555
|
+
`gcloud eventarc triggers list --location=${region} --project=${firebaseProjectId} --format=json`
|
|
556
|
+
);
|
|
557
|
+
const servicesWithTriggers = new Set();
|
|
558
|
+
if (triggersResult.ok && triggersResult.stdout.trim().startsWith('[')) {
|
|
559
|
+
try {
|
|
560
|
+
for (const t of JSON.parse(triggersResult.stdout)) {
|
|
561
|
+
const svc = t.destination?.cloudRun?.service;
|
|
562
|
+
if (svc) servicesWithTriggers.add(svc);
|
|
563
|
+
}
|
|
564
|
+
} catch (_) {}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// List only Cloud Run services deployed by firebase-tools (label set by Firebase CLI).
|
|
568
|
+
const listResult = await run(
|
|
569
|
+
`gcloud run services list --region=${region} --project=${firebaseProjectId} --format='value(metadata.name)' --filter="labels.'firebase-functions-codebase'=default"`
|
|
570
|
+
);
|
|
571
|
+
if (!listResult.ok || !listResult.stdout.trim()) continue;
|
|
572
|
+
|
|
573
|
+
for (const service of listResult.stdout.trim().split('\n').filter(Boolean)) {
|
|
574
|
+
if (!servicesWithTriggers.has(service)) {
|
|
575
|
+
// No Eventarc trigger points to this service — it's stuck in HTTPS state.
|
|
576
|
+
// Delete it so the next deploy can recreate it correctly (with trigger or as HTTP).
|
|
577
|
+
await run(
|
|
578
|
+
`gcloud run services delete ${service} --region=${region} --project=${firebaseProjectId} --quiet`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Allow GCP to fully propagate all deletions before the caller retries.
|
|
585
|
+
// Cloud Run service deletion can take 30–45s in org projects — too short a wait
|
|
586
|
+
// causes the next attempt to still see the old services and hit the conflict again.
|
|
587
|
+
await sleep(45 * 1000);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Build a human-readable error message with the manual gcloud commands the user
|
|
591
|
+
// (or their GCP admin) needs to run to fix the IAM configuration.
|
|
592
|
+
function buildManualFixMessage(pNum, pId, reason) {
|
|
593
|
+
const computeSa = `${pNum}-compute@developer.gserviceaccount.com`;
|
|
594
|
+
const eventarcSa = `service-${pNum}@gcp-sa-eventarc.iam.gserviceaccount.com`;
|
|
595
|
+
return (
|
|
596
|
+
`${reason}\n\n` +
|
|
597
|
+
`Execute os comandos abaixo manualmente e rode "kasy deploy" novamente:\n\n` +
|
|
598
|
+
` # 1. Permissão de Storage para Cloud Build (authTriggers)\n` +
|
|
599
|
+
` gcloud storage buckets add-iam-policy-binding gs://gcf-sources-${pNum}-us-central1 \\\n` +
|
|
600
|
+
` --member="serviceAccount:${computeSa}" \\\n` +
|
|
601
|
+
` --role="roles/storage.objectAdmin" \\\n` +
|
|
602
|
+
` --project=${pId}\n\n` +
|
|
603
|
+
` # 2. Permissão do Eventarc Service Agent (notificationsTriggers)\n` +
|
|
604
|
+
` gcloud projects add-iam-policy-binding ${pId} \\\n` +
|
|
605
|
+
` --member="serviceAccount:${eventarcSa}" \\\n` +
|
|
606
|
+
` --role="roles/eventarc.serviceAgent"\n\n` +
|
|
607
|
+
` # 3. Editor no projeto (conta de serviço do Cloud Build)\n` +
|
|
608
|
+
` gcloud projects add-iam-policy-binding ${pId} \\\n` +
|
|
609
|
+
` --member="serviceAccount:${computeSa}" \\\n` +
|
|
610
|
+
` --role="roles/editor"`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Retry loop. Strategy:
|
|
615
|
+
// - Attempt 1: deploy → if fails, grant IAM + verify IAM (up to 60s poll).
|
|
616
|
+
// · If grants NOT in IAM after 60s → org policy blocking grants → stop, show manual fix.
|
|
617
|
+
// · If grants confirmed → retry.
|
|
618
|
+
// - Attempt 2+: if same pure permission error with grants confirmed → VPC SC or deeper
|
|
619
|
+
// org restriction — retrying won't help. Show manual fix and stop immediately.
|
|
620
|
+
// - Trigger conflict on any attempt → always delete broken services and retry once more.
|
|
621
|
+
const MAX_ATTEMPTS = 3; // fail fast — each attempt takes 3-5 min; 3 is enough
|
|
622
|
+
let projectNumber = null;
|
|
623
|
+
let lastResult = null;
|
|
624
|
+
let iamConfirmed = false; // true once grants appeared in IAM policy
|
|
625
|
+
|
|
626
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
627
|
+
lastResult = normalise(await run(cmd, projectDir));
|
|
628
|
+
if (lastResult.ok) return lastResult;
|
|
629
|
+
|
|
630
|
+
const triggerConflict = hasTriggerConflict(lastResult);
|
|
631
|
+
const permError = hasPermissionError(lastResult);
|
|
632
|
+
|
|
633
|
+
if (!triggerConflict && !permError) {
|
|
634
|
+
break; // Unknown error — stop retrying.
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!projectNumber) projectNumber = await getProjectNumber(firebaseProjectId);
|
|
638
|
+
|
|
639
|
+
// If we still can't get the project number, bail out with a clear error.
|
|
640
|
+
if (!projectNumber) {
|
|
641
|
+
lastResult = {
|
|
642
|
+
ok: false,
|
|
643
|
+
error:
|
|
644
|
+
`Não foi possível obter o número do projeto "${firebaseProjectId}" via gcloud.\n` +
|
|
645
|
+
`Verifique se o projeto existe e se você tem acesso:\n` +
|
|
646
|
+
` gcloud projects describe ${firebaseProjectId}`,
|
|
647
|
+
stdout: '', stderr: '',
|
|
648
|
+
};
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// If grants were already confirmed in IAM (from a previous attempt) but the SAME
|
|
653
|
+
// permission error recurs without a trigger conflict to clean up, retrying won't fix it.
|
|
654
|
+
// The issue is beyond IAM (VPC Service Controls, org policy on API calls, etc.).
|
|
655
|
+
if (iamConfirmed && permError && !triggerConflict) {
|
|
656
|
+
lastResult = {
|
|
657
|
+
ok: false,
|
|
658
|
+
error: buildManualFixMessage(
|
|
659
|
+
projectNumber, firebaseProjectId,
|
|
660
|
+
'As permissões foram concedidas e confirmadas no IAM, mas o Cloud Build ainda está com acesso negado.\n' +
|
|
661
|
+
'Isso indica VPC Service Controls ou uma Org Policy bloqueando o acesso a nível de rede.\n' +
|
|
662
|
+
'Configure as permissões abaixo manualmente no console do GCP e tente novamente:'
|
|
663
|
+
),
|
|
664
|
+
stdout: '', stderr: '',
|
|
665
|
+
};
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Always sweep for broken Cloud Run services (HTTPS without Eventarc trigger).
|
|
670
|
+
// Previously this only ran when triggerConflict appeared in the error output,
|
|
671
|
+
// but when a storage permission error causes Cloud Build to fail FIRST, the
|
|
672
|
+
// trigger conflict error is masked — Firebase never reaches the deploy phase
|
|
673
|
+
// where it would report the conflict. Running the sweep unconditionally ensures
|
|
674
|
+
// leftover broken services from previous failed attempts are always cleaned up.
|
|
675
|
+
if (onProgress) onProgress('deploy-fixing-triggers');
|
|
676
|
+
await deleteBrokenFunctions(lastResult);
|
|
677
|
+
|
|
678
|
+
// Re-grant all required IAM roles.
|
|
679
|
+
if (onProgress) onProgress('deploy-granting-iam');
|
|
680
|
+
if (projectNumber) await grantBuildPermissions(firebaseProjectId, projectNumber, functionsRegion);
|
|
681
|
+
|
|
682
|
+
// Verify the critical grants actually appeared in IAM (up to 60s poll).
|
|
683
|
+
// If they never appear, the grant command itself was blocked — stop immediately.
|
|
684
|
+
const eventarcOk = projectNumber
|
|
685
|
+
? await waitForEventarcIam(firebaseProjectId, projectNumber, 60 * 1000)
|
|
686
|
+
: true;
|
|
687
|
+
const storageOk = projectNumber
|
|
688
|
+
? await verifyStorageGrant(projectNumber, 'us-central1')
|
|
689
|
+
: true;
|
|
690
|
+
|
|
691
|
+
if (!eventarcOk || !storageOk) {
|
|
692
|
+
lastResult = {
|
|
693
|
+
ok: false,
|
|
694
|
+
error: buildManualFixMessage(
|
|
695
|
+
projectNumber, firebaseProjectId,
|
|
696
|
+
'Tentamos conceder as permissões automaticamente, mas elas não aparecem no IAM após 60s.\n' +
|
|
697
|
+
'Uma Org Policy está bloqueando os grants automáticos.\n' +
|
|
698
|
+
'Configure manualmente e rode "kasy deploy" novamente:'
|
|
699
|
+
),
|
|
700
|
+
stdout: '', stderr: '',
|
|
701
|
+
};
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
iamConfirmed = true;
|
|
706
|
+
if (onProgress) onProgress('deploy-retry-wait');
|
|
707
|
+
await sleep(30 * 1000);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return lastResult;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Create an App Engine app in the project.
|
|
715
|
+
* Required for Cloud Functions v1 (auth triggers always deploy to us-central1 via v1).
|
|
716
|
+
* Firebase uses App Engine infrastructure to store function source code for v1 functions.
|
|
717
|
+
* Safe to call multiple times — if the app already exists, returns ok: true.
|
|
718
|
+
* Non-fatal: if creation fails for any reason other than "already exists", we continue.
|
|
719
|
+
*/
|
|
720
|
+
async function ensureAppEngineApp(firebaseProjectId) {
|
|
721
|
+
// App Engine regions differ from GCP regions. The standard for Firebase projects
|
|
722
|
+
// is us-central (maps to us-central1), which is where v1 auth triggers deploy.
|
|
723
|
+
const result = await run(`gcloud app create --project=${firebaseProjectId} --region=us-central`);
|
|
724
|
+
if (result.ok) return { ok: true, existed: false };
|
|
725
|
+
const msg = (result.stderr || result.error || '').toLowerCase();
|
|
726
|
+
if (msg.includes('already exists') || msg.includes('already contains') || msg.includes('already been created')) {
|
|
727
|
+
return { ok: true, existed: true };
|
|
728
|
+
}
|
|
729
|
+
// Non-fatal — return error info but don't abort the deploy flow.
|
|
730
|
+
return { ok: false, error: result.stderr || result.error };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Check whether the currently authenticated gcloud account can grant IAM roles
|
|
735
|
+
* on the project. Requires roles/owner or roles/resourcemanager.projectIamAdmin.
|
|
736
|
+
*
|
|
737
|
+
* Returns { ok: true } if the account has permission, or
|
|
738
|
+
* { ok: false, account, missing } with a descriptive error to show the user.
|
|
739
|
+
*/
|
|
740
|
+
async function checkIamGrantPermission(projectId) {
|
|
741
|
+
// Get active account.
|
|
742
|
+
const authResult = await run('gcloud auth list --filter="status=ACTIVE" --format="value(account)" --limit=1');
|
|
743
|
+
const account = authResult.ok ? authResult.stdout.trim() : null;
|
|
744
|
+
|
|
745
|
+
// Read the project IAM policy.
|
|
746
|
+
const policyResult = await run(`gcloud projects get-iam-policy ${projectId} --format=json`);
|
|
747
|
+
if (!policyResult.ok) {
|
|
748
|
+
// Can't even read the policy — likely no access at all.
|
|
749
|
+
return { ok: false, account, missing: 'roles/viewer (sem acesso ao projeto)' };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
let policy;
|
|
753
|
+
try { policy = JSON.parse(policyResult.stdout); } catch (_) {
|
|
754
|
+
return { ok: false, account, missing: 'policy ilegível' };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const rolesWithIam = new Set([
|
|
758
|
+
'roles/owner',
|
|
759
|
+
'roles/resourcemanager.projectIamAdmin',
|
|
760
|
+
// Editor alone does NOT allow granting roles — owner/iam.admin required.
|
|
761
|
+
]);
|
|
762
|
+
|
|
763
|
+
const accountMember = account ? `user:${account}` : null;
|
|
764
|
+
for (const binding of (policy.bindings || [])) {
|
|
765
|
+
if (!rolesWithIam.has(binding.role)) continue;
|
|
766
|
+
if (!accountMember) continue;
|
|
767
|
+
if (binding.members && binding.members.includes(accountMember)) return { ok: true, account };
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
ok: false,
|
|
772
|
+
account,
|
|
773
|
+
missing: 'roles/owner ou roles/resourcemanager.projectIamAdmin',
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Full deploy sequence:
|
|
779
|
+
* 1. Write .firebaserc (links project)
|
|
780
|
+
* 2. Enable Secret Manager API + ensure required secrets exist
|
|
781
|
+
* 3. Create App Engine app (required for Cloud Functions v1 auth trigger)
|
|
782
|
+
* 4. Pre-grant project-level IAM roles (Cloud Build builder, Eventarc service agent)
|
|
783
|
+
* so the first deploy attempt doesn't fail due to missing project-level permissions.
|
|
784
|
+
* Bucket-level grants still happen post-first-attempt (buckets don't exist yet).
|
|
785
|
+
* 5. npm install in functions/
|
|
786
|
+
* 6. firebase deploy — on first-project permission errors: grants bucket roles and retries
|
|
787
|
+
*/
|
|
788
|
+
async function runDeploy(projectDir, serviceAccountPath, firebaseProjectId, { onProgress, functionsRegion = 'us-central1', secretValues = {} } = {}) {
|
|
789
|
+
const steps = [];
|
|
790
|
+
|
|
791
|
+
// Verify the current gcloud account can actually grant IAM roles before attempting
|
|
792
|
+
// the full setup. All add-iam-policy-binding commands fail silently when this
|
|
793
|
+
// permission is missing, leaving the project in a broken half-configured state.
|
|
794
|
+
const iamCheck = await checkIamGrantPermission(firebaseProjectId);
|
|
795
|
+
if (!iamCheck.ok) {
|
|
796
|
+
const who = iamCheck.account ? `Conta atual: ${iamCheck.account}` : 'Conta gcloud não detectada';
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Permissão insuficiente para configurar IAM no projeto "${firebaseProjectId}".\n` +
|
|
799
|
+
`${who}\n` +
|
|
800
|
+
`Role necessária: ${iamCheck.missing}\n\n` +
|
|
801
|
+
`Peça ao dono do projeto para conceder essa role à sua conta:\n` +
|
|
802
|
+
` gcloud projects add-iam-policy-binding ${firebaseProjectId} \\\n` +
|
|
803
|
+
` --member="user:${iamCheck.account || 'SEU_EMAIL'}" \\\n` +
|
|
804
|
+
` --role="roles/owner"`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
await writeFirebaseRc(projectDir, firebaseProjectId);
|
|
809
|
+
|
|
810
|
+
// Enable all required APIs in one batch command — GCP provisions them in parallel,
|
|
811
|
+
// which is faster than enabling one by one. appengine is needed for Cloud Functions v1
|
|
812
|
+
// upload URLs. firebaseextensions is checked by firebase-tools during deploy.
|
|
813
|
+
await run(
|
|
814
|
+
`gcloud services enable ` +
|
|
815
|
+
`secretmanager.googleapis.com ` +
|
|
816
|
+
`appengine.googleapis.com ` +
|
|
817
|
+
`cloudfunctions.googleapis.com ` +
|
|
818
|
+
`cloudbuild.googleapis.com ` +
|
|
819
|
+
`run.googleapis.com ` +
|
|
820
|
+
`artifactregistry.googleapis.com ` +
|
|
821
|
+
`eventarc.googleapis.com ` +
|
|
822
|
+
`pubsub.googleapis.com ` +
|
|
823
|
+
`firebaseextensions.googleapis.com ` +
|
|
824
|
+
`--project=${firebaseProjectId}`
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
// Create App Engine app — required for Cloud Functions v1 (auth triggers use v1 and
|
|
828
|
+
// always deploy to us-central1). Firebase needs App Engine to generate upload URLs
|
|
829
|
+
// for v1 function source code. Non-fatal if it fails.
|
|
830
|
+
await ensureAppEngineApp(firebaseProjectId);
|
|
831
|
+
|
|
832
|
+
// Install functions dependencies early so the project is ready for manual deploy
|
|
833
|
+
// even if waitForApis or IAM grants fail later.
|
|
834
|
+
const npmInstall = await installFunctionsDeps(projectDir);
|
|
835
|
+
steps.push({
|
|
836
|
+
name: 'npm install (functions)',
|
|
837
|
+
ok: npmInstall.ok,
|
|
838
|
+
skipped: npmInstall.skipped,
|
|
839
|
+
detail: npmInstall.ok ? null : npmInstall.error,
|
|
840
|
+
});
|
|
841
|
+
if (!npmInstall.ok && !npmInstall.skipped) return steps;
|
|
842
|
+
|
|
843
|
+
// Wait for APIs — non-fatal. In org GCP projects propagation can exceed 10 min;
|
|
844
|
+
// if the check times out we proceed anyway. deployFirebase has retry logic that
|
|
845
|
+
// handles "API not enabled" errors (added to isPermissionError below), so the
|
|
846
|
+
// deploy attempt will fail gracefully and retry after a wait if needed.
|
|
847
|
+
// secretmanager.googleapis.com is included here because ensureSecretsExist runs
|
|
848
|
+
// immediately after — if the API isn't ready yet, secret creation fails silently
|
|
849
|
+
// and firebase deploy hangs waiting for secrets that don't exist.
|
|
850
|
+
try {
|
|
851
|
+
await waitForApis(firebaseProjectId, [
|
|
852
|
+
'secretmanager.googleapis.com',
|
|
853
|
+
'cloudfunctions.googleapis.com',
|
|
854
|
+
'cloudbuild.googleapis.com',
|
|
855
|
+
'run.googleapis.com',
|
|
856
|
+
'artifactregistry.googleapis.com',
|
|
857
|
+
'eventarc.googleapis.com',
|
|
858
|
+
'pubsub.googleapis.com',
|
|
859
|
+
], { onProgress, timeoutMs: 60 * 1000 });
|
|
860
|
+
} catch (_) {
|
|
861
|
+
// APIs not confirmed ready — proceed anyway. If firebase deploy fails due to
|
|
862
|
+
// APIs still propagating it will be caught by the permission-error retry path.
|
|
863
|
+
if (onProgress) onProgress('wait-apis-timeout');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Create secrets only after secretmanager API is confirmed ready (or timed out).
|
|
867
|
+
// Running this before waitForApis caused silent failures when the API was still
|
|
868
|
+
// propagating — the deploy would then fail because the secrets didn't exist yet.
|
|
869
|
+
await ensureSecretsExist(firebaseProjectId, secretValues);
|
|
870
|
+
|
|
871
|
+
// Quick IAM readiness check: if the two critical grants (Eventarc + Storage) are
|
|
872
|
+
// already confirmed in IAM, skip the entire grant block below. On re-deploy this
|
|
873
|
+
// check returns in ~1s and saves 30-60s of redundant gcloud calls.
|
|
874
|
+
// On first deploy both checks fail → full grant block runs as before.
|
|
875
|
+
const projectNumber = await getProjectNumber(firebaseProjectId);
|
|
876
|
+
if (!projectNumber) {
|
|
877
|
+
throw new Error(
|
|
878
|
+
`Não foi possível obter o número do projeto "${firebaseProjectId}" via gcloud.\n` +
|
|
879
|
+
`Verifique se o projeto existe e se você tem acesso:\n` +
|
|
880
|
+
` gcloud projects describe ${firebaseProjectId}`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const iamReady =
|
|
885
|
+
await waitForEventarcIam(firebaseProjectId, projectNumber, 3 * 1000) &&
|
|
886
|
+
await verifyStorageGrant(projectNumber, 'us-central1') &&
|
|
887
|
+
await verifyAppEngineSaGrant(firebaseProjectId);
|
|
888
|
+
if (iamReady) {
|
|
889
|
+
// Permissions already in place — go straight to deploy.
|
|
890
|
+
if (onProgress) onProgress('iam-ready-skipping-grants');
|
|
891
|
+
} else {
|
|
892
|
+
await runIamSetup(firebaseProjectId, projectNumber, functionsRegion);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const deploy = await deployFirebase(projectDir, firebaseProjectId, { onProgress, functionsRegion });
|
|
896
|
+
steps.push({
|
|
897
|
+
name: 'firebase deploy (Functions + Firestore rules + Indexes + Storage)',
|
|
898
|
+
ok: deploy.ok,
|
|
899
|
+
detail: deploy.ok ? null : [deploy.error, deploy.stderr, deploy.stdout].filter(Boolean).join('\n').trim(),
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
return steps;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Full IAM + bucket setup for first deploy (or when permissions are missing).
|
|
907
|
+
* Extracted so runDeploy can skip this on re-deploy when IAM is already ready.
|
|
908
|
+
*/
|
|
909
|
+
async function runIamSetup(firebaseProjectId, projectNumber, functionsRegion = 'us-central1') {
|
|
910
|
+
const computeSa = `${projectNumber}-compute@developer.gserviceaccount.com`;
|
|
911
|
+
const cloudBuildSa = `${projectNumber}@cloudbuild.gserviceaccount.com`;
|
|
912
|
+
const gcfSa = `service-${projectNumber}@gcf-admin-robot.iam.gserviceaccount.com`;
|
|
913
|
+
const cloudBuildAgentSa = `service-${projectNumber}@gcp-sa-cloudbuild.iam.gserviceaccount.com`;
|
|
914
|
+
const eventarcSa = `service-${projectNumber}@gcp-sa-eventarc.iam.gserviceaccount.com`;
|
|
915
|
+
// Cloud Run service agent — needed for Gen 2 functions (Cloud Run-based).
|
|
916
|
+
// Must be able to impersonate the compute SA to run Cloud Run services.
|
|
917
|
+
const cloudRunSa = `service-${projectNumber}@serverless-robot-prod.iam.gserviceaccount.com`;
|
|
918
|
+
|
|
919
|
+
// Force-create service agents so their default roles are provisioned synchronously.
|
|
920
|
+
// On brand-new projects these agents may not exist yet even after enableApis.
|
|
921
|
+
// gcloud beta services identity create is idempotent (safe to re-run).
|
|
922
|
+
await Promise.all([
|
|
923
|
+
run(`gcloud beta services identity create --service=cloudfunctions.googleapis.com --project=${firebaseProjectId}`),
|
|
924
|
+
run(`gcloud beta services identity create --service=cloudbuild.googleapis.com --project=${firebaseProjectId}`),
|
|
925
|
+
run(`gcloud beta services identity create --service=eventarc.googleapis.com --project=${firebaseProjectId}`),
|
|
926
|
+
run(`gcloud beta services identity create --service=run.googleapis.com --project=${firebaseProjectId}`),
|
|
927
|
+
run(`gcloud beta services identity create --service=pubsub.googleapis.com --project=${firebaseProjectId}`),
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
// Wait for all service accounts to be visible via IAM before granting roles.
|
|
931
|
+
// On brand-new projects these SAs are created by enabling APIs / identity create but may
|
|
932
|
+
// take a minute to propagate — IAM policy bindings silently fail if the SA doesn't exist yet.
|
|
933
|
+
// All SAs that receive SA-level bindings below must be polled here first.
|
|
934
|
+
const pubsubSa = `service-${projectNumber}@gcp-sa-pubsub.iam.gserviceaccount.com`;
|
|
935
|
+
await Promise.all([
|
|
936
|
+
waitForServiceAccount(computeSa, firebaseProjectId),
|
|
937
|
+
waitForServiceAccount(cloudBuildSa, firebaseProjectId),
|
|
938
|
+
waitForServiceAccount(gcfSa, firebaseProjectId),
|
|
939
|
+
waitForServiceAccount(cloudBuildAgentSa, firebaseProjectId),
|
|
940
|
+
waitForServiceAccount(cloudRunSa, firebaseProjectId),
|
|
941
|
+
// eventarcSa recebe role abaixo — precisa existir antes do grant.
|
|
942
|
+
// Em projetos novos pode demorar até 30s após identity create.
|
|
943
|
+
waitForServiceAccount(eventarcSa, firebaseProjectId),
|
|
944
|
+
// pubsubSa é necessário para triggers Firestore v2 (Eventarc usa Pub/Sub para entregar eventos).
|
|
945
|
+
waitForServiceAccount(pubsubSa, firebaseProjectId),
|
|
946
|
+
]);
|
|
947
|
+
|
|
948
|
+
// Apply all project-level IAM bindings in a single atomic set-iam-policy call.
|
|
949
|
+
// Using parallel add-iam-policy-binding calls causes a race condition: each command
|
|
950
|
+
// reads the current policy, adds its binding, and writes back — later writes overwrite
|
|
951
|
+
// earlier ones, leaving some grants missing. set-iam-policy is one atomic write.
|
|
952
|
+
await applyProjectIamBindings(firebaseProjectId, [
|
|
953
|
+
{ role: 'roles/cloudbuild.builds.builder', member: `serviceAccount:${computeSa}` },
|
|
954
|
+
{ role: 'roles/cloudbuild.builds.builder', member: `serviceAccount:${cloudBuildSa}` },
|
|
955
|
+
{ role: 'roles/eventarc.serviceAgent', member: `serviceAccount:${eventarcSa}` },
|
|
956
|
+
{ role: 'roles/cloudfunctions.serviceAgent', member: `serviceAccount:${computeSa}` },
|
|
957
|
+
{ role: 'roles/cloudfunctions.serviceAgent', member: `serviceAccount:${cloudBuildSa}` },
|
|
958
|
+
{ role: 'roles/run.serviceAgent', member: `serviceAccount:${cloudRunSa}` },
|
|
959
|
+
{ role: 'roles/logging.logWriter', member: `serviceAccount:${computeSa}` },
|
|
960
|
+
{ role: 'roles/logging.logWriter', member: `serviceAccount:${cloudBuildSa}` },
|
|
961
|
+
{ role: 'roles/storage.objectAdmin', member: `serviceAccount:${computeSa}` },
|
|
962
|
+
{ role: 'roles/storage.objectAdmin', member: `serviceAccount:${cloudBuildSa}` },
|
|
963
|
+
{ role: 'roles/artifactregistry.writer', member: `serviceAccount:${computeSa}` },
|
|
964
|
+
{ role: 'roles/artifactregistry.writer', member: `serviceAccount:${cloudBuildSa}` },
|
|
965
|
+
{ role: 'roles/eventarc.eventReceiver', member: `serviceAccount:${computeSa}` },
|
|
966
|
+
{ role: 'roles/firebase.admin', member: `serviceAccount:${computeSa}` },
|
|
967
|
+
{ role: 'roles/datastore.user', member: `serviceAccount:${computeSa}` },
|
|
968
|
+
]);
|
|
969
|
+
|
|
970
|
+
// Service-account-level bindings: allow service agents to "use" the compute SA.
|
|
971
|
+
// These are SA-level bindings (not project-level) so they don't conflict with the
|
|
972
|
+
// project policy above and can safely run in parallel.
|
|
973
|
+
await Promise.all([
|
|
974
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${gcfSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
975
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${cloudBuildAgentSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
976
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${cloudBuildSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
977
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${cloudRunSa} --role=roles/iam.serviceAccountUser --project=${firebaseProjectId}`),
|
|
978
|
+
run(`gcloud iam service-accounts add-iam-policy-binding ${computeSa} --member=serviceAccount:${pubsubSa} --role=roles/iam.serviceAccountTokenCreator --project=${firebaseProjectId}`),
|
|
979
|
+
]);
|
|
980
|
+
|
|
981
|
+
// Grant Firestore write access to the App Engine default service account.
|
|
982
|
+
// Cloud Functions v1 (auth triggers) run as PROJECT_ID@appspot.gserviceaccount.com.
|
|
983
|
+
// On new GCP projects (post April 2024) this SA no longer auto-receives Editor role,
|
|
984
|
+
// causing PERMISSION_DENIED when the function writes to Firestore via Admin SDK.
|
|
985
|
+
// Must wait for the SA to exist — it's created by App Engine app provisioning and can
|
|
986
|
+
// take 30-60s to propagate after the App Engine app is first created.
|
|
987
|
+
const appEngineSa = `${firebaseProjectId}@appspot.gserviceaccount.com`;
|
|
988
|
+
await waitForServiceAccount(appEngineSa, firebaseProjectId);
|
|
989
|
+
// Also use atomic set-iam-policy for these three grants to avoid the same race condition.
|
|
990
|
+
await applyProjectIamBindings(firebaseProjectId, [
|
|
991
|
+
{ role: 'roles/datastore.user', member: `serviceAccount:${appEngineSa}` },
|
|
992
|
+
{ role: 'roles/firebase.admin', member: `serviceAccount:${appEngineSa}` },
|
|
993
|
+
{ role: 'roles/logging.logWriter', member: `serviceAccount:${appEngineSa}` },
|
|
994
|
+
]);
|
|
995
|
+
|
|
996
|
+
// Pre-create gcf-sources and gcf-v2-sources buckets and grant bucket-level permissions.
|
|
997
|
+
// In org projects, project-level IAM inheritance to newly created GCS buckets can take
|
|
998
|
+
// 60-120s. Cloud Build fails in that window even though project-level storage.objectAdmin
|
|
999
|
+
// was already granted. Bucket-level grants are immediate and avoid the race condition.
|
|
1000
|
+
await ensureGcfSourceBuckets(firebaseProjectId, projectNumber, functionsRegion);
|
|
1001
|
+
|
|
1002
|
+
// Wait for Eventarc binding to propagate before deploy (up to 60s; instant on re-deploy).
|
|
1003
|
+
await waitForEventarcIam(firebaseProjectId, projectNumber);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
module.exports = { runDeploy, ensureServiceAccountKey };
|