kasy-cli 1.21.9 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +80 -37
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +2 -2
- package/lib/utils/i18n/messages-en.js +50 -35
- package/lib/utils/i18n/messages-es.js +50 -35
- package/lib/utils/i18n/messages-pt.js +52 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +4 -5
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import 'package:kasy_kit/features/onboarding/api/entities/user_info_entity.dart';
|
|
2
2
|
|
|
3
3
|
enum UserInfoKeys {
|
|
4
|
-
|
|
4
|
+
gender,
|
|
5
5
|
age,
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
enum
|
|
8
|
+
enum Gender {
|
|
9
9
|
male,
|
|
10
10
|
female,
|
|
11
11
|
none,
|
|
@@ -44,11 +44,11 @@ class UserAgeInfo extends UserInfoDetail<AgeRange> {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
factory UserAgeInfo.fromString(String value) {
|
|
47
|
-
final
|
|
47
|
+
final range = AgeRange.values.firstWhere(
|
|
48
48
|
(element) => element.name == value,
|
|
49
49
|
orElse: () => AgeRange.none,
|
|
50
50
|
);
|
|
51
|
-
return UserAgeInfo(
|
|
51
|
+
return UserAgeInfo(range);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
|
|
@@ -62,32 +62,32 @@ class UserAgeInfo extends UserInfoDetail<AgeRange> {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/// ======================================
|
|
65
|
-
/// user
|
|
65
|
+
/// user gender info
|
|
66
66
|
/// ======================================
|
|
67
|
-
class
|
|
68
|
-
|
|
67
|
+
class UserGenderInfo extends UserInfoDetail<Gender> {
|
|
68
|
+
UserGenderInfo(super.value);
|
|
69
69
|
|
|
70
|
-
factory
|
|
71
|
-
final value =
|
|
70
|
+
factory UserGenderInfo.fromEntity(UserInfoEntity entity) {
|
|
71
|
+
final value = Gender.values.firstWhere(
|
|
72
72
|
(element) => element.name == entity.value,
|
|
73
|
-
orElse: () =>
|
|
73
|
+
orElse: () => Gender.none,
|
|
74
74
|
);
|
|
75
|
-
return
|
|
75
|
+
return UserGenderInfo(value);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
factory
|
|
79
|
-
final
|
|
78
|
+
factory UserGenderInfo.fromString(String value) {
|
|
79
|
+
final gender = Gender.values.firstWhere(
|
|
80
80
|
(element) => element.name == value,
|
|
81
|
-
orElse: () =>
|
|
81
|
+
orElse: () => Gender.none,
|
|
82
82
|
);
|
|
83
|
-
return
|
|
83
|
+
return UserGenderInfo(gender);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
@override
|
|
89
89
|
UserInfoEntity toEntity(String userId) => UserInfoEntity(
|
|
90
|
-
key: UserInfoKeys.
|
|
90
|
+
key: UserInfoKeys.gender.name,
|
|
91
91
|
value: value.name,
|
|
92
92
|
userId: userId,
|
|
93
93
|
);
|
|
@@ -6,7 +6,8 @@ import 'package:kasy_kit/core/data/api/tracking_api.dart';
|
|
|
6
6
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
7
7
|
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
|
|
8
8
|
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart';
|
|
9
|
-
import 'package:kasy_kit/features/
|
|
9
|
+
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_module_mockups.dart';
|
|
10
|
+
import 'package:kasy_kit/features/subscriptions/repositories/subscription_repository.dart';
|
|
10
11
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
11
12
|
import 'package:permission_handler/permission_handler.dart';
|
|
12
13
|
|
|
@@ -29,10 +30,10 @@ class AttPermissionStep extends ConsumerWidget {
|
|
|
29
30
|
|
|
30
31
|
return OnboardingBackground(
|
|
31
32
|
child: OnboardingIllustrationScaffold(
|
|
32
|
-
|
|
33
|
+
step: 6,
|
|
33
34
|
title: translations.title,
|
|
34
35
|
description: translations.description,
|
|
35
|
-
|
|
36
|
+
image: const TrackingPermissionMockup(),
|
|
36
37
|
footerActions: [
|
|
37
38
|
KasyButton(
|
|
38
39
|
label: translations.continue_button,
|
|
@@ -7,6 +7,7 @@ import 'package:kasy_kit/core/theme/theme.dart';
|
|
|
7
7
|
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
8
8
|
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_background.dart';
|
|
9
9
|
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_progress.dart';
|
|
10
|
+
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_step_header.dart';
|
|
10
11
|
import 'package:kasy_kit/features/onboarding/ui/widgets/onboarding_sticky_footer.dart';
|
|
11
12
|
import 'package:kasy_kit/features/onboarding/ui/widgets/selectable_row_tile.dart';
|
|
12
13
|
|
|
@@ -18,9 +19,11 @@ typedef OnOptionIdSelected = void Function(String id);
|
|
|
18
19
|
|
|
19
20
|
typedef OnValidate = void Function(String? key);
|
|
20
21
|
|
|
21
|
-
/// Single choice question with
|
|
22
|
+
/// Single choice question with selectable tiles, matching the clean‑premium
|
|
23
|
+
/// onboarding layout (shared header, left‑aligned title, sticky footer).
|
|
22
24
|
class OnboardingRadioQuestion extends ConsumerStatefulWidget {
|
|
23
|
-
final
|
|
25
|
+
final int step;
|
|
26
|
+
final int totalSteps;
|
|
24
27
|
final String title;
|
|
25
28
|
final String description;
|
|
26
29
|
final String btnText;
|
|
@@ -37,7 +40,8 @@ class OnboardingRadioQuestion extends ConsumerStatefulWidget {
|
|
|
37
40
|
required this.btnText,
|
|
38
41
|
required this.optionIds,
|
|
39
42
|
required this.optionBuilder,
|
|
40
|
-
this.
|
|
43
|
+
required this.step,
|
|
44
|
+
this.totalSteps = kOnboardingSteps,
|
|
41
45
|
this.onOptionIdSelected,
|
|
42
46
|
this.onValidate,
|
|
43
47
|
this.reassuranceBuilder,
|
|
@@ -54,61 +58,68 @@ class _OnboardingRadioQuestionState
|
|
|
54
58
|
|
|
55
59
|
@override
|
|
56
60
|
Widget build(BuildContext context) {
|
|
61
|
+
const gutter = KasySpacing.lg;
|
|
62
|
+
|
|
57
63
|
final scrollBody = Column(
|
|
58
|
-
crossAxisAlignment: CrossAxisAlignment.
|
|
64
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
59
65
|
children: [
|
|
60
|
-
|
|
61
|
-
OnboardingProgress(value: widget.progress!),
|
|
66
|
+
OnboardingStepHeader(step: widget.step, totalSteps: widget.totalSteps),
|
|
62
67
|
Padding(
|
|
63
|
-
padding: const EdgeInsets.fromLTRB(
|
|
64
|
-
KasySpacing.md,
|
|
65
|
-
KasySpacing.xl,
|
|
66
|
-
KasySpacing.md,
|
|
67
|
-
0,
|
|
68
|
-
),
|
|
68
|
+
padding: const EdgeInsets.fromLTRB(gutter, KasySpacing.lg, gutter, 0),
|
|
69
69
|
child: MoveFadeAnim(
|
|
70
|
-
delayInMs:
|
|
70
|
+
delayInMs: 120,
|
|
71
71
|
child: Text(
|
|
72
72
|
widget.title,
|
|
73
|
-
textAlign: TextAlign.
|
|
74
|
-
style: context.textTheme.
|
|
73
|
+
textAlign: TextAlign.start,
|
|
74
|
+
style: context.textTheme.headlineMedium?.copyWith(
|
|
75
75
|
color: context.colors.onBackground,
|
|
76
|
+
fontSize: 26,
|
|
77
|
+
fontWeight: FontWeight.w700,
|
|
78
|
+
letterSpacing: -0.2,
|
|
79
|
+
height: 1.2,
|
|
76
80
|
),
|
|
77
81
|
),
|
|
78
82
|
),
|
|
79
83
|
),
|
|
80
84
|
Padding(
|
|
81
85
|
padding: const EdgeInsets.fromLTRB(
|
|
86
|
+
gutter,
|
|
87
|
+
KasySpacing.smd,
|
|
88
|
+
gutter,
|
|
82
89
|
KasySpacing.lg,
|
|
83
|
-
KasySpacing.md,
|
|
84
|
-
KasySpacing.lg,
|
|
85
|
-
KasySpacing.xl,
|
|
86
90
|
),
|
|
87
91
|
child: MoveFadeAnim(
|
|
88
92
|
delayInMs: 200,
|
|
89
93
|
child: Text(
|
|
90
94
|
widget.description,
|
|
91
|
-
textAlign: TextAlign.
|
|
95
|
+
textAlign: TextAlign.start,
|
|
92
96
|
style: context.textTheme.bodyLarge?.copyWith(
|
|
93
|
-
color: context.colors.
|
|
97
|
+
color: context.colors.muted,
|
|
98
|
+
height: 1.45,
|
|
94
99
|
),
|
|
95
100
|
),
|
|
96
101
|
),
|
|
97
102
|
),
|
|
98
103
|
Padding(
|
|
99
|
-
padding: const EdgeInsets.symmetric(horizontal:
|
|
104
|
+
padding: const EdgeInsets.symmetric(horizontal: gutter),
|
|
100
105
|
child: OnboardingSelectableRowGroup(
|
|
101
106
|
physics: const NeverScrollableScrollPhysics(),
|
|
102
|
-
options: widget.optionIds
|
|
103
|
-
.map(
|
|
107
|
+
options: widget.optionIds.map(
|
|
104
108
|
(e) {
|
|
105
109
|
final index = widget.optionIds.indexOf(e);
|
|
106
110
|
return Animate(
|
|
107
111
|
effects: [
|
|
108
112
|
FadeEffect(
|
|
109
|
-
delay: Duration(milliseconds:
|
|
110
|
-
duration: const Duration(milliseconds:
|
|
111
|
-
curve: Curves.
|
|
113
|
+
delay: Duration(milliseconds: 280 + index * 80),
|
|
114
|
+
duration: const Duration(milliseconds: 450),
|
|
115
|
+
curve: Curves.easeOut,
|
|
116
|
+
),
|
|
117
|
+
MoveEffect(
|
|
118
|
+
delay: Duration(milliseconds: 280 + index * 80),
|
|
119
|
+
duration: const Duration(milliseconds: 450),
|
|
120
|
+
curve: Curves.easeOut,
|
|
121
|
+
begin: const Offset(0, 24),
|
|
122
|
+
end: Offset.zero,
|
|
112
123
|
),
|
|
113
124
|
],
|
|
114
125
|
child: widget.optionBuilder(e, e == selectedChoiceId),
|
|
@@ -153,8 +164,7 @@ class _OnboardingRadioQuestionState
|
|
|
153
164
|
),
|
|
154
165
|
DeviceSizeBuilder(
|
|
155
166
|
builder: (device) => OnboardingStickyFooter(
|
|
156
|
-
maxContentWidth:
|
|
157
|
-
device == DeviceType.small ? null : 600,
|
|
167
|
+
maxContentWidth: device == DeviceType.small ? null : 600,
|
|
158
168
|
children: [
|
|
159
169
|
KasyButton(
|
|
160
170
|
label: widget.btnText,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import 'package:dio/dio.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
|
|
4
|
+
import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
5
|
+
|
|
6
|
+
/// One row of the admin Users table, as returned by `GET /admin/users`. Your
|
|
7
|
+
/// backend MUST verify the caller's `role == "admin"` (and return 403 otherwise)
|
|
8
|
+
/// before serving this — it is never readable by a non-admin client.
|
|
9
|
+
///
|
|
10
|
+
/// This file mirrors the shared admin data-layer contract (see the Firebase
|
|
11
|
+
/// version): the public surface — [AdminUser], [AdminUsersResult] and
|
|
12
|
+
/// [adminUsersApiProvider] — is identical across backends so the Users tab UI
|
|
13
|
+
/// stays the same; only the body of [AdminUsersApi.fetch] differs.
|
|
14
|
+
class AdminUser {
|
|
15
|
+
final String id;
|
|
16
|
+
final String? email;
|
|
17
|
+
final String? name;
|
|
18
|
+
final DateTime? createdAt;
|
|
19
|
+
final bool subscriber;
|
|
20
|
+
|
|
21
|
+
const AdminUser({
|
|
22
|
+
required this.id,
|
|
23
|
+
required this.subscriber,
|
|
24
|
+
this.email,
|
|
25
|
+
this.name,
|
|
26
|
+
this.createdAt,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
factory AdminUser.fromMap(Map<String, dynamic> m) {
|
|
30
|
+
final createdMillis = m['createdAt'];
|
|
31
|
+
return AdminUser(
|
|
32
|
+
id: (m['id'] as String?) ?? '',
|
|
33
|
+
email: m['email'] as String?,
|
|
34
|
+
name: m['name'] as String?,
|
|
35
|
+
createdAt: createdMillis is num
|
|
36
|
+
? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
|
|
37
|
+
: null,
|
|
38
|
+
subscriber: m['subscriber'] == true,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// The full (bounded) set of users plus collection metadata. The UI searches,
|
|
44
|
+
/// sorts and paginates this list locally, so those interactions are instant.
|
|
45
|
+
class AdminUsersResult {
|
|
46
|
+
final List<AdminUser> users;
|
|
47
|
+
final int totalUsers;
|
|
48
|
+
final bool truncated;
|
|
49
|
+
|
|
50
|
+
const AdminUsersResult({
|
|
51
|
+
required this.users,
|
|
52
|
+
required this.totalUsers,
|
|
53
|
+
required this.truncated,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
final adminUsersApiProvider = Provider<AdminUsersApi>(
|
|
58
|
+
(ref) => AdminUsersApi(client: ref.read(httpClientProvider)),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/// REST endpoint expected:
|
|
62
|
+
/// GET /admin/users
|
|
63
|
+
/// Auth: Authorization Bearer token (attached automatically). The server
|
|
64
|
+
/// MUST verify the caller's role == "admin" and return 403 otherwise.
|
|
65
|
+
/// Response 200: {
|
|
66
|
+
/// "users": [
|
|
67
|
+
/// {"id": "...", "email": "a@b.com"|null, "name": "Ana"|null,
|
|
68
|
+
/// "createdAt": 1700000000000 (epoch millis)|null, "subscriber": true}
|
|
69
|
+
/// ],
|
|
70
|
+
/// "totalUsers": 142, // true size of the collection
|
|
71
|
+
/// "truncated": false // true when more users exist than returned
|
|
72
|
+
/// }
|
|
73
|
+
/// Return the most-recent users (bounded, e.g. 1000); the app does the
|
|
74
|
+
/// search / sort / pagination locally so those interactions are instant.
|
|
75
|
+
class AdminUsersApi {
|
|
76
|
+
final HttpClient _client;
|
|
77
|
+
|
|
78
|
+
AdminUsersApi({required HttpClient client}) : _client = client;
|
|
79
|
+
|
|
80
|
+
/// Loads the users for the admin console. Returns the whole bounded set in one
|
|
81
|
+
/// call; the UI handles search/sort/pagination on the client.
|
|
82
|
+
Future<AdminUsersResult> fetch() async {
|
|
83
|
+
try {
|
|
84
|
+
final response = await _client.get('/admin/users');
|
|
85
|
+
final data = Map<String, dynamic>.from(response.data as Map);
|
|
86
|
+
final rawUsers = (data['users'] as List?) ?? const [];
|
|
87
|
+
return AdminUsersResult(
|
|
88
|
+
users: rawUsers
|
|
89
|
+
.map((e) => AdminUser.fromMap(Map<String, dynamic>.from(e as Map)))
|
|
90
|
+
.toList(),
|
|
91
|
+
totalUsers: (data['totalUsers'] as num?)?.toInt() ?? 0,
|
|
92
|
+
truncated: data['truncated'] == true,
|
|
93
|
+
);
|
|
94
|
+
} on DioException catch (e) {
|
|
95
|
+
throw ApiError.fromDioException(e);
|
|
96
|
+
} catch (e, s) {
|
|
97
|
+
throw ApiError(code: 0, message: '$e: $s');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -15,6 +15,17 @@ enum SubscriptionStatus {
|
|
|
15
15
|
CANCELLED,
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/// Where the subscription was purchased (its origin). `STRIPE` means it was
|
|
19
|
+
/// bought on the web via Stripe. Used to route the user to the correct
|
|
20
|
+
/// management flow regardless of the device they are currently on.
|
|
21
|
+
enum SubscriptionStore {
|
|
22
|
+
PLAY_STORE,
|
|
23
|
+
APPLE_STORE,
|
|
24
|
+
EARLY_BIRD,
|
|
25
|
+
STRIPE,
|
|
26
|
+
unknown,
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
|
|
19
30
|
@freezed
|
|
20
31
|
sealed class SubscriptionEntity with _$SubscriptionEntity {
|
|
@@ -26,6 +37,8 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
|
|
|
26
37
|
@JsonKey(name: 'last_update_date') DateTime? lastUpdateDate,
|
|
27
38
|
@JsonKey(name: 'period_end_date') DateTime? periodEndDate,
|
|
28
39
|
@JsonKey(name: 'status') required SubscriptionStatus status,
|
|
40
|
+
@JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
|
|
41
|
+
SubscriptionStore? store,
|
|
29
42
|
}) = SubscriptionEntityData;
|
|
30
43
|
|
|
31
44
|
factory SubscriptionEntity.fromJson(Map<String, Object?> json) =>
|
package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import 'package:dio/dio.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
4
|
+
import 'package:kasy_kit/features/subscriptions/api/stripe_product.dart';
|
|
5
|
+
|
|
6
|
+
final stripeBackendApiProvider = Provider<StripeBackendApi>(
|
|
7
|
+
(ref) => StripeBackendApi(client: ref.read(httpClientProvider)),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
/// Talks to the Stripe backend (your own server). The user identity must be
|
|
11
|
+
/// taken server-side from the authenticated request (the Bearer token attached
|
|
12
|
+
/// by [HttpClient]) — never trusted from the request body. This file replaces
|
|
13
|
+
/// the Firebase version so the rest of the Stripe client code stays
|
|
14
|
+
/// backend-agnostic.
|
|
15
|
+
///
|
|
16
|
+
/// Your backend must expose these endpoints (see Stripe-Guia.md):
|
|
17
|
+
/// GET /stripe/list-prices -> active recurring prices (offer list)
|
|
18
|
+
/// POST /stripe/checkout-session -> { url } hosted Checkout session
|
|
19
|
+
/// POST /stripe/portal-session -> { url } Customer Portal session
|
|
20
|
+
/// and a Stripe webhook that writes the `subscriptions` record (store=STRIPE).
|
|
21
|
+
class StripeBackendApi {
|
|
22
|
+
final HttpClient _client;
|
|
23
|
+
|
|
24
|
+
StripeBackendApi({required HttpClient client}) : _client = client;
|
|
25
|
+
|
|
26
|
+
/// Active recurring prices, mapped to paywall offers.
|
|
27
|
+
Future<List<StripeProduct>> listPrices() async {
|
|
28
|
+
final Response res = await _client.get('/stripe/list-prices');
|
|
29
|
+
final list = (res.data as List).cast<dynamic>();
|
|
30
|
+
return list
|
|
31
|
+
.map((e) => StripeProduct.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
32
|
+
.toList();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Create a hosted Checkout session for [priceId] and return its URL.
|
|
36
|
+
Future<String> createCheckoutSession({
|
|
37
|
+
required String priceId,
|
|
38
|
+
String? successUrl,
|
|
39
|
+
String? cancelUrl,
|
|
40
|
+
}) async {
|
|
41
|
+
final Response res = await _client.post(
|
|
42
|
+
'/stripe/checkout-session',
|
|
43
|
+
data: {
|
|
44
|
+
'priceId': priceId,
|
|
45
|
+
if (successUrl != null) 'successUrl': successUrl,
|
|
46
|
+
if (cancelUrl != null) 'cancelUrl': cancelUrl,
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
return (res.data as Map)['url'] as String;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Create a Customer Portal session (manage / cancel) and return its URL.
|
|
53
|
+
Future<String> createPortalSession({String? returnUrl}) async {
|
|
54
|
+
final Response res = await _client.post(
|
|
55
|
+
'/stripe/portal-session',
|
|
56
|
+
data: {if (returnUrl != null) 'returnUrl': returnUrl},
|
|
57
|
+
);
|
|
58
|
+
return (res.data as Map)['url'] as String;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
|
|
2
2
|
import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
3
|
-
import 'package:kasy_kit/features/
|
|
3
|
+
import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
|
|
4
4
|
import 'package:dio/dio.dart';
|
|
5
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
6
|
import 'package:logger/logger.dart';
|
|
@@ -93,7 +93,6 @@ flutter:
|
|
|
93
93
|
assets:
|
|
94
94
|
- .env
|
|
95
95
|
- assets/images/
|
|
96
|
-
- assets/images/onboarding/
|
|
97
96
|
- assets/icons/
|
|
98
97
|
flutter_launcher_icons:
|
|
99
98
|
image_path: assets/images/icon.png
|
|
@@ -108,15 +107,15 @@ flutter_launcher_icons:
|
|
|
108
107
|
background_color: "#01171f"
|
|
109
108
|
theme_color: "#01171f"
|
|
110
109
|
flutter_native_splash:
|
|
111
|
-
color: "#
|
|
112
|
-
color_dark: "#
|
|
110
|
+
color: "#FAF9FC"
|
|
111
|
+
color_dark: "#0C0A14"
|
|
113
112
|
fullscreen: true
|
|
114
113
|
ios: true
|
|
115
114
|
android: true
|
|
116
115
|
image: assets/images/splash_logo_light.png
|
|
117
116
|
image_dark: assets/images/splash_logo_dark.png
|
|
118
117
|
android_12:
|
|
119
|
-
color: "#
|
|
120
|
-
color_dark: "#
|
|
118
|
+
color: "#FAF9FC"
|
|
119
|
+
color_dark: "#0C0A14"
|
|
121
120
|
image: assets/images/splash_logo_light_android12.png
|
|
122
121
|
image_dark: assets/images/splash_logo_dark_android12.png
|
|
@@ -256,11 +256,17 @@ async function writeFirebaseRc(projectDir, firebaseProjectId) {
|
|
|
256
256
|
* These must exist in Secret Manager before `firebase deploy` runs,
|
|
257
257
|
* otherwise firebase-tools prompts interactively and hangs in non-TTY environments.
|
|
258
258
|
*/
|
|
259
|
-
const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WEBHOOK_KEY', '
|
|
259
|
+
const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WEBHOOK_KEY', 'AI_API_KEY', 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'];
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
262
|
+
* Ensure every required secret exists AND holds a value, so `firebase deploy` never
|
|
263
|
+
* blocks waiting for one. A secret has two layers: the "container" and its versions.
|
|
264
|
+
* A container can exist with zero enabled versions (e.g. created by hand but never
|
|
265
|
+
* filled) — in that state `firebase deploy` still aborts with
|
|
266
|
+
* "Secret Manager has no latest version of ...". Checking only the container (describe)
|
|
267
|
+
* misses this, so we also verify there is an enabled version and add a placeholder when
|
|
268
|
+
* there isn't. The user replaces placeholders with real values later via `kasy configure`.
|
|
269
|
+
*
|
|
264
270
|
* Uses gcloud to avoid firebase-tools interactive prompts.
|
|
265
271
|
* Non-fatal: if a secret can't be created (e.g. API not enabled yet) we continue anyway.
|
|
266
272
|
*
|
|
@@ -268,22 +274,87 @@ const REQUIRED_SECRETS = ['META_ACCESS_TOKEN', 'META_DATASET_ID', 'REVENUECAT_WE
|
|
|
268
274
|
* @param {Record<string,string>} [secretValues] - map of secret name → real value (optional)
|
|
269
275
|
*/
|
|
270
276
|
async function ensureSecretsExist(firebaseProjectId, secretValues = {}) {
|
|
277
|
+
const escape = (v) => v.replace(/'/g, "'\\''");
|
|
271
278
|
for (const secret of REQUIRED_SECRETS) {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
const realValue = secretValues[secret]?.trim();
|
|
280
|
+
const exists = (await run(`gcloud secrets describe ${secret} --project=${firebaseProjectId}`)).ok;
|
|
281
|
+
|
|
282
|
+
if (!exists) {
|
|
283
|
+
// No container yet — create it with the real value, or a placeholder so deploy never blocks.
|
|
284
|
+
const value = realValue || 'placeholder';
|
|
285
|
+
await run(
|
|
286
|
+
`printf '${escape(value)}' | gcloud secrets create ${secret} --data-file=- --replication-policy=automatic --project=${firebaseProjectId}`
|
|
287
|
+
);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Container exists. Does it hold an enabled version (an actual value)?
|
|
292
|
+
const versions = await run(`gcloud secrets versions list ${secret} --format='value(state)' --project=${firebaseProjectId}`);
|
|
293
|
+
const hasEnabledVersion = versions.ok && /enabled/i.test(versions.stdout);
|
|
294
|
+
|
|
295
|
+
if (realValue) {
|
|
296
|
+
// User provided a real value — always add it as the latest version.
|
|
275
297
|
await run(
|
|
276
|
-
`printf '${
|
|
298
|
+
`printf '${escape(realValue)}' | gcloud secrets versions add ${secret} --data-file=- --project=${firebaseProjectId}`
|
|
277
299
|
);
|
|
278
|
-
} else if (
|
|
279
|
-
//
|
|
300
|
+
} else if (!hasEnabledVersion) {
|
|
301
|
+
// Container exists but is empty — add a placeholder so deploy never blocks.
|
|
280
302
|
await run(
|
|
281
|
-
`printf '
|
|
303
|
+
`printf 'placeholder' | gcloud secrets versions add ${secret} --data-file=- --project=${firebaseProjectId}`
|
|
282
304
|
);
|
|
283
305
|
}
|
|
284
306
|
}
|
|
285
307
|
}
|
|
286
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Cloud Functions declare non-secret config via defineString()/defineInt()/etc. In
|
|
311
|
+
* non-interactive mode `firebase deploy` aborts if such a param has no value AND no
|
|
312
|
+
* usable default (an empty-string default isn't reliably honored across firebase-tools
|
|
313
|
+
* versions). To make deploy never block on missing config, we ensure functions/.env has
|
|
314
|
+
* an (empty) entry for every declared param. The user fills real values later via
|
|
315
|
+
* `kasy configure`; an empty value keeps the documented "unset" behavior.
|
|
316
|
+
*
|
|
317
|
+
* Non-fatal: any failure here is swallowed so it never breaks a deploy.
|
|
318
|
+
*/
|
|
319
|
+
async function ensureEnvParamsExist(projectDir) {
|
|
320
|
+
try {
|
|
321
|
+
const srcDir = path.join(projectDir, 'functions', 'src');
|
|
322
|
+
if (!(await fs.pathExists(srcDir))) return;
|
|
323
|
+
|
|
324
|
+
// Collect every defineString/Int/Boolean/List param name from the functions source.
|
|
325
|
+
const params = new Set();
|
|
326
|
+
const walk = async (dir) => {
|
|
327
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
328
|
+
const full = path.join(dir, entry.name);
|
|
329
|
+
if (entry.isDirectory()) { await walk(full); continue; }
|
|
330
|
+
if (!/\.(ts|js)$/.test(entry.name)) continue;
|
|
331
|
+
const code = await fs.readFile(full, 'utf8');
|
|
332
|
+
const re = /define(?:String|Int|Boolean|List)\(\s*["']([A-Z0-9_]+)["']/g;
|
|
333
|
+
let m;
|
|
334
|
+
while ((m = re.exec(code)) !== null) params.add(m[1]);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
await walk(srcDir);
|
|
338
|
+
if (params.size === 0) return;
|
|
339
|
+
|
|
340
|
+
const envPath = path.join(projectDir, 'functions', '.env');
|
|
341
|
+
let content = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, 'utf8') : '';
|
|
342
|
+
const existing = new Set(
|
|
343
|
+
content.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'))
|
|
344
|
+
.map((l) => (l.includes('=') ? l.slice(0, l.indexOf('=')).trim() : ''))
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const missing = [...params].filter((p) => !existing.has(p));
|
|
349
|
+
if (missing.length === 0) return;
|
|
350
|
+
if (content && !content.endsWith('\n')) content += '\n';
|
|
351
|
+
content += missing.map((p) => `${p}=`).join('\n') + '\n';
|
|
352
|
+
await fs.outputFile(envPath, content, 'utf8');
|
|
353
|
+
} catch (_) {
|
|
354
|
+
// Never let env-bootstrap break a deploy.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
287
358
|
/**
|
|
288
359
|
* Get the numeric project number for a GCP project ID.
|
|
289
360
|
*/
|
|
@@ -867,6 +938,9 @@ async function runDeploy(projectDir, serviceAccountPath, firebaseProjectId, { on
|
|
|
867
938
|
// Running this before waitForApis caused silent failures when the API was still
|
|
868
939
|
// propagating — the deploy would then fail because the secrets didn't exist yet.
|
|
869
940
|
await ensureSecretsExist(firebaseProjectId, secretValues);
|
|
941
|
+
// Bootstrap empty entries for any defineString/etc. config param so deploy never
|
|
942
|
+
// blocks asking for one (e.g. STRIPE_PRODUCT_ID). Real values come later via configure.
|
|
943
|
+
await ensureEnvParamsExist(projectDir);
|
|
870
944
|
|
|
871
945
|
// Quick IAM readiness check: if the two critical grants (Eventarc + Storage) are
|
|
872
946
|
// already confirmed in IAM, skip the entire grant block below. On re-deploy this
|
|
@@ -938,10 +1012,10 @@ async function runIamSetup(firebaseProjectId, projectNumber, functionsRegion = '
|
|
|
938
1012
|
waitForServiceAccount(gcfSa, firebaseProjectId),
|
|
939
1013
|
waitForServiceAccount(cloudBuildAgentSa, firebaseProjectId),
|
|
940
1014
|
waitForServiceAccount(cloudRunSa, firebaseProjectId),
|
|
941
|
-
// eventarcSa
|
|
942
|
-
//
|
|
1015
|
+
// eventarcSa gets its role below — it must exist before the grant.
|
|
1016
|
+
// On new projects it can take up to 30s after identity create.
|
|
943
1017
|
waitForServiceAccount(eventarcSa, firebaseProjectId),
|
|
944
|
-
// pubsubSa
|
|
1018
|
+
// pubsubSa is required for Firestore v2 triggers (Eventarc uses Pub/Sub to deliver events).
|
|
945
1019
|
waitForServiceAccount(pubsubSa, firebaseProjectId),
|
|
946
1020
|
]);
|
|
947
1021
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Firebase project generator.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Thin wrapper over generateProject().
|
|
5
|
+
* The common generation logic (copy, pub get, slang, build_runner, flutterfire)
|
|
6
|
+
* lives in cli/lib/scaffold/generate.js.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* postBuild → runDeploy (
|
|
8
|
+
* Specific hook:
|
|
9
|
+
* postBuild → runDeploy (optional, controlled by options.deploy)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { generateProject } = require('../../generate');
|
|
@@ -21,7 +21,7 @@ const ORIGINAL_SHORT_NAME = 'appfirebase';
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Normalize app name to a valid Dart package name.
|
|
24
|
-
* "
|
|
24
|
+
* "My Café App" → "my_cafe_app"
|
|
25
25
|
*/
|
|
26
26
|
function toPackageName(appName) {
|
|
27
27
|
return (appName || '')
|
|
@@ -52,8 +52,8 @@ function bundleIdToPath(bundleId) {
|
|
|
52
52
|
|
|
53
53
|
const ORIGINAL_BUNDLE_ID_PATH = bundleIdToPath(ORIGINAL_BUNDLE_ID);
|
|
54
54
|
|
|
55
|
-
// Facebook —
|
|
56
|
-
//
|
|
55
|
+
// Facebook — only the characters allowed in Apple's URL scheme (RFC1738).
|
|
56
|
+
// Without credentials in `kasy new`, keep these placeholders (FB login won't work until replaced).
|
|
57
57
|
const FB_APP_ID_PLACEHOLDER = '000000000000000';
|
|
58
58
|
const FB_CLIENT_TOKEN_PLACEHOLDER = '00000000000000000000000000000000';
|
|
59
59
|
|
|
@@ -91,7 +91,7 @@ function buildTokens({ appName, bundleId, fbAppId, fbToken, defaultPaywall = 'ba
|
|
|
91
91
|
[`"${ORIGINAL_SHORT_NAME}"`]: `"${shortName}"`,
|
|
92
92
|
// pubspec.yaml name field
|
|
93
93
|
[`name: ${ORIGINAL_PACKAGE}`]: `name: ${packageName}`,
|
|
94
|
-
// Facebook (placeholders → user values
|
|
94
|
+
// Facebook (placeholders → user values, or keep a valid placeholder for the App Store)
|
|
95
95
|
[FB_APP_ID_PLACEHOLDER]: fbAppId?.trim() || FB_APP_ID_PLACEHOLDER,
|
|
96
96
|
[`fb${FB_APP_ID_PLACEHOLDER}`]: fbAppId?.trim() ? `fb${fbAppId.trim()}` : `fb${FB_APP_ID_PLACEHOLDER}`,
|
|
97
97
|
[FB_CLIENT_TOKEN_PLACEHOLDER]: fbToken?.trim() || FB_CLIENT_TOKEN_PLACEHOLDER,
|