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
|
@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
|
|
|
6
6
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
7
7
|
import 'package:kasy_kit/core/haptics/haptic_feedback_notifier.dart';
|
|
8
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
9
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
9
10
|
|
|
10
11
|
/// Universal hover/press wrapper — works on any widget shape, any platform.
|
|
11
12
|
///
|
|
@@ -45,8 +46,10 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
45
46
|
this.semanticLabel,
|
|
46
47
|
this.hapticEnabled = true,
|
|
47
48
|
this.hoverEnabled = true,
|
|
49
|
+
this.pressEnabled = true,
|
|
48
50
|
this.hoverColor,
|
|
49
51
|
this.pressColor,
|
|
52
|
+
this.focusable = false,
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
final Widget child;
|
|
@@ -75,6 +78,13 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
75
78
|
/// settings) where a hover highlight is visually unwanted.
|
|
76
79
|
final bool hoverEnabled;
|
|
77
80
|
|
|
81
|
+
/// Whether the pressed-state background fill is shown on tap.
|
|
82
|
+
///
|
|
83
|
+
/// When false, tapping triggers [onTap] (and haptic/cursor) with no visible
|
|
84
|
+
/// background highlight. Use on plain list rows (e.g. settings) where the tap
|
|
85
|
+
/// should just navigate without leaving a grey flash behind.
|
|
86
|
+
final bool pressEnabled;
|
|
87
|
+
|
|
78
88
|
/// Exact background colour used while the pointer hovers over the widget.
|
|
79
89
|
///
|
|
80
90
|
/// When non-null, this solid colour replaces the default semi-transparent
|
|
@@ -95,6 +105,13 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
95
105
|
/// background is already tinted so the feedback stays on-palette.
|
|
96
106
|
final Color? pressColor;
|
|
97
107
|
|
|
108
|
+
/// When true, the control becomes a keyboard tab-stop by wrapping its visual
|
|
109
|
+
/// in the kit's [KasyFocusRing]: a focus ring appears during keyboard
|
|
110
|
+
/// navigation (never on pointer/touch) and Enter/Space triggers [onTap].
|
|
111
|
+
/// Pointer and touch behaviour are unchanged. Defaults to false so existing
|
|
112
|
+
/// call sites stay plain, non-focusable rows.
|
|
113
|
+
final bool focusable;
|
|
114
|
+
|
|
98
115
|
@override
|
|
99
116
|
ConsumerState<KasyHover> createState() => _KasyHoverState();
|
|
100
117
|
}
|
|
@@ -117,22 +134,34 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
|
|
|
117
134
|
Color _overlayColor(BuildContext context) {
|
|
118
135
|
final bool isDark = Theme.of(context).brightness == Brightness.dark;
|
|
119
136
|
final Color base = widget.pressColor ?? context.colors.onSurface;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// transparent to a light solid colour passes through dark intermediates,
|
|
131
|
-
// causing a visible flash. Lerping from the same hue at alpha=0 to
|
|
132
|
-
// alpha=1 only changes opacity → no dark artefact.
|
|
137
|
+
|
|
138
|
+
// Nav-item style: a solid hover/selected colour was provided. Keep the
|
|
139
|
+
// resting, hover and pressed states on the SAME hue — only the intensity
|
|
140
|
+
// changes, never the colour — so clicking never flashes.
|
|
141
|
+
//
|
|
142
|
+
// Resting must be that hue at alpha=0 (NOT Colors.transparent, which is
|
|
143
|
+
// transparent *black*): an AnimatedContainer lerping from black-transparent
|
|
144
|
+
// to a light solid passes through dark intermediates and flickers.
|
|
145
|
+
// Press = the hover fill blended a touch deeper, so hover→press→hover stays
|
|
146
|
+
// continuous instead of swapping to a different translucent overlay.
|
|
133
147
|
if (widget.hoverColor != null) {
|
|
148
|
+
if (_pressed && widget.pressEnabled) {
|
|
149
|
+
return Color.alphaBlend(
|
|
150
|
+
base.withValues(alpha: isDark ? 0.06 : 0.08),
|
|
151
|
+
widget.hoverColor!,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (_hovered && widget.hoverEnabled) return widget.hoverColor!;
|
|
134
155
|
return widget.hoverColor!.withValues(alpha: 0);
|
|
135
156
|
}
|
|
157
|
+
|
|
158
|
+
// Plain rows: a subtle translucent overlay that fades in on hover/press.
|
|
159
|
+
if (_pressed && widget.pressEnabled) {
|
|
160
|
+
return base.withValues(alpha: isDark ? 0.04 : 0.10);
|
|
161
|
+
}
|
|
162
|
+
if (_hovered && widget.hoverEnabled) {
|
|
163
|
+
return base.withValues(alpha: isDark ? 0.02 : 0.06);
|
|
164
|
+
}
|
|
136
165
|
return Colors.transparent;
|
|
137
166
|
}
|
|
138
167
|
|
|
@@ -174,13 +203,23 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
|
|
|
174
203
|
child: widget.child,
|
|
175
204
|
);
|
|
176
205
|
|
|
206
|
+
// Keyboard focus is owned by the kit's single focus indicator so Tab +
|
|
207
|
+
// Enter/Space behave identically to login, signup and the chat send button.
|
|
208
|
+
final Widget focusContent = widget.focusable
|
|
209
|
+
? KasyFocusRing(
|
|
210
|
+
borderRadius: widget.borderRadius,
|
|
211
|
+
onActivate: widget.onTap,
|
|
212
|
+
child: content,
|
|
213
|
+
)
|
|
214
|
+
: content;
|
|
215
|
+
|
|
177
216
|
Widget interactive = GestureDetector(
|
|
178
217
|
behavior: HitTestBehavior.opaque,
|
|
179
218
|
onTap: _handleTap,
|
|
180
219
|
onTapDown: _onTapDown,
|
|
181
220
|
onTapUp: _onTapUp,
|
|
182
221
|
onTapCancel: _onTapCancel,
|
|
183
|
-
child:
|
|
222
|
+
child: focusContent,
|
|
184
223
|
);
|
|
185
224
|
|
|
186
225
|
// MouseRegion is active on all platforms:
|
|
@@ -10,20 +10,20 @@ import 'package:kasy_kit/core/haptics/haptic_feedback_notifier.dart';
|
|
|
10
10
|
/// Press-in with slight overshoot-back (same tactility as [KasyAlert] actions).
|
|
11
11
|
///
|
|
12
12
|
/// No Material ripple. Optional [pressOverlayColor] + [clipBorderRadius]:
|
|
13
|
-
///
|
|
14
|
-
///
|
|
13
|
+
/// a quick flash on tap (veil), not while the finger merely rests on it.
|
|
14
|
+
/// Also exposes semantics for TalkBack/VoiceOver and an optional light haptic ([hapticFeedbackEnabled]).
|
|
15
15
|
class KasyPressableDepth extends ConsumerStatefulWidget {
|
|
16
16
|
final Widget child;
|
|
17
17
|
final VoidCallback onPressed;
|
|
18
18
|
final String semanticLabel;
|
|
19
19
|
|
|
20
|
-
///
|
|
20
|
+
/// Brief flash on tap (translucent veil); not while merely pressing.
|
|
21
21
|
final Color? pressOverlayColor;
|
|
22
22
|
|
|
23
23
|
/// Required with [pressOverlayColor] when the surface has rounded clips (pill, etc.).
|
|
24
24
|
final BorderRadius? clipBorderRadius;
|
|
25
25
|
|
|
26
|
-
///
|
|
26
|
+
/// If false, does not call [HapticFeedback.lightImpact] on tap.
|
|
27
27
|
final bool hapticFeedbackEnabled;
|
|
28
28
|
|
|
29
29
|
const KasyPressableDepth({
|
|
@@ -122,15 +122,20 @@ class _KasyPressableDepthState extends ConsumerState<KasyPressableDepth>
|
|
|
122
122
|
|
|
123
123
|
return Transform.scale(
|
|
124
124
|
scale: _depthScale,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
// Clip only the press veil to the rounded shape — never [rawChild]. The
|
|
126
|
+
// child may host a keyboard focus ring that paints just outside its box;
|
|
127
|
+
// clipping the whole stack would shave that ring off.
|
|
128
|
+
child: Stack(
|
|
129
|
+
fit: StackFit.passthrough,
|
|
130
|
+
children: <Widget>[
|
|
131
|
+
rawChild,
|
|
132
|
+
Positioned.fill(
|
|
133
|
+
child: ClipRRect(
|
|
134
|
+
borderRadius: widget.clipBorderRadius!,
|
|
135
|
+
child: pressVeil,
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
],
|
|
134
139
|
),
|
|
135
140
|
);
|
|
136
141
|
}
|
|
@@ -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 = '';
|
|
@@ -37,10 +37,6 @@ sealed class Environment with _$Environment {
|
|
|
37
37
|
/// (only if you want to use in-app purchases with RevenueCat)
|
|
38
38
|
String? revenueCatIOSApiKey,
|
|
39
39
|
|
|
40
|
-
/// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
|
|
41
|
-
/// (only if you want to use subscriptions on web)
|
|
42
|
-
String? revenueCatWebApiKey,
|
|
43
|
-
|
|
44
40
|
/// this is used to open the app store page of your app for reviews
|
|
45
41
|
String? appStoreId,
|
|
46
42
|
|
|
@@ -73,10 +69,6 @@ sealed class Environment with _$Environment {
|
|
|
73
69
|
/// (only if you want to use in-app purchases with RevenueCat)
|
|
74
70
|
String? revenueCatIOSApiKey,
|
|
75
71
|
|
|
76
|
-
/// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
|
|
77
|
-
/// (only if you want to use subscriptions on web)
|
|
78
|
-
String? revenueCatWebApiKey,
|
|
79
|
-
|
|
80
72
|
/// only if you want to use ads
|
|
81
73
|
String? androidInterstitialAdUnitId,
|
|
82
74
|
|
|
@@ -113,7 +105,6 @@ sealed class Environment with _$Environment {
|
|
|
113
105
|
appStoreId: '',
|
|
114
106
|
revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
|
|
115
107
|
revenueCatIOSApiKey: AppEnv.rcIosApiKey,
|
|
116
|
-
revenueCatWebApiKey: AppEnv.rcWebApiKey,
|
|
117
108
|
mixpanelToken: AppEnv.mixpanelToken,
|
|
118
109
|
// ─── Authentication mode ──────────────────────────────────────────
|
|
119
110
|
// AuthenticationMode.anonymous (default) → a hidden anonymous Firebase Auth
|
|
@@ -134,7 +125,6 @@ sealed class Environment with _$Environment {
|
|
|
134
125
|
appStoreId: AppEnv.appStoreId,
|
|
135
126
|
revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
|
|
136
127
|
revenueCatIOSApiKey: AppEnv.rcIosApiKey,
|
|
137
|
-
revenueCatWebApiKey: AppEnv.rcWebApiKey,
|
|
138
128
|
sentryDsn: AppEnv.sentryDsn,
|
|
139
129
|
mixpanelToken: AppEnv.mixpanelToken,
|
|
140
130
|
// See dev comment above for details on each mode.
|
|
@@ -150,9 +140,8 @@ sealed class Environment with _$Environment {
|
|
|
150
140
|
revenueCatIOSApiKey != null && revenueCatIOSApiKey!.isNotEmpty,
|
|
151
141
|
TargetPlatform.android =>
|
|
152
142
|
revenueCatAndroidApiKey != null && revenueCatAndroidApiKey!.isNotEmpty,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
(revenueCatWebApiKey != null && revenueCatWebApiKey!.isNotEmpty),
|
|
143
|
+
// RevenueCat is mobile-only; web/desktop are never RevenueCat-configured.
|
|
144
|
+
_ => false,
|
|
156
145
|
};
|
|
157
146
|
}
|
|
158
147
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/components/components.dart';
|
|
4
|
+
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
5
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
6
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
7
|
+
import 'package:kasy_kit/features/ai_chat/providers/ai_conversations_notifier.dart';
|
|
8
|
+
import 'package:kasy_kit/features/ai_chat/ui/widgets/ai_chat_avatars.dart';
|
|
9
|
+
import 'package:kasy_kit/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart';
|
|
10
|
+
import 'package:kasy_kit/features/ai_chat/ui/widgets/ai_conversation_list.dart';
|
|
11
|
+
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
|
+
|
|
13
|
+
/// Support / AI chat screen with conversation history.
|
|
14
|
+
///
|
|
15
|
+
/// Responsive:
|
|
16
|
+
/// - phones (small): single pane. The conversation list is shown until a
|
|
17
|
+
/// conversation is opened; opening one swaps to the thread (back returns).
|
|
18
|
+
/// - tablet/desktop (medium+): two panes — the list beside a full-height
|
|
19
|
+
/// container that hosts the active thread (or a placeholder when none).
|
|
20
|
+
class AiChatPage extends StatelessWidget {
|
|
21
|
+
/// When true the page renders as a root bottom-tab (theme toggle, no back).
|
|
22
|
+
final bool isRootTab;
|
|
23
|
+
|
|
24
|
+
const AiChatPage({super.key, this.isRootTab = false});
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
Widget build(BuildContext context) {
|
|
28
|
+
return ResponsiveLayout(
|
|
29
|
+
small: _MobileLayout(isRootTab: isRootTab),
|
|
30
|
+
medium: const _WideLayout(),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class _MobileLayout extends StatelessWidget {
|
|
36
|
+
const _MobileLayout({required this.isRootTab});
|
|
37
|
+
|
|
38
|
+
final bool isRootTab;
|
|
39
|
+
|
|
40
|
+
@override
|
|
41
|
+
Widget build(BuildContext context) {
|
|
42
|
+
// Phones show only the conversation list; opening a conversation pushes a
|
|
43
|
+
// full-screen route (so the bottom nav bar is hidden for the composer).
|
|
44
|
+
return Scaffold(
|
|
45
|
+
backgroundColor: context.colors.background,
|
|
46
|
+
body: Column(
|
|
47
|
+
children: [
|
|
48
|
+
KasyAppBar(
|
|
49
|
+
title: t.ai_chat.title,
|
|
50
|
+
style: isRootTab
|
|
51
|
+
? KasyAppBarStyle.rootTab
|
|
52
|
+
: KasyAppBarStyle.subpage,
|
|
53
|
+
onThemeToggle: isRootTab
|
|
54
|
+
? () {
|
|
55
|
+
KasyHaptics.light(context);
|
|
56
|
+
ThemeProvider.of(context).toggle();
|
|
57
|
+
}
|
|
58
|
+
: null,
|
|
59
|
+
),
|
|
60
|
+
const Expanded(
|
|
61
|
+
child: AiConversationList(swipeToDelete: true, isPhone: true),
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class _WideLayout extends ConsumerWidget {
|
|
70
|
+
const _WideLayout();
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
74
|
+
final selectedId = ref.watch(selectedConversationIdProvider);
|
|
75
|
+
|
|
76
|
+
return Scaffold(
|
|
77
|
+
backgroundColor: context.colors.background,
|
|
78
|
+
body: Row(
|
|
79
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
80
|
+
children: [
|
|
81
|
+
DecoratedBox(
|
|
82
|
+
// Vertical hairline matching the sidebar/web-header line.
|
|
83
|
+
decoration: BoxDecoration(
|
|
84
|
+
border: Border(
|
|
85
|
+
right: BorderSide(color: context.colors.border, width: 0.5),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
child: const SizedBox(
|
|
89
|
+
width: 340,
|
|
90
|
+
child: AiConversationList(),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
Expanded(
|
|
94
|
+
child: Padding(
|
|
95
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
96
|
+
child: _DetailContainer(
|
|
97
|
+
child: selectedId == null
|
|
98
|
+
? const _DetailPlaceholder()
|
|
99
|
+
: const AiChatConversationView(),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
],
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Rounded, full-height surface that hosts the active thread (it scrolls
|
|
110
|
+
/// internally). Mirrors the Figma reading-pane container.
|
|
111
|
+
class _DetailContainer extends StatelessWidget {
|
|
112
|
+
const _DetailContainer({required this.child});
|
|
113
|
+
|
|
114
|
+
final Widget child;
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
Widget build(BuildContext context) {
|
|
118
|
+
return Container(
|
|
119
|
+
clipBehavior: Clip.antiAlias,
|
|
120
|
+
decoration: BoxDecoration(
|
|
121
|
+
color: context.colors.surface,
|
|
122
|
+
borderRadius: BorderRadius.circular(24),
|
|
123
|
+
border: Border.all(color: context.colors.border, width: 0.5),
|
|
124
|
+
),
|
|
125
|
+
child: child,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class _DetailPlaceholder extends StatelessWidget {
|
|
131
|
+
const _DetailPlaceholder();
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
Widget build(BuildContext context) {
|
|
135
|
+
return Center(
|
|
136
|
+
child: Padding(
|
|
137
|
+
padding: const EdgeInsets.all(KasySpacing.lg),
|
|
138
|
+
child: Column(
|
|
139
|
+
mainAxisSize: MainAxisSize.min,
|
|
140
|
+
children: [
|
|
141
|
+
const AiChatAssistantAvatar(
|
|
142
|
+
diameter: kAiChatEmptyAvatarDiameter,
|
|
143
|
+
showShadow: true,
|
|
144
|
+
),
|
|
145
|
+
const SizedBox(height: KasySpacing.md),
|
|
146
|
+
Text(
|
|
147
|
+
t.ai_chat.no_conversation_selected,
|
|
148
|
+
textAlign: TextAlign.center,
|
|
149
|
+
style: context.textTheme.bodyLarge?.copyWith(
|
|
150
|
+
color: context.colors.muted,
|
|
151
|
+
height: 1.4,
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
],
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.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:logger/logger.dart';
|
|
6
|
+
|
|
7
|
+
final aiChatApiProvider = Provider<AiChatApi>(
|
|
8
|
+
(ref) => AiChatApi(client: FirebaseFirestore.instance),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
/// Firestore layout (one user has many conversations, each has many messages):
|
|
12
|
+
/// users/{userId}/ai_conversations/{conversationId}
|
|
13
|
+
/// users/{userId}/ai_conversations/{conversationId}/messages/{messageId}
|
|
14
|
+
class AiChatApi {
|
|
15
|
+
final FirebaseFirestore _client;
|
|
16
|
+
final Logger _logger = Logger();
|
|
17
|
+
|
|
18
|
+
AiChatApi({required FirebaseFirestore client}) : _client = client;
|
|
19
|
+
|
|
20
|
+
CollectionReference<Map<String, dynamic>> _conversationsRef(String userId) =>
|
|
21
|
+
_client.collection('users').doc(userId).collection('ai_conversations');
|
|
22
|
+
|
|
23
|
+
CollectionReference<Map<String, dynamic>> _messagesRef(
|
|
24
|
+
String userId,
|
|
25
|
+
String conversationId,
|
|
26
|
+
) => _conversationsRef(userId).doc(conversationId).collection('messages');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Conversations
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/// Returns the user's conversations, most recently updated first.
|
|
33
|
+
Future<List<AiChatConversationEntity>> loadConversations(
|
|
34
|
+
String userId,
|
|
35
|
+
) async {
|
|
36
|
+
final snapshot = await _conversationsRef(
|
|
37
|
+
userId,
|
|
38
|
+
).orderBy('updatedAt', descending: true).get();
|
|
39
|
+
return snapshot.docs
|
|
40
|
+
.map((doc) => AiChatConversationEntity.fromFirestore(doc.id, doc.data()))
|
|
41
|
+
.toList();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Creates an empty conversation and returns it (with its generated id).
|
|
45
|
+
Future<AiChatConversationEntity> createConversation(String userId) async {
|
|
46
|
+
final now = DateTime.now();
|
|
47
|
+
final seed = AiChatConversationEntity(
|
|
48
|
+
id: '',
|
|
49
|
+
createdAt: now,
|
|
50
|
+
updatedAt: now,
|
|
51
|
+
);
|
|
52
|
+
final ref = await _conversationsRef(userId).add(seed.toFirestore());
|
|
53
|
+
return AiChatConversationEntity(
|
|
54
|
+
id: ref.id,
|
|
55
|
+
createdAt: now,
|
|
56
|
+
updatedAt: now,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Deletes a conversation and all of its messages.
|
|
61
|
+
Future<void> deleteConversation(String userId, String conversationId) async {
|
|
62
|
+
final messages = await _messagesRef(userId, conversationId).get();
|
|
63
|
+
final batch = _client.batch();
|
|
64
|
+
for (final doc in messages.docs) {
|
|
65
|
+
batch.delete(doc.reference);
|
|
66
|
+
}
|
|
67
|
+
batch.delete(_conversationsRef(userId).doc(conversationId));
|
|
68
|
+
await batch.commit();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Messages
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/// Returns all messages in a conversation, oldest first.
|
|
76
|
+
Future<List<AiChatMessageEntity>> loadMessages(
|
|
77
|
+
String userId,
|
|
78
|
+
String conversationId,
|
|
79
|
+
) async {
|
|
80
|
+
final snapshot = await _messagesRef(
|
|
81
|
+
userId,
|
|
82
|
+
conversationId,
|
|
83
|
+
).orderBy('createdAt', descending: false).get();
|
|
84
|
+
return snapshot.docs
|
|
85
|
+
.map((doc) => AiChatMessageEntity.fromFirestore(doc.id, doc.data()))
|
|
86
|
+
.toList();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Persists a message and denormalizes it onto the parent conversation
|
|
90
|
+
/// (last message preview + updatedAt) so the list stays cheap to render.
|
|
91
|
+
Future<void> saveMessage(
|
|
92
|
+
String userId,
|
|
93
|
+
String conversationId,
|
|
94
|
+
AiChatMessageEntity message,
|
|
95
|
+
) async {
|
|
96
|
+
try {
|
|
97
|
+
await _messagesRef(userId, conversationId).add(message.toFirestore());
|
|
98
|
+
await _conversationsRef(userId).doc(conversationId).update({
|
|
99
|
+
'updatedAt': Timestamp.fromDate(message.createdAt),
|
|
100
|
+
'lastMessageRole': message.role,
|
|
101
|
+
'lastMessageContent': message.content,
|
|
102
|
+
});
|
|
103
|
+
} catch (e) {
|
|
104
|
+
_logger.e('Failed to persist AI message: $e');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
2
|
+
|
|
3
|
+
/// Firestore document mapping for a single AI chat conversation.
|
|
4
|
+
/// Stored at: users/{userId}/ai_conversations/{conversationId}
|
|
5
|
+
///
|
|
6
|
+
/// The last message is denormalized onto the conversation so the list can be
|
|
7
|
+
/// rendered with a single read per conversation (no need to open each
|
|
8
|
+
/// messages subcollection just to show a preview + timestamp).
|
|
9
|
+
class AiChatConversationEntity {
|
|
10
|
+
final String id;
|
|
11
|
+
final DateTime createdAt;
|
|
12
|
+
final DateTime updatedAt;
|
|
13
|
+
|
|
14
|
+
/// Role of the most recent message ('user' or 'assistant'), or null when the
|
|
15
|
+
/// conversation has no messages yet.
|
|
16
|
+
final String? lastMessageRole;
|
|
17
|
+
|
|
18
|
+
/// Content of the most recent message, or null when empty.
|
|
19
|
+
final String? lastMessageContent;
|
|
20
|
+
|
|
21
|
+
const AiChatConversationEntity({
|
|
22
|
+
required this.id,
|
|
23
|
+
required this.createdAt,
|
|
24
|
+
required this.updatedAt,
|
|
25
|
+
this.lastMessageRole,
|
|
26
|
+
this.lastMessageContent,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
bool get isEmpty => lastMessageContent == null;
|
|
30
|
+
|
|
31
|
+
AiChatConversationEntity copyWith({
|
|
32
|
+
DateTime? updatedAt,
|
|
33
|
+
String? lastMessageRole,
|
|
34
|
+
String? lastMessageContent,
|
|
35
|
+
}) {
|
|
36
|
+
return AiChatConversationEntity(
|
|
37
|
+
id: id,
|
|
38
|
+
createdAt: createdAt,
|
|
39
|
+
updatedAt: updatedAt ?? this.updatedAt,
|
|
40
|
+
lastMessageRole: lastMessageRole ?? this.lastMessageRole,
|
|
41
|
+
lastMessageContent: lastMessageContent ?? this.lastMessageContent,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
factory AiChatConversationEntity.fromFirestore(
|
|
46
|
+
String id,
|
|
47
|
+
Map<String, dynamic> data,
|
|
48
|
+
) {
|
|
49
|
+
return AiChatConversationEntity(
|
|
50
|
+
id: id,
|
|
51
|
+
createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
|
52
|
+
updatedAt: (data['updatedAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
|
53
|
+
lastMessageRole: data['lastMessageRole'] as String?,
|
|
54
|
+
lastMessageContent: data['lastMessageContent'] as String?,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Map<String, dynamic> toFirestore() => {
|
|
59
|
+
'createdAt': Timestamp.fromDate(createdAt),
|
|
60
|
+
'updatedAt': Timestamp.fromDate(updatedAt),
|
|
61
|
+
'lastMessageRole': lastMessageRole,
|
|
62
|
+
'lastMessageContent': lastMessageContent,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
2
2
|
|
|
3
|
-
/// Firestore document mapping for a single
|
|
4
|
-
/// Stored at: users/{userId}/
|
|
5
|
-
class
|
|
3
|
+
/// Firestore document mapping for a single AI chat message.
|
|
4
|
+
/// Stored at: users/{userId}/ai_messages/{messageId}
|
|
5
|
+
class AiChatMessageEntity {
|
|
6
6
|
final String? id;
|
|
7
7
|
final String role; // 'user' or 'assistant'
|
|
8
8
|
final String content;
|
|
9
9
|
final DateTime createdAt;
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const AiChatMessageEntity({
|
|
12
12
|
this.id,
|
|
13
13
|
required this.role,
|
|
14
14
|
required this.content,
|
|
15
15
|
required this.createdAt,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
factory
|
|
18
|
+
factory AiChatMessageEntity.fromFirestore(
|
|
19
19
|
String id,
|
|
20
20
|
Map<String, dynamic> data,
|
|
21
21
|
) {
|
|
22
|
-
return
|
|
22
|
+
return AiChatMessageEntity(
|
|
23
23
|
id: id,
|
|
24
24
|
role: data['role'] as String,
|
|
25
25
|
content: data['content'] as String,
|