kasy-cli 1.21.9 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/add.js +93 -80
- package/lib/commands/configure.js +100 -32
- package/lib/commands/doctor.js +28 -2
- package/lib/commands/new.js +80 -37
- package/lib/commands/notifications.js +1 -1
- package/lib/commands/remove.js +43 -15
- package/lib/commands/run.js +2 -2
- package/lib/commands/update.js +2 -2
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/generator.js +14 -14
- package/lib/scaffold/backends/api/patch/README.md +83 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +5 -0
- package/lib/scaffold/backends/api/patch/lib/{environnements.dart → environments.dart} +3 -11
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_api.dart +108 -0
- package/lib/scaffold/backends/api/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +51 -0
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +5 -5
- package/lib/scaffold/backends/api/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +71 -38
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +2 -2
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +54 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +100 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +60 -0
- package/lib/scaffold/backends/api/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/firebase/deploy.js +87 -13
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/tokens.js +4 -4
- package/lib/scaffold/backends/supabase/deploy.js +63 -11
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +149 -0
- package/lib/scaffold/backends/supabase/edge-functions/{llm-chat → ai-chat}/index.ts +19 -19
- package/lib/scaffold/backends/supabase/edge-functions/revenuecat-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +123 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +97 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +83 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +138 -0
- package/lib/scaffold/backends/supabase/generator.js +17 -17
- package/lib/scaffold/backends/supabase/migrations/20240101000009_ai_messages.sql +50 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_stripe_customers.sql +36 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000013_admin_role.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +4 -0
- package/lib/scaffold/backends/supabase/patch/lib/{environnements.dart → environments.dart} +3 -13
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_api.dart +95 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +63 -35
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/api/feature_request_api.dart +46 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/models/user_info.dart +16 -16
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +93 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +13 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +52 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -5
- package/lib/scaffold/backends/supabase/tokens.js +3 -3
- package/lib/scaffold/catalog.js +9 -11
- package/lib/scaffold/generate.js +45 -31
- package/lib/scaffold/shared/generator-utils.js +188 -81
- package/lib/scaffold/shared/sort-imports.js +191 -0
- package/lib/scaffold/shared/template-strings.js +3 -3
- package/lib/utils/checks.js +22 -6
- package/lib/utils/env-tools.js +7 -0
- package/lib/utils/flutter-install.js +114 -0
- package/lib/utils/i18n/messages-en.js +52 -35
- package/lib/utils/i18n/messages-es.js +52 -35
- package/lib/utils/i18n/messages-pt.js +54 -37
- package/lib/utils/updates.js +15 -15
- package/package.json +1 -1
- package/templates/firebase/.env.example +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +1 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -1
- package/templates/firebase/assets/images/logo_wordmark_dark.png +0 -0
- package/templates/firebase/assets/images/logo_wordmark_light.png +0 -0
- package/templates/firebase/docs/revenuecat-setup.es.md +1 -1
- package/templates/firebase/docs/revenuecat-setup.pt.md +1 -1
- package/templates/firebase/firestore.rules +24 -5
- package/templates/firebase/functions/package-lock.json +22 -1
- package/templates/firebase/functions/package.json +2 -1
- package/templates/firebase/functions/src/admin/functions.ts +113 -0
- package/templates/firebase/functions/src/{llm_chat → ai_chat}/index.ts +16 -16
- package/templates/firebase/functions/src/index.ts +8 -2
- package/templates/firebase/functions/src/notifications/device_triggers.ts +2 -2
- package/templates/firebase/functions/src/notifications/triggers.ts +3 -3
- package/templates/firebase/functions/src/subscriptions/models/subscription_status.ts +2 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +222 -0
- package/templates/firebase/functions/src/subscriptions/subscriptions_functions.ts +2 -2
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/lib/components/components.dart +4 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +22 -7
- package/templates/firebase/lib/components/kasy_avatar.dart +7 -6
- package/templates/firebase/lib/components/kasy_button.dart +23 -99
- package/templates/firebase/lib/components/kasy_dialog.dart +11 -11
- package/templates/firebase/lib/components/kasy_image_viewer.dart +84 -0
- package/templates/firebase/lib/components/{kasy_sidebar_pro.dart → kasy_sidebar.dart} +702 -425
- package/templates/firebase/lib/components/kasy_status_tag.dart +91 -0
- package/templates/firebase/lib/components/kasy_text_area.dart +1 -3
- package/templates/firebase/lib/components/kasy_text_field.dart +5 -6
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -3
- package/templates/firebase/lib/components/kasy_toast.dart +2 -2
- package/templates/firebase/lib/components/kasy_web_header.dart +209 -0
- package/templates/firebase/lib/core/ads/ads_provider.dart +1 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +136 -23
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +19 -91
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +196 -96
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +54 -0
- package/templates/firebase/lib/core/config/app_env.dart +5 -11
- package/templates/firebase/lib/core/config/features.dart +5 -4
- package/templates/firebase/lib/core/data/api/analytics_api.dart +1 -1
- package/templates/firebase/lib/core/data/api/http_client.dart +1 -1
- package/templates/firebase/lib/core/data/entities/user_entity.dart +3 -0
- package/templates/firebase/lib/core/data/models/entitlement.dart +35 -0
- package/templates/firebase/lib/core/data/models/subscription.dart +13 -186
- package/templates/firebase/lib/core/data/models/user.dart +11 -0
- package/templates/firebase/lib/core/data/repositories/user_repository.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_background_task.dart +1 -1
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +1 -1
- package/templates/firebase/lib/core/icons/kasy_icons.dart +31 -1
- package/templates/firebase/lib/core/rating/api/rating_api.dart +2 -2
- package/templates/firebase/lib/core/states/logout_action.dart +25 -0
- package/templates/firebase/lib/core/states/user_state_notifier.dart +3 -3
- package/templates/firebase/lib/core/theme/colors.dart +488 -188
- package/templates/firebase/lib/core/theme/radius.dart +22 -11
- package/templates/firebase/lib/core/theme/shadows.dart +66 -0
- package/templates/firebase/lib/core/theme/texts.dart +75 -41
- package/templates/firebase/lib/core/theme/universal_theme.dart +9 -4
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +5 -4
- package/templates/firebase/lib/core/web_viewport_scale.dart +52 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_badge.dart +118 -0
- package/templates/firebase/lib/core/widgets/kasy_brand_logo.dart +40 -0
- package/templates/firebase/lib/core/widgets/kasy_focus_ring.dart +125 -0
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +53 -14
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +18 -13
- package/templates/firebase/lib/{environnements.dart → environments.dart} +3 -14
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +159 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_api.dart +107 -0
- package/templates/firebase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart +64 -0
- package/templates/firebase/lib/features/{llm_chat/api/llm_chat_message_entity.dart → ai_chat/api/ai_chat_message_entity.dart} +6 -6
- package/templates/firebase/lib/features/{llm_chat/providers/llm_chat_notifier.dart → ai_chat/providers/ai_chat_notifier.dart} +73 -38
- package/templates/firebase/lib/features/ai_chat/providers/ai_conversations_notifier.dart +103 -0
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_avatars.dart → ai_chat/ui/widgets/ai_chat_avatars.dart} +13 -13
- package/templates/firebase/lib/features/{llm_chat/ui/widgets/llm_chat_composer.dart → ai_chat/ui/widgets/ai_chat_composer.dart} +10 -10
- package/templates/firebase/lib/features/{llm_chat/llm_chat_page.dart → ai_chat/ui/widgets/ai_chat_conversation_view.dart} +80 -67
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +285 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +163 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +52 -13
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/popup_dismiss_watcher_web.dart +35 -0
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +108 -68
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +38 -51
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +128 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +61 -44
- package/templates/firebase/lib/features/feedbacks/api/feature_request_api.dart +32 -0
- package/templates/firebase/lib/features/feedbacks/models/feedback_state.dart +5 -5
- package/templates/firebase/lib/features/feedbacks/providers/feedback_page_notifier.dart +2 -2
- package/templates/firebase/lib/features/home/design_system_page.dart +808 -170
- package/templates/firebase/lib/features/home/home_components_page.dart +6 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +6 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +325 -186
- package/templates/firebase/lib/features/home/home_feed.dart +289 -0
- package/templates/firebase/lib/features/home/home_image_grid.dart +355 -0
- package/templates/firebase/lib/features/home/home_page.dart +11 -250
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/providers/reminder_notifier.dart +1 -1
- package/templates/firebase/lib/features/{local_reminder → local_reminders}/ui/reminder_page.dart +2 -2
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +1 -1
- package/templates/firebase/lib/features/notifications/ui/request_notification_permission.dart +25 -61
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +117 -0
- package/templates/firebase/lib/features/onboarding/models/user_info.dart +16 -16
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_att_setup.dart +4 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +7 -9
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +71 -48
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +3 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_questions.dart +5 -5
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +4 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_background.dart +4 -2
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +39 -121
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +105 -70
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +639 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_progress.dart +62 -50
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +38 -28
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_step_header.dart +75 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +16 -5
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +26 -22
- package/templates/firebase/lib/features/settings/settings_page.dart +601 -90
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1193 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +2 -3
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +86 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +1215 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +236 -0
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +3 -3
- package/templates/firebase/lib/features/settings/ui/widgets/kasy_user_avatar.dart +77 -0
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/entities/subscription_entity.dart +17 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/inapp_subscription_api.dart +67 -46
- package/templates/firebase/lib/features/subscriptions/api/revenuecat_product.dart +189 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +55 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +82 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +178 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +53 -0
- package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart +21 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/providers/premium_page_provider.dart +9 -6
- package/templates/firebase/lib/features/{subscription → subscriptions}/repositories/subscription_repository.dart +26 -30
- package/{lib/scaffold/backends/supabase/patch/lib/features/subscription → templates/firebase/lib/features/subscriptions}/shared/maybeshow_premium.dart +0 -2
- package/templates/firebase/lib/features/subscriptions/shared/subscription_management.dart +45 -0
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/active_premium_content.dart +28 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_minimal.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_row.dart +8 -8
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/paywall_with_switch.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_content.dart +7 -7
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/component/premium_page_factory.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/premium_page.dart +5 -5
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/paywall_empty_state.dart +4 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_bottom_menu.dart +3 -4
- package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_col.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +171 -46
- package/templates/firebase/lib/i18n/es.i18n.json +175 -50
- package/templates/firebase/lib/i18n/pt.i18n.json +166 -41
- package/templates/firebase/lib/main.dart +6 -3
- package/templates/firebase/lib/router.dart +15 -23
- package/templates/firebase/pubspec.yaml +5 -6
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +3 -3
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +4 -4
- package/templates/firebase/test/core/widgets/focus_ring_shape_test.dart +55 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_inapp_subscription_api.dart +11 -4
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_revenuecat_product.dart +1 -0
- package/templates/firebase/test/features/{subscription → subscriptions}/api/fake_subscription_api.dart +2 -2
- package/templates/firebase/test/features/{subscription → subscriptions}/subscription_page_test.dart +4 -4
- package/templates/firebase/test/test_utils.dart +6 -6
- package/templates/firebase/web/index.html +5 -2
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -58
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -86
- package/lib/scaffold/backends/supabase/migrations/20240101000009_llm_messages.sql +0 -22
- package/lib/scaffold/backends/supabase/patch/lib/features/llm_chat/api/llm_chat_api.dart +0 -47
- package/templates/firebase/assets/images/onboarding/authentication-login-template.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img2.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/img3.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/notifications.png +0 -0
- package/templates/firebase/assets/images/onboarding/purchase.png +0 -0
- package/templates/firebase/lib/core/sidebar/kasy_sidebar.dart +0 -2021
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_brand.dart +0 -35
- package/templates/firebase/lib/features/home/home_features_page.dart +0 -207
- package/templates/firebase/lib/features/llm_chat/api/llm_chat_api.dart +0 -50
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +0 -316
- package/templates/firebase/lib/features/subscription/shared/maybeshow_premium.dart +0 -85
- /package/templates/firebase/lib/features/{local_reminder → local_reminders}/repositories/reminder_preferences.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/providers/models/premium_state.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/feature_line.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_background_gradient.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_banner.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_card.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_close_button.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/premium_feature.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/selectable_row.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/trial_switcher.dart +0 -0
- /package/templates/firebase/lib/features/{subscription → subscriptions}/ui/widgets/unsubscribe_feedback_popup.dart +0 -0
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
2
|
+
import 'package:flutter/foundation.dart';
|
|
3
|
+
import 'package:flutter/material.dart';
|
|
4
|
+
import 'package:flutter/services.dart';
|
|
5
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
|
+
import 'package:go_router/go_router.dart';
|
|
7
|
+
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
8
|
+
import 'package:kasy_kit/components/kasy_bottom_sheet.dart';
|
|
9
|
+
import 'package:kasy_kit/components/kasy_button.dart';
|
|
10
|
+
import 'package:kasy_kit/components/kasy_status_tag.dart';
|
|
11
|
+
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
12
|
+
import 'package:kasy_kit/components/kasy_text_field.dart';
|
|
13
|
+
import 'package:kasy_kit/core/config/features.dart';
|
|
14
|
+
import 'package:kasy_kit/core/data/models/user.dart';
|
|
15
|
+
import 'package:kasy_kit/core/dev_inspector/dev_inspector.dart';
|
|
16
|
+
import 'package:kasy_kit/core/rating/widgets/review_popup.dart';
|
|
17
|
+
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
18
|
+
import 'package:kasy_kit/core/theme/theme.dart';
|
|
19
|
+
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
20
|
+
import 'package:kasy_kit/core/web_device_preview/web_device_preview.dart';
|
|
21
|
+
import 'package:kasy_kit/core/widgets/update_bottom_sheet.dart';
|
|
22
|
+
import 'package:kasy_kit/features/feedbacks/api/entities/feature_request_entity.dart';
|
|
23
|
+
import 'package:kasy_kit/features/feedbacks/api/feature_request_api.dart';
|
|
24
|
+
import 'package:kasy_kit/features/notifications/api/local_notifier.dart';
|
|
25
|
+
import 'package:kasy_kit/features/notifications/providers/models/notification.dart'
|
|
26
|
+
as kasy_kit;
|
|
27
|
+
import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
|
|
28
|
+
import 'package:kasy_kit/features/settings/ui/components/admin/admin_users_tab.dart';
|
|
29
|
+
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
30
|
+
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
31
|
+
import 'package:kasy_kit/router.dart';
|
|
32
|
+
import 'package:package_info_plus/package_info_plus.dart';
|
|
33
|
+
import 'package:shared_preferences/shared_preferences.dart';
|
|
34
|
+
|
|
35
|
+
/// Full-screen developer admin console (debug only).
|
|
36
|
+
///
|
|
37
|
+
/// Vercel-style layout: frosted "Admin" app bar + horizontally-scrollable
|
|
38
|
+
/// underline tabs over a content column that is centred and width-capped so the
|
|
39
|
+
/// console reads as a real dashboard on web/desktop instead of a stretched
|
|
40
|
+
/// phone screen. Replaces the old admin bottom sheet — reached via the
|
|
41
|
+
/// double-tap on the settings version label; popping returns to Settings.
|
|
42
|
+
///
|
|
43
|
+
/// TOP-LEVEL route (sibling of '/', outside the BottomMenu shell) so it never
|
|
44
|
+
/// renders the web sidebar — the admin is a dev tool, not product chrome.
|
|
45
|
+
class AdminPage extends ConsumerStatefulWidget {
|
|
46
|
+
const AdminPage({super.key});
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
ConsumerState<AdminPage> createState() => _AdminPageState();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class _AdminPageState extends ConsumerState<AdminPage> {
|
|
53
|
+
int _tab = 0;
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
Widget build(BuildContext context) {
|
|
57
|
+
final bool isAdmin = ref.watch(userStateNotifierProvider).user.isAdmin;
|
|
58
|
+
final ac = t.admin_console;
|
|
59
|
+
|
|
60
|
+
// The console itself opens for admins (even in release) or anyone in debug.
|
|
61
|
+
if (!isAdmin && !kDebugMode) {
|
|
62
|
+
return Scaffold(
|
|
63
|
+
backgroundColor: context.colors.background,
|
|
64
|
+
body: Column(
|
|
65
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
66
|
+
children: [
|
|
67
|
+
const KasyAppBar(title: 'Admin'),
|
|
68
|
+
Expanded(
|
|
69
|
+
child: _EmptyState(
|
|
70
|
+
icon: Icons.lock_outline_rounded,
|
|
71
|
+
title: ac.tabs.overview,
|
|
72
|
+
message: ac.requires_admin,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Server-data tabs always present (gated by role inside). Dev tabs (Kit,
|
|
81
|
+
// Tools) only in debug — they are developer conveniences, not admin data.
|
|
82
|
+
final List<({String label, Widget view})> entries = [
|
|
83
|
+
(label: ac.tabs.overview, view: const _OverviewTab()),
|
|
84
|
+
(label: ac.tabs.users, view: const _UsersTab()),
|
|
85
|
+
(label: ac.tabs.requests, view: const _RequestsTab()),
|
|
86
|
+
if (kDebugMode) (label: ac.tabs.kit, view: const _KitTab()),
|
|
87
|
+
if (kDebugMode) (label: ac.tabs.tools, view: const _ToolsTab()),
|
|
88
|
+
];
|
|
89
|
+
final int tab = _tab.clamp(0, entries.length - 1);
|
|
90
|
+
|
|
91
|
+
return Scaffold(
|
|
92
|
+
backgroundColor: context.colors.background,
|
|
93
|
+
body: Column(
|
|
94
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
95
|
+
children: [
|
|
96
|
+
const KasyAppBar(title: 'Admin'),
|
|
97
|
+
const SizedBox(height: KasySpacing.sm),
|
|
98
|
+
_MaxWidth(
|
|
99
|
+
child: Padding(
|
|
100
|
+
padding: const EdgeInsets.symmetric(
|
|
101
|
+
horizontal: KasySpacing.pageHorizontalGutter,
|
|
102
|
+
),
|
|
103
|
+
child: KasyTabs(
|
|
104
|
+
tabs: [for (final e in entries) e.label],
|
|
105
|
+
selectedIndex: tab,
|
|
106
|
+
onTabSelected: (i) => setState(() => _tab = i),
|
|
107
|
+
variant: KasyTabsVariant.secondary,
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
Expanded(
|
|
112
|
+
child: IndexedStack(
|
|
113
|
+
index: tab,
|
|
114
|
+
children: [for (final e in entries) e.view],
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Live feature requests (active + hidden), highest-voted first — real data for
|
|
124
|
+
/// the Requests tab and the Overview count. Invalidate to refresh after a change.
|
|
125
|
+
final _adminRequestsProvider =
|
|
126
|
+
FutureProvider.autoDispose<List<FeatureRequestEntity>>((ref) {
|
|
127
|
+
return ref.read(featureRequestApiProvider).getAll();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// Layout primitives
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const double _cardRadius = 18;
|
|
135
|
+
const double _contentMaxWidth = 1080;
|
|
136
|
+
|
|
137
|
+
/// Centres content and caps its width so the console looks deliberate on wide
|
|
138
|
+
/// web/desktop viewports (mobile fills the screen normally).
|
|
139
|
+
class _MaxWidth extends StatelessWidget {
|
|
140
|
+
final Widget child;
|
|
141
|
+
const _MaxWidth({required this.child});
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
Widget build(BuildContext context) {
|
|
145
|
+
return Center(
|
|
146
|
+
child: ConstrainedBox(
|
|
147
|
+
constraints: const BoxConstraints(maxWidth: _contentMaxWidth),
|
|
148
|
+
child: child,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Scrollable, gutter-padded, width-capped body shared by every tab.
|
|
155
|
+
class _TabScroll extends StatelessWidget {
|
|
156
|
+
final List<Widget> children;
|
|
157
|
+
const _TabScroll({required this.children});
|
|
158
|
+
|
|
159
|
+
@override
|
|
160
|
+
Widget build(BuildContext context) {
|
|
161
|
+
return SingleChildScrollView(
|
|
162
|
+
child: _MaxWidth(
|
|
163
|
+
child: Padding(
|
|
164
|
+
padding: EdgeInsets.fromLTRB(
|
|
165
|
+
KasySpacing.pageHorizontalGutter,
|
|
166
|
+
KasySpacing.lg,
|
|
167
|
+
KasySpacing.pageHorizontalGutter,
|
|
168
|
+
MediaQuery.paddingOf(context).bottom + KasySpacing.xl,
|
|
169
|
+
),
|
|
170
|
+
child: Column(
|
|
171
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
172
|
+
children: children,
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Small uppercase label that opens a group (Vercel-style section head).
|
|
181
|
+
class _GroupLabel extends StatelessWidget {
|
|
182
|
+
final String label;
|
|
183
|
+
const _GroupLabel(this.label);
|
|
184
|
+
|
|
185
|
+
@override
|
|
186
|
+
Widget build(BuildContext context) {
|
|
187
|
+
return Padding(
|
|
188
|
+
padding: const EdgeInsets.only(
|
|
189
|
+
left: KasySpacing.xs,
|
|
190
|
+
bottom: KasySpacing.smd,
|
|
191
|
+
),
|
|
192
|
+
child: Text(
|
|
193
|
+
label.toUpperCase(),
|
|
194
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
195
|
+
fontSize: 12,
|
|
196
|
+
color: context.colors.muted,
|
|
197
|
+
letterSpacing: 1.2,
|
|
198
|
+
fontWeight: FontWeight.w700,
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Rounded surface card with a hairline border and the design-system shadow.
|
|
206
|
+
class _CardShell extends StatelessWidget {
|
|
207
|
+
final Widget child;
|
|
208
|
+
final EdgeInsetsGeometry padding;
|
|
209
|
+
const _CardShell({
|
|
210
|
+
required this.child,
|
|
211
|
+
this.padding = const EdgeInsets.all(KasySpacing.md),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
@override
|
|
215
|
+
Widget build(BuildContext context) {
|
|
216
|
+
return DecoratedBox(
|
|
217
|
+
decoration: BoxDecoration(
|
|
218
|
+
color: context.colors.surface,
|
|
219
|
+
borderRadius: BorderRadius.circular(_cardRadius),
|
|
220
|
+
border: Border.all(
|
|
221
|
+
color: context.colors.outline.withValues(
|
|
222
|
+
alpha: context.isDark ? 0.45 : 0.6,
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
boxShadow: [KasyShadows.component(context)],
|
|
226
|
+
),
|
|
227
|
+
child: Padding(padding: padding, child: child),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Soft-tinted rounded-square icon container.
|
|
233
|
+
class _IconBubble extends StatelessWidget {
|
|
234
|
+
final IconData icon;
|
|
235
|
+
final Color tone;
|
|
236
|
+
final double size;
|
|
237
|
+
const _IconBubble({required this.icon, required this.tone, this.size = 40});
|
|
238
|
+
|
|
239
|
+
@override
|
|
240
|
+
Widget build(BuildContext context) {
|
|
241
|
+
return Container(
|
|
242
|
+
width: size,
|
|
243
|
+
height: size,
|
|
244
|
+
decoration: BoxDecoration(
|
|
245
|
+
color: tone.withValues(alpha: context.isDark ? 0.22 : 0.12),
|
|
246
|
+
borderRadius: BorderRadius.circular(size * 0.3),
|
|
247
|
+
),
|
|
248
|
+
child: Icon(icon, size: size * 0.5, color: tone),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// Metric card: an icon bubble, a big value, and a muted label.
|
|
254
|
+
class _StatCard extends StatelessWidget {
|
|
255
|
+
final IconData icon;
|
|
256
|
+
final Color tone;
|
|
257
|
+
final String value;
|
|
258
|
+
final String label;
|
|
259
|
+
const _StatCard({
|
|
260
|
+
required this.icon,
|
|
261
|
+
required this.tone,
|
|
262
|
+
required this.value,
|
|
263
|
+
required this.label,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
@override
|
|
267
|
+
Widget build(BuildContext context) {
|
|
268
|
+
return _CardShell(
|
|
269
|
+
child: Column(
|
|
270
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
271
|
+
mainAxisSize: MainAxisSize.min,
|
|
272
|
+
children: [
|
|
273
|
+
_IconBubble(icon: icon, tone: tone),
|
|
274
|
+
const SizedBox(height: KasySpacing.smd),
|
|
275
|
+
Text(
|
|
276
|
+
value,
|
|
277
|
+
maxLines: 1,
|
|
278
|
+
overflow: TextOverflow.ellipsis,
|
|
279
|
+
style: context.textTheme.headlineSmall?.copyWith(
|
|
280
|
+
color: context.colors.onSurface,
|
|
281
|
+
fontWeight: FontWeight.w800,
|
|
282
|
+
letterSpacing: -0.5,
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
const SizedBox(height: 2),
|
|
286
|
+
Text(
|
|
287
|
+
label,
|
|
288
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
289
|
+
color: context.colors.muted,
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
],
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Tappable action card: icon bubble + title/subtitle + chevron, with ripple.
|
|
299
|
+
class _ActionCard extends StatelessWidget {
|
|
300
|
+
final IconData icon;
|
|
301
|
+
final String title;
|
|
302
|
+
final String? subtitle;
|
|
303
|
+
final VoidCallback onTap;
|
|
304
|
+
final Color? tone;
|
|
305
|
+
const _ActionCard({
|
|
306
|
+
required this.icon,
|
|
307
|
+
required this.title,
|
|
308
|
+
required this.onTap,
|
|
309
|
+
this.subtitle,
|
|
310
|
+
this.tone,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
@override
|
|
314
|
+
Widget build(BuildContext context) {
|
|
315
|
+
final BorderRadius radius = BorderRadius.circular(_cardRadius);
|
|
316
|
+
final Color accent = tone ?? context.colors.primary;
|
|
317
|
+
return DecoratedBox(
|
|
318
|
+
decoration: BoxDecoration(
|
|
319
|
+
borderRadius: radius,
|
|
320
|
+
boxShadow: [KasyShadows.component(context)],
|
|
321
|
+
),
|
|
322
|
+
child: Material(
|
|
323
|
+
color: context.colors.surface,
|
|
324
|
+
borderRadius: radius,
|
|
325
|
+
clipBehavior: Clip.antiAlias,
|
|
326
|
+
child: InkWell(
|
|
327
|
+
onTap: onTap,
|
|
328
|
+
child: Container(
|
|
329
|
+
decoration: BoxDecoration(
|
|
330
|
+
borderRadius: radius,
|
|
331
|
+
border: Border.all(
|
|
332
|
+
color: context.colors.outline.withValues(
|
|
333
|
+
alpha: context.isDark ? 0.45 : 0.6,
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
padding: const EdgeInsets.all(KasySpacing.md),
|
|
338
|
+
child: Row(
|
|
339
|
+
children: [
|
|
340
|
+
_IconBubble(icon: icon, tone: accent),
|
|
341
|
+
const SizedBox(width: KasySpacing.smd),
|
|
342
|
+
Expanded(
|
|
343
|
+
child: Column(
|
|
344
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
345
|
+
mainAxisSize: MainAxisSize.min,
|
|
346
|
+
children: [
|
|
347
|
+
Text(
|
|
348
|
+
title,
|
|
349
|
+
maxLines: 1,
|
|
350
|
+
overflow: TextOverflow.ellipsis,
|
|
351
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
352
|
+
color: context.colors.onSurface,
|
|
353
|
+
fontWeight: FontWeight.w700,
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
if (subtitle != null) ...[
|
|
357
|
+
const SizedBox(height: 2),
|
|
358
|
+
Text(
|
|
359
|
+
subtitle!,
|
|
360
|
+
maxLines: 2,
|
|
361
|
+
overflow: TextOverflow.ellipsis,
|
|
362
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
363
|
+
color: context.colors.muted,
|
|
364
|
+
height: 1.3,
|
|
365
|
+
),
|
|
366
|
+
),
|
|
367
|
+
],
|
|
368
|
+
],
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
const SizedBox(width: KasySpacing.sm),
|
|
372
|
+
Icon(
|
|
373
|
+
KasyIcons.chevronRight,
|
|
374
|
+
size: 18,
|
|
375
|
+
color: context.colors.muted,
|
|
376
|
+
),
|
|
377
|
+
],
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// Lays children out in as many equal columns as fit [minItemWidth], capped at
|
|
387
|
+
/// [maxCols]. Collapses to a single full-width column on narrow screens.
|
|
388
|
+
class _ResponsiveGrid extends StatelessWidget {
|
|
389
|
+
final List<Widget> children;
|
|
390
|
+
final double minItemWidth;
|
|
391
|
+
final int maxCols;
|
|
392
|
+
const _ResponsiveGrid({
|
|
393
|
+
required this.children,
|
|
394
|
+
this.minItemWidth = 240,
|
|
395
|
+
this.maxCols = 4,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
@override
|
|
399
|
+
Widget build(BuildContext context) {
|
|
400
|
+
if (children.isEmpty) return const SizedBox.shrink();
|
|
401
|
+
const double gap = KasySpacing.md;
|
|
402
|
+
return LayoutBuilder(
|
|
403
|
+
builder: (context, constraints) {
|
|
404
|
+
final double maxW = constraints.maxWidth;
|
|
405
|
+
int cols = ((maxW + gap) / (minItemWidth + gap)).floor();
|
|
406
|
+
cols = cols.clamp(1, maxCols);
|
|
407
|
+
if (cols > children.length) cols = children.length;
|
|
408
|
+
final double itemW =
|
|
409
|
+
cols <= 1 ? maxW : (maxW - gap * (cols - 1)) / cols;
|
|
410
|
+
return Wrap(
|
|
411
|
+
spacing: gap,
|
|
412
|
+
runSpacing: gap,
|
|
413
|
+
children: [
|
|
414
|
+
for (final child in children) SizedBox(width: itemW, child: child),
|
|
415
|
+
],
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Pops the admin page back to Settings — used by actions that historically
|
|
423
|
+
/// closed the bottom sheet so their result is visible over the app.
|
|
424
|
+
void _backToApp(BuildContext context) {
|
|
425
|
+
if (context.canPop()) context.pop();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
429
|
+
// Overview — real project + session data (no vanity metrics)
|
|
430
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
class _OverviewTab extends ConsumerWidget {
|
|
433
|
+
const _OverviewTab();
|
|
434
|
+
|
|
435
|
+
@override
|
|
436
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
437
|
+
final userState = ref.watch(userStateNotifierProvider);
|
|
438
|
+
final user = userState.user;
|
|
439
|
+
final bool isAuth = user is AuthenticatedUserData;
|
|
440
|
+
final bool isAdmin = user.isAdmin;
|
|
441
|
+
final ov = t.admin_console.overview;
|
|
442
|
+
final String account = isAuth ? user.email : ov.guest;
|
|
443
|
+
final String uid = userState.user.idOrNull ?? '—';
|
|
444
|
+
|
|
445
|
+
return _TabScroll(
|
|
446
|
+
children: [
|
|
447
|
+
_GroupLabel(ov.section),
|
|
448
|
+
_ResponsiveGrid(
|
|
449
|
+
minItemWidth: 200,
|
|
450
|
+
children: [
|
|
451
|
+
_StatCard(
|
|
452
|
+
icon: Icons.cloud_done_rounded,
|
|
453
|
+
tone: context.colors.primary,
|
|
454
|
+
value: 'Firebase',
|
|
455
|
+
label: ov.backend,
|
|
456
|
+
),
|
|
457
|
+
// Feature-request count reads the server — admins only.
|
|
458
|
+
if (isAdmin)
|
|
459
|
+
_StatCard(
|
|
460
|
+
icon: Icons.how_to_vote_rounded,
|
|
461
|
+
tone: context.colors.primary,
|
|
462
|
+
value: ref.watch(_adminRequestsProvider).maybeWhen(
|
|
463
|
+
data: (l) => '${l.length}',
|
|
464
|
+
orElse: () => '…',
|
|
465
|
+
),
|
|
466
|
+
label: ov.requests_metric,
|
|
467
|
+
),
|
|
468
|
+
],
|
|
469
|
+
),
|
|
470
|
+
const SizedBox(height: KasySpacing.lg),
|
|
471
|
+
_GroupLabel(ov.session_title),
|
|
472
|
+
_CardShell(
|
|
473
|
+
padding: const EdgeInsets.symmetric(
|
|
474
|
+
horizontal: KasySpacing.md,
|
|
475
|
+
vertical: KasySpacing.xs,
|
|
476
|
+
),
|
|
477
|
+
child: Column(
|
|
478
|
+
children: [
|
|
479
|
+
_InfoRow(
|
|
480
|
+
label: ov.account,
|
|
481
|
+
value: account,
|
|
482
|
+
valueColor: isAuth ? null : context.colors.muted,
|
|
483
|
+
),
|
|
484
|
+
const SettingsDivider(),
|
|
485
|
+
_InfoRow(
|
|
486
|
+
label: ov.user_id,
|
|
487
|
+
value: uid,
|
|
488
|
+
trailing: _CopyButton(
|
|
489
|
+
onTap: () {
|
|
490
|
+
Clipboard.setData(ClipboardData(text: uid));
|
|
491
|
+
ref.read(toastProvider).alert(
|
|
492
|
+
title: '',
|
|
493
|
+
text: t.settings.admin.user_id_copied,
|
|
494
|
+
);
|
|
495
|
+
},
|
|
496
|
+
),
|
|
497
|
+
),
|
|
498
|
+
const SettingsDivider(),
|
|
499
|
+
FutureBuilder<PackageInfo>(
|
|
500
|
+
future: PackageInfo.fromPlatform(),
|
|
501
|
+
builder: (context, snap) => _InfoRow(
|
|
502
|
+
label: ov.build,
|
|
503
|
+
value: snap.hasData
|
|
504
|
+
? 'v${snap.data!.version} (${snap.data!.buildNumber})'
|
|
505
|
+
: '…',
|
|
506
|
+
),
|
|
507
|
+
),
|
|
508
|
+
],
|
|
509
|
+
),
|
|
510
|
+
),
|
|
511
|
+
const SizedBox(height: KasySpacing.md),
|
|
512
|
+
Text(
|
|
513
|
+
ov.users_hint,
|
|
514
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
515
|
+
color: context.colors.muted,
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
const SizedBox(height: KasySpacing.xs),
|
|
519
|
+
Text(
|
|
520
|
+
ov.debug_note,
|
|
521
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
522
|
+
color: context.colors.muted,
|
|
523
|
+
),
|
|
524
|
+
),
|
|
525
|
+
],
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
class _InfoRow extends StatelessWidget {
|
|
531
|
+
final String label;
|
|
532
|
+
final String value;
|
|
533
|
+
final Color? valueColor;
|
|
534
|
+
final Widget? trailing;
|
|
535
|
+
const _InfoRow({
|
|
536
|
+
required this.label,
|
|
537
|
+
required this.value,
|
|
538
|
+
this.valueColor,
|
|
539
|
+
this.trailing,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
@override
|
|
543
|
+
Widget build(BuildContext context) {
|
|
544
|
+
return Padding(
|
|
545
|
+
padding: const EdgeInsets.symmetric(vertical: KasySpacing.smd),
|
|
546
|
+
child: Row(
|
|
547
|
+
children: [
|
|
548
|
+
SizedBox(
|
|
549
|
+
width: 110,
|
|
550
|
+
child: Text(
|
|
551
|
+
label,
|
|
552
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
553
|
+
color: context.colors.muted,
|
|
554
|
+
),
|
|
555
|
+
),
|
|
556
|
+
),
|
|
557
|
+
const SizedBox(width: KasySpacing.sm),
|
|
558
|
+
Expanded(
|
|
559
|
+
child: Text(
|
|
560
|
+
value,
|
|
561
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
562
|
+
color: valueColor ?? context.colors.onSurface,
|
|
563
|
+
fontWeight: FontWeight.w600,
|
|
564
|
+
),
|
|
565
|
+
),
|
|
566
|
+
),
|
|
567
|
+
if (trailing != null) trailing!,
|
|
568
|
+
],
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
class _CopyButton extends StatelessWidget {
|
|
575
|
+
final VoidCallback onTap;
|
|
576
|
+
const _CopyButton({required this.onTap});
|
|
577
|
+
|
|
578
|
+
@override
|
|
579
|
+
Widget build(BuildContext context) {
|
|
580
|
+
return InkWell(
|
|
581
|
+
onTap: onTap,
|
|
582
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
583
|
+
child: Padding(
|
|
584
|
+
padding: const EdgeInsets.all(6),
|
|
585
|
+
child: Icon(
|
|
586
|
+
Icons.content_copy_rounded,
|
|
587
|
+
size: 16,
|
|
588
|
+
color: context.colors.primary,
|
|
589
|
+
),
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
596
|
+
// Empty-state tabs (real content lands next: Users via a server function,
|
|
597
|
+
// Requests reads the live feature_requests collection)
|
|
598
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
class _EmptyState extends StatelessWidget {
|
|
601
|
+
final IconData icon;
|
|
602
|
+
final String title;
|
|
603
|
+
final String message;
|
|
604
|
+
const _EmptyState({
|
|
605
|
+
required this.icon,
|
|
606
|
+
required this.title,
|
|
607
|
+
required this.message,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
@override
|
|
611
|
+
Widget build(BuildContext context) {
|
|
612
|
+
return _TabScroll(
|
|
613
|
+
children: [
|
|
614
|
+
_CardShell(
|
|
615
|
+
padding: const EdgeInsets.symmetric(
|
|
616
|
+
horizontal: KasySpacing.lg,
|
|
617
|
+
vertical: KasySpacing.xxl,
|
|
618
|
+
),
|
|
619
|
+
child: Column(
|
|
620
|
+
mainAxisSize: MainAxisSize.min,
|
|
621
|
+
children: [
|
|
622
|
+
_IconBubble(icon: icon, tone: context.colors.primary, size: 64),
|
|
623
|
+
const SizedBox(height: KasySpacing.md),
|
|
624
|
+
Text(
|
|
625
|
+
title,
|
|
626
|
+
textAlign: TextAlign.center,
|
|
627
|
+
style: context.textTheme.titleMedium?.copyWith(
|
|
628
|
+
fontWeight: FontWeight.w700,
|
|
629
|
+
color: context.colors.onSurface,
|
|
630
|
+
),
|
|
631
|
+
),
|
|
632
|
+
const SizedBox(height: KasySpacing.xs),
|
|
633
|
+
Text(
|
|
634
|
+
message,
|
|
635
|
+
textAlign: TextAlign.center,
|
|
636
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
637
|
+
color: context.colors.muted,
|
|
638
|
+
height: 1.45,
|
|
639
|
+
),
|
|
640
|
+
),
|
|
641
|
+
],
|
|
642
|
+
),
|
|
643
|
+
),
|
|
644
|
+
],
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
class _UsersTab extends ConsumerWidget {
|
|
650
|
+
const _UsersTab();
|
|
651
|
+
|
|
652
|
+
@override
|
|
653
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
654
|
+
final bool isAdmin = ref.watch(userStateNotifierProvider).user.isAdmin;
|
|
655
|
+
final u = t.admin_console.users;
|
|
656
|
+
if (!isAdmin) {
|
|
657
|
+
return _EmptyState(
|
|
658
|
+
icon: Icons.lock_outline_rounded,
|
|
659
|
+
title: u.title,
|
|
660
|
+
message: t.admin_console.requires_admin,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return const AdminUsersTab();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
class _RequestsTab extends ConsumerWidget {
|
|
668
|
+
const _RequestsTab();
|
|
669
|
+
|
|
670
|
+
@override
|
|
671
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
672
|
+
final bool isAdmin = ref.watch(userStateNotifierProvider).user.isAdmin;
|
|
673
|
+
final r = t.admin_console.requests;
|
|
674
|
+
if (!isAdmin) {
|
|
675
|
+
return _EmptyState(
|
|
676
|
+
icon: Icons.lock_outline_rounded,
|
|
677
|
+
title: r.title,
|
|
678
|
+
message: t.admin_console.requires_admin,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
final AsyncValue<List<FeatureRequestEntity>> async =
|
|
682
|
+
ref.watch(_adminRequestsProvider);
|
|
683
|
+
return async.when(
|
|
684
|
+
loading: () => const Center(child: CircularProgressIndicator.adaptive()),
|
|
685
|
+
error: (_, _) => _EmptyState(
|
|
686
|
+
icon: KasyIcons.message,
|
|
687
|
+
title: r.title,
|
|
688
|
+
message: r.error,
|
|
689
|
+
),
|
|
690
|
+
data: (list) {
|
|
691
|
+
if (list.isEmpty) {
|
|
692
|
+
return _EmptyState(
|
|
693
|
+
icon: KasyIcons.message,
|
|
694
|
+
title: r.title,
|
|
695
|
+
message: r.empty,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
return _TabScroll(
|
|
699
|
+
children: [
|
|
700
|
+
for (final req in list) ...[
|
|
701
|
+
_RequestCard(req),
|
|
702
|
+
const SizedBox(height: KasySpacing.md),
|
|
703
|
+
],
|
|
704
|
+
],
|
|
705
|
+
);
|
|
706
|
+
},
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/// Picks the value for [lang], falling back to English then anything present.
|
|
712
|
+
String _pickLocale(Map<String, String> m, String lang) =>
|
|
713
|
+
m[lang] ?? m['en'] ?? (m.isNotEmpty ? m.values.first : '');
|
|
714
|
+
|
|
715
|
+
class _RequestCard extends ConsumerWidget {
|
|
716
|
+
final FeatureRequestEntity req;
|
|
717
|
+
const _RequestCard(this.req);
|
|
718
|
+
|
|
719
|
+
@override
|
|
720
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
721
|
+
final r = t.admin_console.requests;
|
|
722
|
+
final String lang = Localizations.localeOf(context).languageCode;
|
|
723
|
+
final String title = _pickLocale(req.title, lang);
|
|
724
|
+
final String desc = _pickLocale(req.description, lang);
|
|
725
|
+
final bool active = req.active;
|
|
726
|
+
|
|
727
|
+
return _CardShell(
|
|
728
|
+
child: Column(
|
|
729
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
730
|
+
children: [
|
|
731
|
+
Row(
|
|
732
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
733
|
+
children: [
|
|
734
|
+
_VotesChip(req.votes),
|
|
735
|
+
const SizedBox(width: KasySpacing.md),
|
|
736
|
+
Expanded(
|
|
737
|
+
child: Column(
|
|
738
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
739
|
+
children: [
|
|
740
|
+
Text(
|
|
741
|
+
title,
|
|
742
|
+
maxLines: 2,
|
|
743
|
+
overflow: TextOverflow.ellipsis,
|
|
744
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
745
|
+
color: context.colors.onSurface,
|
|
746
|
+
fontWeight: FontWeight.w700,
|
|
747
|
+
),
|
|
748
|
+
),
|
|
749
|
+
const SizedBox(height: 2),
|
|
750
|
+
Text(
|
|
751
|
+
desc,
|
|
752
|
+
maxLines: 3,
|
|
753
|
+
overflow: TextOverflow.ellipsis,
|
|
754
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
755
|
+
color: context.colors.muted,
|
|
756
|
+
height: 1.35,
|
|
757
|
+
),
|
|
758
|
+
),
|
|
759
|
+
],
|
|
760
|
+
),
|
|
761
|
+
),
|
|
762
|
+
],
|
|
763
|
+
),
|
|
764
|
+
const SizedBox(height: KasySpacing.smd),
|
|
765
|
+
Divider(
|
|
766
|
+
height: 1,
|
|
767
|
+
color: context.colors.outline.withValues(alpha: 0.35),
|
|
768
|
+
),
|
|
769
|
+
const SizedBox(height: KasySpacing.sm),
|
|
770
|
+
Row(
|
|
771
|
+
children: [
|
|
772
|
+
KasyStatusTag(
|
|
773
|
+
label: active ? r.visible : r.hidden,
|
|
774
|
+
tone: active
|
|
775
|
+
? KasyStatusTagTone.success
|
|
776
|
+
: KasyStatusTagTone.neutral,
|
|
777
|
+
),
|
|
778
|
+
const SizedBox(width: KasySpacing.sm),
|
|
779
|
+
Transform.scale(
|
|
780
|
+
scale: 0.8,
|
|
781
|
+
child: Switch.adaptive(
|
|
782
|
+
value: active,
|
|
783
|
+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
784
|
+
onChanged: (v) async {
|
|
785
|
+
await ref
|
|
786
|
+
.read(featureRequestApiProvider)
|
|
787
|
+
.setActive(req.id!, v);
|
|
788
|
+
ref.invalidate(_adminRequestsProvider);
|
|
789
|
+
if (context.mounted) {
|
|
790
|
+
ref.read(toastProvider).alert(title: '', text: r.saved);
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
),
|
|
794
|
+
),
|
|
795
|
+
const Spacer(),
|
|
796
|
+
KasyButton(
|
|
797
|
+
label: r.edit,
|
|
798
|
+
variant: KasyButtonVariant.soft,
|
|
799
|
+
onPressed: () => _openRequestEditor(context, req),
|
|
800
|
+
),
|
|
801
|
+
],
|
|
802
|
+
),
|
|
803
|
+
],
|
|
804
|
+
),
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
class _VotesChip extends StatelessWidget {
|
|
810
|
+
final int votes;
|
|
811
|
+
const _VotesChip(this.votes);
|
|
812
|
+
|
|
813
|
+
@override
|
|
814
|
+
Widget build(BuildContext context) {
|
|
815
|
+
return Container(
|
|
816
|
+
width: 48,
|
|
817
|
+
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
818
|
+
decoration: BoxDecoration(
|
|
819
|
+
color: context.colors.primary.withValues(
|
|
820
|
+
alpha: context.isDark ? 0.16 : 0.08,
|
|
821
|
+
),
|
|
822
|
+
borderRadius: BorderRadius.circular(KasyRadius.sm),
|
|
823
|
+
border: Border.all(
|
|
824
|
+
color: context.colors.primary.withValues(alpha: 0.25),
|
|
825
|
+
),
|
|
826
|
+
),
|
|
827
|
+
child: Column(
|
|
828
|
+
mainAxisSize: MainAxisSize.min,
|
|
829
|
+
children: [
|
|
830
|
+
Icon(KasyIcons.voteUp, size: 18, color: context.colors.primary),
|
|
831
|
+
const SizedBox(height: 2),
|
|
832
|
+
Text(
|
|
833
|
+
'$votes',
|
|
834
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
835
|
+
color: context.colors.primary,
|
|
836
|
+
fontWeight: FontWeight.w800,
|
|
837
|
+
height: 1,
|
|
838
|
+
),
|
|
839
|
+
),
|
|
840
|
+
],
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
Future<void> _openRequestEditor(BuildContext context, FeatureRequestEntity req) {
|
|
847
|
+
return showKasyBlurBottomSheet<void>(
|
|
848
|
+
context: context,
|
|
849
|
+
builder: (_) => _RequestEditorSheet(req: req),
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
class _RequestEditorSheet extends ConsumerStatefulWidget {
|
|
854
|
+
final FeatureRequestEntity req;
|
|
855
|
+
const _RequestEditorSheet({required this.req});
|
|
856
|
+
|
|
857
|
+
@override
|
|
858
|
+
ConsumerState<_RequestEditorSheet> createState() =>
|
|
859
|
+
_RequestEditorSheetState();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
class _RequestEditorSheetState extends ConsumerState<_RequestEditorSheet> {
|
|
863
|
+
static const List<String> _langs = ['en', 'pt', 'es'];
|
|
864
|
+
late final Map<String, TextEditingController> _title;
|
|
865
|
+
late final Map<String, TextEditingController> _desc;
|
|
866
|
+
bool _saving = false;
|
|
867
|
+
|
|
868
|
+
@override
|
|
869
|
+
void initState() {
|
|
870
|
+
super.initState();
|
|
871
|
+
_title = {
|
|
872
|
+
for (final l in _langs)
|
|
873
|
+
l: TextEditingController(text: widget.req.title[l] ?? ''),
|
|
874
|
+
};
|
|
875
|
+
_desc = {
|
|
876
|
+
for (final l in _langs)
|
|
877
|
+
l: TextEditingController(text: widget.req.description[l] ?? ''),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
@override
|
|
882
|
+
void dispose() {
|
|
883
|
+
for (final c in _title.values) {
|
|
884
|
+
c.dispose();
|
|
885
|
+
}
|
|
886
|
+
for (final c in _desc.values) {
|
|
887
|
+
c.dispose();
|
|
888
|
+
}
|
|
889
|
+
super.dispose();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
String _langLabel(String l) {
|
|
893
|
+
final r = t.admin_console.requests;
|
|
894
|
+
return switch (l) {
|
|
895
|
+
'en' => r.lang_en,
|
|
896
|
+
'pt' => r.lang_pt,
|
|
897
|
+
_ => r.lang_es,
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
Future<void> _save() async {
|
|
902
|
+
final r = t.admin_console.requests;
|
|
903
|
+
setState(() => _saving = true);
|
|
904
|
+
try {
|
|
905
|
+
await ref.read(featureRequestApiProvider).updateTexts(
|
|
906
|
+
id: widget.req.id!,
|
|
907
|
+
title: {for (final l in _langs) l: _title[l]!.text.trim()},
|
|
908
|
+
description: {for (final l in _langs) l: _desc[l]!.text.trim()},
|
|
909
|
+
);
|
|
910
|
+
ref.invalidate(_adminRequestsProvider);
|
|
911
|
+
if (!mounted) return;
|
|
912
|
+
context.pop();
|
|
913
|
+
ref.read(toastProvider).alert(title: '', text: r.saved);
|
|
914
|
+
} catch (_) {
|
|
915
|
+
if (!mounted) return;
|
|
916
|
+
setState(() => _saving = false);
|
|
917
|
+
ref.read(toastProvider).alert(title: '', text: r.error);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
@override
|
|
922
|
+
Widget build(BuildContext context) {
|
|
923
|
+
final r = t.admin_console.requests;
|
|
924
|
+
return KasyBottomSheet(
|
|
925
|
+
title: r.editor_title,
|
|
926
|
+
addKeyboardInset: true,
|
|
927
|
+
body: SizedBox(
|
|
928
|
+
height: MediaQuery.sizeOf(context).height * 0.5,
|
|
929
|
+
child: SingleChildScrollView(
|
|
930
|
+
child: Column(
|
|
931
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
932
|
+
children: [
|
|
933
|
+
for (final l in _langs) ...[
|
|
934
|
+
_GroupLabel(_langLabel(l)),
|
|
935
|
+
KasyTextField(
|
|
936
|
+
controller: _title[l],
|
|
937
|
+
label: r.field_title,
|
|
938
|
+
maxLength: 60,
|
|
939
|
+
textInputAction: TextInputAction.next,
|
|
940
|
+
),
|
|
941
|
+
const SizedBox(height: KasySpacing.sm),
|
|
942
|
+
KasyTextField(
|
|
943
|
+
controller: _desc[l],
|
|
944
|
+
label: r.field_description,
|
|
945
|
+
minLines: 2,
|
|
946
|
+
maxLines: 4,
|
|
947
|
+
maxLength: 1000,
|
|
948
|
+
),
|
|
949
|
+
const SizedBox(height: KasySpacing.md),
|
|
950
|
+
],
|
|
951
|
+
],
|
|
952
|
+
),
|
|
953
|
+
),
|
|
954
|
+
),
|
|
955
|
+
actions: [
|
|
956
|
+
KasyButton(
|
|
957
|
+
label: r.save,
|
|
958
|
+
expand: true,
|
|
959
|
+
isLoading: _saving,
|
|
960
|
+
onPressed: _saving ? null : _save,
|
|
961
|
+
),
|
|
962
|
+
],
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
968
|
+
// Kit — feature demos + the component gallery
|
|
969
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
970
|
+
|
|
971
|
+
class _KitTab extends ConsumerWidget {
|
|
972
|
+
const _KitTab();
|
|
973
|
+
|
|
974
|
+
@override
|
|
975
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
976
|
+
final page = t.home.features_page;
|
|
977
|
+
final dash = t.home.dashboard;
|
|
978
|
+
final groups = t.admin_console.groups;
|
|
979
|
+
|
|
980
|
+
final List<Widget> features = <Widget>[
|
|
981
|
+
if (withFeedback)
|
|
982
|
+
_ActionCard(
|
|
983
|
+
icon: KasyIcons.message,
|
|
984
|
+
title: page.feedback_title,
|
|
985
|
+
subtitle: page.feedback_description,
|
|
986
|
+
onTap: () => context.push('/feedback'),
|
|
987
|
+
),
|
|
988
|
+
_ActionCard(
|
|
989
|
+
icon: KasyIcons.notification,
|
|
990
|
+
title: page.notification_title,
|
|
991
|
+
subtitle: page.notification_description,
|
|
992
|
+
onTap: () {
|
|
993
|
+
final settings = ref.read(notificationsSettingsProvider);
|
|
994
|
+
final localNotifier = ref.read(localNotifierProvider);
|
|
995
|
+
kasy_kit.Notification.withData(
|
|
996
|
+
id: 'fake-id',
|
|
997
|
+
title: page.notification_demo_title,
|
|
998
|
+
body: page.notification_demo_body,
|
|
999
|
+
createdAt: DateTime.now(),
|
|
1000
|
+
notifier: localNotifier,
|
|
1001
|
+
notifierSettings: settings,
|
|
1002
|
+
).show();
|
|
1003
|
+
},
|
|
1004
|
+
),
|
|
1005
|
+
_ActionCard(
|
|
1006
|
+
icon: KasyIcons.notificationActive,
|
|
1007
|
+
title: page.send_push_title,
|
|
1008
|
+
subtitle: page.send_push_description,
|
|
1009
|
+
onTap: () => context.push(adminRouteSendPush),
|
|
1010
|
+
),
|
|
1011
|
+
if (withRevenuecat)
|
|
1012
|
+
_ActionCard(
|
|
1013
|
+
icon: KasyIcons.payment,
|
|
1014
|
+
title: page.paywall_title,
|
|
1015
|
+
subtitle: page.paywall_description,
|
|
1016
|
+
onTap: () => context.push('/premium'),
|
|
1017
|
+
),
|
|
1018
|
+
];
|
|
1019
|
+
|
|
1020
|
+
return _TabScroll(
|
|
1021
|
+
children: [
|
|
1022
|
+
_GroupLabel(groups.features),
|
|
1023
|
+
_ResponsiveGrid(minItemWidth: 280, maxCols: 2, children: features),
|
|
1024
|
+
const SizedBox(height: KasySpacing.lg),
|
|
1025
|
+
_GroupLabel(groups.showcase),
|
|
1026
|
+
_ResponsiveGrid(
|
|
1027
|
+
minItemWidth: 280,
|
|
1028
|
+
maxCols: 2,
|
|
1029
|
+
children: [
|
|
1030
|
+
_ActionCard(
|
|
1031
|
+
icon: KasyIcons.widgets,
|
|
1032
|
+
title: dash.components_title,
|
|
1033
|
+
subtitle: dash.components_subtitle,
|
|
1034
|
+
tone: context.colors.success,
|
|
1035
|
+
onTap: () => context.push('/components'),
|
|
1036
|
+
),
|
|
1037
|
+
],
|
|
1038
|
+
),
|
|
1039
|
+
],
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1045
|
+
// Tools — every developer toggle/action migrated from the old admin sheet
|
|
1046
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1047
|
+
|
|
1048
|
+
class _ToolsTab extends ConsumerWidget {
|
|
1049
|
+
const _ToolsTab();
|
|
1050
|
+
|
|
1051
|
+
@override
|
|
1052
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
1053
|
+
final userState = ref.watch(userStateNotifierProvider);
|
|
1054
|
+
final admin = t.settings.admin;
|
|
1055
|
+
final groups = t.admin_console.groups;
|
|
1056
|
+
|
|
1057
|
+
final List<Widget> previews = <Widget>[
|
|
1058
|
+
ValueListenableBuilder<bool>(
|
|
1059
|
+
valueListenable: devInspectorEnabledNotifier,
|
|
1060
|
+
builder: (context, enabled, _) {
|
|
1061
|
+
return SettingsSwitchTile(
|
|
1062
|
+
icon: KasyIcons.widgets,
|
|
1063
|
+
title: admin.inspector_fab_title,
|
|
1064
|
+
subtitle:
|
|
1065
|
+
'${admin.inspector_fab_subtitle_prefix} ${devInspectorShortcutLabel()}',
|
|
1066
|
+
value: enabled,
|
|
1067
|
+
onChanged: (v) async {
|
|
1068
|
+
final p = await SharedPreferences.getInstance();
|
|
1069
|
+
await p.setBool(devInspectorEnabledPrefKey, v);
|
|
1070
|
+
devInspectorEnabledNotifier.value = v;
|
|
1071
|
+
if (v && context.mounted) _backToApp(context);
|
|
1072
|
+
},
|
|
1073
|
+
);
|
|
1074
|
+
},
|
|
1075
|
+
),
|
|
1076
|
+
if (kIsWeb)
|
|
1077
|
+
ValueListenableBuilder<bool>(
|
|
1078
|
+
valueListenable: webDevicePreviewEnabledNotifier,
|
|
1079
|
+
builder: (context, enabled, _) {
|
|
1080
|
+
return SettingsSwitchTile(
|
|
1081
|
+
icon: KasyIcons.phoneAndroid,
|
|
1082
|
+
title: admin.device_preview_title,
|
|
1083
|
+
subtitle:
|
|
1084
|
+
'${admin.inspector_fab_subtitle_prefix} ${webDevicePreviewShortcutLabel()}',
|
|
1085
|
+
value: enabled,
|
|
1086
|
+
onChanged: (v) async {
|
|
1087
|
+
final p = await SharedPreferences.getInstance();
|
|
1088
|
+
await p.setBool(webDevicePreviewEnabledPrefKey, v);
|
|
1089
|
+
webDevicePreviewEnabledNotifier.value = v;
|
|
1090
|
+
if (v && context.mounted) _backToApp(context);
|
|
1091
|
+
},
|
|
1092
|
+
);
|
|
1093
|
+
},
|
|
1094
|
+
),
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
final List<Widget> tools = <Widget>[
|
|
1098
|
+
_ActionCard(
|
|
1099
|
+
icon: KasyIcons.note,
|
|
1100
|
+
title: admin.update_bottom_sheet,
|
|
1101
|
+
onTap: () {
|
|
1102
|
+
_backToApp(context);
|
|
1103
|
+
showUpdateBottomSheet(
|
|
1104
|
+
context: navigatorKey.currentContext!,
|
|
1105
|
+
version: '0.0.0',
|
|
1106
|
+
);
|
|
1107
|
+
},
|
|
1108
|
+
),
|
|
1109
|
+
_ActionCard(
|
|
1110
|
+
icon: KasyIcons.payment,
|
|
1111
|
+
title: admin.paywalls,
|
|
1112
|
+
onTap: () => context.push(adminRoutePaywalls),
|
|
1113
|
+
),
|
|
1114
|
+
_ActionCard(
|
|
1115
|
+
icon: KasyIcons.check,
|
|
1116
|
+
title: admin.test_onboarding,
|
|
1117
|
+
onTap: () => ref.read(goRouterProvider).go('/onboarding'),
|
|
1118
|
+
),
|
|
1119
|
+
_ActionCard(
|
|
1120
|
+
icon: KasyIcons.notificationActive,
|
|
1121
|
+
title: admin.send_push_title,
|
|
1122
|
+
onTap: () => context.push(adminRouteSendPush),
|
|
1123
|
+
),
|
|
1124
|
+
_ActionCard(
|
|
1125
|
+
icon: KasyIcons.star,
|
|
1126
|
+
title: admin.ask_review,
|
|
1127
|
+
onTap: () => showReviewDialog(context, ref, force: true),
|
|
1128
|
+
),
|
|
1129
|
+
_ActionCard(
|
|
1130
|
+
icon: KasyIcons.message,
|
|
1131
|
+
title: admin.home_widgets_panel,
|
|
1132
|
+
onTap: () => context.push(adminRouteHomeWidgets),
|
|
1133
|
+
),
|
|
1134
|
+
];
|
|
1135
|
+
|
|
1136
|
+
final List<Widget> identity = <Widget>[
|
|
1137
|
+
_ActionCard(
|
|
1138
|
+
icon: KasyIcons.person,
|
|
1139
|
+
title: admin.copy_user_id,
|
|
1140
|
+
onTap: () {
|
|
1141
|
+
Clipboard.setData(
|
|
1142
|
+
ClipboardData(text: userState.user.idOrNull ?? 'no-id (guest)'),
|
|
1143
|
+
);
|
|
1144
|
+
ref.read(toastProvider).alert(title: '', text: admin.user_id_copied);
|
|
1145
|
+
},
|
|
1146
|
+
),
|
|
1147
|
+
_ActionCard(
|
|
1148
|
+
icon: KasyIcons.notification,
|
|
1149
|
+
title: admin.copy_fcm_token,
|
|
1150
|
+
onTap: () async {
|
|
1151
|
+
final token = await FirebaseMessaging.instance.getToken();
|
|
1152
|
+
if (token == null) {
|
|
1153
|
+
ref.read(toastProvider).alert(
|
|
1154
|
+
title: '',
|
|
1155
|
+
text: admin.fcm_token_unavailable,
|
|
1156
|
+
);
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
await Clipboard.setData(ClipboardData(text: token));
|
|
1160
|
+
ref.read(toastProvider).alert(title: '', text: admin.fcm_token_copied);
|
|
1161
|
+
},
|
|
1162
|
+
),
|
|
1163
|
+
_ActionCard(
|
|
1164
|
+
icon: KasyIcons.notificationActive,
|
|
1165
|
+
title: admin.ask_notification,
|
|
1166
|
+
onTap: () => ref.read(notificationsSettingsProvider).askPermission(),
|
|
1167
|
+
),
|
|
1168
|
+
];
|
|
1169
|
+
|
|
1170
|
+
return _TabScroll(
|
|
1171
|
+
children: [
|
|
1172
|
+
_GroupLabel(groups.preview),
|
|
1173
|
+
_CardShell(
|
|
1174
|
+
child: Column(
|
|
1175
|
+
mainAxisSize: MainAxisSize.min,
|
|
1176
|
+
children: [
|
|
1177
|
+
for (int i = 0; i < previews.length; i++) ...[
|
|
1178
|
+
if (i > 0) const SettingsDivider(),
|
|
1179
|
+
previews[i],
|
|
1180
|
+
],
|
|
1181
|
+
],
|
|
1182
|
+
),
|
|
1183
|
+
),
|
|
1184
|
+
const SizedBox(height: KasySpacing.lg),
|
|
1185
|
+
_GroupLabel(groups.debug_actions),
|
|
1186
|
+
_ResponsiveGrid(minItemWidth: 280, maxCols: 2, children: tools),
|
|
1187
|
+
const SizedBox(height: KasySpacing.lg),
|
|
1188
|
+
_GroupLabel(groups.identity),
|
|
1189
|
+
_ResponsiveGrid(minItemWidth: 280, maxCols: 2, children: identity),
|
|
1190
|
+
],
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
}
|