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
|
@@ -12,6 +12,7 @@ import 'package:kasy_kit/features/notifications/api/notifications_api.dart';
|
|
|
12
12
|
import 'package:kasy_kit/features/settings/ui/components/admin/send_push_notifier.dart';
|
|
13
13
|
import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
|
|
14
14
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
15
|
+
import 'package:package_info_plus/package_info_plus.dart';
|
|
15
16
|
|
|
16
17
|
class SendPushNotificationPage extends ConsumerStatefulWidget {
|
|
17
18
|
const SendPushNotificationPage({super.key});
|
|
@@ -38,6 +39,15 @@ class _SendPushNotificationPageState
|
|
|
38
39
|
|
|
39
40
|
bool _sendToAll = false;
|
|
40
41
|
final List<String> _emails = [];
|
|
42
|
+
String _appName = '';
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
void initState() {
|
|
46
|
+
super.initState();
|
|
47
|
+
PackageInfo.fromPlatform().then((info) {
|
|
48
|
+
if (mounted) setState(() => _appName = info.appName);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
41
51
|
|
|
42
52
|
@override
|
|
43
53
|
void dispose() {
|
|
@@ -165,6 +175,28 @@ class _SendPushNotificationPageState
|
|
|
165
175
|
child: Column(
|
|
166
176
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
167
177
|
children: [
|
|
178
|
+
Padding(
|
|
179
|
+
padding: const EdgeInsets.only(
|
|
180
|
+
left: KasySpacing.xs,
|
|
181
|
+
bottom: KasySpacing.sm,
|
|
182
|
+
),
|
|
183
|
+
child: Text(
|
|
184
|
+
tr.send_push_preview_label.toUpperCase(),
|
|
185
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
186
|
+
color: context.colors.muted,
|
|
187
|
+
letterSpacing: 1.2,
|
|
188
|
+
fontWeight: FontWeight.w700,
|
|
189
|
+
fontSize: 12,
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
_NotificationPreview(
|
|
194
|
+
appName: _appName,
|
|
195
|
+
title: _titleCtrl.text,
|
|
196
|
+
body: _bodyCtrl.text,
|
|
197
|
+
imageUrl: _imageCtrl.text,
|
|
198
|
+
),
|
|
199
|
+
const SizedBox(height: KasySpacing.lg),
|
|
168
200
|
SettingsSwitchTile(
|
|
169
201
|
icon: KasyIcons.notificationActive,
|
|
170
202
|
title: tr.send_push_to_all,
|
|
@@ -214,6 +246,7 @@ class _SendPushNotificationPageState
|
|
|
214
246
|
hint: tr.send_push_image_hint,
|
|
215
247
|
controller: _imageCtrl,
|
|
216
248
|
focusNode: _imageFocus,
|
|
249
|
+
onChanged: (_) => setState(() {}),
|
|
217
250
|
textInputAction: TextInputAction.next,
|
|
218
251
|
onEditingComplete: () => _routeFocus.requestFocus(),
|
|
219
252
|
),
|
|
@@ -364,3 +397,206 @@ class _EmailChip extends StatelessWidget {
|
|
|
364
397
|
);
|
|
365
398
|
}
|
|
366
399
|
}
|
|
400
|
+
|
|
401
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
402
|
+
// Live notification preview — an iOS-style banner that mirrors what the push
|
|
403
|
+
// will look like, updating as the admin types. Cosmetic only (no logic).
|
|
404
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
class _NotificationPreview extends StatelessWidget {
|
|
407
|
+
final String appName;
|
|
408
|
+
final String title;
|
|
409
|
+
final String body;
|
|
410
|
+
final String imageUrl;
|
|
411
|
+
|
|
412
|
+
const _NotificationPreview({
|
|
413
|
+
required this.appName,
|
|
414
|
+
required this.title,
|
|
415
|
+
required this.body,
|
|
416
|
+
required this.imageUrl,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
@override
|
|
420
|
+
Widget build(BuildContext context) {
|
|
421
|
+
final tr = t.settings.admin;
|
|
422
|
+
final String name = appName.trim().isEmpty ? 'App' : appName.trim();
|
|
423
|
+
final String url = imageUrl.trim();
|
|
424
|
+
final bool showImage = url.startsWith('http');
|
|
425
|
+
final bool titleEmpty = title.trim().isEmpty;
|
|
426
|
+
final bool bodyEmpty = body.trim().isEmpty;
|
|
427
|
+
|
|
428
|
+
return Container(
|
|
429
|
+
padding: const EdgeInsets.all(12),
|
|
430
|
+
decoration: BoxDecoration(
|
|
431
|
+
color: context.colors.surface,
|
|
432
|
+
borderRadius: BorderRadius.circular(20),
|
|
433
|
+
border: Border.all(
|
|
434
|
+
color: context.colors.outline.withValues(
|
|
435
|
+
alpha: context.isDark ? 0.4 : 0.2,
|
|
436
|
+
),
|
|
437
|
+
),
|
|
438
|
+
boxShadow: [
|
|
439
|
+
BoxShadow(
|
|
440
|
+
color: Colors.black.withValues(alpha: context.isDark ? 0.45 : 0.10),
|
|
441
|
+
blurRadius: 24,
|
|
442
|
+
offset: const Offset(0, 10),
|
|
443
|
+
),
|
|
444
|
+
],
|
|
445
|
+
),
|
|
446
|
+
child: Row(
|
|
447
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
448
|
+
children: [
|
|
449
|
+
_AppIconBadge(name: name),
|
|
450
|
+
const SizedBox(width: 10),
|
|
451
|
+
Expanded(
|
|
452
|
+
child: Column(
|
|
453
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
454
|
+
mainAxisSize: MainAxisSize.min,
|
|
455
|
+
children: [
|
|
456
|
+
Row(
|
|
457
|
+
children: [
|
|
458
|
+
Expanded(
|
|
459
|
+
child: Text(
|
|
460
|
+
name.toUpperCase(),
|
|
461
|
+
maxLines: 1,
|
|
462
|
+
overflow: TextOverflow.ellipsis,
|
|
463
|
+
style: context.textTheme.labelSmall?.copyWith(
|
|
464
|
+
color: context.colors.muted,
|
|
465
|
+
letterSpacing: 0.4,
|
|
466
|
+
fontWeight: FontWeight.w600,
|
|
467
|
+
),
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
const SizedBox(width: 6),
|
|
471
|
+
Text(
|
|
472
|
+
tr.send_push_preview_now,
|
|
473
|
+
style: context.textTheme.labelSmall?.copyWith(
|
|
474
|
+
color: context.colors.muted,
|
|
475
|
+
),
|
|
476
|
+
),
|
|
477
|
+
],
|
|
478
|
+
),
|
|
479
|
+
const SizedBox(height: 3),
|
|
480
|
+
Text(
|
|
481
|
+
titleEmpty ? tr.send_push_preview_title_placeholder : title.trim(),
|
|
482
|
+
maxLines: 1,
|
|
483
|
+
overflow: TextOverflow.ellipsis,
|
|
484
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
485
|
+
color: titleEmpty
|
|
486
|
+
? context.colors.muted
|
|
487
|
+
: context.colors.onSurface,
|
|
488
|
+
fontWeight: FontWeight.w600,
|
|
489
|
+
),
|
|
490
|
+
),
|
|
491
|
+
const SizedBox(height: 2),
|
|
492
|
+
Text(
|
|
493
|
+
bodyEmpty ? tr.send_push_preview_body_placeholder : body.trim(),
|
|
494
|
+
maxLines: 2,
|
|
495
|
+
overflow: TextOverflow.ellipsis,
|
|
496
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
497
|
+
color: context.colors.muted,
|
|
498
|
+
height: 1.3,
|
|
499
|
+
),
|
|
500
|
+
),
|
|
501
|
+
],
|
|
502
|
+
),
|
|
503
|
+
),
|
|
504
|
+
if (showImage) ...[
|
|
505
|
+
const SizedBox(width: 10),
|
|
506
|
+
_PreviewThumb(url: url),
|
|
507
|
+
],
|
|
508
|
+
],
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// Rounded-square app icon (iOS app-icon look). Uses the REAL launcher icon
|
|
515
|
+
/// (`assets/images/icon.png`) so the preview always mirrors the app's actual
|
|
516
|
+
/// logo — change the icon and this follows. Falls back to a tinted square with
|
|
517
|
+
/// the app's initial if the asset can't be loaded.
|
|
518
|
+
class _AppIconBadge extends StatelessWidget {
|
|
519
|
+
final String name;
|
|
520
|
+
const _AppIconBadge({required this.name});
|
|
521
|
+
|
|
522
|
+
@override
|
|
523
|
+
Widget build(BuildContext context) {
|
|
524
|
+
return ClipRRect(
|
|
525
|
+
borderRadius: BorderRadius.circular(10),
|
|
526
|
+
child: Image.asset(
|
|
527
|
+
'assets/images/icon.png',
|
|
528
|
+
width: 40,
|
|
529
|
+
height: 40,
|
|
530
|
+
fit: BoxFit.cover,
|
|
531
|
+
errorBuilder: (context, error, stack) => _fallback(context),
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
Widget _fallback(BuildContext context) {
|
|
537
|
+
final String letter =
|
|
538
|
+
name.isNotEmpty ? name.substring(0, 1).toUpperCase() : 'A';
|
|
539
|
+
return Container(
|
|
540
|
+
width: 40,
|
|
541
|
+
height: 40,
|
|
542
|
+
decoration: BoxDecoration(
|
|
543
|
+
gradient: LinearGradient(
|
|
544
|
+
begin: Alignment.topLeft,
|
|
545
|
+
end: Alignment.bottomRight,
|
|
546
|
+
colors: [
|
|
547
|
+
context.colors.primary,
|
|
548
|
+
context.colors.primary.withValues(alpha: 0.7),
|
|
549
|
+
],
|
|
550
|
+
),
|
|
551
|
+
borderRadius: BorderRadius.circular(10),
|
|
552
|
+
),
|
|
553
|
+
alignment: Alignment.center,
|
|
554
|
+
child: Text(
|
|
555
|
+
letter,
|
|
556
|
+
style: context.textTheme.titleMedium?.copyWith(
|
|
557
|
+
color: context.colors.onPrimary,
|
|
558
|
+
fontWeight: FontWeight.w800,
|
|
559
|
+
),
|
|
560
|
+
),
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/// Thumbnail of the notification image, loaded live from the URL. Keeps the old
|
|
566
|
+
/// frame while a new URL loads (gaplessPlayback) and shows a fallback on error.
|
|
567
|
+
class _PreviewThumb extends StatelessWidget {
|
|
568
|
+
final String url;
|
|
569
|
+
const _PreviewThumb({required this.url});
|
|
570
|
+
|
|
571
|
+
@override
|
|
572
|
+
Widget build(BuildContext context) {
|
|
573
|
+
return ClipRRect(
|
|
574
|
+
borderRadius: BorderRadius.circular(8),
|
|
575
|
+
child: SizedBox(
|
|
576
|
+
width: 40,
|
|
577
|
+
height: 40,
|
|
578
|
+
child: Image.network(
|
|
579
|
+
url,
|
|
580
|
+
fit: BoxFit.cover,
|
|
581
|
+
gaplessPlayback: true,
|
|
582
|
+
loadingBuilder: (context, child, progress) =>
|
|
583
|
+
progress == null ? child : _placeholder(context, loading: true),
|
|
584
|
+
errorBuilder: (context, error, stack) =>
|
|
585
|
+
_placeholder(context, loading: false),
|
|
586
|
+
),
|
|
587
|
+
),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
Widget _placeholder(BuildContext context, {required bool loading}) {
|
|
592
|
+
return Container(
|
|
593
|
+
color: context.colors.surfaceNeutralSoft,
|
|
594
|
+
alignment: Alignment.center,
|
|
595
|
+
child: Icon(
|
|
596
|
+
loading ? Icons.image_outlined : Icons.broken_image_outlined,
|
|
597
|
+
size: 18,
|
|
598
|
+
color: context.colors.muted,
|
|
599
|
+
),
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
@@ -354,7 +354,7 @@ class _AvatarContent extends StatelessWidget {
|
|
|
354
354
|
|
|
355
355
|
@override
|
|
356
356
|
Widget build(BuildContext context) {
|
|
357
|
-
// 1.
|
|
357
|
+
// 1. Temporary file picked from the gallery (highest priority)
|
|
358
358
|
if (localImage != null) {
|
|
359
359
|
return KasyAvatar(
|
|
360
360
|
diameter: diameter,
|
|
@@ -376,7 +376,7 @@ class _AvatarContent extends StatelessWidget {
|
|
|
376
376
|
|
|
377
377
|
final id = userId;
|
|
378
378
|
if (id != null) {
|
|
379
|
-
// 2. URL
|
|
379
|
+
// 2. Cached URL: synchronous, no FutureBuilder, no fallback flash
|
|
380
380
|
final cachedUrl = getCachedAvatarUrl(id);
|
|
381
381
|
if (cachedUrl != null && cachedUrl.isNotEmpty) {
|
|
382
382
|
return KasyAvatar(
|
|
@@ -387,7 +387,7 @@ class _AvatarContent extends StatelessWidget {
|
|
|
387
387
|
);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
// 3. URL
|
|
390
|
+
// 3. URL not cached: async fetch (first access or after upload)
|
|
391
391
|
return FutureBuilder<String>(
|
|
392
392
|
future: resolveAvatarUrl(id),
|
|
393
393
|
builder: (context, snapshot) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/components/kasy_avatar.dart';
|
|
4
|
+
import 'package:kasy_kit/components/kasy_avatar_presets.dart';
|
|
5
|
+
import 'package:kasy_kit/core/data/models/user.dart';
|
|
6
|
+
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
7
|
+
import 'package:kasy_kit/features/settings/ui/widgets/avatar_utils.dart';
|
|
8
|
+
|
|
9
|
+
/// The signed-in user's avatar, resolved through the exact same path as the
|
|
10
|
+
/// settings screen — so it behaves identically across the **Firebase, Supabase
|
|
11
|
+
/// and API** backends. [avatar_utils] (the URL resolver) is swapped per backend
|
|
12
|
+
/// by the CLI, and the user identity comes from [userStateNotifierProvider]
|
|
13
|
+
/// (backend-agnostic), so this widget never references a specific backend.
|
|
14
|
+
///
|
|
15
|
+
/// Resolution order: the user's `avatarPath` URL → the in-memory cache →
|
|
16
|
+
/// the backend's avatar store (async). Falls back to a [gradient] fill while
|
|
17
|
+
/// loading, for guests, or when no photo exists.
|
|
18
|
+
class KasyUserAvatar extends ConsumerWidget {
|
|
19
|
+
const KasyUserAvatar({
|
|
20
|
+
super.key,
|
|
21
|
+
this.diameter = 36,
|
|
22
|
+
this.gradient = KasyAvatarGradients.indigo,
|
|
23
|
+
this.onTap,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/// Avatar diameter in logical pixels.
|
|
27
|
+
final double diameter;
|
|
28
|
+
|
|
29
|
+
/// Gradient shown when the user has no photo (or while it loads).
|
|
30
|
+
final KasyAvatarGradientData gradient;
|
|
31
|
+
|
|
32
|
+
/// Tap handler (open account menu / profile). Null = inert.
|
|
33
|
+
final VoidCallback? onTap;
|
|
34
|
+
|
|
35
|
+
KasyAvatar _avatar([ImageProvider? image]) => KasyAvatar(
|
|
36
|
+
diameter: diameter,
|
|
37
|
+
image: image,
|
|
38
|
+
backgroundGradient: gradient,
|
|
39
|
+
onTap: onTap,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
44
|
+
final User user = ref.watch(userStateNotifierProvider).user;
|
|
45
|
+
|
|
46
|
+
// 1. A direct URL on the user record (social logins; Supabase/API users).
|
|
47
|
+
final String? avatarPath = switch (user) {
|
|
48
|
+
AuthenticatedUserData(:final avatarPath) => avatarPath,
|
|
49
|
+
_ => null,
|
|
50
|
+
};
|
|
51
|
+
if (avatarPath != null && avatarPath.isNotEmpty) {
|
|
52
|
+
return _avatar(NetworkImage(avatarPath));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
final String? userId = user.idOrNull;
|
|
56
|
+
if (userId != null) {
|
|
57
|
+
// 2. Cached URL — synchronous, avoids a fallback flash.
|
|
58
|
+
final String? cached = getCachedAvatarUrl(userId);
|
|
59
|
+
if (cached != null && cached.isNotEmpty) {
|
|
60
|
+
return _avatar(NetworkImage(cached));
|
|
61
|
+
}
|
|
62
|
+
// 3. Resolve through the backend's avatar store (async, per-backend).
|
|
63
|
+
return FutureBuilder<String>(
|
|
64
|
+
future: resolveAvatarUrl(userId),
|
|
65
|
+
builder: (context, snapshot) {
|
|
66
|
+
final String? url = snapshot.data;
|
|
67
|
+
return _avatar(
|
|
68
|
+
(url != null && url.isNotEmpty) ? NetworkImage(url) : null,
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Guest / loading → gradient fill.
|
|
75
|
+
return _avatar();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -15,6 +15,19 @@ enum SubscriptionStatus {
|
|
|
15
15
|
CANCELLED,
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/// Where the subscription was purchased (its origin). Used to route the user to
|
|
19
|
+
/// the correct management flow regardless of the device they are currently on.
|
|
20
|
+
/// `STRIPE` means the subscription was bought on the web via Stripe.
|
|
21
|
+
enum SubscriptionStore {
|
|
22
|
+
PLAY_STORE,
|
|
23
|
+
APPLE_STORE,
|
|
24
|
+
EARLY_BIRD,
|
|
25
|
+
STRIPE,
|
|
26
|
+
// Forward-compat: any value the backend sends that we don't know yet maps
|
|
27
|
+
// here, so parsing never throws and routing safely falls back to platform.
|
|
28
|
+
unknown,
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
|
|
19
32
|
@freezed
|
|
20
33
|
sealed class SubscriptionEntity with _$SubscriptionEntity {
|
|
@@ -32,6 +45,10 @@ sealed class SubscriptionEntity with _$SubscriptionEntity {
|
|
|
32
45
|
@TimestampConverter()
|
|
33
46
|
DateTime? periodEndDate,
|
|
34
47
|
@JsonKey(name: 'status') required SubscriptionStatus status,
|
|
48
|
+
// Origin of the subscription (apple/play/stripe/...). Nullable + tolerant
|
|
49
|
+
// parsing so old records (no `store`) and future values never break.
|
|
50
|
+
@JsonKey(name: 'store', unknownEnumValue: SubscriptionStore.unknown)
|
|
51
|
+
SubscriptionStore? store,
|
|
35
52
|
}) = SubscriptionEntityData;
|
|
36
53
|
|
|
37
54
|
factory SubscriptionEntity.fromJson(String id, Map<String, Object?> json) =>
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import 'package:flutter/foundation.dart';
|
|
2
|
-
import 'package:
|
|
2
|
+
import 'package:flutter/services.dart' show PlatformException;
|
|
3
|
+
import 'package:kasy_kit/core/data/models/entitlement.dart';
|
|
3
4
|
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
4
|
-
import 'package:kasy_kit/
|
|
5
|
+
import 'package:kasy_kit/environments.dart';
|
|
6
|
+
import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
|
|
7
|
+
import 'package:kasy_kit/features/subscriptions/api/revenuecat_product.dart';
|
|
8
|
+
import 'package:kasy_kit/features/subscriptions/api/subscription_payment_api.dart';
|
|
5
9
|
import 'package:logger/logger.dart';
|
|
6
10
|
import 'package:purchases_flutter/purchases_flutter.dart';
|
|
7
11
|
import 'package:universal_io/io.dart';
|
|
8
12
|
import 'package:url_launcher/url_launcher.dart';
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// We chose to use RevenueCat for in-app subscription
|
|
15
|
-
// but you can use any other provider (RevenueCat, ...)
|
|
14
|
+
// We chose to use RevenueCat for in-app subscription (mobile: App Store / Play
|
|
15
|
+
// Store). Web subscriptions are handled by the separate Stripe module via
|
|
16
|
+
// [StripePaymentApi]; both implement [SubscriptionPaymentApi].
|
|
16
17
|
// ---
|
|
17
18
|
// We wrap the RevenueCat API to be able to fake it properly (Glassfy use static methods which are hard for tests)
|
|
18
|
-
class RevenueCatPaymentApi {
|
|
19
|
+
class RevenueCatPaymentApi implements SubscriptionPaymentApi {
|
|
19
20
|
bool _hasInit = false;
|
|
20
21
|
final Environment _environment;
|
|
21
22
|
|
|
@@ -30,16 +31,7 @@ class RevenueCatPaymentApi {
|
|
|
30
31
|
return key.startsWith('test_');
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/// Web Billing sandbox keys (`rcb_sb_`) must not ship in release.
|
|
34
|
-
static bool isRevenueCatWebSandboxKey(String? key) {
|
|
35
|
-
if (key == null || key.isEmpty) return false;
|
|
36
|
-
return key.startsWith('rcb_sb_');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
String? _apiKeyForCurrentPlatform() {
|
|
40
|
-
if (kIsWeb) {
|
|
41
|
-
return _environment.revenueCatWebApiKey;
|
|
42
|
-
}
|
|
43
35
|
if (Platform.isAndroid) {
|
|
44
36
|
return _environment.revenueCatAndroidApiKey;
|
|
45
37
|
}
|
|
@@ -52,13 +44,26 @@ class RevenueCatPaymentApi {
|
|
|
52
44
|
bool _mustSkipConfigureDueToSandboxKeyInRelease() {
|
|
53
45
|
if (!kReleaseMode) return false;
|
|
54
46
|
final String? key = _apiKeyForCurrentPlatform();
|
|
55
|
-
if (kIsWeb) {
|
|
56
|
-
return isRevenueCatWebSandboxKey(key);
|
|
57
|
-
}
|
|
58
47
|
return isRevenueCatTestStorePublicKey(key);
|
|
59
48
|
}
|
|
60
49
|
|
|
50
|
+
/// Map a RevenueCat [EntitlementInfo] to the provider-agnostic [Entitlement].
|
|
51
|
+
Entitlement _toEntitlement(EntitlementInfo e) => Entitlement(
|
|
52
|
+
identifier: e.identifier,
|
|
53
|
+
isActive: e.isActive,
|
|
54
|
+
isInTrial: e.periodType == PeriodType.trial,
|
|
55
|
+
willRenew: e.willRenew,
|
|
56
|
+
productId: e.productIdentifier,
|
|
57
|
+
expirationDate: e.expirationDate != null
|
|
58
|
+
? DateTime.tryParse(e.expirationDate!)
|
|
59
|
+
: null,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
@override
|
|
61
63
|
Future<void> init() async {
|
|
64
|
+
// RevenueCat is mobile-only (Android/iOS). Web subscriptions are handled by
|
|
65
|
+
// the Stripe module when present; never configure RevenueCat on web.
|
|
66
|
+
if (kIsWeb) return;
|
|
62
67
|
if (!_environment.isRevenueCatConfigured) {
|
|
63
68
|
Logger().w('=== RevenueCat API key not provided ===');
|
|
64
69
|
Logger().w(
|
|
@@ -77,15 +82,7 @@ class RevenueCatPaymentApi {
|
|
|
77
82
|
}
|
|
78
83
|
await Purchases.setLogLevel(kDebugMode ? LogLevel.debug : LogLevel.warn);
|
|
79
84
|
PurchasesConfiguration configuration;
|
|
80
|
-
if (
|
|
81
|
-
final webKey = _environment.revenueCatWebApiKey;
|
|
82
|
-
if (webKey == null || webKey.isEmpty) {
|
|
83
|
-
throw Exception(
|
|
84
|
-
'RevenueCat Web API key not configured. Set RC_WEB_API_KEY.',
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
configuration = PurchasesConfiguration(webKey);
|
|
88
|
-
} else if (Platform.isAndroid) {
|
|
85
|
+
if (Platform.isAndroid) {
|
|
89
86
|
configuration = PurchasesConfiguration(
|
|
90
87
|
_environment.revenueCatAndroidApiKey!,
|
|
91
88
|
);
|
|
@@ -99,6 +96,7 @@ class RevenueCatPaymentApi {
|
|
|
99
96
|
}
|
|
100
97
|
|
|
101
98
|
// We use a custom subscriber id to be able to identify the user
|
|
99
|
+
@override
|
|
102
100
|
Future<void> initUser(String userId) async {
|
|
103
101
|
if (kDebugMode && !_environment.isRevenueCatConfigured) {
|
|
104
102
|
Logger().w('=== RevenueCat API key not provided ===');
|
|
@@ -115,6 +113,7 @@ class RevenueCatPaymentApi {
|
|
|
115
113
|
}
|
|
116
114
|
|
|
117
115
|
// We can dissociate the user from the subscription
|
|
116
|
+
@override
|
|
118
117
|
Future<void> disconnectUser() async {
|
|
119
118
|
if (!_hasInit) {
|
|
120
119
|
return;
|
|
@@ -122,6 +121,7 @@ class RevenueCatPaymentApi {
|
|
|
122
121
|
await Purchases.logOut();
|
|
123
122
|
}
|
|
124
123
|
|
|
124
|
+
@override
|
|
125
125
|
Future<List<SubscriptionProduct>> getOffers(String? offerId) async {
|
|
126
126
|
if (kDebugMode && !_environment.isRevenueCatConfigured) {
|
|
127
127
|
Logger().d(
|
|
@@ -211,6 +211,25 @@ class RevenueCatPaymentApi {
|
|
|
211
211
|
return Purchases.purchase(purchaseParams);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
@override
|
|
215
|
+
Future<void> purchaseProduct(SubscriptionProduct product) async {
|
|
216
|
+
if (product is! RevenueCatProduct) {
|
|
217
|
+
throw ArgumentError(
|
|
218
|
+
'RevenueCatPaymentApi can only purchase a RevenueCatProduct',
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await purchasePackage(product.revenueCatPackage);
|
|
223
|
+
} on PlatformException catch (e) {
|
|
224
|
+
final error = PurchasesErrorHelper.getErrorCode(e);
|
|
225
|
+
if (error == PurchasesErrorCode.purchaseCancelledError) {
|
|
226
|
+
throw UserCancelledPurchaseException();
|
|
227
|
+
}
|
|
228
|
+
rethrow;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@override
|
|
214
233
|
Future<void> presentCodeRedemptionSheet() {
|
|
215
234
|
if (TargetPlatform.iOS != defaultTargetPlatform) {
|
|
216
235
|
throw "Only available on iOS";
|
|
@@ -221,12 +240,11 @@ class RevenueCatPaymentApi {
|
|
|
221
240
|
return Purchases.presentCodeRedemptionSheet();
|
|
222
241
|
}
|
|
223
242
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
243
|
+
@override
|
|
244
|
+
Future<void> unsubscribe(SubscriptionStore? origin) async {
|
|
245
|
+
// RevenueCat only manages App Store / Play Store subscriptions, and only on
|
|
246
|
+
// the matching device. The caller (provider) already decided this is the
|
|
247
|
+
// right place to manage; here we just open the current platform's store.
|
|
230
248
|
if (Platform.isAndroid) {
|
|
231
249
|
launchUrl(
|
|
232
250
|
Uri.parse("https://play.google.com/store/account/subscriptions"),
|
|
@@ -266,6 +284,7 @@ class RevenueCatPaymentApi {
|
|
|
266
284
|
return result;
|
|
267
285
|
}
|
|
268
286
|
|
|
287
|
+
@override
|
|
269
288
|
Future<SubscriptionProduct?> getFromProductId(String productId) async {
|
|
270
289
|
if (!_hasInit) {
|
|
271
290
|
return null;
|
|
@@ -323,10 +342,10 @@ class RevenueCatPaymentApi {
|
|
|
323
342
|
return products;
|
|
324
343
|
}
|
|
325
344
|
|
|
345
|
+
@override
|
|
326
346
|
Future<void> restorePurchase() async {
|
|
327
347
|
if (kIsWeb) {
|
|
328
|
-
// Restore is not supported on web
|
|
329
|
-
// via the customer portal. Return without error.
|
|
348
|
+
// Restore is not supported on web for RevenueCat. Return without error.
|
|
330
349
|
return;
|
|
331
350
|
}
|
|
332
351
|
if (!_hasInit) {
|
|
@@ -338,20 +357,26 @@ class RevenueCatPaymentApi {
|
|
|
338
357
|
}
|
|
339
358
|
}
|
|
340
359
|
|
|
341
|
-
|
|
360
|
+
@override
|
|
361
|
+
Future<List<Entitlement>> getPermissions() async {
|
|
342
362
|
if (!_hasInit) {
|
|
343
363
|
return [];
|
|
344
364
|
}
|
|
345
365
|
final customerInfo = await Purchases.getCustomerInfo();
|
|
346
|
-
return customerInfo.entitlements.active.values
|
|
366
|
+
return customerInfo.entitlements.active.values
|
|
367
|
+
.map(_toEntitlement)
|
|
368
|
+
.toList();
|
|
347
369
|
}
|
|
348
370
|
|
|
349
|
-
|
|
371
|
+
@override
|
|
372
|
+
Future<List<Entitlement>?> getEntitlements() async {
|
|
350
373
|
if (!_hasInit) {
|
|
351
374
|
return [];
|
|
352
375
|
}
|
|
353
376
|
final customerInfo = await Purchases.getCustomerInfo();
|
|
354
|
-
return customerInfo.entitlements.active.values
|
|
377
|
+
return customerInfo.entitlements.active.values
|
|
378
|
+
.map(_toEntitlement)
|
|
379
|
+
.toList();
|
|
355
380
|
}
|
|
356
381
|
}
|
|
357
382
|
|
|
@@ -383,7 +408,3 @@ class SubscriptionProductAdapter {
|
|
|
383
408
|
}
|
|
384
409
|
}
|
|
385
410
|
}
|
|
386
|
-
|
|
387
|
-
class UserCancelledPurchaseException implements Exception {
|
|
388
|
-
UserCancelledPurchaseException();
|
|
389
|
-
}
|