kasy-cli 1.21.8 → 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 +86 -38
- 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/enable-auth-via-cli.js +14 -6
- package/lib/scaffold/backends/firebase/generator.js +5 -5
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -45
- 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
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Firebase Cloud Function:
|
|
2
|
+
* Firebase Cloud Function: AI Chat Proxy (streaming)
|
|
3
3
|
*
|
|
4
4
|
* Receives {message, history} from the Flutter app and streams the response
|
|
5
5
|
* back as Server-Sent Events (SSE). The API key never leaves the server.
|
|
6
6
|
*
|
|
7
7
|
* Secrets required (set via `firebase functions:secrets:set`):
|
|
8
|
-
* -
|
|
8
|
+
* - AI_API_KEY: API key for OpenAI or Gemini
|
|
9
9
|
*
|
|
10
10
|
* Environment variables (set in functions/.env):
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
11
|
+
* - AI_PROVIDER: "openai" (default) or "gemini"
|
|
12
|
+
* - AI_SYSTEM_PROMPT: System prompt for the agent (optional)
|
|
13
13
|
*
|
|
14
14
|
* App dart-define:
|
|
15
|
-
* -
|
|
16
|
-
* Example: https://europe-west1-<project-id>.cloudfunctions.net/
|
|
15
|
+
* - AI_CHAT_ENDPOINT: URL of this function after deploy
|
|
16
|
+
* Example: https://europe-west1-<project-id>.cloudfunctions.net/aiChat
|
|
17
17
|
*
|
|
18
|
-
* Deploy: kasy deploy (or: firebase deploy --only functions:
|
|
18
|
+
* Deploy: kasy deploy (or: firebase deploy --only functions:aiChat)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { onRequest } from "firebase-functions/v2/https";
|
|
@@ -23,7 +23,7 @@ import { defineSecret } from "firebase-functions/params";
|
|
|
23
23
|
import { getAuth } from "firebase-admin/auth";
|
|
24
24
|
import type { Request, Response } from "express";
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const aiApiKey = defineSecret("AI_API_KEY");
|
|
27
27
|
|
|
28
28
|
interface ChatMessage {
|
|
29
29
|
role: "user" | "assistant";
|
|
@@ -130,8 +130,8 @@ async function verifyFirebaseToken(req: Request, res: Response): Promise<string
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
export const
|
|
134
|
-
{ cors: true, secrets: [
|
|
133
|
+
export const aiChat = onRequest(
|
|
134
|
+
{ cors: true, secrets: [aiApiKey] },
|
|
135
135
|
async (req, res) => {
|
|
136
136
|
if (req.method !== "POST") {
|
|
137
137
|
res.status(405).json({ error: "Method not allowed" });
|
|
@@ -147,16 +147,16 @@ export const llmChat = onRequest(
|
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
const apiKey =
|
|
150
|
+
const apiKey = aiApiKey.value();
|
|
151
151
|
if (!apiKey) {
|
|
152
152
|
res
|
|
153
153
|
.status(500)
|
|
154
|
-
.json({ error: "
|
|
154
|
+
.json({ error: "AI_API_KEY not configured. Run: firebase functions:secrets:set AI_API_KEY" });
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const provider = process.env.
|
|
159
|
-
const systemPrompt = process.env.
|
|
158
|
+
const provider = process.env.AI_PROVIDER ?? "openai";
|
|
159
|
+
const systemPrompt = process.env.AI_SYSTEM_PROMPT ?? "";
|
|
160
160
|
|
|
161
161
|
// SSE headers — must be set before any write
|
|
162
162
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -172,9 +172,9 @@ export const llmChat = onRequest(
|
|
|
172
172
|
}
|
|
173
173
|
res.end();
|
|
174
174
|
} catch (err) {
|
|
175
|
-
console.error("[
|
|
175
|
+
console.error("[ai-chat]", err);
|
|
176
176
|
// Send error as SSE event so the Flutter client can surface it
|
|
177
|
-
res.write(`data: ${JSON.stringify({ error: "
|
|
177
|
+
res.write(`data: ${JSON.stringify({ error: "AI request failed" })}\n\n`);
|
|
178
178
|
res.end();
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -19,7 +19,13 @@ exports.deviceTriggers = require("./notifications/device_triggers");
|
|
|
19
19
|
exports.subscriptions = require("./subscriptions/subscriptions_functions");
|
|
20
20
|
exports.subscriptionTriggers = require("./subscriptions/triggers");
|
|
21
21
|
|
|
22
|
+
// stripe web subscriptions (activated when the Stripe module is enabled)
|
|
23
|
+
exports.stripeFunctions = require("./subscriptions/stripe_functions");
|
|
24
|
+
|
|
25
|
+
// admin console (listUsers — gated on users/{uid}.role == "admin")
|
|
26
|
+
exports.adminFunctions = require("./admin/functions");
|
|
27
|
+
|
|
22
28
|
// feature requests: vote counter updated atomically by client (WriteBatch)
|
|
23
29
|
|
|
24
|
-
// llm chat proxy (activated when
|
|
25
|
-
exports.
|
|
30
|
+
// llm chat proxy (activated when withAiChat = true)
|
|
31
|
+
exports.aiChat = require("./ai_chat").aiChat;
|
|
@@ -14,7 +14,7 @@ import {Logger} from "../core/logger/logger";
|
|
|
14
14
|
* any time. Without it, sending a push to user A could deliver to a phone now
|
|
15
15
|
* signed in as user B.
|
|
16
16
|
*/
|
|
17
|
-
export const
|
|
17
|
+
export const dedupeDeviceTokens = onDocumentWritten(
|
|
18
18
|
"users/{userId}/devices/{deviceId}",
|
|
19
19
|
async (event) => {
|
|
20
20
|
const after = event.data?.after?.data();
|
|
@@ -25,7 +25,7 @@ export const onDeviceWritten = onDocumentWritten(
|
|
|
25
25
|
|
|
26
26
|
const currentUserId = event.params.userId;
|
|
27
27
|
const currentDeviceId = event.params.deviceId;
|
|
28
|
-
const logger = new Logger("
|
|
28
|
+
const logger = new Logger("dedupeDeviceTokens");
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
31
|
const duplicates = await admin
|
|
@@ -7,13 +7,13 @@ import {onDocumentCreated} from "firebase-functions/v2/firestore";
|
|
|
7
7
|
|
|
8
8
|
const kChannelId = "appfirebase";
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const onNotificationCreated = onDocumentCreated(
|
|
11
11
|
"users/{userId}/notifications/{notificationId}",
|
|
12
12
|
async (event) => {
|
|
13
13
|
if (!event.data) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
const logger = new Logger("
|
|
16
|
+
const logger = new Logger("onNotificationCreated");
|
|
17
17
|
try {
|
|
18
18
|
const notificationEntity = NotificationEntity.fromDocument(event.data);
|
|
19
19
|
const userId = event.params.userId;
|
|
@@ -97,6 +97,6 @@ export const onNewNotificationRequest = onDocumentCreated(
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
} catch (e) {
|
|
100
|
-
logger.error(`Error
|
|
100
|
+
logger.error(`Error onNotificationCreated users/${event.params.userId}/notifications/${event.id} : ${e}`);
|
|
101
101
|
}
|
|
102
102
|
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {error} from "firebase-functions/logger";
|
|
2
|
+
import {onCall, onRequest, HttpsError} from "firebase-functions/v2/https";
|
|
3
|
+
import {defineSecret, defineString} from "firebase-functions/params";
|
|
4
|
+
import * as admin from "firebase-admin";
|
|
5
|
+
import {Timestamp} from "firebase-admin/firestore";
|
|
6
|
+
import Stripe from "stripe";
|
|
7
|
+
import {Subscription} from "./models/subscriptions";
|
|
8
|
+
import {subscriptionsRepository} from "../core/data/repositories/repositories";
|
|
9
|
+
import {Stores, SubscriptionStatus} from "./models/subscription_status";
|
|
10
|
+
|
|
11
|
+
// Server-side only. Never exposed to the client.
|
|
12
|
+
const stripeSecretKey = defineSecret("STRIPE_SECRET_KEY");
|
|
13
|
+
const stripeWebhookSecret = defineSecret("STRIPE_WEBHOOK_SECRET");
|
|
14
|
+
// Optional: restrict the listed prices to a single Stripe product.
|
|
15
|
+
const stripeProductId = defineString("STRIPE_PRODUCT_ID", {default: ""});
|
|
16
|
+
|
|
17
|
+
// Firestore collection mapping a Firebase uid -> its Stripe customer id.
|
|
18
|
+
const CUSTOMERS_COLLECTION = "stripe_customers";
|
|
19
|
+
|
|
20
|
+
function stripeClient(): Stripe {
|
|
21
|
+
return new Stripe(stripeSecretKey.value());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getOrCreateCustomer(stripe: Stripe, uid: string): Promise<string> {
|
|
25
|
+
const db = admin.firestore();
|
|
26
|
+
const ref = db.collection(CUSTOMERS_COLLECTION).doc(uid);
|
|
27
|
+
const snap = await ref.get();
|
|
28
|
+
const existing = snap.data()?.customerId as string | undefined;
|
|
29
|
+
if (existing) return existing;
|
|
30
|
+
const customer = await stripe.customers.create({metadata: {firebaseUID: uid}});
|
|
31
|
+
await ref.set({customerId: customer.id, created_at: Timestamp.now()});
|
|
32
|
+
return customer.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function trialDaysFor(price: Stripe.Price, product: Stripe.Product): number | null {
|
|
36
|
+
const meta = price.metadata?.trial_days ?? product.metadata?.trial_days;
|
|
37
|
+
return meta ? Number(meta) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// listPrices — active recurring prices, mapped to the paywall offer contract.
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export const listPrices = onCall({secrets: [stripeSecretKey]}, async () => {
|
|
44
|
+
const stripe = stripeClient();
|
|
45
|
+
const params: Stripe.PriceListParams = {
|
|
46
|
+
active: true,
|
|
47
|
+
type: "recurring",
|
|
48
|
+
expand: ["data.product"],
|
|
49
|
+
limit: 100,
|
|
50
|
+
};
|
|
51
|
+
const productFilter = stripeProductId.value();
|
|
52
|
+
if (productFilter) params.product = productFilter;
|
|
53
|
+
|
|
54
|
+
const prices = await stripe.prices.list(params);
|
|
55
|
+
return prices.data
|
|
56
|
+
.filter((p) => Boolean(p.recurring))
|
|
57
|
+
.map((p) => {
|
|
58
|
+
const product = p.product as Stripe.Product;
|
|
59
|
+
const features = (product.marketing_features ?? [])
|
|
60
|
+
.map((f) => f.name)
|
|
61
|
+
.filter((n): n is string => Boolean(n));
|
|
62
|
+
return {
|
|
63
|
+
priceId: p.id,
|
|
64
|
+
productId: typeof p.product === "string" ? p.product : product.id,
|
|
65
|
+
productName: product.name ?? "",
|
|
66
|
+
description: product.description ?? "",
|
|
67
|
+
unitAmount: p.unit_amount ?? 0,
|
|
68
|
+
currency: p.currency,
|
|
69
|
+
interval: p.recurring?.interval ?? "month",
|
|
70
|
+
intervalCount: p.recurring?.interval_count ?? 1,
|
|
71
|
+
trialDays: trialDaysFor(p, product),
|
|
72
|
+
features,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// createCheckoutSession — hosted Stripe Checkout (mode=subscription).
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
export const createCheckoutSession = onCall(
|
|
81
|
+
{secrets: [stripeSecretKey]},
|
|
82
|
+
async (request) => {
|
|
83
|
+
const uid = request.auth?.uid;
|
|
84
|
+
if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
|
|
85
|
+
const priceId = request.data?.priceId as string | undefined;
|
|
86
|
+
if (!priceId) throw new HttpsError("invalid-argument", "priceId is required");
|
|
87
|
+
const successUrl = (request.data?.successUrl as string | undefined) ?? "";
|
|
88
|
+
const cancelUrl = (request.data?.cancelUrl as string | undefined) ?? successUrl;
|
|
89
|
+
|
|
90
|
+
const stripe = stripeClient();
|
|
91
|
+
const customerId = await getOrCreateCustomer(stripe, uid);
|
|
92
|
+
|
|
93
|
+
const price = await stripe.prices.retrieve(priceId, {expand: ["product"]});
|
|
94
|
+
const trialDays = trialDaysFor(price, price.product as Stripe.Product);
|
|
95
|
+
|
|
96
|
+
const session = await stripe.checkout.sessions.create({
|
|
97
|
+
mode: "subscription",
|
|
98
|
+
customer: customerId,
|
|
99
|
+
client_reference_id: uid,
|
|
100
|
+
line_items: [{price: priceId, quantity: 1}],
|
|
101
|
+
success_url: successUrl,
|
|
102
|
+
cancel_url: cancelUrl,
|
|
103
|
+
subscription_data: {
|
|
104
|
+
metadata: {firebaseUID: uid},
|
|
105
|
+
...(trialDays ? {trial_period_days: trialDays} : {}),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
return {url: session.url};
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// createPortalSession — Stripe Customer Portal (manage / cancel).
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
export const createPortalSession = onCall(
|
|
116
|
+
{secrets: [stripeSecretKey]},
|
|
117
|
+
async (request) => {
|
|
118
|
+
const uid = request.auth?.uid;
|
|
119
|
+
if (!uid) throw new HttpsError("unauthenticated", "Sign in required");
|
|
120
|
+
const returnUrl = (request.data?.returnUrl as string | undefined) ?? "";
|
|
121
|
+
|
|
122
|
+
const stripe = stripeClient();
|
|
123
|
+
const snap = await admin
|
|
124
|
+
.firestore()
|
|
125
|
+
.collection(CUSTOMERS_COLLECTION)
|
|
126
|
+
.doc(uid)
|
|
127
|
+
.get();
|
|
128
|
+
const customerId = snap.data()?.customerId as string | undefined;
|
|
129
|
+
if (!customerId) {
|
|
130
|
+
throw new HttpsError("failed-precondition", "No Stripe customer for user");
|
|
131
|
+
}
|
|
132
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
133
|
+
customer: customerId,
|
|
134
|
+
return_url: returnUrl,
|
|
135
|
+
});
|
|
136
|
+
return {url: session.url};
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// stripeWebhook — webhook. The ONLY writer of `subscriptions` for Stripe.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
function statusFromStripe(sub: Stripe.Subscription): SubscriptionStatus {
|
|
144
|
+
switch (sub.status) {
|
|
145
|
+
case "active":
|
|
146
|
+
case "trialing":
|
|
147
|
+
return SubscriptionStatus.ACTIVE;
|
|
148
|
+
default:
|
|
149
|
+
// canceled, unpaid, past_due, incomplete, incomplete_expired, paused
|
|
150
|
+
return SubscriptionStatus.EXPIRED;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function upsertFromStripeSubscription(sub: Stripe.Subscription): Promise<void> {
|
|
155
|
+
const uid = sub.metadata?.firebaseUID;
|
|
156
|
+
if (!uid) {
|
|
157
|
+
console.log("[stripe-webhook] subscription without firebaseUID metadata, skipping");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const now = Timestamp.now();
|
|
161
|
+
const existing = await subscriptionsRepository.getFromUserId(uid);
|
|
162
|
+
// In Stripe API v18 the billing period lives on each subscription item.
|
|
163
|
+
const item = sub.items.data[0];
|
|
164
|
+
const priceId = item?.price?.id ?? "";
|
|
165
|
+
const periodEnd = item?.current_period_end;
|
|
166
|
+
const expiration = periodEnd
|
|
167
|
+
? Timestamp.fromMillis(periodEnd * 1000)
|
|
168
|
+
: undefined;
|
|
169
|
+
|
|
170
|
+
const subscription = new Subscription(
|
|
171
|
+
{
|
|
172
|
+
userId: uid,
|
|
173
|
+
status: statusFromStripe(sub),
|
|
174
|
+
creationDate: existing?.creationDate ?? now,
|
|
175
|
+
lastUpdate: now,
|
|
176
|
+
expirationDate: expiration,
|
|
177
|
+
store: Stores.STRIPE,
|
|
178
|
+
productId: priceId,
|
|
179
|
+
},
|
|
180
|
+
subscriptionsRepository,
|
|
181
|
+
);
|
|
182
|
+
await subscription.save();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const stripeWebhook = onRequest(
|
|
186
|
+
{cors: false, secrets: [stripeSecretKey, stripeWebhookSecret]},
|
|
187
|
+
async (req, res) => {
|
|
188
|
+
const signature = req.header("stripe-signature");
|
|
189
|
+
if (!signature) {
|
|
190
|
+
res.status(400).send("Missing signature");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const stripe = stripeClient();
|
|
194
|
+
let event: Stripe.Event;
|
|
195
|
+
try {
|
|
196
|
+
event = stripe.webhooks.constructEvent(
|
|
197
|
+
req.rawBody,
|
|
198
|
+
signature,
|
|
199
|
+
stripeWebhookSecret.value(),
|
|
200
|
+
);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
console.log(`[stripe-webhook] signature verification failed: ${e}`);
|
|
203
|
+
res.status(400).send("Invalid signature");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
switch (event.type) {
|
|
208
|
+
case "customer.subscription.created":
|
|
209
|
+
case "customer.subscription.updated":
|
|
210
|
+
case "customer.subscription.deleted":
|
|
211
|
+
await upsertFromStripeSubscription(event.data.object as Stripe.Subscription);
|
|
212
|
+
break;
|
|
213
|
+
default:
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
res.status(200).send("ok");
|
|
217
|
+
} catch (e) {
|
|
218
|
+
error(e);
|
|
219
|
+
res.status(500).send(e instanceof Error ? e.message : String(e));
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
);
|
|
@@ -84,11 +84,11 @@ async function sendMetaEventsForSubscription(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export const
|
|
87
|
+
export const revenuecatWebhook = onRequest({cors: true, secrets: [revenuecatToken, metaAccessToken, metaDatasetId]}, async (
|
|
88
88
|
req: https.Request,
|
|
89
89
|
res: express.Response,
|
|
90
90
|
) => {
|
|
91
|
-
console.log("[
|
|
91
|
+
console.log("[revenuecatWebhook]");
|
|
92
92
|
const authorization = req.header("Authorization");
|
|
93
93
|
if (!authorization) {
|
|
94
94
|
console.log("Unauthorized - no token provided");
|
package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
CHANGED
|
Binary file
|
package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png
CHANGED
|
Binary file
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
/// import 'package:kasy_kit/components/components.dart';
|
|
7
7
|
library;
|
|
8
8
|
|
|
9
|
-
export '../core/sidebar/kasy_sidebar.dart';
|
|
10
9
|
export 'kasy_accordion.dart';
|
|
11
10
|
export 'kasy_alert.dart';
|
|
12
11
|
export 'kasy_app_bar.dart';
|
|
@@ -20,11 +19,15 @@ export 'kasy_checkbox.dart';
|
|
|
20
19
|
export 'kasy_chip.dart';
|
|
21
20
|
export 'kasy_date_picker.dart';
|
|
22
21
|
export 'kasy_dialog.dart';
|
|
22
|
+
export 'kasy_image_viewer.dart';
|
|
23
23
|
export 'kasy_otp_verification_bottom_sheet.dart';
|
|
24
|
+
export 'kasy_sidebar.dart';
|
|
24
25
|
export 'kasy_skeleton.dart';
|
|
26
|
+
export 'kasy_status_tag.dart';
|
|
25
27
|
export 'kasy_swipe_action.dart';
|
|
26
28
|
export 'kasy_tabs.dart';
|
|
27
29
|
export 'kasy_text_area.dart';
|
|
28
30
|
export 'kasy_text_field.dart';
|
|
29
31
|
export 'kasy_text_field_otp.dart';
|
|
30
32
|
export 'kasy_toast.dart';
|
|
33
|
+
export 'kasy_web_header.dart';
|
|
@@ -29,9 +29,17 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|
|
29
29
|
import 'package:flutter/material.dart';
|
|
30
30
|
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
|
|
31
31
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
32
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
32
33
|
|
|
33
34
|
/// Inner toolbar band height (orbit hit targets, title baseline).
|
|
34
35
|
const double kasyAppBarToolbarRowHeight = 44;
|
|
36
|
+
|
|
37
|
+
/// Effective toolbar band height. [KasyAppBar] serves phone and tablet only — on
|
|
38
|
+
/// desktop the richer web header takes over — so the band keeps a single
|
|
39
|
+
/// compact height across every viewport where the app bar appears.
|
|
40
|
+
double kasyAppBarToolbarRowHeightOf(BuildContext context) =>
|
|
41
|
+
kasyAppBarToolbarRowHeight;
|
|
42
|
+
|
|
35
43
|
const double kasyAppBarTitleFontScale = 0.92;
|
|
36
44
|
|
|
37
45
|
/// Tight vertical padding inside the frost, above/below [kasyAppBarToolbarRowHeight].
|
|
@@ -48,9 +56,13 @@ enum KasyAppBarScrollTreatment { overlay, intrinsicScroll }
|
|
|
48
56
|
|
|
49
57
|
/// Vertical inset for scroll/viewport when using [KasyAppBarScrollTreatment.overlay].
|
|
50
58
|
double kasyAppBarBodyTopOverlap(BuildContext context) {
|
|
59
|
+
// On desktop the app bar hides (the web header owns the top chrome).
|
|
60
|
+
if (MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
51
63
|
return MediaQuery.paddingOf(context).top +
|
|
52
64
|
kasyAppBarChromePaddingTop +
|
|
53
|
-
|
|
65
|
+
kasyAppBarToolbarRowHeightOf(context) +
|
|
54
66
|
kasyAppBarChromePaddingBottom;
|
|
55
67
|
}
|
|
56
68
|
|
|
@@ -112,13 +124,11 @@ class KasyFrostedChromeBackground extends StatelessWidget {
|
|
|
112
124
|
});
|
|
113
125
|
|
|
114
126
|
/// Translucent tint so blurred content shows through; tuned per brightness.
|
|
127
|
+
/// Derived from the global `surface` token so the bar lifts off the canvas
|
|
128
|
+
/// and follows light/dark automatically.
|
|
115
129
|
Color _tint(BuildContext context) {
|
|
116
130
|
final bool dark = Theme.of(context).brightness == Brightness.dark;
|
|
117
|
-
|
|
118
|
-
return const Color(0xFF161618).withValues(alpha: 0.88);
|
|
119
|
-
}
|
|
120
|
-
// White (surface) instead of background grey so the bar lifts off the canvas.
|
|
121
|
-
return const Color(0xFFFFFFFF).withValues(alpha: 0.82);
|
|
131
|
+
return context.colors.surface.withValues(alpha: dark ? 0.88 : 0.82);
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
@override
|
|
@@ -227,11 +237,16 @@ class KasyAppBar extends StatelessWidget {
|
|
|
227
237
|
|
|
228
238
|
@override
|
|
229
239
|
Widget build(BuildContext context) {
|
|
240
|
+
// On desktop the web header owns the top chrome, so the page app bar hides.
|
|
241
|
+
if (MediaQuery.sizeOf(context).width >= DeviceType.large.breakpoint) {
|
|
242
|
+
return const SizedBox.shrink();
|
|
243
|
+
}
|
|
230
244
|
final Color orbFg = context.colors.onSurface;
|
|
231
245
|
final Color orbFill = kasyChromeOrbFillColor(context);
|
|
232
246
|
final VoidCallback handleBack = onBack ?? () => Navigator.maybePop(context);
|
|
233
247
|
|
|
234
|
-
final double rowHeight =
|
|
248
|
+
final double rowHeight =
|
|
249
|
+
toolbarHeight ?? kasyAppBarToolbarRowHeightOf(context);
|
|
235
250
|
final Widget leading = switch (style) {
|
|
236
251
|
KasyAppBarStyle.rootTab => SizedBox(
|
|
237
252
|
width: 44,
|
|
@@ -153,9 +153,9 @@ class KasyAvatar extends StatelessWidget {
|
|
|
153
153
|
width: _d,
|
|
154
154
|
height: _d,
|
|
155
155
|
fit: BoxFit.cover,
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// frame
|
|
156
|
+
// Avoids the blur from progressive JPEG decoding:
|
|
157
|
+
// while the image loads, show the fallback; when the first
|
|
158
|
+
// frame is ready, do a smooth crossfade.
|
|
159
159
|
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
|
160
160
|
if (wasSynchronouslyLoaded) return child;
|
|
161
161
|
return SizedBox(
|
|
@@ -301,7 +301,7 @@ class KasyAvatar extends StatelessWidget {
|
|
|
301
301
|
width: badge,
|
|
302
302
|
height: badge,
|
|
303
303
|
decoration: BoxDecoration(
|
|
304
|
-
color:
|
|
304
|
+
color: context.colors.success,
|
|
305
305
|
shape: BoxShape.circle,
|
|
306
306
|
border: Border.all(color: context.colors.surface, width: 2),
|
|
307
307
|
),
|
|
@@ -451,8 +451,9 @@ _KasyAvatarColors _colorsForTone(
|
|
|
451
451
|
final KasyColors k = context.colors;
|
|
452
452
|
final bool dark = context.isDark;
|
|
453
453
|
|
|
454
|
-
// Accent color per tone
|
|
455
|
-
//
|
|
454
|
+
// Accent color per tone, from the global theme tokens: the "blue" tone maps
|
|
455
|
+
// to the primary accent, success/warning/danger to their semantic tokens,
|
|
456
|
+
// neutral to the foreground.
|
|
456
457
|
Color accentColor() => switch (tone) {
|
|
457
458
|
KasyAvatarTone.blue => k.primary,
|
|
458
459
|
KasyAvatarTone.neutral => k.grey3,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
2
|
import 'package:flutter/material.dart';
|
|
3
|
-
import 'package:flutter/services.dart';
|
|
4
3
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
4
|
+
import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
|
|
5
5
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
6
6
|
import 'package:kasy_kit/core/widgets/kasy_pressable_depth.dart';
|
|
7
7
|
|
|
@@ -496,8 +496,8 @@ class _KasyButtonShell extends StatelessWidget {
|
|
|
496
496
|
|
|
497
497
|
Widget _wrapFocus(Widget child, BorderRadius borderRadius) {
|
|
498
498
|
if (!enabled || onPressed == null) return child;
|
|
499
|
-
return
|
|
500
|
-
onActivate: onPressed
|
|
499
|
+
return KasyFocusRing(
|
|
500
|
+
onActivate: onPressed,
|
|
501
501
|
borderRadius: borderRadius,
|
|
502
502
|
child: child,
|
|
503
503
|
);
|
|
@@ -625,30 +625,28 @@ class _KasyButtonShell extends StatelessWidget {
|
|
|
625
625
|
|
|
626
626
|
if (pressEffect == KasyButtonPressEffect.both) {
|
|
627
627
|
final bool isDark = Theme.of(context).brightness == Brightness.dark;
|
|
628
|
-
return
|
|
629
|
-
KasyPressableDepth(
|
|
630
|
-
semanticLabel: _accessibilityLabel,
|
|
631
|
-
onPressed: onPressed!,
|
|
632
|
-
clipBorderRadius: resolvedRadius,
|
|
633
|
-
hapticFeedbackEnabled: hapticFeedbackEnabled,
|
|
634
|
-
pressOverlayColor: palette.foreground.withValues(
|
|
635
|
-
alpha: isDark ? 0.04 : 0.10,
|
|
636
|
-
),
|
|
637
|
-
child: painted,
|
|
638
|
-
),
|
|
639
|
-
resolvedRadius,
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return _wrapFocus(
|
|
644
|
-
KasyPressableDepth(
|
|
628
|
+
return KasyPressableDepth(
|
|
645
629
|
semanticLabel: _accessibilityLabel,
|
|
646
630
|
onPressed: onPressed!,
|
|
647
631
|
clipBorderRadius: resolvedRadius,
|
|
648
632
|
hapticFeedbackEnabled: hapticFeedbackEnabled,
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
633
|
+
pressOverlayColor: palette.foreground.withValues(
|
|
634
|
+
alpha: isDark ? 0.04 : 0.10,
|
|
635
|
+
),
|
|
636
|
+
// Wrap the focus ring around the painted visual, not the pressable: the
|
|
637
|
+
// pressable enforces a 44px min tap target and stretches to its parent,
|
|
638
|
+
// so ringing it would draw an oversized oval around a small round orb.
|
|
639
|
+
child: _wrapFocus(painted, resolvedRadius),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return KasyPressableDepth(
|
|
644
|
+
semanticLabel: _accessibilityLabel,
|
|
645
|
+
onPressed: onPressed!,
|
|
646
|
+
clipBorderRadius: resolvedRadius,
|
|
647
|
+
hapticFeedbackEnabled: hapticFeedbackEnabled,
|
|
648
|
+
// Ring hugs the visual, not the (larger) tap target. See note above.
|
|
649
|
+
child: _wrapFocus(painted, resolvedRadius),
|
|
652
650
|
);
|
|
653
651
|
}
|
|
654
652
|
|
|
@@ -840,10 +838,10 @@ class _KasyButtonMetrics {
|
|
|
840
838
|
final EdgeInsets horizontalPadding;
|
|
841
839
|
final double labelFontSize;
|
|
842
840
|
|
|
843
|
-
///
|
|
841
|
+
/// Icon next to the label (buttons with text).
|
|
844
842
|
final double iconSize;
|
|
845
843
|
|
|
846
|
-
///
|
|
844
|
+
/// Glyph size inside the icon-only button (may differ from [iconSize]).
|
|
847
845
|
final double iconOnlyGlyphSize;
|
|
848
846
|
final double loadingSpinnerExtent;
|
|
849
847
|
|
|
@@ -858,77 +856,3 @@ class _KasyButtonMetrics {
|
|
|
858
856
|
});
|
|
859
857
|
}
|
|
860
858
|
|
|
861
|
-
/// Makes a button keyboard-navigable (Tab) and activates it on Enter/Space.
|
|
862
|
-
/// Shows a focus ring when focused. Inert on touch-only platforms.
|
|
863
|
-
class _KeyboardFocus extends StatefulWidget {
|
|
864
|
-
final VoidCallback onActivate;
|
|
865
|
-
final Widget child;
|
|
866
|
-
final BorderRadius borderRadius;
|
|
867
|
-
|
|
868
|
-
const _KeyboardFocus({
|
|
869
|
-
required this.onActivate,
|
|
870
|
-
required this.child,
|
|
871
|
-
required this.borderRadius,
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
@override
|
|
875
|
-
State<_KeyboardFocus> createState() => _KeyboardFocusState();
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
class _KeyboardFocusState extends State<_KeyboardFocus> {
|
|
879
|
-
late final FocusNode _node;
|
|
880
|
-
bool _focused = false;
|
|
881
|
-
|
|
882
|
-
@override
|
|
883
|
-
void initState() {
|
|
884
|
-
super.initState();
|
|
885
|
-
_node = FocusNode();
|
|
886
|
-
_node.addListener(_onFocusChange);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
@override
|
|
890
|
-
void dispose() {
|
|
891
|
-
_node.removeListener(_onFocusChange);
|
|
892
|
-
_node.dispose();
|
|
893
|
-
super.dispose();
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
void _onFocusChange() {
|
|
897
|
-
if (mounted) setState(() => _focused = _node.hasFocus);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
|
901
|
-
if (event is KeyDownEvent &&
|
|
902
|
-
(event.logicalKey == LogicalKeyboardKey.enter ||
|
|
903
|
-
event.logicalKey == LogicalKeyboardKey.space ||
|
|
904
|
-
event.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
|
905
|
-
widget.onActivate();
|
|
906
|
-
return KeyEventResult.handled;
|
|
907
|
-
}
|
|
908
|
-
return KeyEventResult.ignored;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
@override
|
|
912
|
-
Widget build(BuildContext context) {
|
|
913
|
-
final Color focusColor = Theme.of(context).colorScheme.primary;
|
|
914
|
-
return Focus(
|
|
915
|
-
focusNode: _node,
|
|
916
|
-
onKeyEvent: _onKeyEvent,
|
|
917
|
-
child: AnimatedContainer(
|
|
918
|
-
duration: const Duration(milliseconds: 120),
|
|
919
|
-
decoration: _focused
|
|
920
|
-
? BoxDecoration(
|
|
921
|
-
borderRadius: widget.borderRadius,
|
|
922
|
-
boxShadow: [
|
|
923
|
-
BoxShadow(
|
|
924
|
-
color: focusColor.withValues(alpha: 0.5),
|
|
925
|
-
spreadRadius: 2,
|
|
926
|
-
),
|
|
927
|
-
],
|
|
928
|
-
)
|
|
929
|
-
: const BoxDecoration(),
|
|
930
|
-
child: widget.child,
|
|
931
|
-
),
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
}
|