kasy-cli 1.21.9 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +80 -37
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +2 -2
- package/lib/utils/i18n/messages-en.js +50 -35
- package/lib/utils/i18n/messages-es.js +50 -35
- package/lib/utils/i18n/messages-pt.js +52 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +692 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +57 -4
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +41 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +33 -13
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +118 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +4 -5
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
|
@@ -4,7 +4,7 @@ import 'dart:typed_data';
|
|
|
4
4
|
import 'package:kasy_kit/core/data/api/base_api_exceptions.dart';
|
|
5
5
|
import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
6
6
|
import 'package:kasy_kit/core/data/entities/upload_result.dart';
|
|
7
|
-
import 'package:kasy_kit/
|
|
7
|
+
import 'package:kasy_kit/environments.dart';
|
|
8
8
|
import 'package:dio/dio.dart';
|
|
9
9
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
10
10
|
import 'package:logger/logger.dart';
|
|
@@ -16,6 +16,11 @@ sealed class UserEntity with _$UserEntity {
|
|
|
16
16
|
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: your backend must NEVER let a client
|
|
21
|
+
// write this — return it from GET /users/{id} (and in the JWT) but reject any
|
|
22
|
+
// client attempt to set it.
|
|
23
|
+
String? role,
|
|
19
24
|
}) = UserEntityData;
|
|
20
25
|
|
|
21
26
|
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
|
|
|
12
12
|
|
|
@@ -40,9 +40,6 @@ sealed class Environment with _$Environment {
|
|
|
40
40
|
/// (only if you want to use in-app purchases with RevenueCat)
|
|
41
41
|
String? revenueCatIOSApiKey,
|
|
42
42
|
|
|
43
|
-
/// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
|
|
44
|
-
/// (only if you want to use subscriptions on web)
|
|
45
|
-
String? revenueCatWebApiKey,
|
|
46
43
|
|
|
47
44
|
/// this is used to open the app store page of your app for reviews
|
|
48
45
|
String? appStoreId,
|
|
@@ -76,9 +73,6 @@ sealed class Environment with _$Environment {
|
|
|
76
73
|
/// (only if you want to use in-app purchases with RevenueCat)
|
|
77
74
|
String? revenueCatIOSApiKey,
|
|
78
75
|
|
|
79
|
-
/// RevenueCat Web Billing API key (rcb_xxx or rcb_sb_xxx)
|
|
80
|
-
/// (only if you want to use subscriptions on web)
|
|
81
|
-
String? revenueCatWebApiKey,
|
|
82
76
|
|
|
83
77
|
/// only if you want to use ads
|
|
84
78
|
String? androidInterstitialAdUnitId,
|
|
@@ -116,7 +110,6 @@ sealed class Environment with _$Environment {
|
|
|
116
110
|
appStoreId: '',
|
|
117
111
|
revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
|
|
118
112
|
revenueCatIOSApiKey: AppEnv.rcIosApiKey,
|
|
119
|
-
revenueCatWebApiKey: AppEnv.rcWebApiKey,
|
|
120
113
|
mixpanelToken: AppEnv.mixpanelToken,
|
|
121
114
|
authenticationMode: AuthenticationMode.authRequired,
|
|
122
115
|
);
|
|
@@ -127,7 +120,6 @@ sealed class Environment with _$Environment {
|
|
|
127
120
|
appStoreId: AppEnv.appStoreId,
|
|
128
121
|
revenueCatAndroidApiKey: AppEnv.rcAndroidApiKey,
|
|
129
122
|
revenueCatIOSApiKey: AppEnv.rcIosApiKey,
|
|
130
|
-
revenueCatWebApiKey: AppEnv.rcWebApiKey,
|
|
131
123
|
sentryDsn: AppEnv.sentryDsn,
|
|
132
124
|
mixpanelToken: AppEnv.mixpanelToken,
|
|
133
125
|
authenticationMode: AuthenticationMode.authRequired,
|
|
@@ -142,8 +134,8 @@ sealed class Environment with _$Environment {
|
|
|
142
134
|
revenueCatIOSApiKey != null && revenueCatIOSApiKey!.isNotEmpty,
|
|
143
135
|
TargetPlatform.android =>
|
|
144
136
|
revenueCatAndroidApiKey != null && revenueCatAndroidApiKey!.isNotEmpty,
|
|
145
|
-
|
|
146
|
-
|
|
137
|
+
// RevenueCat is mobile-only; web/desktop are never RevenueCat-configured.
|
|
138
|
+
_ => false,
|
|
147
139
|
};
|
|
148
140
|
}
|
|
149
141
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import 'package:dio/dio.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:logger/logger.dart';
|
|
4
|
+
import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
5
|
+
import 'package:kasy_kit/features/ai_chat/api/ai_chat_conversation_entity.dart';
|
|
6
|
+
import 'package:kasy_kit/features/ai_chat/api/ai_chat_message_entity.dart';
|
|
7
|
+
|
|
8
|
+
final aiChatApiProvider = Provider<AiChatApi>(
|
|
9
|
+
(ref) => AiChatApi(client: ref.read(httpClientProvider)),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/// REST endpoints expected on your backend (the server identifies the user via
|
|
13
|
+
/// the Authorization: Bearer token injected automatically by HttpClient):
|
|
14
|
+
///
|
|
15
|
+
/// GET /ai-conversations → `List<AiChatConversationEntity>`
|
|
16
|
+
/// POST /ai-conversations → creates + returns one conversation
|
|
17
|
+
/// DELETE /ai-conversations/{id} → deletes a conversation + its messages
|
|
18
|
+
/// GET /ai-conversations/{id}/messages → `List<AiChatMessageEntity>` (oldest first)
|
|
19
|
+
/// POST /ai-conversations/{id}/messages → saves one message
|
|
20
|
+
///
|
|
21
|
+
/// Conversations are returned most-recently-updated first, with the last
|
|
22
|
+
/// message denormalized (`last_message_role` / `last_message_content`).
|
|
23
|
+
class AiChatApi {
|
|
24
|
+
final HttpClient _client;
|
|
25
|
+
final Logger _logger = Logger();
|
|
26
|
+
|
|
27
|
+
AiChatApi({required HttpClient client}) : _client = client;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Conversations
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/// Returns the user's conversations, most recently updated first.
|
|
34
|
+
Future<List<AiChatConversationEntity>> loadConversations(
|
|
35
|
+
String userId,
|
|
36
|
+
) async {
|
|
37
|
+
try {
|
|
38
|
+
final response = await _client.get('/ai-conversations');
|
|
39
|
+
final list = response.data as List<dynamic>;
|
|
40
|
+
return list
|
|
41
|
+
.map(
|
|
42
|
+
(e) =>
|
|
43
|
+
AiChatConversationEntity.fromJson(e as Map<String, dynamic>),
|
|
44
|
+
)
|
|
45
|
+
.toList();
|
|
46
|
+
} on DioException catch (e) {
|
|
47
|
+
_logger.e('Failed to load conversations: $e');
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Creates an empty conversation and returns it (with its generated id).
|
|
53
|
+
Future<AiChatConversationEntity> createConversation(String userId) async {
|
|
54
|
+
final response = await _client.post('/ai-conversations');
|
|
55
|
+
return AiChatConversationEntity.fromJson(
|
|
56
|
+
response.data as Map<String, dynamic>,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Deletes a conversation (and its messages) on the server.
|
|
61
|
+
Future<void> deleteConversation(String userId, String conversationId) async {
|
|
62
|
+
try {
|
|
63
|
+
await _client.delete('/ai-conversations/$conversationId');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
_logger.e('Failed to delete conversation: $e');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Messages
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/// Returns all messages in a conversation, oldest first.
|
|
74
|
+
Future<List<AiChatMessageEntity>> loadMessages(
|
|
75
|
+
String userId,
|
|
76
|
+
String conversationId,
|
|
77
|
+
) async {
|
|
78
|
+
try {
|
|
79
|
+
final response = await _client.get(
|
|
80
|
+
'/ai-conversations/$conversationId/messages',
|
|
81
|
+
);
|
|
82
|
+
final list = response.data as List<dynamic>;
|
|
83
|
+
return list
|
|
84
|
+
.map((e) => AiChatMessageEntity.fromJson(e as Map<String, dynamic>))
|
|
85
|
+
.toList();
|
|
86
|
+
} on DioException catch (e) {
|
|
87
|
+
_logger.e('Failed to load AI messages: $e');
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Persists a message in a conversation via POST. The server is expected to
|
|
93
|
+
/// update the conversation's denormalized last-message fields + timestamp.
|
|
94
|
+
Future<void> saveMessage(
|
|
95
|
+
String userId,
|
|
96
|
+
String conversationId,
|
|
97
|
+
AiChatMessageEntity message,
|
|
98
|
+
) async {
|
|
99
|
+
try {
|
|
100
|
+
await _client.post(
|
|
101
|
+
'/ai-conversations/$conversationId/messages',
|
|
102
|
+
data: message.toJson(),
|
|
103
|
+
);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
_logger.e('Failed to persist AI message: $e');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/// REST mapping for a single AI chat conversation.
|
|
2
|
+
///
|
|
3
|
+
/// The last message is denormalized onto the conversation so the list can be
|
|
4
|
+
/// rendered from one request (no need to fetch each conversation's messages
|
|
5
|
+
/// just to show a preview + timestamp).
|
|
6
|
+
class AiChatConversationEntity {
|
|
7
|
+
final String id;
|
|
8
|
+
final DateTime createdAt;
|
|
9
|
+
final DateTime updatedAt;
|
|
10
|
+
|
|
11
|
+
/// Role of the most recent message ('user' or 'assistant'), or null when the
|
|
12
|
+
/// conversation has no messages yet.
|
|
13
|
+
final String? lastMessageRole;
|
|
14
|
+
|
|
15
|
+
/// Content of the most recent message, or null when empty.
|
|
16
|
+
final String? lastMessageContent;
|
|
17
|
+
|
|
18
|
+
const AiChatConversationEntity({
|
|
19
|
+
required this.id,
|
|
20
|
+
required this.createdAt,
|
|
21
|
+
required this.updatedAt,
|
|
22
|
+
this.lastMessageRole,
|
|
23
|
+
this.lastMessageContent,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
bool get isEmpty => lastMessageContent == null;
|
|
27
|
+
|
|
28
|
+
AiChatConversationEntity copyWith({
|
|
29
|
+
DateTime? updatedAt,
|
|
30
|
+
String? lastMessageRole,
|
|
31
|
+
String? lastMessageContent,
|
|
32
|
+
}) {
|
|
33
|
+
return AiChatConversationEntity(
|
|
34
|
+
id: id,
|
|
35
|
+
createdAt: createdAt,
|
|
36
|
+
updatedAt: updatedAt ?? this.updatedAt,
|
|
37
|
+
lastMessageRole: lastMessageRole ?? this.lastMessageRole,
|
|
38
|
+
lastMessageContent: lastMessageContent ?? this.lastMessageContent,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
factory AiChatConversationEntity.fromJson(Map<String, dynamic> json) {
|
|
43
|
+
return AiChatConversationEntity(
|
|
44
|
+
id: json['id'] as String,
|
|
45
|
+
createdAt: DateTime.parse(json['created_at'] as String),
|
|
46
|
+
updatedAt: DateTime.parse(json['updated_at'] as String),
|
|
47
|
+
lastMessageRole: json['last_message_role'] as String?,
|
|
48
|
+
lastMessageContent: json['last_message_content'] as String?,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
/// REST API response mapping for a single
|
|
2
|
-
class
|
|
1
|
+
/// REST API response mapping for a single AI chat message.
|
|
2
|
+
class AiChatMessageEntity {
|
|
3
3
|
final String? id;
|
|
4
4
|
final String role; // 'user' or 'assistant'
|
|
5
5
|
final String content;
|
|
6
6
|
final DateTime createdAt;
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const AiChatMessageEntity({
|
|
9
9
|
this.id,
|
|
10
10
|
required this.role,
|
|
11
11
|
required this.content,
|
|
12
12
|
required this.createdAt,
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
factory
|
|
16
|
-
return
|
|
15
|
+
factory AiChatMessageEntity.fromJson(Map<String, dynamic> json) {
|
|
16
|
+
return AiChatMessageEntity(
|
|
17
17
|
id: json['id'] as String?,
|
|
18
18
|
role: json['role'] as String,
|
|
19
19
|
content: json['content'] as String,
|
|
@@ -6,8 +6,9 @@ import 'package:kasy_kit/core/config/app_env.dart';
|
|
|
6
6
|
import 'package:kasy_kit/core/data/api/http_client.dart';
|
|
7
7
|
import 'package:kasy_kit/core/states/translations.dart';
|
|
8
8
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
9
|
-
import 'package:kasy_kit/features/
|
|
10
|
-
import 'package:kasy_kit/features/
|
|
9
|
+
import 'package:kasy_kit/features/ai_chat/api/ai_chat_api.dart';
|
|
10
|
+
import 'package:kasy_kit/features/ai_chat/api/ai_chat_message_entity.dart';
|
|
11
|
+
import 'package:kasy_kit/features/ai_chat/providers/ai_conversations_notifier.dart';
|
|
11
12
|
import 'package:logger/logger.dart';
|
|
12
13
|
|
|
13
14
|
/// Maximum number of recent messages sent to the AI as context.
|
|
@@ -34,39 +35,39 @@ class ChatMessage {
|
|
|
34
35
|
factory ChatMessage.assistant(String content) =>
|
|
35
36
|
ChatMessage(role: 'assistant', content: content);
|
|
36
37
|
|
|
37
|
-
factory ChatMessage.fromEntity(
|
|
38
|
+
factory ChatMessage.fromEntity(AiChatMessageEntity entity) =>
|
|
38
39
|
ChatMessage(role: entity.role, content: entity.content);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
AiChatMessageEntity toEntity() => AiChatMessageEntity(
|
|
41
42
|
role: role,
|
|
42
43
|
content: content,
|
|
43
44
|
createdAt: DateTime.now(),
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/// UI state for the
|
|
48
|
-
class
|
|
48
|
+
/// UI state for the AI chat screen.
|
|
49
|
+
class AiChatState {
|
|
49
50
|
final List<ChatMessage> messages;
|
|
50
51
|
|
|
51
|
-
/// True while the HTTP request to the
|
|
52
|
+
/// True while the HTTP request to the AI backend is in-flight.
|
|
52
53
|
final bool isReplying;
|
|
53
54
|
|
|
54
55
|
/// True once the first SSE chunk has been received.
|
|
55
56
|
/// While true the last message in [messages] is the partial assistant reply.
|
|
56
57
|
final bool streamingStarted;
|
|
57
58
|
|
|
58
|
-
const
|
|
59
|
+
const AiChatState({
|
|
59
60
|
required this.messages,
|
|
60
61
|
this.isReplying = false,
|
|
61
62
|
this.streamingStarted = false,
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
AiChatState copyWith({
|
|
65
66
|
List<ChatMessage>? messages,
|
|
66
67
|
bool? isReplying,
|
|
67
68
|
bool? streamingStarted,
|
|
68
69
|
}) {
|
|
69
|
-
return
|
|
70
|
+
return AiChatState(
|
|
70
71
|
messages: messages ?? this.messages,
|
|
71
72
|
isReplying: isReplying ?? this.isReplying,
|
|
72
73
|
streamingStarted: streamingStarted ?? this.streamingStarted,
|
|
@@ -74,39 +75,54 @@ class LlmChatState {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
/// Manages
|
|
78
|
+
/// Manages AI chat state: loads history from the backend on init,
|
|
78
79
|
/// persists each message, and streams the assistant reply word-by-word via SSE.
|
|
79
|
-
final
|
|
80
|
-
AsyncNotifierProvider<
|
|
80
|
+
final aiChatNotifierProvider =
|
|
81
|
+
AsyncNotifierProvider<AiChatNotifier, AiChatState>(AiChatNotifier.new);
|
|
81
82
|
|
|
82
|
-
class
|
|
83
|
+
class AiChatNotifier extends AsyncNotifier<AiChatState> {
|
|
83
84
|
final Logger _logger = Logger();
|
|
84
85
|
|
|
86
|
+
/// The conversation a reply is currently streaming into. Used to ignore UI
|
|
87
|
+
/// updates if the user switches conversations mid-stream (the reply is still
|
|
88
|
+
/// persisted to the right conversation, just not shown on the wrong one).
|
|
89
|
+
String? _streamingConversationId;
|
|
90
|
+
|
|
91
|
+
bool get _streamStillActive =>
|
|
92
|
+
ref.read(selectedConversationIdProvider) == _streamingConversationId;
|
|
93
|
+
|
|
85
94
|
@override
|
|
86
|
-
Future<
|
|
95
|
+
Future<AiChatState> build() async {
|
|
96
|
+
final conversationId = ref.watch(selectedConversationIdProvider);
|
|
87
97
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
88
|
-
if (
|
|
98
|
+
if (conversationId == null || userId == null) {
|
|
99
|
+
return const AiChatState(messages: []);
|
|
100
|
+
}
|
|
89
101
|
|
|
90
102
|
try {
|
|
91
|
-
final entities = await ref
|
|
92
|
-
|
|
103
|
+
final entities = await ref
|
|
104
|
+
.read(aiChatApiProvider)
|
|
105
|
+
.loadMessages(userId, conversationId);
|
|
106
|
+
return AiChatState(
|
|
93
107
|
messages: entities.map(ChatMessage.fromEntity).toList(),
|
|
94
108
|
);
|
|
95
109
|
} catch (e) {
|
|
96
|
-
_logger.e('Failed to load
|
|
110
|
+
_logger.e('Failed to load AI chat history: $e');
|
|
97
111
|
// Graceful fallback: start fresh if the backend is unavailable.
|
|
98
|
-
return const
|
|
112
|
+
return const AiChatState(messages: []);
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
/// Adds the user message to the conversation, persists it, then streams
|
|
103
117
|
/// the assistant reply chunk-by-chunk via SSE, updating state on each token.
|
|
104
118
|
Future<void> sendMessage(String prompt) async {
|
|
119
|
+
final conversationId = ref.read(selectedConversationIdProvider);
|
|
105
120
|
final current = switch (state) {
|
|
106
121
|
AsyncData(:final value) => value,
|
|
107
122
|
_ => null,
|
|
108
123
|
};
|
|
109
|
-
if (current == null || current.isReplying) return;
|
|
124
|
+
if (conversationId == null || current == null || current.isReplying) return;
|
|
125
|
+
_streamingConversationId = conversationId;
|
|
110
126
|
|
|
111
127
|
final userMsg = ChatMessage.user(prompt);
|
|
112
128
|
state = AsyncData(
|
|
@@ -117,26 +133,29 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
|
|
|
117
133
|
),
|
|
118
134
|
);
|
|
119
135
|
|
|
120
|
-
_persistMessage(userMsg);
|
|
136
|
+
_persistMessage(userMsg, conversationId);
|
|
121
137
|
|
|
122
138
|
// Pass only the last _kMaxContextMessages messages to the AI.
|
|
123
139
|
// The full history is still stored in the DB for the user to read.
|
|
124
|
-
final allMessages = (state as AsyncData<
|
|
140
|
+
final allMessages = (state as AsyncData<AiChatState>).value.messages;
|
|
125
141
|
final history = allMessages.length > _kMaxContextMessages
|
|
126
142
|
? allMessages.sublist(allMessages.length - _kMaxContextMessages)
|
|
127
143
|
: allMessages;
|
|
128
|
-
await _requestReplyStream(history);
|
|
144
|
+
await _requestReplyStream(history, conversationId);
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
// ---------------------------------------------------------------------------
|
|
132
148
|
// SSE streaming
|
|
133
149
|
// ---------------------------------------------------------------------------
|
|
134
150
|
|
|
135
|
-
Future<void> _requestReplyStream(
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
Future<void> _requestReplyStream(
|
|
152
|
+
List<ChatMessage> history,
|
|
153
|
+
String conversationId,
|
|
154
|
+
) async {
|
|
155
|
+
final t = ref.read(translationsProvider).ai_chat;
|
|
156
|
+
final String aiChatEndpoint = AppEnv.aiChatEndpoint;
|
|
138
157
|
|
|
139
|
-
if (
|
|
158
|
+
if (aiChatEndpoint.isEmpty) {
|
|
140
159
|
_finalizeAssistantMessage(t.error_not_configured);
|
|
141
160
|
return;
|
|
142
161
|
}
|
|
@@ -160,7 +179,7 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
|
|
|
160
179
|
|
|
161
180
|
try {
|
|
162
181
|
final response = await dio.post<ResponseBody>(
|
|
163
|
-
|
|
182
|
+
aiChatEndpoint,
|
|
164
183
|
data: {
|
|
165
184
|
'message': history.last.content,
|
|
166
185
|
'history': history
|
|
@@ -201,7 +220,7 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
|
|
|
201
220
|
// Persist the complete assistant reply once the stream ends.
|
|
202
221
|
final fullContent = contentBuffer.toString();
|
|
203
222
|
if (fullContent.isNotEmpty) {
|
|
204
|
-
_persistMessage(ChatMessage.assistant(fullContent));
|
|
223
|
+
_persistMessage(ChatMessage.assistant(fullContent), conversationId);
|
|
205
224
|
} else {
|
|
206
225
|
// Stream ended with no content (e.g. error event)
|
|
207
226
|
_finalizeAssistantMessage(t.error_no_reply);
|
|
@@ -213,19 +232,21 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
|
|
|
213
232
|
AsyncData(:final value) => value,
|
|
214
233
|
_ => null,
|
|
215
234
|
};
|
|
216
|
-
if (latest != null) {
|
|
235
|
+
if (latest != null && _streamStillActive) {
|
|
217
236
|
state = AsyncData(
|
|
218
237
|
latest.copyWith(isReplying: false, streamingStarted: false),
|
|
219
238
|
);
|
|
220
239
|
}
|
|
221
240
|
} catch (error) {
|
|
222
|
-
_logger.e('
|
|
241
|
+
_logger.e('AI chat stream failed: $error');
|
|
223
242
|
_finalizeAssistantMessage(t.error_network);
|
|
224
243
|
}
|
|
225
244
|
}
|
|
226
245
|
|
|
227
246
|
/// Called on every incoming SSE token to update the last assistant bubble.
|
|
228
247
|
void _appendStreamingChunk(String partialContent) {
|
|
248
|
+
// Ignore chunks if the user navigated to another conversation mid-stream.
|
|
249
|
+
if (!_streamStillActive) return;
|
|
229
250
|
final current = switch (state) {
|
|
230
251
|
AsyncData(:final value) => value,
|
|
231
252
|
_ => null,
|
|
@@ -243,22 +264,24 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
|
|
|
243
264
|
: [...msgs, ChatMessage.assistant(partialContent)];
|
|
244
265
|
|
|
245
266
|
state = AsyncData(
|
|
246
|
-
|
|
267
|
+
AiChatState(messages: newMsgs, isReplying: true, streamingStarted: true),
|
|
247
268
|
);
|
|
248
269
|
}
|
|
249
270
|
|
|
250
271
|
/// Replaces (or appends) the assistant bubble with [content] and marks
|
|
251
272
|
/// the reply as finished. Used for error messages and empty-stream fallback.
|
|
252
273
|
void _finalizeAssistantMessage(String content) {
|
|
274
|
+
// Ignore if the user navigated to another conversation mid-stream.
|
|
275
|
+
if (!_streamStillActive) return;
|
|
253
276
|
final current = switch (state) {
|
|
254
277
|
AsyncData(:final value) => value,
|
|
255
|
-
_ => const
|
|
278
|
+
_ => const AiChatState(messages: []),
|
|
256
279
|
};
|
|
257
280
|
final msgs = current.messages;
|
|
258
281
|
final newMsgs = current.streamingStarted
|
|
259
282
|
? [...msgs.sublist(0, msgs.length - 1), ChatMessage.assistant(content)]
|
|
260
283
|
: [...msgs, ChatMessage.assistant(content)];
|
|
261
|
-
state = AsyncData(
|
|
284
|
+
state = AsyncData(AiChatState(messages: newMsgs));
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
// ---------------------------------------------------------------------------
|
|
@@ -304,14 +327,24 @@ class LlmChatNotifier extends AsyncNotifier<LlmChatState> {
|
|
|
304
327
|
// Persistence
|
|
305
328
|
// ---------------------------------------------------------------------------
|
|
306
329
|
|
|
307
|
-
void _persistMessage(ChatMessage message) {
|
|
330
|
+
void _persistMessage(ChatMessage message, String conversationId) {
|
|
308
331
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
309
332
|
if (userId == null) return;
|
|
333
|
+
final entity = message.toEntity();
|
|
310
334
|
ref
|
|
311
|
-
.read(
|
|
312
|
-
.saveMessage(userId,
|
|
335
|
+
.read(aiChatApiProvider)
|
|
336
|
+
.saveMessage(userId, conversationId, entity)
|
|
313
337
|
.catchError((e) {
|
|
314
338
|
_logger.e('Failed to persist message: $e');
|
|
315
339
|
});
|
|
340
|
+
// Keep the conversation list preview + ordering in sync.
|
|
341
|
+
ref
|
|
342
|
+
.read(aiConversationsNotifierProvider.notifier)
|
|
343
|
+
.touch(
|
|
344
|
+
conversationId,
|
|
345
|
+
role: message.role,
|
|
346
|
+
content: message.content,
|
|
347
|
+
at: entity.createdAt,
|
|
348
|
+
);
|
|
316
349
|
}
|
|
317
350
|
}
|
package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart
CHANGED
|
@@ -135,7 +135,7 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
135
135
|
}
|
|
136
136
|
rethrow;
|
|
137
137
|
}
|
|
138
|
-
final googleAuth =
|
|
138
|
+
final googleAuth = googleUser.authentication;
|
|
139
139
|
throw UnimplementedError('''
|
|
140
140
|
❌ You must edit lib/features/authentication/api/authentication_api.dart
|
|
141
141
|
to send the Oauth2 token result to your backend.
|
|
@@ -228,7 +228,7 @@ class HttpAuthenticationApi implements AuthenticationApi {
|
|
|
228
228
|
}
|
|
229
229
|
rethrow;
|
|
230
230
|
}
|
|
231
|
-
final googleAuth =
|
|
231
|
+
final googleAuth = googleUser.authentication;
|
|
232
232
|
throw UnimplementedError(
|
|
233
233
|
'❌ Send the tokens to your backend to merge the guest account.\n'
|
|
234
234
|
' idToken: ${googleAuth.idToken}\n'
|
|
@@ -14,6 +14,10 @@ final featureRequestApiProvider = Provider<FeatureRequestApi>(
|
|
|
14
14
|
/// GET /feature-requests → `List<FeatureRequestEntity>` (only active)
|
|
15
15
|
/// POST /feature-requests body: {title, description} → 201 Created
|
|
16
16
|
/// Server creates with active: false and stores title/description in all locales.
|
|
17
|
+
///
|
|
18
|
+
/// Admin (server MUST verify role == "admin", else 403):
|
|
19
|
+
/// GET /admin/feature-requests → `List<FeatureRequestEntity>` (active + hidden)
|
|
20
|
+
/// PATCH /admin/feature-requests/{id} body: {active} | {title, description}
|
|
17
21
|
class FeatureRequestApi {
|
|
18
22
|
final HttpClient _client;
|
|
19
23
|
|
|
@@ -49,4 +53,54 @@ class FeatureRequestApi {
|
|
|
49
53
|
throw ApiError(code: 0, message: '$e: $s');
|
|
50
54
|
}
|
|
51
55
|
}
|
|
56
|
+
|
|
57
|
+
/// Admin: every request (active + hidden), highest-voted first.
|
|
58
|
+
/// Expected: GET /admin/feature-requests (server gates on role == "admin").
|
|
59
|
+
Future<List<FeatureRequestEntity>> getAll() async {
|
|
60
|
+
try {
|
|
61
|
+
final response = await _client.get('/admin/feature-requests');
|
|
62
|
+
final list = response.data as List<dynamic>;
|
|
63
|
+
return list
|
|
64
|
+
.map((e) => FeatureRequestEntity.fromJson(e as Map<String, dynamic>))
|
|
65
|
+
.toList();
|
|
66
|
+
} on DioException catch (e) {
|
|
67
|
+
throw ApiError.fromDioException(e);
|
|
68
|
+
} catch (e, s) {
|
|
69
|
+
throw ApiError(code: 0, message: '$e: $s');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Admin: toggle whether a request is visible (and votable) to users.
|
|
74
|
+
/// Expected: PATCH /admin/feature-requests/{id} body {active} (admin-gated).
|
|
75
|
+
Future<void> setActive(String id, bool active) async {
|
|
76
|
+
try {
|
|
77
|
+
await _client.patch(
|
|
78
|
+
'/admin/feature-requests/$id',
|
|
79
|
+
data: {'active': active},
|
|
80
|
+
);
|
|
81
|
+
} on DioException catch (e) {
|
|
82
|
+
throw ApiError.fromDioException(e);
|
|
83
|
+
} catch (e, s) {
|
|
84
|
+
throw ApiError(code: 0, message: '$e: $s');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Admin: update the localized title/description maps (en/pt/es).
|
|
89
|
+
/// Expected: PATCH /admin/feature-requests/{id} body {title, description}.
|
|
90
|
+
Future<void> updateTexts({
|
|
91
|
+
required String id,
|
|
92
|
+
required Map<String, String> title,
|
|
93
|
+
required Map<String, String> description,
|
|
94
|
+
}) async {
|
|
95
|
+
try {
|
|
96
|
+
await _client.patch(
|
|
97
|
+
'/admin/feature-requests/$id',
|
|
98
|
+
data: {'title': title, 'description': description},
|
|
99
|
+
);
|
|
100
|
+
} on DioException catch (e) {
|
|
101
|
+
throw ApiError.fromDioException(e);
|
|
102
|
+
} catch (e, s) {
|
|
103
|
+
throw ApiError(code: 0, message: '$e: $s');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
52
106
|
}
|