kasy-cli 1.21.9 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +80 -37
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +22 -6
- package/lib/utils/env-tools.js +7 -0
- package/lib/utils/flutter-install.js +114 -0
- package/lib/utils/i18n/messages-en.js +52 -35
- package/lib/utils/i18n/messages-es.js +52 -35
- package/lib/utils/i18n/messages-pt.js +54 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +702 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +136 -23
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +54 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +53 -14
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +128 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +5 -6
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
|
@@ -2,8 +2,22 @@ rules_version = '2';
|
|
|
2
2
|
service cloud.firestore {
|
|
3
3
|
match /databases/{database}/documents {
|
|
4
4
|
|
|
5
|
+
// True when the signed-in user has role == "admin" on their OWN user doc.
|
|
6
|
+
// `role` is writable only server-side (see the users rules below), so a
|
|
7
|
+
// client cannot forge it — this check is trustworthy and server-evaluated.
|
|
8
|
+
function isAdmin() {
|
|
9
|
+
return request.auth != null
|
|
10
|
+
&& exists(/databases/$(database)/documents/users/$(request.auth.uid))
|
|
11
|
+
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.get('role', '') == 'admin';
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
match /users/{id} {
|
|
6
|
-
allow read,
|
|
15
|
+
allow read, delete: if request.auth.uid != null && request.auth.uid == id;
|
|
16
|
+
// Owners update their own profile, but NEVER their own `role`
|
|
17
|
+
// (no self-promotion to admin). `role` is set only server-side
|
|
18
|
+
// (Cloud Function / console), never from the client.
|
|
19
|
+
allow update: if request.auth.uid != null && request.auth.uid == id
|
|
20
|
+
&& !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']);
|
|
7
21
|
allow create: if false;
|
|
8
22
|
}
|
|
9
23
|
match /users/{id}/{document=**} {
|
|
@@ -23,10 +37,15 @@ service cloud.firestore {
|
|
|
23
37
|
allow create: if request.auth.uid != null
|
|
24
38
|
&& request.resource.data.active == false
|
|
25
39
|
&& request.resource.data.votes == 0;
|
|
40
|
+
// Either a normal ±1 vote by any signed-in user, OR full moderation
|
|
41
|
+
// (toggle visibility, edit translations) by an admin.
|
|
26
42
|
allow update: if request.auth.uid != null
|
|
27
|
-
&&
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
&& (
|
|
44
|
+
(request.resource.data.diff(resource.data).affectedKeys().hasOnly(['votes', 'last_update_date'])
|
|
45
|
+
&& (request.resource.data.votes == resource.data.votes + 1
|
|
46
|
+
|| request.resource.data.votes == resource.data.votes - 1))
|
|
47
|
+
|| isAdmin()
|
|
48
|
+
);
|
|
30
49
|
allow delete: if false;
|
|
31
50
|
}
|
|
32
51
|
match /feature_requests/{featureId}/votes/{id} {
|
|
@@ -48,4 +67,4 @@ service cloud.firestore {
|
|
|
48
67
|
allow read, write: if false;
|
|
49
68
|
}
|
|
50
69
|
}
|
|
51
|
-
}
|
|
70
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"name": "functions",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"firebase-admin": "^13.6.0",
|
|
10
|
-
"firebase-functions": "^7.1.1"
|
|
10
|
+
"firebase-functions": "^7.1.1",
|
|
11
|
+
"stripe": "^18.5.0"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
13
14
|
"@types/jest": "^29.5.14",
|
|
@@ -6239,6 +6240,26 @@
|
|
|
6239
6240
|
"url": "https://github.com/sponsors/sindresorhus"
|
|
6240
6241
|
}
|
|
6241
6242
|
},
|
|
6243
|
+
"node_modules/stripe": {
|
|
6244
|
+
"version": "18.5.0",
|
|
6245
|
+
"resolved": "https://registry.npmjs.org/stripe/-/stripe-18.5.0.tgz",
|
|
6246
|
+
"integrity": "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA==",
|
|
6247
|
+
"license": "MIT",
|
|
6248
|
+
"dependencies": {
|
|
6249
|
+
"qs": "^6.11.0"
|
|
6250
|
+
},
|
|
6251
|
+
"engines": {
|
|
6252
|
+
"node": ">=12.*"
|
|
6253
|
+
},
|
|
6254
|
+
"peerDependencies": {
|
|
6255
|
+
"@types/node": ">=12.x.x"
|
|
6256
|
+
},
|
|
6257
|
+
"peerDependenciesMeta": {
|
|
6258
|
+
"@types/node": {
|
|
6259
|
+
"optional": true
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
},
|
|
6242
6263
|
"node_modules/strnum": {
|
|
6243
6264
|
"version": "2.2.0",
|
|
6244
6265
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {onCall, HttpsError} from "firebase-functions/v2/https";
|
|
2
|
+
import * as admin from "firebase-admin";
|
|
3
|
+
import {Logger} from "../core/logger/logger";
|
|
4
|
+
|
|
5
|
+
// In-memory cap: the admin console loads up to this many of the most recent
|
|
6
|
+
// users in ONE call, then searches / sorts / paginates entirely on the client
|
|
7
|
+
// for an instant experience (no extra reads when typing or paging). Apps that
|
|
8
|
+
// outgrow this should add a dedicated search index (Algolia/Typesense) — see
|
|
9
|
+
// README. Reads are admin-only and bounded, so the cost stays predictable.
|
|
10
|
+
const MAX_SCAN = 1000;
|
|
11
|
+
|
|
12
|
+
// getAll() batch size for the subscription lookups (Firestore caps a single
|
|
13
|
+
// getAll at a few hundred refs comfortably; chunking keeps it safe).
|
|
14
|
+
const SUB_CHUNK = 200;
|
|
15
|
+
|
|
16
|
+
// A user counts as a subscriber when their subscription is currently usable.
|
|
17
|
+
const ACTIVE_SUBSCRIPTION_STATUSES = ["ACTIVE", "LIFETIME"];
|
|
18
|
+
|
|
19
|
+
interface AdminUser {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string | null;
|
|
22
|
+
name: string | null;
|
|
23
|
+
createdAt: number | null; // epoch millis
|
|
24
|
+
subscriber: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lists app users for the admin console.
|
|
29
|
+
*
|
|
30
|
+
* Security: the caller must be authenticated AND have `role == "admin"` on their
|
|
31
|
+
* own `users/{uid}` document. Clients cannot read the whole users collection
|
|
32
|
+
* (Firestore rules), so this runs with Admin privileges and gates on the role.
|
|
33
|
+
*
|
|
34
|
+
* Returns the most-recent users (bounded to MAX_SCAN) with their subscriber
|
|
35
|
+
* status already resolved. The client (admin_users_tab.dart) does the search,
|
|
36
|
+
* sort and pagination locally so those interactions are instant. Shape:
|
|
37
|
+
* { users: AdminUser[], totalUsers: number, truncated: boolean }
|
|
38
|
+
* - totalUsers: the true size of the collection (for an honest count).
|
|
39
|
+
* - truncated: true when the collection is larger than what we returned, so
|
|
40
|
+
* the UI can warn that search only covers the loaded set.
|
|
41
|
+
*/
|
|
42
|
+
export const listUsers = onCall(async (request) => {
|
|
43
|
+
if (!request.auth) {
|
|
44
|
+
throw new HttpsError("unauthenticated", "Authentication required");
|
|
45
|
+
}
|
|
46
|
+
const callerUid = request.auth.uid;
|
|
47
|
+
const db = admin.firestore();
|
|
48
|
+
const logger = new Logger("listUsers");
|
|
49
|
+
|
|
50
|
+
// Admin gate: the caller's own user doc must carry role == "admin".
|
|
51
|
+
const callerDoc = await db.collection("users").doc(callerUid).get();
|
|
52
|
+
if (!callerDoc.exists || callerDoc.get("role") !== "admin") {
|
|
53
|
+
throw new HttpsError("permission-denied", "Admin role required");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const usersRef = db.collection("users");
|
|
58
|
+
|
|
59
|
+
// Bounded fetch — the client does the search/sort/paginate.
|
|
60
|
+
//
|
|
61
|
+
// IMPORTANT: do NOT order this query by "creation_date". A Firestore
|
|
62
|
+
// orderBy silently DROPS every document that lacks the ordered field, and
|
|
63
|
+
// some user docs have no creation_date (e.g. accounts whose doc was created
|
|
64
|
+
// client-side during anonymous→social linking; the auth trigger then only
|
|
65
|
+
// patches the email and never backfills creation_date). Ordering by it
|
|
66
|
+
// would hide exactly those real, active users. We fetch unordered and sort
|
|
67
|
+
// in memory below (newest first, undated last) so every user is returned.
|
|
68
|
+
const snapshot = await usersRef.limit(MAX_SCAN).get();
|
|
69
|
+
const docs = snapshot.docs;
|
|
70
|
+
|
|
71
|
+
// Subscriber status for the whole batch, in chunked getAll() reads.
|
|
72
|
+
const activeSubscribers = new Set<string>();
|
|
73
|
+
for (let i = 0; i < docs.length; i += SUB_CHUNK) {
|
|
74
|
+
const chunk = docs.slice(i, i + SUB_CHUNK);
|
|
75
|
+
const refs = chunk.map((d) => db.collection("subscriptions").doc(d.id));
|
|
76
|
+
const snaps = await db.getAll(...refs);
|
|
77
|
+
for (const s of snaps) {
|
|
78
|
+
if (
|
|
79
|
+
s.exists &&
|
|
80
|
+
ACTIVE_SUBSCRIPTION_STATUSES.includes(s.get("status"))
|
|
81
|
+
) {
|
|
82
|
+
activeSubscribers.add(s.id);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const users: AdminUser[] = docs.map((d) => {
|
|
88
|
+
const createdAt = d.get("creation_date");
|
|
89
|
+
return {
|
|
90
|
+
id: d.id,
|
|
91
|
+
email: (d.get("email") as string) || null,
|
|
92
|
+
name: (d.get("name") as string) || null,
|
|
93
|
+
createdAt: createdAt ? createdAt.toMillis() : null,
|
|
94
|
+
subscriber: activeSubscribers.has(d.id),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Newest first; users without a creation_date sort to the end.
|
|
99
|
+
users.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
100
|
+
|
|
101
|
+
const countSnap = await usersRef.count().get();
|
|
102
|
+
const totalUsers = countSnap.data().count;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
users,
|
|
106
|
+
totalUsers,
|
|
107
|
+
truncated: totalUsers > users.length,
|
|
108
|
+
};
|
|
109
|
+
} catch (e) {
|
|
110
|
+
logger.error(`listUsers error: ${e}`);
|
|
111
|
+
throw new HttpsError("internal", "Failed to list users");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Firebase Cloud Function:
|
|
2
|
+
* Firebase Cloud Function: AI Chat Proxy (streaming)
|
|
3
3
|
*
|
|
4
4
|
* Receives {message, history} from the Flutter app and streams the response
|
|
5
5
|
* back as Server-Sent Events (SSE). The API key never leaves the server.
|
|
6
6
|
*
|
|
7
7
|
* Secrets required (set via `firebase functions:secrets:set`):
|
|
8
|
-
* -
|
|
8
|
+
* - AI_API_KEY: API key for OpenAI or Gemini
|
|
9
9
|
*
|
|
10
10
|
* Environment variables (set in functions/.env):
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
11
|
+
* - AI_PROVIDER: "openai" (default) or "gemini"
|
|
12
|
+
* - AI_SYSTEM_PROMPT: System prompt for the agent (optional)
|
|
13
13
|
*
|
|
14
14
|
* App dart-define:
|
|
15
|
-
* -
|
|
16
|
-
* Example: https://europe-west1-<project-id>.cloudfunctions.net/
|
|
15
|
+
* - AI_CHAT_ENDPOINT: URL of this function after deploy
|
|
16
|
+
* Example: https://europe-west1-<project-id>.cloudfunctions.net/aiChat
|
|
17
17
|
*
|
|
18
|
-
* Deploy: kasy deploy (or: firebase deploy --only functions:
|
|
18
|
+
* Deploy: kasy deploy (or: firebase deploy --only functions:aiChat)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { onRequest } from "firebase-functions/v2/https";
|
|
@@ -23,7 +23,7 @@ import { defineSecret } from "firebase-functions/params";
|
|
|
23
23
|
import { getAuth } from "firebase-admin/auth";
|
|
24
24
|
import type { Request, Response } from "express";
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const aiApiKey = defineSecret("AI_API_KEY");
|
|
27
27
|
|
|
28
28
|
interface ChatMessage {
|
|
29
29
|
role: "user" | "assistant";
|
|
@@ -130,8 +130,8 @@ async function verifyFirebaseToken(req: Request, res: Response): Promise<string
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
export const
|
|
134
|
-
{ cors: true, secrets: [
|
|
133
|
+
export const aiChat = onRequest(
|
|
134
|
+
{ cors: true, secrets: [aiApiKey] },
|
|
135
135
|
async (req, res) => {
|
|
136
136
|
if (req.method !== "POST") {
|
|
137
137
|
res.status(405).json({ error: "Method not allowed" });
|
|
@@ -147,16 +147,16 @@ export const llmChat = onRequest(
|
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
const apiKey =
|
|
150
|
+
const apiKey = aiApiKey.value();
|
|
151
151
|
if (!apiKey) {
|
|
152
152
|
res
|
|
153
153
|
.status(500)
|
|
154
|
-
.json({ error: "
|
|
154
|
+
.json({ error: "AI_API_KEY not configured. Run: firebase functions:secrets:set AI_API_KEY" });
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const provider = process.env.
|
|
159
|
-
const systemPrompt = process.env.
|
|
158
|
+
const provider = process.env.AI_PROVIDER ?? "openai";
|
|
159
|
+
const systemPrompt = process.env.AI_SYSTEM_PROMPT ?? "";
|
|
160
160
|
|
|
161
161
|
// SSE headers — must be set before any write
|
|
162
162
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -172,9 +172,9 @@ export const llmChat = onRequest(
|
|
|
172
172
|
}
|
|
173
173
|
res.end();
|
|
174
174
|
} catch (err) {
|
|
175
|
-
console.error("[
|
|
175
|
+
console.error("[ai-chat]", err);
|
|
176
176
|
// Send error as SSE event so the Flutter client can surface it
|
|
177
|
-
res.write(`data: ${JSON.stringify({ error: "
|
|
177
|
+
res.write(`data: ${JSON.stringify({ error: "AI request failed" })}\n\n`);
|
|
178
178
|
res.end();
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -19,7 +19,13 @@ exports.deviceTriggers = require("./notifications/device_triggers");
|
|
|
19
19
|
exports.subscriptions = require("./subscriptions/subscriptions_functions");
|
|
20
20
|
exports.subscriptionTriggers = require("./subscriptions/triggers");
|
|
21
21
|
|
|
22
|
+
// stripe web subscriptions (activated when the Stripe module is enabled)
|
|
23
|
+
exports.stripeFunctions = require("./subscriptions/stripe_functions");
|
|
24
|
+
|
|
25
|
+
// admin console (listUsers — gated on users/{uid}.role == "admin")
|
|
26
|
+
exports.adminFunctions = require("./admin/functions");
|
|
27
|
+
|
|
22
28
|
// feature requests: vote counter updated atomically by client (WriteBatch)
|
|
23
29
|
|
|
24
|
-
// llm chat proxy (activated when
|
|
25
|
-
exports.
|
|
30
|
+
// llm chat proxy (activated when withAiChat = true)
|
|
31
|
+
exports.aiChat = require("./ai_chat").aiChat;
|
|
@@ -14,7 +14,7 @@ import {Logger} from "../core/logger/logger";
|
|
|
14
14
|
* any time. Without it, sending a push to user A could deliver to a phone now
|
|
15
15
|
* signed in as user B.
|
|
16
16
|
*/
|
|
17
|
-
export const
|
|
17
|
+
export const dedupeDeviceTokens = onDocumentWritten(
|
|
18
18
|
"users/{userId}/devices/{deviceId}",
|
|
19
19
|
async (event) => {
|
|
20
20
|
const after = event.data?.after?.data();
|
|
@@ -25,7 +25,7 @@ export const onDeviceWritten = onDocumentWritten(
|
|
|
25
25
|
|
|
26
26
|
const currentUserId = event.params.userId;
|
|
27
27
|
const currentDeviceId = event.params.deviceId;
|
|
28
|
-
const logger = new Logger("
|
|
28
|
+
const logger = new Logger("dedupeDeviceTokens");
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
31
|
const duplicates = await admin
|
|
@@ -7,13 +7,13 @@ import {onDocumentCreated} from "firebase-functions/v2/firestore";
|
|
|
7
7
|
|
|
8
8
|
const kChannelId = "appfirebase";
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const onNotificationCreated = onDocumentCreated(
|
|
11
11
|
"users/{userId}/notifications/{notificationId}",
|
|
12
12
|
async (event) => {
|
|
13
13
|
if (!event.data) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
const logger = new Logger("
|
|
16
|
+
const logger = new Logger("onNotificationCreated");
|
|
17
17
|
try {
|
|
18
18
|
const notificationEntity = NotificationEntity.fromDocument(event.data);
|
|
19
19
|
const userId = event.params.userId;
|
|
@@ -97,6 +97,6 @@ export const onNewNotificationRequest = onDocumentCreated(
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
} catch (e) {
|
|
100
|
-
logger.error(`Error
|
|
100
|
+
logger.error(`Error onNotificationCreated users/${event.params.userId}/notifications/${event.id} : ${e}`);
|
|
101
101
|
}
|
|
102
102
|
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {error} from "firebase-functions/logger";
|
|
2
|
+
import {onCall, onRequest, HttpsError} from "firebase-functions/v2/https";
|
|
3
|
+
import {defineSecret, defineString} from "firebase-functions/params";
|
|
4
|
+
import * as admin from "firebase-admin";
|
|
5
|
+
import {Timestamp} from "firebase-admin/firestore";
|
|
6
|
+
import Stripe from "stripe";
|
|
7
|
+
import {Subscription} from "./models/subscriptions";
|
|
8
|
+
import {subscriptionsRepository} from "../core/data/repositories/repositories";
|
|
9
|
+
import {Stores, SubscriptionStatus} from "./models/subscription_status";
|
|
10
|
+
|
|
11
|
+
// Server-side only. Never exposed to the client.
|
|
12
|
+
const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY");
|
|
13
|
+
const stripeWebhookSecret = defineSecret("STRIPE_WEBHOOK_SECRET");
|
|
14
|
+
// Optional: restrict the listed prices to a single Stripe product.
|
|
15
|
+
const stripeProductId = defineString("STRIPE_PRODUCT_ID", {default: ""});
|
|
16
|
+
|
|
17
|
+
// Firestore collection mapping a Firebase uid -> its Stripe customer id.
|
|
18
|
+
const CUSTOMERS_COLLECTION = "stripe_customers";
|
|
19
|
+
|
|
20
|
+
function stripeClient(): Stripe {
|
|
21
|
+
return new Stripe(stripeSecretKey.value());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getOrCreateCustomer(stripe: Stripe, uid: string): Promise<string> {
|
|
25
|
+
const db = admin.firestore();
|
|
26
|
+
const ref = db.collection(CUSTOMERS_COLLECTION).doc(uid);
|
|
27
|
+
const snap = await ref.get();
|
|
28
|
+
const existing = snap.data()?.customerId as string | undefined;
|
|
29
|
+
if (existing) return existing;
|
|
30
|
+
const customer = await stripe.customers.create({metadata: {firebaseUID: uid}});
|
|
31
|
+
await ref.set({customerId: customer.id, created_at: Timestamp.now()});
|
|
32
|
+
return customer.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function trialDaysFor(price: Stripe.Price, product: Stripe.Product): number | null {
|
|
36
|
+
const meta = price.metadata?.trial_days ?? product.metadata?.trial_days;
|
|
37
|
+
return meta ? Number(meta) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// listPrices — active recurring prices, mapped to the paywall offer contract.
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export const listPrices = onCall({secrets: [stripeSecretKey]}, async () => {
|
|
44
|
+
const stripe = stripeClient();
|
|
45
|
+
const params: Stripe.PriceListParams = {
|
|
46
|
+
active: true,
|
|
47
|
+
type: "recurring",
|
|
48
|
+
expand: ["data.product"],
|
|
49
|
+
limit: 100,
|
|
50
|
+
};
|
|
51
|
+
const productFilter = stripeProductId.value();
|
|
52
|
+
if (productFilter) params.product = productFilter;
|
|
53
|
+
|
|
54
|
+
const prices = await stripe.prices.list(params);
|
|
55
|
+
return prices.data
|
|
56
|
+
.filter((p) => Boolean(p.recurring))
|
|
57
|
+
.map((p) => {
|
|
58
|
+
const product = p.product as Stripe.Product;
|
|
59
|
+
const features = (product.marketing_features ?? [])
|
|
60
|
+
.map((f) => f.name)
|
|
61
|
+
.filter((n): n is string => Boolean(n));
|
|
62
|
+
return {
|
|
63
|
+
priceId: p.id,
|
|
64
|
+
productId: typeof p.product === "string" ? p.product : product.id,
|
|
65
|
+
productName: product.name ?? "",
|
|
66
|
+
description: product.description ?? "",
|
|
67
|
+
unitAmount: p.unit_amount ?? 0,
|
|
68
|
+
currency: p.currency,
|
|
69
|
+
interval: p.recurring?.interval ?? "month",
|
|
70
|
+
intervalCount: p.recurring?.interval_count ?? 1,
|
|
71
|
+
trialDays: trialDaysFor(p, product),
|
|
72
|
+
features,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// createCheckoutSession — hosted Stripe Checkout (mode=subscription).
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
export const createCheckoutSession = onCall(
|
|
81
|
+
{secrets: [stripeSecretKey]},
|
|
82
|
+
async (request) => {
|
|
83
|
+
const uid = request.auth?.uid;
|
|
84
|
+
if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
|
|
85
|
+
const priceId = request.data?.priceId as string | undefined;
|
|
86
|
+
if (!priceId) throw new HttpsError("invalid-argument", "priceId is required");
|
|
87
|
+
const successUrl = (request.data?.successUrl as string | undefined) ?? "";
|
|
88
|
+
const cancelUrl = (request.data?.cancelUrl as string | undefined) ?? successUrl;
|
|
89
|
+
|
|
90
|
+
const stripe = stripeClient();
|
|
91
|
+
const customerId = await getOrCreateCustomer(stripe, uid);
|
|
92
|
+
|
|
93
|
+
const price = await stripe.prices.retrieve(priceId, {expand: ["product"]});
|
|
94
|
+
const trialDays = trialDaysFor(price, price.product as Stripe.Product);
|
|
95
|
+
|
|
96
|
+
const session = await stripe.checkout.sessions.create({
|
|
97
|
+
mode: "subscription",
|
|
98
|
+
customer: customerId,
|
|
99
|
+
client_reference_id: uid,
|
|
100
|
+
line_items: [{price: priceId, quantity: 1}],
|
|
101
|
+
success_url: successUrl,
|
|
102
|
+
cancel_url: cancelUrl,
|
|
103
|
+
subscription_data: {
|
|
104
|
+
metadata: {firebaseUID: uid},
|
|
105
|
+
...(trialDays ? {trial_period_days: trialDays} : {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return {url: session.url};
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// createPortalSession — Stripe Customer Portal (manage / cancel).
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
export const createPortalSession = onCall(
|
|
116
|
+
{secrets: [stripeSecretKey]},
|
|
117
|
+
async (request) => {
|
|
118
|
+
const uid = request.auth?.uid;
|
|
119
|
+
if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
|
|
120
|
+
const returnUrl = (request.data?.returnUrl as string | undefined) ?? "";
|
|
121
|
+
|
|
122
|
+
const stripe = stripeClient();
|
|
123
|
+
const snap = await admin
|
|
124
|
+
.firestore()
|
|
125
|
+
.collection(CUSTOMERS_COLLECTION)
|
|
126
|
+
.doc(uid)
|
|
127
|
+
.get();
|
|
128
|
+
const customerId = snap.data()?.customerId as string | undefined;
|
|
129
|
+
if (!customerId) {
|
|
130
|
+
throw new HttpsError("failed-precondition", "No Stripe customer for user");
|
|
131
|
+
}
|
|
132
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
133
|
+
customer: customerId,
|
|
134
|
+
return_url: returnUrl,
|
|
135
|
+
});
|
|
136
|
+
return {url: session.url};
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// stripeWebhook — webhook. The ONLY writer of `subscriptions` for Stripe.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
function statusFromStripe(sub: Stripe.Subscription): SubscriptionStatus {
|
|
144
|
+
switch (sub.status) {
|
|
145
|
+
case "active":
|
|
146
|
+
case "trialing":
|
|
147
|
+
return SubscriptionStatus.ACTIVE;
|
|
148
|
+
default:
|
|
149
|
+
// canceled, unpaid, past_due, incomplete, incomplete_expired, paused
|
|
150
|
+
return SubscriptionStatus.EXPIRED;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<void> {
|
|
155
|
+
const uid = sub.metadata?.firebaseUID;
|
|
156
|
+
if (!uid) {
|
|
157
|
+
console.log("[stripe-webhook] subscription without firebaseUID metadata, skipping");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const now = Timestamp.now();
|
|
161
|
+
const existing = await subscriptionsRepository.getFromUserId(uid);
|
|
162
|
+
// In Stripe API v18 the billing period lives on each subscription item.
|
|
163
|
+
const item = sub.items.data[0];
|
|
164
|
+
const priceId = item?.price?.id ?? "";
|
|
165
|
+
const periodEnd = item?.current_period_end;
|
|
166
|
+
const expiration = periodEnd
|
|
167
|
+
? Timestamp.fromMillis(periodEnd * 1000)
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
const subscription = new Subscription(
|
|
171
|
+
{
|
|
172
|
+
userId: uid,
|
|
173
|
+
status: statusFromStripe(sub),
|
|
174
|
+
creationDate: existing?.creationDate ?? now,
|
|
175
|
+
lastUpdate: now,
|
|
176
|
+
expirationDate: expiration,
|
|
177
|
+
store: Stores.STRIPE,
|
|
178
|
+
productId: priceId,
|
|
179
|
+
},
|
|
180
|
+
subscriptionsRepository,
|
|
181
|
+
);
|
|
182
|
+
await subscription.save();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const stripeWebhook = onRequest(
|
|
186
|
+
{cors: false, secrets: [stripeSecretKey, stripeWebhookSecret]},
|
|
187
|
+
async (req, res) => {
|
|
188
|
+
const signature = req.header("stripe-signature");
|
|
189
|
+
if (!signature) {
|
|
190
|
+
res.status(400).send("Missing signature");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const stripe = stripeClient();
|
|
194
|
+
let event: Stripe.Event;
|
|
195
|
+
try {
|
|
196
|
+
event = stripe.webhooks.constructEvent(
|
|
197
|
+
req.rawBody,
|
|
198
|
+
signature,
|
|
199
|
+
stripeWebhookSecret.value(),
|
|
200
|
+
);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.log(`[stripe-webhook] signature verification failed: ${e}`);
|
|
203
|
+
res.status(400).send("Invalid signature");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
switch (event.type) {
|
|
208
|
+
case "customer.subscription.created":
|
|
209
|
+
case "customer.subscription.updated":
|
|
210
|
+
case "customer.subscription.deleted":
|
|
211
|
+
await upsertFromStripeSubscription(event.data.object as Stripe.Subscription);
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
res.status(200).send("ok");
|
|
217
|
+
} catch (e) {
|
|
218
|
+
error(e);
|
|
219
|
+
res.status(500).send(e instanceof Error ? e.message : String(e));
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
);
|
|
@@ -84,11 +84,11 @@ async function sendMetaEventsForSubscription(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export const
|
|
87
|
+
export const revenuecatWebhook = onRequest({cors: true, secrets: [revenuecatToken, metaAccessToken, metaDatasetId]}, async (
|
|
88
88
|
req: https.Request,
|
|
89
89
|
res: express.Response,
|
|
90
90
|
) => {
|
|
91
|
-
console.log("[
|
|
91
|
+
console.log("[revenuecatWebhook]");
|
|
92
92
|
const authorization = req.header("Authorization");
|
|
93
93
|
if (!authorization) {
|
|
94
94
|
console.log("Unauthorized - no token provided");
|
package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
CHANGED
|
Binary file
|
package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
CHANGED
|
Binary file
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
/// import 'package:kasy_kit/components/components.dart';
|
|
7
7
|
library;
|
|
8
8
|
|
|
9
|
-
export '../core/sidebar/kasy_sidebar.dart';
|
|
10
9
|
export 'kasy_accordion.dart';
|
|
11
10
|
export 'kasy_alert.dart';
|
|
12
11
|
export 'kasy_app_bar.dart';
|
|
@@ -20,11 +19,15 @@ export 'kasy_checkbox.dart';
|
|
|
20
19
|
export 'kasy_chip.dart';
|
|
21
20
|
export 'kasy_date_picker.dart';
|
|
22
21
|
export 'kasy_dialog.dart';
|
|
22
|
+
export 'kasy_image_viewer.dart';
|
|
23
23
|
export 'kasy_otp_verification_bottom_sheet.dart';
|
|
24
|
+
export 'kasy_sidebar.dart';
|
|
24
25
|
export 'kasy_skeleton.dart';
|
|
26
|
+
export 'kasy_status_tag.dart';
|
|
25
27
|
export 'kasy_swipe_action.dart';
|
|
26
28
|
export 'kasy_tabs.dart';
|
|
27
29
|
export 'kasy_text_area.dart';
|
|
28
30
|
export 'kasy_text_field.dart';
|
|
29
31
|
export 'kasy_text_field_otp.dart';
|
|
30
32
|
export 'kasy_toast.dart';
|
|
33
|
+
export 'kasy_web_header.dart';
|