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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Edge Function: Stripe Webhook
|
|
3
|
+
*
|
|
4
|
+
* The ONLY writer of the `subscriptions` table for Stripe (web) subscriptions.
|
|
5
|
+
* Verifies the Stripe signature, then upserts the user's subscription with
|
|
6
|
+
* store='STRIPE'. The user id is read from the subscription metadata
|
|
7
|
+
* (supabaseUID), which the checkout function set server-side.
|
|
8
|
+
*
|
|
9
|
+
* Deployed WITHOUT JWT verification (Stripe calls it with a stripe-signature,
|
|
10
|
+
* not a Supabase JWT) — authenticity is enforced by the signature check.
|
|
11
|
+
*
|
|
12
|
+
* Secrets required (set via `supabase secrets set`):
|
|
13
|
+
* - STRIPE_SECRET_KEY
|
|
14
|
+
* - STRIPE_WEBHOOK_SECRET: whsec_... (from the Stripe webhook endpoint config)
|
|
15
|
+
* - SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY (auto-provided)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import Stripe from "npm:stripe@18";
|
|
19
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.47.10";
|
|
20
|
+
|
|
21
|
+
const SubscriptionStatus = {
|
|
22
|
+
ACTIVE: "ACTIVE",
|
|
23
|
+
EXPIRED: "EXPIRED",
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
function statusFromStripe(sub: Stripe.Subscription): string {
|
|
27
|
+
switch (sub.status) {
|
|
28
|
+
case "active":
|
|
29
|
+
case "trialing":
|
|
30
|
+
return SubscriptionStatus.ACTIVE;
|
|
31
|
+
default:
|
|
32
|
+
// canceled, unpaid, past_due, incomplete, incomplete_expired, paused
|
|
33
|
+
return SubscriptionStatus.EXPIRED;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// In Stripe API v18 the billing period lives on each subscription item; older
|
|
38
|
+
// versions exposed it on the subscription. Read whichever is present.
|
|
39
|
+
function periodEndMs(sub: Stripe.Subscription): number | null {
|
|
40
|
+
const item = sub.items?.data?.[0] as { current_period_end?: number } | undefined;
|
|
41
|
+
const fromItem = item?.current_period_end;
|
|
42
|
+
const fromSub = (sub as unknown as { current_period_end?: number }).current_period_end;
|
|
43
|
+
const sec = fromItem ?? fromSub;
|
|
44
|
+
return sec ? sec * 1000 : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Deno.serve(async (req: Request) => {
|
|
48
|
+
const signature = req.headers.get("stripe-signature");
|
|
49
|
+
if (!signature) return new Response("Missing signature", { status: 400 });
|
|
50
|
+
|
|
51
|
+
const secretKey = Deno.env.get("STRIPE_SECRET_KEY");
|
|
52
|
+
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET");
|
|
53
|
+
const supabaseUrl = Deno.env.get("SUPABASE_URL");
|
|
54
|
+
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
55
|
+
if (!secretKey || !webhookSecret || !supabaseUrl || !serviceRoleKey) {
|
|
56
|
+
console.error("[stripe-webhook] missing secrets");
|
|
57
|
+
return new Response("Server configuration error", { status: 500 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const stripe = new Stripe(secretKey);
|
|
61
|
+
const bodyText = await req.text();
|
|
62
|
+
|
|
63
|
+
let event: Stripe.Event;
|
|
64
|
+
try {
|
|
65
|
+
event = await stripe.webhooks.constructEventAsync(bodyText, signature, webhookSecret);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.log(`[stripe-webhook] signature verification failed: ${e}`);
|
|
68
|
+
return new Response("Invalid signature", { status: 400 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const handled = [
|
|
72
|
+
"customer.subscription.created",
|
|
73
|
+
"customer.subscription.updated",
|
|
74
|
+
"customer.subscription.deleted",
|
|
75
|
+
];
|
|
76
|
+
if (!handled.includes(event.type)) {
|
|
77
|
+
return new Response("ok");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sub = event.data.object as Stripe.Subscription;
|
|
81
|
+
const uid = sub.metadata?.supabaseUID;
|
|
82
|
+
if (!uid) {
|
|
83
|
+
console.log("[stripe-webhook] subscription without supabaseUID metadata, skipping");
|
|
84
|
+
return new Response("ok");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const supabase = createClient(supabaseUrl, serviceRoleKey);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// The subscriptions row references public.users(id). Skip unknown users.
|
|
91
|
+
const { data: userRows } = await supabase
|
|
92
|
+
.from("users")
|
|
93
|
+
.select("id")
|
|
94
|
+
.eq("id", uid)
|
|
95
|
+
.limit(1);
|
|
96
|
+
if (!userRows?.length) {
|
|
97
|
+
console.log("[stripe-webhook] user not found:", uid);
|
|
98
|
+
return new Response("ok");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const item = sub.items?.data?.[0];
|
|
102
|
+
const priceId = item?.price?.id ?? "";
|
|
103
|
+
const ms = periodEndMs(sub);
|
|
104
|
+
const now = new Date().toISOString();
|
|
105
|
+
const payload = {
|
|
106
|
+
status: statusFromStripe(sub),
|
|
107
|
+
last_update_date: now,
|
|
108
|
+
period_end_date: ms ? new Date(ms).toISOString() : null,
|
|
109
|
+
sku_id: priceId,
|
|
110
|
+
offer_id: priceId,
|
|
111
|
+
store: "STRIPE",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const { data: existing } = await supabase
|
|
115
|
+
.from("subscriptions")
|
|
116
|
+
.select("user_id")
|
|
117
|
+
.eq("user_id", uid)
|
|
118
|
+
.maybeSingle();
|
|
119
|
+
|
|
120
|
+
if (existing) {
|
|
121
|
+
const { error: updateErr } = await supabase
|
|
122
|
+
.from("subscriptions")
|
|
123
|
+
.update(payload)
|
|
124
|
+
.eq("user_id", uid);
|
|
125
|
+
if (updateErr) console.error("[stripe-webhook] update error:", updateErr);
|
|
126
|
+
} else {
|
|
127
|
+
const { error: insertErr } = await supabase
|
|
128
|
+
.from("subscriptions")
|
|
129
|
+
.insert({ user_id: uid, creation_date: now, ...payload });
|
|
130
|
+
if (insertErr) console.error("[stripe-webhook] insert error:", insertErr);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new Response("ok");
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error("[stripe-webhook] error:", err);
|
|
136
|
+
return new Response(err instanceof Error ? err.message : "Internal error", { status: 500 });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Supabase 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
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
12
|
-
* 4.
|
|
13
|
-
* 5.
|
|
8
|
+
* Specific hook (applyBackendSetup):
|
|
9
|
+
* 1. Applies supabase/patch/ over the copied Firebase template
|
|
10
|
+
* 2. Replaces pubspec.yaml with the Supabase template (pubspec.yaml.tpl)
|
|
11
|
+
* 3. Removes Firebase-only artifacts (e.g. GoogleService-Info.plist paths)
|
|
12
|
+
* 4. Writes environment overrides (supabaseUrl, supabaseAnonKey)
|
|
13
|
+
* 5. Copies config.toml, migrations and edge-functions for the Supabase backend
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const path = require('node:path');
|
|
17
17
|
const fs = require('fs-extra');
|
|
18
18
|
const { applyPatch, copyWithTokens } = require('../../engine');
|
|
19
19
|
const { generateProject } = require('../../generate');
|
|
20
|
-
const {
|
|
20
|
+
const { writeEnvironmentsOverrides } = require('../../shared/generator-utils');
|
|
21
21
|
const { removeBackendSpecificArtifacts } = require('../../shared/backend-config');
|
|
22
22
|
|
|
23
23
|
const SUPABASE_PATCH_DIR = path.join(__dirname, 'patch');
|
|
@@ -40,7 +40,7 @@ async function generateSupabaseProject(targetDir, options) {
|
|
|
40
40
|
// 1. Patch Supabase (auth, storage, notifications, etc.)
|
|
41
41
|
const { filesApplied } = await applyPatch(SUPABASE_PATCH_DIR, dir, tokens, pathReplacements);
|
|
42
42
|
|
|
43
|
-
// 1b. README
|
|
43
|
+
// 1b. Backend README (excluded from applyPatch) — copy it manually
|
|
44
44
|
const language = opts.language ?? 'pt';
|
|
45
45
|
const readmeLang = ['en', 'pt', 'es'].includes(language) ? language : 'en';
|
|
46
46
|
const candidates = [
|
|
@@ -54,7 +54,7 @@ async function generateSupabaseProject(targetDir, options) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// 2.
|
|
57
|
+
// 2. Replace pubspec.yaml with the Supabase deps
|
|
58
58
|
const pubspecContent = await fs.readFile(SUPABASE_PUBSPEC, 'utf8');
|
|
59
59
|
let pubspecReplaced = pubspecContent;
|
|
60
60
|
for (const [from, to] of Object.entries(tokens)) {
|
|
@@ -62,20 +62,20 @@ async function generateSupabaseProject(targetDir, options) {
|
|
|
62
62
|
}
|
|
63
63
|
await fs.outputFile(path.join(dir, 'pubspec.yaml'), pubspecReplaced, 'utf8');
|
|
64
64
|
|
|
65
|
-
// 3.
|
|
65
|
+
// 3. Remove Firebase-only artifacts
|
|
66
66
|
await removeBackendSpecificArtifacts(dir, 'supabase', opts.modules ?? []);
|
|
67
67
|
|
|
68
|
-
// 4.
|
|
69
|
-
await
|
|
68
|
+
// 4. Write environment overrides with the Supabase credentials
|
|
69
|
+
await writeEnvironmentsOverrides(dir, 'supabase', tokens, {
|
|
70
70
|
supabaseUrl: opts.supabaseUrl,
|
|
71
71
|
supabaseAnonKey: opts.supabaseAnonKey,
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
// 5.
|
|
74
|
+
// 5. Copy the Supabase infrastructure files
|
|
75
75
|
const supabaseDir = path.join(dir, 'supabase');
|
|
76
76
|
await fs.ensureDir(supabaseDir);
|
|
77
77
|
|
|
78
|
-
//
|
|
78
|
+
// Extra tokens for substitution in the SQL migrations
|
|
79
79
|
const supabaseTokens = {
|
|
80
80
|
...tokens,
|
|
81
81
|
'supabaseplaceholder.supabase.co': (opts.supabaseUrl || '').replace(/^https?:\/\//, ''),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- AI chat history (one user has many conversations, each with many messages).
|
|
2
|
+
|
|
3
|
+
-- Conversations. The last message is denormalized onto the row so the list can
|
|
4
|
+
-- be rendered with a single query (no need to read each conversation's messages
|
|
5
|
+
-- just to show a preview + timestamp).
|
|
6
|
+
CREATE TABLE IF NOT EXISTS public.ai_conversations (
|
|
7
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
8
|
+
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
9
|
+
last_message_role TEXT CHECK (last_message_role IN ('user', 'assistant')),
|
|
10
|
+
last_message_content TEXT,
|
|
11
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- Row Level Security: users can only access their own conversations.
|
|
16
|
+
ALTER TABLE public.ai_conversations ENABLE ROW LEVEL SECURITY;
|
|
17
|
+
|
|
18
|
+
CREATE POLICY "Users can manage their own ai conversations"
|
|
19
|
+
ON public.ai_conversations
|
|
20
|
+
FOR ALL
|
|
21
|
+
USING (auth.uid() = user_id)
|
|
22
|
+
WITH CHECK (auth.uid() = user_id);
|
|
23
|
+
|
|
24
|
+
-- Index for listing a user's conversations, most recently updated first.
|
|
25
|
+
CREATE INDEX IF NOT EXISTS ai_conversations_user_updated_at_idx
|
|
26
|
+
ON public.ai_conversations (user_id, updated_at DESC);
|
|
27
|
+
|
|
28
|
+
-- Messages. Each row stores one message (user or assistant) inside a
|
|
29
|
+
-- conversation. user_id is kept for a simple RLS policy.
|
|
30
|
+
CREATE TABLE IF NOT EXISTS public.ai_messages (
|
|
31
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
32
|
+
conversation_id UUID NOT NULL REFERENCES public.ai_conversations(id) ON DELETE CASCADE,
|
|
33
|
+
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
|
34
|
+
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
|
35
|
+
content TEXT NOT NULL,
|
|
36
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
-- Row Level Security: users can only access their own messages.
|
|
40
|
+
ALTER TABLE public.ai_messages ENABLE ROW LEVEL SECURITY;
|
|
41
|
+
|
|
42
|
+
CREATE POLICY "Users can manage their own ai messages"
|
|
43
|
+
ON public.ai_messages
|
|
44
|
+
FOR ALL
|
|
45
|
+
USING (auth.uid() = user_id)
|
|
46
|
+
WITH CHECK (auth.uid() = user_id);
|
|
47
|
+
|
|
48
|
+
-- Index for fast ordered queries within a conversation.
|
|
49
|
+
CREATE INDEX IF NOT EXISTS ai_messages_conversation_created_at_idx
|
|
50
|
+
ON public.ai_messages (conversation_id, created_at ASC);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Stripe (web) subscriptions support + subscription write hardening.
|
|
2
|
+
--
|
|
3
|
+
-- 1. `stripe_customers` maps a Supabase auth user to its single Stripe customer
|
|
4
|
+
-- id, so the checkout/portal Edge Functions can find-or-create one customer
|
|
5
|
+
-- per user.
|
|
6
|
+
-- 2. Hardens `subscriptions` so ONLY the server (service role, used by the
|
|
7
|
+
-- Stripe and RevenueCat webhooks) can write it. The client can only read its
|
|
8
|
+
-- own row. This prevents a client from forging a premium subscription.
|
|
9
|
+
-- Previously a `FOR ALL` policy let a client insert/update its own row.
|
|
10
|
+
|
|
11
|
+
-- Map auth user -> Stripe customer id. Written only by the server (service role,
|
|
12
|
+
-- which bypasses RLS). Clients may read their own mapping but never write it.
|
|
13
|
+
CREATE TABLE IF NOT EXISTS public.stripe_customers (
|
|
14
|
+
user_id UUID PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE,
|
|
15
|
+
customer_id TEXT NOT NULL,
|
|
16
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_stripe_customers_customer_id
|
|
20
|
+
ON public.stripe_customers(customer_id);
|
|
21
|
+
|
|
22
|
+
ALTER TABLE public.stripe_customers ENABLE ROW LEVEL SECURITY;
|
|
23
|
+
|
|
24
|
+
-- Read-only for the owning user; no insert/update/delete policy => clients
|
|
25
|
+
-- cannot write (only the service role, which bypasses RLS, can).
|
|
26
|
+
DROP POLICY IF EXISTS "Stripe customers read own" ON public.stripe_customers;
|
|
27
|
+
CREATE POLICY "Stripe customers read own" ON public.stripe_customers
|
|
28
|
+
FOR SELECT USING (auth.uid() = user_id);
|
|
29
|
+
|
|
30
|
+
-- Harden subscriptions: clients read their own row only; all writes come from
|
|
31
|
+
-- the webhook via the service role. Replaces the previous FOR ALL policy that
|
|
32
|
+
-- allowed a client to insert/update (forge) its own subscription.
|
|
33
|
+
DROP POLICY IF EXISTS "Subscriptions own" ON public.subscriptions;
|
|
34
|
+
DROP POLICY IF EXISTS "Subscriptions read own" ON public.subscriptions;
|
|
35
|
+
CREATE POLICY "Subscriptions read own" ON public.subscriptions
|
|
36
|
+
FOR SELECT USING (auth.uid() = user_id);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- Admin role support
|
|
2
|
+
--
|
|
3
|
+
-- Adds a server-only `role` column to public.users. null/absent = normal user,
|
|
4
|
+
-- 'admin' = administrator (unlocks the admin console's server data). The column
|
|
5
|
+
-- can be written ONLY by the service role (Edge Functions) or the dashboard /
|
|
6
|
+
-- SQL editor — never by the app client, so a user can never promote themselves
|
|
7
|
+
-- to admin. This mirrors the Firebase rule that blocks `role` writes.
|
|
8
|
+
|
|
9
|
+
ALTER TABLE public.users ADD COLUMN IF NOT EXISTS role TEXT;
|
|
10
|
+
|
|
11
|
+
-- Guard trigger: the app client (anon / authenticated) can update its own row
|
|
12
|
+
-- but never the `role` column. Privileged roles pass straight through:
|
|
13
|
+
-- - service_role : used by Edge Functions and the dashboard
|
|
14
|
+
-- - postgres / supabase_admin : migrations and the SQL editor
|
|
15
|
+
-- so an admin can still assign roles by hand.
|
|
16
|
+
CREATE OR REPLACE FUNCTION public.enforce_role_immutable()
|
|
17
|
+
RETURNS TRIGGER
|
|
18
|
+
LANGUAGE plpgsql
|
|
19
|
+
AS $$
|
|
20
|
+
BEGIN
|
|
21
|
+
IF current_user IN ('service_role', 'postgres', 'supabase_admin') THEN
|
|
22
|
+
RETURN NEW; -- privileged caller: allow the change
|
|
23
|
+
END IF;
|
|
24
|
+
|
|
25
|
+
IF TG_OP = 'INSERT' THEN
|
|
26
|
+
NEW.role := NULL; -- clients never set a role on signup
|
|
27
|
+
ELSIF NEW.role IS DISTINCT FROM OLD.role THEN
|
|
28
|
+
RAISE EXCEPTION 'The role field can only be changed by the server';
|
|
29
|
+
END IF;
|
|
30
|
+
|
|
31
|
+
RETURN NEW;
|
|
32
|
+
END;
|
|
33
|
+
$$;
|
|
34
|
+
|
|
35
|
+
DROP TRIGGER IF EXISTS users_enforce_role_immutable ON public.users;
|
|
36
|
+
CREATE TRIGGER users_enforce_role_immutable
|
|
37
|
+
BEFORE INSERT OR UPDATE ON public.users
|
|
38
|
+
FOR EACH ROW EXECUTE FUNCTION public.enforce_role_immutable();
|
|
39
|
+
|
|
40
|
+
-- Admin helper: true when the current caller is an administrator. SECURITY
|
|
41
|
+
-- DEFINER so it reads the role regardless of the caller's own RLS (and avoids
|
|
42
|
+
-- policy recursion). Used by admin-only policies below.
|
|
43
|
+
CREATE OR REPLACE FUNCTION public.is_admin()
|
|
44
|
+
RETURNS BOOLEAN
|
|
45
|
+
LANGUAGE sql
|
|
46
|
+
SECURITY DEFINER
|
|
47
|
+
SET search_path = public
|
|
48
|
+
STABLE
|
|
49
|
+
AS $$
|
|
50
|
+
SELECT EXISTS (
|
|
51
|
+
SELECT 1 FROM public.users
|
|
52
|
+
WHERE id = auth.uid() AND role = 'admin'
|
|
53
|
+
);
|
|
54
|
+
$$;
|
|
55
|
+
|
|
56
|
+
-- Feature requests moderation (admin console "Requests" tab): only admins can
|
|
57
|
+
-- toggle visibility (active) or edit the localized texts. Voting still flows
|
|
58
|
+
-- through the feature_votes triggers, so this admin policy is the ONLY way a
|
|
59
|
+
-- client can UPDATE feature_requests directly.
|
|
60
|
+
DROP POLICY IF EXISTS "Feature requests admin update" ON public.feature_requests;
|
|
61
|
+
CREATE POLICY "Feature requests admin update" ON public.feature_requests
|
|
62
|
+
FOR UPDATE USING (public.is_admin()) WITH CHECK (public.is_admin());
|
|
@@ -16,6 +16,10 @@ sealed class UserEntity with _$UserEntity {
|
|
|
16
16
|
@JsonKey(name: 'avatar_url') String? avatarPath,
|
|
17
17
|
bool? onboarded,
|
|
18
18
|
String? locale,
|
|
19
|
+
// Access-control role. null/absent = normal user; "admin" unlocks the admin
|
|
20
|
+
// console's server data. Server-only: the `users` guard trigger blocks the
|
|
21
|
+
// client from writing it (set it from the Supabase dashboard / SQL editor).
|
|
22
|
+
String? role,
|
|
19
23
|
}) = UserEntityData;
|
|
20
24
|
|
|
21
25
|
const UserEntity._();
|
|
@@ -6,7 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|
|
6
6
|
import 'package:kasy_kit/core/config/app_env.dart';
|
|
7
7
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
8
8
|
|
|
9
|
-
part '
|
|
9
|
+
part 'environments.freezed.dart';
|
|
10
10
|
|
|
11
11
|
// URLs for terms of service and privacy policy
|
|
12
12
|
const kTermsUrl = '';
|
|
@@ -38,10 +38,6 @@ sealed class Environment with _$Environment {
|
|
|
38
38
|
/// (only if you want to use in-app purchases with RevenueCat)
|
|
39
39
|
String? revenueCatIOSApiKey,
|
|
40
40
|
|
|
41
|
-
/// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
|
|
42
|
-
/// (only if you want to use subscriptions on web)
|
|
43
|
-
String? revenueCatWebApiKey,
|
|
44
|
-
|
|
45
41
|
/// this is used to open the app store page of your app for reviews
|
|
46
42
|
String? appStoreId,
|
|
47
43
|
|
|
@@ -74,10 +70,6 @@ sealed class Environment with _$Environment {
|
|
|
74
70
|
/// (only if you want to use in-app purchases with RevenueCat)
|
|
75
71
|
String? revenueCatIOSApiKey,
|
|
76
72
|
|
|
77
|
-
/// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
|
|
78
|
-
/// (only if you want to use subscriptions on web)
|
|
79
|
-
String? revenueCatWebApiKey,
|
|
80
|
-
|
|
81
73
|
/// only if you want to use ads
|
|
82
74
|
String? androidInterstitialAdUnitId,
|
|
83
75
|
|
|
@@ -114,7 +106,6 @@ sealed class Environment with _$Environment {
|
|
|
114
106
|
appStoreId: '',
|
|
115
107
|
revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
|
|
116
108
|
revenueCatIOSApiKey: AppEnv.rcIosApiKey,
|
|
117
|
-
revenueCatWebApiKey: AppEnv.rcWebApiKey,
|
|
118
109
|
mixpanelToken: AppEnv.mixpanelToken,
|
|
119
110
|
authenticationMode: AuthenticationMode.anonymous,
|
|
120
111
|
);
|
|
@@ -125,7 +116,6 @@ sealed class Environment with _$Environment {
|
|
|
125
116
|
appStoreId: AppEnv.appStoreId,
|
|
126
117
|
revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
|
|
127
118
|
revenueCatIOSApiKey: AppEnv.rcIosApiKey,
|
|
128
|
-
revenueCatWebApiKey: AppEnv.rcWebApiKey,
|
|
129
119
|
sentryDsn: AppEnv.sentryDsn,
|
|
130
120
|
mixpanelToken: AppEnv.mixpanelToken,
|
|
131
121
|
authenticationMode: AuthenticationMode.anonymous,
|
|
@@ -140,8 +130,8 @@ sealed class Environment with _$Environment {
|
|
|
140
130
|
revenueCatIOSApiKey != null && revenueCatIOSApiKey!.isNotEmpty,
|
|
141
131
|
TargetPlatform.android =>
|
|
142
132
|
revenueCatAndroidApiKey != null && revenueCatAndroidApiKey!.isNotEmpty,
|
|
143
|
-
|
|
144
|
-
|
|
133
|
+
// RevenueCat is mobile-only; web/desktop are never RevenueCat-configured.
|
|
134
|
+
_ => false,
|
|
145
135
|
};
|
|
146
136
|
}
|
|
147
137
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
2
|
+
import 'package:logger/logger.dart';
|
|
3
|
+
import 'package:kasy_kit/features/ai_chat/api/ai_chat_conversation_entity.dart';
|
|
4
|
+
import 'package:kasy_kit/features/ai_chat/api/ai_chat_message_entity.dart';
|
|
5
|
+
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
6
|
+
|
|
7
|
+
final aiChatApiProvider = Provider<AiChatApi>(
|
|
8
|
+
(ref) => AiChatApi(client: Supabase.instance.client),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const _kConversationsTable = 'ai_conversations';
|
|
12
|
+
const _kMessagesTable = 'ai_messages';
|
|
13
|
+
|
|
14
|
+
/// One user has many conversations (public.ai_conversations), each with many
|
|
15
|
+
/// messages (public.ai_messages, linked by conversation_id).
|
|
16
|
+
class AiChatApi {
|
|
17
|
+
final SupabaseClient _client;
|
|
18
|
+
final Logger _logger = Logger();
|
|
19
|
+
|
|
20
|
+
AiChatApi({required SupabaseClient client}) : _client = client;
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Conversations
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/// Returns the user's conversations, most recently updated first.
|
|
27
|
+
Future<List<AiChatConversationEntity>> loadConversations(
|
|
28
|
+
String userId,
|
|
29
|
+
) async {
|
|
30
|
+
final rows = await _client
|
|
31
|
+
.from(_kConversationsTable)
|
|
32
|
+
.select()
|
|
33
|
+
.eq('user_id', userId)
|
|
34
|
+
.order('updated_at', ascending: false);
|
|
35
|
+
return rows.map(AiChatConversationEntity.fromJson).toList();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Creates an empty conversation and returns it (with its generated id).
|
|
39
|
+
Future<AiChatConversationEntity> createConversation(String userId) async {
|
|
40
|
+
final row = await _client
|
|
41
|
+
.from(_kConversationsTable)
|
|
42
|
+
.insert({'user_id': userId})
|
|
43
|
+
.select()
|
|
44
|
+
.single();
|
|
45
|
+
return AiChatConversationEntity.fromJson(row);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Deletes a conversation; its messages cascade via the foreign key.
|
|
49
|
+
Future<void> deleteConversation(String userId, String conversationId) async {
|
|
50
|
+
await _client.from(_kConversationsTable).delete().eq('id', conversationId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Messages
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/// Returns all messages in a conversation, oldest first.
|
|
58
|
+
Future<List<AiChatMessageEntity>> loadMessages(
|
|
59
|
+
String userId,
|
|
60
|
+
String conversationId,
|
|
61
|
+
) async {
|
|
62
|
+
final rows = await _client
|
|
63
|
+
.from(_kMessagesTable)
|
|
64
|
+
.select()
|
|
65
|
+
.eq('conversation_id', conversationId)
|
|
66
|
+
.order('created_at', ascending: true);
|
|
67
|
+
return rows.map(AiChatMessageEntity.fromJson).toList();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Persists a message and denormalizes it onto the parent conversation
|
|
71
|
+
/// (last message preview + updated_at) so the list stays cheap to render.
|
|
72
|
+
Future<void> saveMessage(
|
|
73
|
+
String userId,
|
|
74
|
+
String conversationId,
|
|
75
|
+
AiChatMessageEntity message,
|
|
76
|
+
) async {
|
|
77
|
+
try {
|
|
78
|
+
await _client.from(_kMessagesTable).insert({
|
|
79
|
+
'user_id': userId,
|
|
80
|
+
'conversation_id': conversationId,
|
|
81
|
+
...message.toJson(),
|
|
82
|
+
});
|
|
83
|
+
await _client
|
|
84
|
+
.from(_kConversationsTable)
|
|
85
|
+
.update({
|
|
86
|
+
'updated_at': message.createdAt.toUtc().toIso8601String(),
|
|
87
|
+
'last_message_role': message.role,
|
|
88
|
+
'last_message_content': message.content,
|
|
89
|
+
})
|
|
90
|
+
.eq('id', conversationId);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
_logger.e('Failed to persist AI message: $e');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/// Supabase row mapping for a single AI chat conversation.
|
|
2
|
+
/// Table: public.ai_conversations
|
|
3
|
+
///
|
|
4
|
+
/// The last message is denormalized onto the conversation so the list can be
|
|
5
|
+
/// rendered with a single query (no need to read each conversation's messages
|
|
6
|
+
/// just to show a preview + timestamp).
|
|
7
|
+
class AiChatConversationEntity {
|
|
8
|
+
final String id;
|
|
9
|
+
final DateTime createdAt;
|
|
10
|
+
final DateTime updatedAt;
|
|
11
|
+
|
|
12
|
+
/// Role of the most recent message ('user' or 'assistant'), or null when the
|
|
13
|
+
/// conversation has no messages yet.
|
|
14
|
+
final String? lastMessageRole;
|
|
15
|
+
|
|
16
|
+
/// Content of the most recent message, or null when empty.
|
|
17
|
+
final String? lastMessageContent;
|
|
18
|
+
|
|
19
|
+
const AiChatConversationEntity({
|
|
20
|
+
required this.id,
|
|
21
|
+
required this.createdAt,
|
|
22
|
+
required this.updatedAt,
|
|
23
|
+
this.lastMessageRole,
|
|
24
|
+
this.lastMessageContent,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
bool get isEmpty => lastMessageContent == null;
|
|
28
|
+
|
|
29
|
+
AiChatConversationEntity copyWith({
|
|
30
|
+
DateTime? updatedAt,
|
|
31
|
+
String? lastMessageRole,
|
|
32
|
+
String? lastMessageContent,
|
|
33
|
+
}) {
|
|
34
|
+
return AiChatConversationEntity(
|
|
35
|
+
id: id,
|
|
36
|
+
createdAt: createdAt,
|
|
37
|
+
updatedAt: updatedAt ?? this.updatedAt,
|
|
38
|
+
lastMessageRole: lastMessageRole ?? this.lastMessageRole,
|
|
39
|
+
lastMessageContent: lastMessageContent ?? this.lastMessageContent,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
factory AiChatConversationEntity.fromJson(Map<String, dynamic> json) {
|
|
44
|
+
return AiChatConversationEntity(
|
|
45
|
+
id: json['id'] as String,
|
|
46
|
+
createdAt: DateTime.parse(json['created_at'] as String),
|
|
47
|
+
updatedAt: DateTime.parse(json['updated_at'] as String),
|
|
48
|
+
lastMessageRole: json['last_message_role'] as String?,
|
|
49
|
+
lastMessageContent: json['last_message_content'] as String?,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
/// Supabase row mapping for a single
|
|
2
|
-
/// Table: public.
|
|
3
|
-
class
|
|
1
|
+
/// Supabase row mapping for a single AI chat message.
|
|
2
|
+
/// Table: public.ai_messages
|
|
3
|
+
class AiChatMessageEntity {
|
|
4
4
|
final String? id;
|
|
5
5
|
final String role; // 'user' or 'assistant'
|
|
6
6
|
final String content;
|
|
7
7
|
final DateTime createdAt;
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const AiChatMessageEntity({
|
|
10
10
|
this.id,
|
|
11
11
|
required this.role,
|
|
12
12
|
required this.content,
|
|
13
13
|
required this.createdAt,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
factory
|
|
17
|
-
return
|
|
16
|
+
factory AiChatMessageEntity.fromJson(Map<String, dynamic> json) {
|
|
17
|
+
return AiChatMessageEntity(
|
|
18
18
|
id: json['id'] as String?,
|
|
19
19
|
role: json['role'] as String,
|
|
20
20
|
content: json['content'] as String,
|