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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
3
|
+
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
4
|
+
import 'package:purchases_flutter/purchases_flutter.dart';
|
|
5
|
+
|
|
6
|
+
/// RevenueCat-backed [SubscriptionProduct] (mobile: App Store / Play Store).
|
|
7
|
+
///
|
|
8
|
+
/// Lives in its own file (not in `subscription.dart`) so the subscription core
|
|
9
|
+
/// stays free of `purchases_flutter`. Only compiled when the RevenueCat module
|
|
10
|
+
/// is enabled.
|
|
11
|
+
class RevenueCatProduct implements SubscriptionProduct {
|
|
12
|
+
final Offering revenueCatOffer;
|
|
13
|
+
final Package revenueCatPackage;
|
|
14
|
+
|
|
15
|
+
RevenueCatProduct({
|
|
16
|
+
required this.revenueCatOffer,
|
|
17
|
+
required this.revenueCatPackage,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
String get skuId => revenueCatPackage.identifier;
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
String get id => revenueCatOffer.identifier;
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
String get description => revenueCatOffer.serverDescription;
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
String get label =>
|
|
31
|
+
revenueCatPackage.presentedOfferingContext.offeringIdentifier;
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
double get price => revenueCatPackage.storeProduct.price;
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
String get currency => revenueCatPackage.storeProduct.currencyCode;
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
int? get trialDays {
|
|
41
|
+
final introductory = revenueCatPackage.storeProduct.introductoryPrice;
|
|
42
|
+
if (introductory == null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (introductory.price == 0) {
|
|
46
|
+
final unit = introductory.periodUnit;
|
|
47
|
+
switch (unit) {
|
|
48
|
+
case PeriodUnit.day:
|
|
49
|
+
return introductory.periodNumberOfUnits;
|
|
50
|
+
case PeriodUnit.week:
|
|
51
|
+
return introductory.periodNumberOfUnits * 7;
|
|
52
|
+
case PeriodUnit.month:
|
|
53
|
+
return introductory.periodNumberOfUnits * 30;
|
|
54
|
+
case PeriodUnit.year:
|
|
55
|
+
return introductory.periodNumberOfUnits * 365;
|
|
56
|
+
default:
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
String? get promotion {
|
|
65
|
+
if (revenueCatOffer.metadata["promotions"] == null) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
final data =
|
|
69
|
+
revenueCatOffer.metadata["promotions"]! as Map<Object?, Object?>;
|
|
70
|
+
final locale = LocaleSettings.currentLocale.languageCode;
|
|
71
|
+
final id = skuId;
|
|
72
|
+
if (data.containsKey(id)) {
|
|
73
|
+
final promotion = data[id]! as Map<Object?, Object?>;
|
|
74
|
+
return promotion[locale]! as String;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
String? get title {
|
|
81
|
+
return revenueCatPackage.storeProduct.title;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
List<String>? get features {
|
|
86
|
+
final locale = LocaleSettings.currentLocale.languageCode;
|
|
87
|
+
if (revenueCatOffer.metadata[locale] == null) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
final data = revenueCatOffer.metadata[locale]! as Map<Object?, Object?>;
|
|
91
|
+
final featuerObj = data["features"]! as List<Object?>;
|
|
92
|
+
return featuerObj.map((e) => e! as String).toList();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
String get priceString {
|
|
97
|
+
return revenueCatPackage.storeProduct.priceString;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@override
|
|
101
|
+
Duration get duration =>
|
|
102
|
+
switch (revenueCatPackage.storeProduct.subscriptionPeriod) {
|
|
103
|
+
'P1W' => const Duration(days: 7),
|
|
104
|
+
'P1M' => const Duration(days: 30),
|
|
105
|
+
'P3M' => const Duration(days: 90),
|
|
106
|
+
'P6M' => const Duration(days: 180),
|
|
107
|
+
'P1Y' => const Duration(days: 365),
|
|
108
|
+
_ => Duration.zero,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
@override
|
|
112
|
+
DurationType get durationType {
|
|
113
|
+
final durationCpy = duration;
|
|
114
|
+
if (durationCpy.inDays == 7) {
|
|
115
|
+
return DurationType.week;
|
|
116
|
+
} else if (durationCpy.inDays == 30) {
|
|
117
|
+
return DurationType.month;
|
|
118
|
+
} else if (durationCpy.inDays == 90) {
|
|
119
|
+
return DurationType.threeMonth;
|
|
120
|
+
} else if (durationCpy.inDays == 180) {
|
|
121
|
+
return DurationType.sixMonth;
|
|
122
|
+
} else if (durationCpy.inDays == 365) {
|
|
123
|
+
return DurationType.year;
|
|
124
|
+
}
|
|
125
|
+
return DurationType.lifetime;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
String formattedPrice(BuildContext context) {
|
|
130
|
+
final translatedDuration = switch (durationType) {
|
|
131
|
+
DurationType.week => Translations.of(context).premium.duration_weekly,
|
|
132
|
+
DurationType.year => Translations.of(context).premium.duration_annual,
|
|
133
|
+
DurationType.month => Translations.of(context).premium.duration_monthly,
|
|
134
|
+
DurationType.lifetime => Translations.of(
|
|
135
|
+
context,
|
|
136
|
+
).premium.duration_lifetime,
|
|
137
|
+
_ => "",
|
|
138
|
+
};
|
|
139
|
+
return "${revenueCatPackage.storeProduct.priceString} $translatedDuration";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@override
|
|
143
|
+
String pricePerMonth(BuildContext context) {
|
|
144
|
+
final translatedDuration = Translations.of(
|
|
145
|
+
context,
|
|
146
|
+
).premium.duration_monthly;
|
|
147
|
+
if (durationType == DurationType.lifetime) {
|
|
148
|
+
return revenueCatPackage.storeProduct.priceString;
|
|
149
|
+
}
|
|
150
|
+
final pricePerMonth = switch (durationType) {
|
|
151
|
+
DurationType.year => price / 12,
|
|
152
|
+
DurationType.threeMonth => price / 3,
|
|
153
|
+
DurationType.sixMonth => price / 6,
|
|
154
|
+
DurationType.week => (price * 4) / 12,
|
|
155
|
+
_ => 1,
|
|
156
|
+
};
|
|
157
|
+
final pricePerMonthString = pricePerMonth.toStringAsFixed(2);
|
|
158
|
+
final money = revenueCatPackage.storeProduct.currencyCode;
|
|
159
|
+
if (money == "USD") {
|
|
160
|
+
return "\$$pricePerMonthString/$translatedDuration";
|
|
161
|
+
} else if (money == "EUR") {
|
|
162
|
+
return "$pricePerMonthString/$translatedDuration";
|
|
163
|
+
}
|
|
164
|
+
return "$pricePerMonthString $money/$translatedDuration";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
String? pricePerYear(BuildContext context) {
|
|
169
|
+
final translatedDuration = Translations.of(context).premium.duration_annual;
|
|
170
|
+
if (durationType == DurationType.lifetime) {
|
|
171
|
+
return revenueCatPackage.storeProduct.priceString;
|
|
172
|
+
}
|
|
173
|
+
final pricePerMonth = switch (durationType) {
|
|
174
|
+
DurationType.year => price,
|
|
175
|
+
DurationType.threeMonth => price * 4,
|
|
176
|
+
DurationType.sixMonth => price * 2,
|
|
177
|
+
DurationType.week => (price * 4) * 12,
|
|
178
|
+
_ => 1,
|
|
179
|
+
};
|
|
180
|
+
final pricePerMonthString = pricePerMonth.toStringAsFixed(2);
|
|
181
|
+
final money = revenueCatPackage.storeProduct.currencyCode;
|
|
182
|
+
if (money == "USD") {
|
|
183
|
+
return "\$$pricePerMonthString/$translatedDuration";
|
|
184
|
+
} else if (money == "EUR") {
|
|
185
|
+
return "$pricePerMonthString/$translatedDuration";
|
|
186
|
+
}
|
|
187
|
+
return "$pricePerMonthString $money/$translatedDuration";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import 'package:cloud_functions/cloud_functions.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/features/subscriptions/api/stripe_product.dart';
|
|
4
|
+
|
|
5
|
+
final stripeBackendApiProvider = Provider<StripeBackendApi>(
|
|
6
|
+
(ref) => StripeBackendApi(
|
|
7
|
+
functions: FirebaseFunctions.instanceFor(region: 'europe-west1'),
|
|
8
|
+
),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
/// Talks to the Stripe backend (Firebase Cloud Functions). The user identity is
|
|
12
|
+
/// taken server-side from the authenticated callable context — never trusted
|
|
13
|
+
/// from the client. This file is replaced per backend (Supabase / API) so the
|
|
14
|
+
/// rest of the Stripe client code stays backend-agnostic.
|
|
15
|
+
class StripeBackendApi {
|
|
16
|
+
final FirebaseFunctions _functions;
|
|
17
|
+
|
|
18
|
+
StripeBackendApi({required FirebaseFunctions functions})
|
|
19
|
+
: _functions = functions;
|
|
20
|
+
|
|
21
|
+
/// Active recurring prices, mapped to paywall offers.
|
|
22
|
+
Future<List<StripeProduct>> listPrices() async {
|
|
23
|
+
final res = await _functions
|
|
24
|
+
.httpsCallable('stripeFunctions-listPrices')
|
|
25
|
+
.call();
|
|
26
|
+
final list = (res.data as List).cast<dynamic>();
|
|
27
|
+
return list
|
|
28
|
+
.map((e) => StripeProduct.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
29
|
+
.toList();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Create a hosted Checkout session for [priceId] and return its URL.
|
|
33
|
+
Future<String> createCheckoutSession({
|
|
34
|
+
required String priceId,
|
|
35
|
+
String? successUrl,
|
|
36
|
+
String? cancelUrl,
|
|
37
|
+
}) async {
|
|
38
|
+
final res = await _functions
|
|
39
|
+
.httpsCallable('stripeFunctions-createCheckoutSession')
|
|
40
|
+
.call({
|
|
41
|
+
'priceId': priceId,
|
|
42
|
+
if (successUrl != null) 'successUrl': successUrl,
|
|
43
|
+
if (cancelUrl != null) 'cancelUrl': cancelUrl,
|
|
44
|
+
});
|
|
45
|
+
return (res.data as Map)['url'] as String;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Create a Customer Portal session (manage / cancel) and return its URL.
|
|
49
|
+
Future<String> createPortalSession({String? returnUrl}) async {
|
|
50
|
+
final res = await _functions
|
|
51
|
+
.httpsCallable('stripeFunctions-createPortalSession')
|
|
52
|
+
.call({if (returnUrl != null) 'returnUrl': returnUrl});
|
|
53
|
+
return (res.data as Map)['url'] as String;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import 'package:kasy_kit/core/data/models/entitlement.dart';
|
|
2
|
+
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
3
|
+
import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
|
|
4
|
+
import 'package:kasy_kit/features/subscriptions/api/stripe_backend_api.dart';
|
|
5
|
+
import 'package:kasy_kit/features/subscriptions/api/subscription_payment_api.dart';
|
|
6
|
+
import 'package:url_launcher/url_launcher.dart';
|
|
7
|
+
|
|
8
|
+
/// Stripe web subscription provider.
|
|
9
|
+
///
|
|
10
|
+
/// Uses hosted Stripe Checkout + Customer Portal: all card handling / PCI stay
|
|
11
|
+
/// on Stripe and the client only opens the URLs the backend returns. Premium
|
|
12
|
+
/// status lives server-side (the Stripe webhook writes the `subscriptions`
|
|
13
|
+
/// record), so this provider exposes no local entitlements — the repository
|
|
14
|
+
/// reads the status from the backend just like for RevenueCat.
|
|
15
|
+
class StripePaymentApi implements SubscriptionPaymentApi {
|
|
16
|
+
final StripeBackendApi _backend;
|
|
17
|
+
|
|
18
|
+
StripePaymentApi({required StripeBackendApi backend}) : _backend = backend;
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
Future<void> init() async {}
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
Future<void> initUser(String userId) async {}
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
Future<void> disconnectUser() async {}
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
Future<List<SubscriptionProduct>> getOffers(String? offerId) {
|
|
31
|
+
return _backend.listPrices();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
Future<void> purchaseProduct(SubscriptionProduct product) async {
|
|
36
|
+
final returnUrl = Uri.base.toString();
|
|
37
|
+
final url = await _backend.createCheckoutSession(
|
|
38
|
+
priceId: product.skuId,
|
|
39
|
+
successUrl: returnUrl,
|
|
40
|
+
cancelUrl: returnUrl,
|
|
41
|
+
);
|
|
42
|
+
await _open(url);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
Future<void> unsubscribe(SubscriptionStore? origin) async {
|
|
47
|
+
final url = await _backend.createPortalSession(
|
|
48
|
+
returnUrl: Uri.base.toString(),
|
|
49
|
+
);
|
|
50
|
+
await _open(url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
Future<void> restorePurchase() async {
|
|
55
|
+
// Stripe status is server-side (webhook -> backend). Nothing to restore on
|
|
56
|
+
// the device; the repository re-reads the subscription from the backend.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
Future<List<Entitlement>> getPermissions() async => [];
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
Future<List<Entitlement>?> getEntitlements() async => [];
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
Future<SubscriptionProduct?> getFromProductId(String productId) async {
|
|
67
|
+
final prices = await _backend.listPrices();
|
|
68
|
+
for (final price in prices) {
|
|
69
|
+
if (price.skuId == productId) return price;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
Future<void> presentCodeRedemptionSheet() async {}
|
|
76
|
+
|
|
77
|
+
Future<void> _open(String url) async {
|
|
78
|
+
// On web, redirect the current tab so the user returns to the app after
|
|
79
|
+
// Stripe Checkout / the Customer Portal.
|
|
80
|
+
await launchUrl(Uri.parse(url), webOnlyWindowName: '_self');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
3
|
+
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
4
|
+
|
|
5
|
+
/// Stripe-backed [SubscriptionProduct] (web). Built from a Stripe Price returned
|
|
6
|
+
/// by the `stripe-list-prices` backend function, so the existing paywall UI
|
|
7
|
+
/// renders it exactly like a RevenueCat product — no UI changes needed.
|
|
8
|
+
class StripeProduct implements SubscriptionProduct {
|
|
9
|
+
/// Stripe Price id (`price_...`) — used as the sku and to start checkout.
|
|
10
|
+
final String priceId;
|
|
11
|
+
|
|
12
|
+
/// Stripe Product id (`prod_...`).
|
|
13
|
+
final String productId;
|
|
14
|
+
|
|
15
|
+
/// Stripe Product name.
|
|
16
|
+
final String productName;
|
|
17
|
+
|
|
18
|
+
final String _description;
|
|
19
|
+
|
|
20
|
+
/// Amount in the currency's minor unit (e.g. cents).
|
|
21
|
+
final int unitAmount;
|
|
22
|
+
|
|
23
|
+
final String _currency;
|
|
24
|
+
|
|
25
|
+
/// Recurring interval reported by Stripe: `day` | `week` | `month` | `year`.
|
|
26
|
+
final String interval;
|
|
27
|
+
|
|
28
|
+
/// Recurring interval count (e.g. 3 months).
|
|
29
|
+
final int intervalCount;
|
|
30
|
+
|
|
31
|
+
final int? _trialDays;
|
|
32
|
+
|
|
33
|
+
final List<String>? _features;
|
|
34
|
+
|
|
35
|
+
StripeProduct({
|
|
36
|
+
required this.priceId,
|
|
37
|
+
required this.productId,
|
|
38
|
+
required this.productName,
|
|
39
|
+
required String description,
|
|
40
|
+
required this.unitAmount,
|
|
41
|
+
required String currency,
|
|
42
|
+
required this.interval,
|
|
43
|
+
this.intervalCount = 1,
|
|
44
|
+
int? trialDays,
|
|
45
|
+
List<String>? features,
|
|
46
|
+
}) : _description = description,
|
|
47
|
+
_currency = currency,
|
|
48
|
+
_trialDays = trialDays,
|
|
49
|
+
_features = features;
|
|
50
|
+
|
|
51
|
+
factory StripeProduct.fromJson(Map<String, dynamic> json) => StripeProduct(
|
|
52
|
+
priceId: json['priceId'] as String,
|
|
53
|
+
productId: (json['productId'] ?? '') as String,
|
|
54
|
+
productName: (json['productName'] ?? '') as String,
|
|
55
|
+
description: (json['description'] ?? '') as String,
|
|
56
|
+
unitAmount: (json['unitAmount'] as num?)?.toInt() ?? 0,
|
|
57
|
+
currency: (json['currency'] ?? 'usd') as String,
|
|
58
|
+
interval: (json['interval'] ?? 'month') as String,
|
|
59
|
+
intervalCount: (json['intervalCount'] as num?)?.toInt() ?? 1,
|
|
60
|
+
trialDays: (json['trialDays'] as num?)?.toInt(),
|
|
61
|
+
features: (json['features'] as List?)?.map((e) => e.toString()).toList(),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
String get skuId => priceId;
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
String get id => productId;
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
String get description => _description;
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
String get label => productName;
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
double get price => unitAmount / 100.0;
|
|
78
|
+
|
|
79
|
+
@override
|
|
80
|
+
String get currency => _currency.toUpperCase();
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
int? get trialDays => _trialDays;
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
String? get promotion => null;
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
String? get title => productName;
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
List<String>? get features => _features;
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
String get priceString => _formatMoney(price);
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
Duration get duration => switch (durationType) {
|
|
99
|
+
DurationType.week => const Duration(days: 7),
|
|
100
|
+
DurationType.month => const Duration(days: 30),
|
|
101
|
+
DurationType.threeMonth => const Duration(days: 90),
|
|
102
|
+
DurationType.sixMonth => const Duration(days: 180),
|
|
103
|
+
DurationType.year => const Duration(days: 365),
|
|
104
|
+
DurationType.lifetime => Duration.zero,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
DurationType get durationType {
|
|
109
|
+
switch (interval) {
|
|
110
|
+
case 'week':
|
|
111
|
+
return DurationType.week;
|
|
112
|
+
case 'year':
|
|
113
|
+
return DurationType.year;
|
|
114
|
+
case 'month':
|
|
115
|
+
if (intervalCount == 3) return DurationType.threeMonth;
|
|
116
|
+
if (intervalCount == 6) return DurationType.sixMonth;
|
|
117
|
+
if (intervalCount >= 12) return DurationType.year;
|
|
118
|
+
return DurationType.month;
|
|
119
|
+
default:
|
|
120
|
+
return DurationType.month;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
String _formatMoney(double value) {
|
|
125
|
+
final amount = value.toStringAsFixed(2);
|
|
126
|
+
if (currency == 'USD') return "\$$amount";
|
|
127
|
+
if (currency == 'EUR') return "$amount€";
|
|
128
|
+
return "$amount $currency";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@override
|
|
132
|
+
String formattedPrice(BuildContext context) {
|
|
133
|
+
final translatedDuration = switch (durationType) {
|
|
134
|
+
DurationType.week => Translations.of(context).premium.duration_weekly,
|
|
135
|
+
DurationType.year => Translations.of(context).premium.duration_annual,
|
|
136
|
+
DurationType.month => Translations.of(context).premium.duration_monthly,
|
|
137
|
+
DurationType.lifetime => Translations.of(
|
|
138
|
+
context,
|
|
139
|
+
).premium.duration_lifetime,
|
|
140
|
+
_ => "",
|
|
141
|
+
};
|
|
142
|
+
return "$priceString $translatedDuration";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
String pricePerMonth(BuildContext context) {
|
|
147
|
+
final translatedDuration = Translations.of(
|
|
148
|
+
context,
|
|
149
|
+
).premium.duration_monthly;
|
|
150
|
+
if (durationType == DurationType.lifetime) {
|
|
151
|
+
return priceString;
|
|
152
|
+
}
|
|
153
|
+
final monthly = switch (durationType) {
|
|
154
|
+
DurationType.year => price / 12,
|
|
155
|
+
DurationType.threeMonth => price / 3,
|
|
156
|
+
DurationType.sixMonth => price / 6,
|
|
157
|
+
DurationType.week => (price * 4) / 12,
|
|
158
|
+
_ => price,
|
|
159
|
+
};
|
|
160
|
+
return "${_formatMoney(monthly)}/$translatedDuration";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@override
|
|
164
|
+
String? pricePerYear(BuildContext context) {
|
|
165
|
+
final translatedDuration = Translations.of(context).premium.duration_annual;
|
|
166
|
+
if (durationType == DurationType.lifetime) {
|
|
167
|
+
return priceString;
|
|
168
|
+
}
|
|
169
|
+
final yearly = switch (durationType) {
|
|
170
|
+
DurationType.year => price,
|
|
171
|
+
DurationType.threeMonth => price * 4,
|
|
172
|
+
DurationType.sixMonth => price * 2,
|
|
173
|
+
DurationType.week => (price * 4) * 12,
|
|
174
|
+
_ => price * 12,
|
|
175
|
+
};
|
|
176
|
+
return "${_formatMoney(yearly)}/$translatedDuration";
|
|
177
|
+
}
|
|
178
|
+
}
|
package/templates/firebase/lib/features/{subscription → subscriptions}/api/subscription_api.dart
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
2
2
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
3
|
import 'package:kasy_kit/core/data/api/firestore_retry.dart';
|
|
4
|
-
import 'package:kasy_kit/features/
|
|
4
|
+
import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
|
|
5
5
|
|
|
6
6
|
final subscriptionApiProvider = Provider(
|
|
7
7
|
(ref) => SubscriptionApi(client: FirebaseFirestore.instance),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import 'package:kasy_kit/core/data/models/entitlement.dart';
|
|
2
|
+
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
3
|
+
import 'package:kasy_kit/features/subscriptions/api/entities/subscription_entity.dart';
|
|
4
|
+
|
|
5
|
+
/// Provider-agnostic contract for a subscription payment provider.
|
|
6
|
+
///
|
|
7
|
+
/// Implemented by `RevenueCatPaymentApi` (mobile: App Store / Play Store) and
|
|
8
|
+
/// `StripePaymentApi` (web). The repository depends on this interface only, so
|
|
9
|
+
/// the right provider is injected per platform and a project can ship either,
|
|
10
|
+
/// both, or (web-only) just Stripe — without pulling the other's SDK.
|
|
11
|
+
abstract interface class SubscriptionPaymentApi {
|
|
12
|
+
/// Configure the provider SDK (no-op on platforms it does not support).
|
|
13
|
+
Future<void> init();
|
|
14
|
+
|
|
15
|
+
/// Associate the current app user with the provider.
|
|
16
|
+
Future<void> initUser(String userId);
|
|
17
|
+
|
|
18
|
+
/// Dissociate the current user from the provider.
|
|
19
|
+
Future<void> disconnectUser();
|
|
20
|
+
|
|
21
|
+
/// Available offers shown on the paywall.
|
|
22
|
+
Future<List<SubscriptionProduct>> getOffers(String? offerId);
|
|
23
|
+
|
|
24
|
+
/// Start the purchase flow for [product].
|
|
25
|
+
///
|
|
26
|
+
/// Throws [UserCancelledPurchaseException] when the user cancels.
|
|
27
|
+
Future<void> purchaseProduct(SubscriptionProduct product);
|
|
28
|
+
|
|
29
|
+
/// Open the management/cancellation flow for a subscription whose origin is
|
|
30
|
+
/// [origin] (where it was purchased). Implementations only open external
|
|
31
|
+
/// management when allowed on the current platform.
|
|
32
|
+
Future<void> unsubscribe(SubscriptionStore? origin);
|
|
33
|
+
|
|
34
|
+
/// Restore / re-sync previous purchases.
|
|
35
|
+
Future<void> restorePurchase();
|
|
36
|
+
|
|
37
|
+
/// Currently active entitlements (access rights).
|
|
38
|
+
Future<List<Entitlement>> getPermissions();
|
|
39
|
+
|
|
40
|
+
/// Currently active entitlements, or null when none can be determined.
|
|
41
|
+
Future<List<Entitlement>?> getEntitlements();
|
|
42
|
+
|
|
43
|
+
/// Resolve a [SubscriptionProduct] from a store product id.
|
|
44
|
+
Future<SubscriptionProduct?> getFromProductId(String productId);
|
|
45
|
+
|
|
46
|
+
/// iOS-only: present the offer-code redemption sheet.
|
|
47
|
+
Future<void> presentCodeRedemptionSheet();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Thrown when the user cancels a purchase flow.
|
|
51
|
+
class UserCancelledPurchaseException implements Exception {
|
|
52
|
+
UserCancelledPurchaseException();
|
|
53
|
+
}
|
package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api_provider.dart
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart';
|
|
2
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
3
|
+
import 'package:kasy_kit/core/config/features.dart';
|
|
4
|
+
import 'package:kasy_kit/environments.dart';
|
|
5
|
+
import 'package:kasy_kit/features/subscriptions/api/inapp_subscription_api.dart';
|
|
6
|
+
import 'package:kasy_kit/features/subscriptions/api/stripe_backend_api.dart';
|
|
7
|
+
import 'package:kasy_kit/features/subscriptions/api/stripe_payment_api.dart';
|
|
8
|
+
import 'package:kasy_kit/features/subscriptions/api/subscription_payment_api.dart';
|
|
9
|
+
|
|
10
|
+
/// Selects the subscription payment provider per platform × enabled feature:
|
|
11
|
+
/// - web + Stripe module -> [StripePaymentApi]
|
|
12
|
+
/// - mobile + RevenueCat -> [RevenueCatPaymentApi]
|
|
13
|
+
///
|
|
14
|
+
/// The CLI regenerates this file to match the modules a project enabled, so a
|
|
15
|
+
/// web-only Stripe app never references RevenueCat (and vice-versa).
|
|
16
|
+
final inAppSubscriptionApiProvider = Provider<SubscriptionPaymentApi>((ref) {
|
|
17
|
+
if (kIsWeb && withStripe) {
|
|
18
|
+
return StripePaymentApi(backend: ref.read(stripeBackendApiProvider));
|
|
19
|
+
}
|
|
20
|
+
return RevenueCatPaymentApi(environment: ref.read(environmentProvider));
|
|
21
|
+
});
|
|
@@ -7,13 +7,12 @@ import 'package:kasy_kit/core/states/translations.dart';
|
|
|
7
7
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
8
8
|
import 'package:kasy_kit/core/toast/toast_service.dart';
|
|
9
9
|
import 'package:kasy_kit/features/feedbacks/repositories/feature_request_repository.dart';
|
|
10
|
-
import 'package:kasy_kit/features/
|
|
10
|
+
import 'package:kasy_kit/features/subscriptions/api/subscription_payment_api.dart'
|
|
11
11
|
show UserCancelledPurchaseException;
|
|
12
|
-
import 'package:kasy_kit/features/
|
|
13
|
-
import 'package:kasy_kit/features/
|
|
12
|
+
import 'package:kasy_kit/features/subscriptions/providers/models/premium_state.dart';
|
|
13
|
+
import 'package:kasy_kit/features/subscriptions/repositories/subscription_repository.dart';
|
|
14
14
|
import 'package:kasy_kit/router.dart';
|
|
15
15
|
import 'package:logger/logger.dart';
|
|
16
|
-
import 'package:purchases_flutter/purchases_flutter.dart';
|
|
17
16
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
18
17
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
|
19
18
|
|
|
@@ -90,7 +89,7 @@ class PremiumStateNotifier extends _$PremiumStateNotifier {
|
|
|
90
89
|
if (TargetPlatform.iOS != defaultTargetPlatform) {
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
93
|
-
await
|
|
92
|
+
await _subscriptionRepository.presentCodeRedemptionSheet();
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
void selectOffer(SubscriptionProduct offer) {
|
|
@@ -184,7 +183,11 @@ class PremiumStateNotifier extends _$PremiumStateNotifier {
|
|
|
184
183
|
|
|
185
184
|
Future<void> unsubscribe() async {
|
|
186
185
|
// _analytics.logPageOpened("launch_unsubscribe_page");
|
|
187
|
-
|
|
186
|
+
// Route by the subscription's origin (store). The UI only reaches here when
|
|
187
|
+
// the subscription can be managed on the current platform; the cross-platform
|
|
188
|
+
// case is handled with an info dialog before this is called.
|
|
189
|
+
final store = _userState.subscription?.store;
|
|
190
|
+
await _subscriptionRepository.unsubscribe(store);
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
Future<void> saveUnsubscribeReason(String reason) async {
|