kasy-cli 1.32.0 → 1.35.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.
Files changed (169) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +66 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/apple-web.js +222 -0
  5. package/lib/commands/configure.js +3 -91
  6. package/lib/commands/doctor.js +20 -0
  7. package/lib/commands/facebook.js +189 -0
  8. package/lib/commands/new.js +61 -11
  9. package/lib/commands/release-version.js +234 -0
  10. package/lib/commands/update.js +27 -0
  11. package/lib/scaffold/CHANGELOG.json +27 -0
  12. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  14. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
  17. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  18. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  19. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  20. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  21. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  22. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
  23. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  24. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  25. package/lib/scaffold/generate.js +53 -4
  26. package/lib/scaffold/shared/generator-utils.js +18 -6
  27. package/lib/utils/apple-web.js +147 -0
  28. package/lib/utils/facebook.js +162 -0
  29. package/lib/utils/i18n/messages-en.js +85 -0
  30. package/lib/utils/i18n/messages-es.js +85 -0
  31. package/lib/utils/i18n/messages-pt.js +85 -0
  32. package/package.json +5 -2
  33. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  34. package/templates/firebase/AGENTS.md +170 -0
  35. package/templates/firebase/CLAUDE.md +16 -0
  36. package/templates/firebase/DESIGN_SYSTEM.md +269 -0
  37. package/templates/firebase/docs/auth-setup.en.md +4 -2
  38. package/templates/firebase/docs/auth-setup.es.md +4 -2
  39. package/templates/firebase/docs/auth-setup.pt.md +4 -2
  40. package/templates/firebase/firebase.json +56 -1
  41. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  42. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  43. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  44. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  45. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  46. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  47. package/templates/firebase/lib/components/components.dart +1 -0
  48. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  49. package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
  50. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  51. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  52. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  53. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  54. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  55. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  56. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  57. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  58. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  59. package/templates/firebase/lib/components/kasy_toast.dart +108 -73
  60. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  61. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  62. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  63. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  64. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  65. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  66. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  67. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  68. package/templates/firebase/lib/core/config/features.dart +5 -0
  69. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  70. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  71. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  72. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  73. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
  74. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  75. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  76. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  77. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  78. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  79. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  80. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  81. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  82. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  83. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  84. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  85. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  86. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  87. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  88. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  89. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  90. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  91. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
  92. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  93. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  94. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  95. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  96. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  97. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  98. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
  99. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  100. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  101. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  102. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  103. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  104. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  105. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  106. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  107. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  108. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
  109. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  110. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  111. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  112. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  113. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  114. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  115. package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
  116. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  117. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
  118. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  119. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
  120. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  121. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  122. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  123. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  124. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
  125. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
  126. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  127. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  128. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  129. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  130. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  131. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  132. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  133. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
  134. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  135. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  136. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  137. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  138. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  139. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  140. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  141. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  142. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  143. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  144. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  145. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  146. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  147. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  148. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  149. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  150. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  151. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  152. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  153. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  154. package/templates/firebase/lib/i18n/en.i18n.json +54 -7
  155. package/templates/firebase/lib/i18n/es.i18n.json +54 -7
  156. package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
  157. package/templates/firebase/lib/main.dart +11 -2
  158. package/templates/firebase/lib/router.dart +94 -13
  159. package/templates/firebase/pubspec.yaml +1 -1
  160. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  161. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  162. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  163. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  164. package/templates/firebase/tool/design_check.dart +152 -0
  165. package/templates/firebase/web/index.html +162 -14
  166. package/templates/firebase/assets/images/review.png +0 -0
  167. package/templates/firebase/assets/images/update.png +0 -0
  168. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
  169. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -1,19 +1,22 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
3
- import 'package:google_fonts/google_fonts.dart';
3
+ import 'package:go_router/go_router.dart';
4
+ import 'package:intl/intl.dart';
4
5
  import 'package:kasy_kit/components/components.dart';
6
+ import 'package:kasy_kit/core/data/models/subscription.dart';
5
7
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
6
8
  import 'package:kasy_kit/core/theme/theme.dart';
9
+ import 'package:kasy_kit/features/settings/ui/widgets/settings_tile.dart';
10
+ import 'package:kasy_kit/features/subscriptions/providers/premium_page_provider.dart';
7
11
  import 'package:kasy_kit/features/subscriptions/shared/subscription_management.dart';
8
- import 'package:kasy_kit/features/subscriptions/ui/widgets/premium_background.dart';
9
- import 'package:kasy_kit/features/subscriptions/ui/widgets/premium_card.dart';
10
- import 'package:kasy_kit/features/subscriptions/ui/widgets/unsubscribe_feedback_popup.dart';
11
12
  import 'package:kasy_kit/i18n/translations.g.dart';
12
13
 
13
14
  typedef OnTapUnSubscribe = void Function(String reason);
14
15
  typedef OnExit = void Function();
15
16
 
16
17
  class ActivePremiumContent extends ConsumerStatefulWidget {
18
+ // kept for API compatibility — the manage flow no longer uses a feedback
19
+ // reason collected here; PremiumPage still wires this for legacy callers.
17
20
  final OnTapUnSubscribe? onTap;
18
21
  final OnExit? onExit;
19
22
 
@@ -25,171 +28,344 @@ class ActivePremiumContent extends ConsumerStatefulWidget {
25
28
  }
26
29
 
27
30
  class _ActivePremiumContentState extends ConsumerState<ActivePremiumContent> {
28
- bool _isPopupShowing = false;
31
+ bool _isManaging = false;
32
+ bool _isRestoring = false;
29
33
 
30
- /// Decide where the subscription can be managed. If it was bought on another
31
- /// platform (e.g. on the web via Stripe while the user is now on iOS), we must
32
- /// not link out to an external provider (App Store / Play Store policy) — show
33
- /// an info dialog with a "restore purchases" action instead of the cancel flow.
34
- Future<void> _handleManageTap(BuildContext context) async {
34
+ void _goBack() {
35
+ if (widget.onExit != null) {
36
+ widget.onExit!();
37
+ } else if (context.canPop()) {
38
+ context.pop();
39
+ } else {
40
+ context.go('/');
41
+ }
42
+ }
43
+
44
+ /// Opens the subscription management interface for the current platform.
45
+ /// On iOS/Android with RevenueCat → App Store / Play Store subscriptions page.
46
+ /// On web with Stripe → Stripe Customer Portal.
47
+ /// Cross-platform mismatch → informational dialog (subscription managed elsewhere).
48
+ Future<void> _handleManageTap() async {
35
49
  final store = ref.read(userStateNotifierProvider).subscription?.store;
50
+ final t = Translations.of(context).activePremium;
51
+
36
52
  if (resolveManageLocation(store) == ManageLocation.elsewhere) {
37
- final t = Translations.of(context).activePremium;
38
- await showKasyConfirmDialog(
39
- context,
40
- title: t.managed_elsewhere_title,
41
- message: t.managed_elsewhere_description,
42
- cancelLabel: t.cancel_button,
43
- confirmLabel: t.restore_button,
44
- onConfirm: () {
45
- ref.read(userStateNotifierProvider.notifier).refreshSubscription();
46
- },
53
+ await showKasyBlurDialog<void>(
54
+ context: context,
55
+ builder: (dialogCtx) => KasyDialog(
56
+ leadingIcon: KasyIcons.payment,
57
+ iconTone: KasyDialogIconTone.info,
58
+ title: t.managed_elsewhere_title,
59
+ message: t.managed_elsewhere_description,
60
+ showCloseButton: false,
61
+ confirmLabel: t.cancel_button,
62
+ onConfirm: () => Navigator.of(dialogCtx).pop(),
63
+ ),
47
64
  );
48
65
  return;
49
66
  }
50
- await _showUnsubscribePopup(context);
67
+
68
+ if (_isManaging) return;
69
+ setState(() => _isManaging = true);
70
+ try {
71
+ await ref.read(premiumStateProvider.notifier).unsubscribe();
72
+ } finally {
73
+ if (mounted) setState(() => _isManaging = false);
74
+ }
51
75
  }
52
76
 
53
- Future<void> _showUnsubscribePopup(BuildContext context) async {
54
- if (_isPopupShowing) return;
55
- setState(() => _isPopupShowing = true);
77
+ /// Re-syncs the subscription from RevenueCat / Stripe backend.
78
+ Future<void> _handleRestoreTap() async {
79
+ if (_isRestoring) return;
80
+ setState(() => _isRestoring = true);
56
81
  try {
57
- await UnsubscribeFeedbackPopup.show(
58
- context: context,
59
- onConfirm: (String reason) {
60
- Navigator.of(context).pop();
61
- widget.onTap?.call(reason);
62
- },
63
- onCancel: () {},
82
+ await ref
83
+ .read(userStateNotifierProvider.notifier)
84
+ .refreshSubscription();
85
+ if (!mounted) return;
86
+ showKasyToast(
87
+ context,
88
+ title: Translations.of(context).premium.restore_success_title,
89
+ tone: KasyToastTone.success,
64
90
  );
65
91
  } finally {
66
- if (mounted) setState(() => _isPopupShowing = false);
92
+ if (mounted) setState(() => _isRestoring = false);
67
93
  }
68
94
  }
69
95
 
70
96
  @override
71
97
  Widget build(BuildContext context) {
72
98
  final userState = ref.watch(userStateNotifierProvider);
99
+ final t = Translations.of(context).activePremium;
100
+ final sub = userState.subscription;
101
+ final activeData = sub is SubscriptionStateData ? sub : null;
102
+ final planName = activeData?.activeOffer?.title ?? t.plan_fallback;
103
+ final price = activeData?.activeOffer?.price;
104
+ final currency = activeData?.activeOffer?.currency;
105
+ final entitlement = activeData?.entitlements?.firstOrNull;
106
+ final expirationDate = entitlement?.expirationDate;
107
+ final willRenew = entitlement?.willRenew ?? false;
108
+ final isInTrial = entitlement?.isInTrial ?? false;
109
+
110
+ return KasyOverlayScaffold(
111
+ title: t.billing_title,
112
+ onBack: _goBack,
113
+ slivers: [
114
+ SliverToBoxAdapter(
115
+ child: Padding(
116
+ // Horizontal gutter already comes from KasyOverlayScaffold; keep
117
+ // only vertical spacing here so the side padding matches the
118
+ // Notifications screen (a single gutter, not doubled).
119
+ padding: const EdgeInsets.fromLTRB(
120
+ 0,
121
+ KasySpacing.sm,
122
+ 0,
123
+ KasySpacing.xxl,
124
+ ),
125
+ child: Column(
126
+ crossAxisAlignment: CrossAxisAlignment.stretch,
127
+ children: [
128
+ // ── Plan info ─────────────────────────────────────────────
129
+ _SectionLabel(t.plan_label),
130
+ const SizedBox(height: KasySpacing.xs),
131
+ _PlanCard(
132
+ planName: planName,
133
+ price: price,
134
+ currency: currency,
135
+ expirationDate: expirationDate,
136
+ willRenew: willRenew,
137
+ isLifetime: sub?.isLifetime ?? false,
138
+ isInTrial: isInTrial,
139
+ ),
140
+
141
+ const SizedBox(height: KasySpacing.xl),
142
+
143
+ // ── Actions ───────────────────────────────────────────────
144
+ _ActionsCard(
145
+ onManage: _isManaging ? null : _handleManageTap,
146
+ onRestore: _handleRestoreTap,
147
+ isManaging: _isManaging,
148
+ isRestoring: _isRestoring,
149
+ manageLabel: t.manage_subscription,
150
+ restoreLabel: t.restore_purchases,
151
+ ),
152
+ ],
153
+ ),
154
+ ),
155
+ ),
156
+ ],
157
+ );
158
+ }
159
+ }
160
+
161
+ // ─── Private sub-widgets ──────────────────────────────────────────────────────
73
162
 
74
- return PremiumBackground(
75
- bgImagePath: "assets/images/premium-bg.jpg",
76
- child: Scaffold(
77
- backgroundColor: Colors.transparent,
78
- appBar: AppBar(
79
- backgroundColor: Colors.transparent,
80
- elevation: 0,
81
- foregroundColor: context.colors.onPrimary,
82
- leading: widget.onExit != null
83
- ? KasyButton.iconOnly(
84
- icon: KasyIcons.close,
85
- variant: KasyButtonVariant.ghost,
86
- foregroundColor: context.colors.onPrimary,
87
- onPressed: widget.onExit,
88
- semanticLabel:
89
- MaterialLocalizations.of(context).closeButtonTooltip,
90
- )
91
- : null,
92
- automaticallyImplyLeading: widget.onExit == null,
163
+ class _SectionLabel extends StatelessWidget {
164
+ final String label;
165
+ const _SectionLabel(this.label);
166
+
167
+ @override
168
+ Widget build(BuildContext context) {
169
+ return Padding(
170
+ padding: const EdgeInsets.only(left: KasySpacing.xs),
171
+ child: Text(
172
+ label,
173
+ style: context.kasyTextTheme.sectionLabel.copyWith(
174
+ color: context.colors.muted,
93
175
  ),
94
- body: Column(
95
- mainAxisAlignment: MainAxisAlignment.center,
96
- children: [
97
- Padding(
98
- padding: const EdgeInsets.symmetric(horizontal: KasySpacing.pageHorizontalGutter),
99
- child: PremiumCard(
100
- bgColor: context.colors.onBackground.withValues(alpha: .54),
101
- child: Column(
102
- mainAxisSize: MainAxisSize.min,
103
- children: [
104
- Padding(
105
- padding: const EdgeInsets.only(bottom: KasySpacing.sm),
106
- child: Chip(
107
- backgroundColor: context.colors.primary,
108
- label: Text(
109
- "ACTIVE",
110
- style: GoogleFonts.albertSans(
111
- fontSize: 12,
112
- fontWeight: FontWeight.w700,
113
- color: context.colors.onPrimary,
114
- ),
115
- ),
116
- ),
176
+ ),
177
+ );
178
+ }
179
+ }
180
+
181
+ class _PlanCard extends StatelessWidget {
182
+ final String planName;
183
+ final double? price;
184
+ final String? currency;
185
+ final DateTime? expirationDate;
186
+ final bool willRenew;
187
+ final bool isLifetime;
188
+ final bool isInTrial;
189
+
190
+ const _PlanCard({
191
+ required this.planName,
192
+ required this.price,
193
+ required this.currency,
194
+ required this.expirationDate,
195
+ required this.willRenew,
196
+ required this.isLifetime,
197
+ required this.isInTrial,
198
+ });
199
+
200
+ String? _formattedPrice(BuildContext context) {
201
+ if (price == null || currency == null || isLifetime) return null;
202
+ final locale = Localizations.localeOf(context).toString();
203
+ return NumberFormat.simpleCurrency(locale: locale, name: currency)
204
+ .format(price);
205
+ }
206
+
207
+ String? _dateLabel(BuildContext context, {String? formattedPrice}) {
208
+ if (isLifetime || expirationDate == null) return null;
209
+ final locale = Localizations.localeOf(context).toString();
210
+ final formatted = DateFormat.yMMMd(locale).format(expirationDate!);
211
+ final t = context.t.activePremium;
212
+ if (isInTrial) {
213
+ if (formattedPrice == null) return t.trial_until(date: formatted);
214
+ return t.charges_from(price: formattedPrice, date: formatted);
215
+ }
216
+ return willRenew ? t.renews_on(date: formatted) : t.expires_on(date: formatted);
217
+ }
218
+
219
+ @override
220
+ Widget build(BuildContext context) {
221
+ final t = context.t.activePremium;
222
+ final formattedPrice = _formattedPrice(context);
223
+ final dateLabel = _dateLabel(context, formattedPrice: formattedPrice);
224
+ return KasyCard(
225
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
226
+ padding: const EdgeInsets.symmetric(
227
+ horizontal: KasySpacing.md,
228
+ vertical: KasySpacing.xs,
229
+ ),
230
+ child: Column(
231
+ crossAxisAlignment: CrossAxisAlignment.stretch,
232
+ children: [
233
+ Padding(
234
+ padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
235
+ child: Row(
236
+ children: [
237
+ Icon(
238
+ KasyIcons.payment,
239
+ size: KasyIconSize.rowLeading,
240
+ color: context.colors.onSurface,
241
+ ),
242
+ const SizedBox(width: KasySpacing.sm),
243
+ Expanded(
244
+ child: Text(
245
+ planName,
246
+ style: context.textTheme.titleSmall?.copyWith(
247
+ color: context.colors.onSurface,
117
248
  ),
118
- Text(
119
- "Premium",
120
- textAlign: TextAlign.center,
121
- style: GoogleFonts.albertSans(
122
- fontSize: 32,
123
- fontWeight: FontWeight.w700,
124
- color: context.colors.onPrimary,
125
- ),
249
+ ),
250
+ ),
251
+ if (isInTrial)
252
+ _TrialBadge(label: t.trial_label)
253
+ else if (formattedPrice != null)
254
+ Text(
255
+ formattedPrice,
256
+ style: context.textTheme.bodyMedium?.copyWith(
257
+ color: context.colors.muted,
126
258
  ),
127
- const SizedBox(height: KasySpacing.smd),
128
- // for (var i = 0; i < features.length; i++)
129
- // Padding(
130
- // padding: const EdgeInsets.symmetric(horizontal: 8.0),
131
- // child: FeatureLine(
132
- // title: translations.features[i],
133
- // topPadding: i > 0 ? 4 : 0,
134
- // icon: switch (i) {
135
- // 0 => HugeIcons.strokeRoundedTaskDone02,
136
- // 1 => HugeIcons.strokeRoundedCheckmarkSquare01,
137
- // 2 => HugeIcons.strokeRoundedGame,
138
- // _ => Icons.check,
139
- // },
140
- // ),
141
- // ),
142
- const SizedBox(height: KasySpacing.xl),
143
- if (userState.subscription?.isLifetime == true)
144
- Padding(
145
- padding: const EdgeInsets.all(KasySpacing.lg),
146
- child: Text(
147
- Translations.of(
148
- context,
149
- ).activePremium.lifetime_user_description,
150
- style: context.textTheme.bodyMedium?.copyWith(
151
- fontWeight: FontWeight.w700,
152
- ),
153
- textAlign: TextAlign.center,
154
- ),
155
- ),
156
- // SliverToBoxAdapter(
157
- // child: Padding(
158
- // padding: hPadding,
159
- // child: ComparisonTable(
160
- // rows: ComparisonTableRow.getComparisonRows(
161
- // context,
162
- // ref.read(environmentProvider),
163
- // ),
164
- // freeColumnTitle:
165
- // ComparisonTableRow.getFreeColumnTitle(context),
166
- // premiumColumnTitle:
167
- // ComparisonTableRow.getPremiumColumnTitle(context),
168
- // ),
169
- // ),
170
- // ),
171
- if (userState.subscription?.isLifetime == false)
172
- Padding(
173
- padding: const EdgeInsets.fromLTRB(
174
- KasySpacing.pageHorizontalGutter,
175
- 72,
176
- KasySpacing.pageHorizontalGutter,
177
- KasySpacing.lg,
178
- ),
179
- child: KasyButton(
180
- label: Translations.of(context).activePremium.unsubscribe_button,
181
- variant: KasyButtonVariant.soft,
182
- expand: true,
183
- onPressed: () => _handleManageTap(context),
184
- ),
185
- ),
186
- ],
259
+ ),
260
+ ],
261
+ ),
262
+ ),
263
+ if (dateLabel != null)
264
+ Padding(
265
+ padding: const EdgeInsets.only(
266
+ left: KasyIconSize.rowLeading + KasySpacing.sm,
267
+ bottom: KasySpacing.sm,
268
+ ),
269
+ child: Text(
270
+ dateLabel,
271
+ style: context.textTheme.bodySmall?.copyWith(
272
+ color: context.colors.muted,
187
273
  ),
188
274
  ),
189
275
  ),
190
- ],
276
+ ],
277
+ ),
278
+ );
279
+ }
280
+ }
281
+
282
+ class _TrialBadge extends StatelessWidget {
283
+ final String label;
284
+ const _TrialBadge({required this.label});
285
+
286
+ @override
287
+ Widget build(BuildContext context) {
288
+ return Container(
289
+ padding: const EdgeInsets.symmetric(
290
+ horizontal: KasySpacing.sm,
291
+ vertical: 3,
292
+ ),
293
+ decoration: BoxDecoration(
294
+ color: context.colors.accent.withValues(alpha: 0.12),
295
+ borderRadius: BorderRadius.circular(KasyRadius.sm),
296
+ ),
297
+ child: Text(
298
+ label,
299
+ style: context.textTheme.labelSmall?.copyWith(
300
+ color: context.colors.accent,
301
+ fontWeight: FontWeight.w600,
191
302
  ),
192
303
  ),
193
304
  );
194
305
  }
195
306
  }
307
+
308
+ class _ActionsCard extends StatelessWidget {
309
+ final VoidCallback? onManage;
310
+ final VoidCallback onRestore;
311
+ final bool isManaging;
312
+ final bool isRestoring;
313
+ final String manageLabel;
314
+ final String restoreLabel;
315
+
316
+ const _ActionsCard({
317
+ required this.onManage,
318
+ required this.onRestore,
319
+ required this.isManaging,
320
+ required this.isRestoring,
321
+ required this.manageLabel,
322
+ required this.restoreLabel,
323
+ });
324
+
325
+ @override
326
+ Widget build(BuildContext context) {
327
+ return KasyCard(
328
+ borderRadius: BorderRadius.circular(KasyRadius.lg),
329
+ padding: const EdgeInsets.symmetric(
330
+ horizontal: KasySpacing.md,
331
+ vertical: KasySpacing.xs,
332
+ ),
333
+ child: Column(
334
+ crossAxisAlignment: CrossAxisAlignment.stretch,
335
+ children: [
336
+ SettingsTile(
337
+ icon: KasyIcons.payment,
338
+ title: manageLabel,
339
+ onTap: isManaging ? () {} : (onManage ?? () {}),
340
+ trailingWidget: isManaging
341
+ ? SizedBox(
342
+ width: KasyIconSize.rowTrailing,
343
+ height: KasyIconSize.rowTrailing,
344
+ child: CircularProgressIndicator.adaptive(
345
+ strokeWidth: 2,
346
+ valueColor: AlwaysStoppedAnimation(context.colors.muted),
347
+ ),
348
+ )
349
+ : null,
350
+ ),
351
+ const SettingsDivider(),
352
+ SettingsTile(
353
+ icon: KasyIcons.refresh,
354
+ title: restoreLabel,
355
+ onTap: isRestoring ? () {} : onRestore,
356
+ trailingWidget: isRestoring
357
+ ? SizedBox(
358
+ width: KasyIconSize.rowTrailing,
359
+ height: KasyIconSize.rowTrailing,
360
+ child: CircularProgressIndicator.adaptive(
361
+ strokeWidth: 2,
362
+ valueColor: AlwaysStoppedAnimation(context.colors.muted),
363
+ ),
364
+ )
365
+ : null,
366
+ ),
367
+ ],
368
+ ),
369
+ );
370
+ }
371
+ }
@@ -1,6 +1,5 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_animate/flutter_animate.dart';
3
- import 'package:google_fonts/google_fonts.dart';
4
3
  import 'package:kasy_kit/components/components.dart';
5
4
  import 'package:kasy_kit/core/data/models/subscription.dart';
6
5
  import 'package:kasy_kit/core/theme/theme.dart';
@@ -147,9 +146,7 @@ class PaywallMinimal extends StatelessWidget {
147
146
  child: Text(
148
147
  Translations.of(context).premium.title_1,
149
148
  textAlign: TextAlign.center,
150
- style: GoogleFonts.albertSans(
151
- fontSize: 28,
152
- fontWeight: FontWeight.w700,
149
+ style: context.kasyTextTheme.pageTitle.copyWith(
153
150
  color: context.colors.onPrimary,
154
151
  letterSpacing: -0.8,
155
152
  ),
@@ -191,7 +188,6 @@ class PaywallMinimal extends StatelessWidget {
191
188
  _effectiveOffer.formattedPrice(context),
192
189
  textAlign: TextAlign.center,
193
190
  style: context.textTheme.titleLarge?.copyWith(
194
- fontWeight: FontWeight.w600,
195
191
  color: context.colors.onPrimary,
196
192
  ),
197
193
  ),
@@ -123,8 +123,7 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
123
123
  child: Text(
124
124
  translations.title_1,
125
125
  textAlign: TextAlign.center,
126
- style: context.textTheme.headlineLarge?.copyWith(
127
- fontWeight: FontWeight.w700,
126
+ style: context.kasyTextTheme.pageTitle.copyWith(
128
127
  color: context.colors.onBackground,
129
128
  letterSpacing: -0.8,
130
129
  ),
@@ -215,7 +214,6 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
215
214
  child: Text(Translations.of(context).premium.comparison.title,
216
215
  textAlign: TextAlign.center,
217
216
  style: context.textTheme.headlineSmall?.copyWith(
218
- fontWeight: FontWeight.w600,
219
217
  color: context.colors.onBackground,
220
218
  ),
221
219
  ),
@@ -268,7 +266,6 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
268
266
  translations.payment_cancel_reassurance,
269
267
  textAlign: TextAlign.center,
270
268
  style: context.textTheme.bodyMedium?.copyWith(
271
- fontWeight: FontWeight.w400,
272
269
  color: context.colors.background.withValues(alpha: .9),
273
270
  ),
274
271
  ),
@@ -287,7 +284,6 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
287
284
  Text(
288
285
  translations.payment_cancel_reassurance_free_trial,
289
286
  style: context.textTheme.bodyMedium?.copyWith(
290
- fontWeight: FontWeight.w400,
291
287
  color: context.colors.background.withValues(alpha: .9),
292
288
  ),
293
289
  ),
@@ -1,6 +1,5 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_animate/flutter_animate.dart';
3
- import 'package:google_fonts/google_fonts.dart';
4
3
  import 'package:kasy_kit/components/components.dart';
5
4
  import 'package:kasy_kit/core/data/models/subscription.dart';
6
5
  import 'package:kasy_kit/core/theme/theme.dart';
@@ -114,9 +113,7 @@ class PaywallWithSwitch extends StatelessWidget {
114
113
  child: Text(
115
114
  title,
116
115
  textAlign: TextAlign.left,
117
- style: GoogleFonts.albertSans(
118
- fontSize: 24,
119
- fontWeight: FontWeight.w700,
116
+ style: context.kasyTextTheme.pageTitle.copyWith(
120
117
  color: context.colors.background,
121
118
  letterSpacing: -0.8,
122
119
  ),
@@ -164,7 +161,7 @@ class PaywallWithSwitch extends StatelessWidget {
164
161
  key: ValueKey(detailedPrice),
165
162
  detailedPrice,
166
163
  textAlign: TextAlign.center,
167
- style: context.textTheme.titleLarge?.copyWith(
164
+ style: context.textTheme.bodyLarge?.copyWith(
168
165
  fontWeight: FontWeight.w500,
169
166
  color: context.colors.background,
170
167
  ),
@@ -1,7 +1,6 @@
1
1
  import 'package:flutter/foundation.dart';
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:flutter_animate/flutter_animate.dart';
4
- import 'package:google_fonts/google_fonts.dart';
5
4
  import 'package:kasy_kit/components/components.dart';
6
5
  import 'package:kasy_kit/core/animations/movefade_anim.dart';
7
6
  import 'package:kasy_kit/core/data/models/subscription.dart';
@@ -72,9 +71,7 @@ class BasicPaywall extends StatelessWidget {
72
71
  child: Text(
73
72
  Translations.of(context).premium.title_1,
74
73
  textAlign: TextAlign.center,
75
- style: GoogleFonts.albertSans(
76
- fontSize: 32,
77
- fontWeight: FontWeight.w700,
74
+ style: context.kasyTextTheme.pageTitle.copyWith(
78
75
  color: context.colors.onPrimary,
79
76
  ),
80
77
  ),
@@ -104,10 +104,9 @@ class FeatureWithTwoLines extends StatelessWidget {
104
104
  Flexible(
105
105
  child: Text(
106
106
  title,
107
- style: context.textTheme.bodyMedium?.copyWith(
107
+ style: context.textTheme.titleMedium?.copyWith(
108
108
  color: context.colors.onBackground,
109
109
  decoration: crossed ? TextDecoration.lineThrough : null,
110
- fontWeight: FontWeight.w900,
111
110
  ),
112
111
  ),
113
112
  ),
@@ -1,5 +1,4 @@
1
1
  import 'package:flutter/material.dart';
2
- import 'package:google_fonts/google_fonts.dart';
3
2
  import 'package:kasy_kit/core/theme/theme.dart';
4
3
  import 'package:kasy_kit/core/widgets/kasy_focus_ring.dart';
5
4
 
@@ -55,18 +54,14 @@ class PremiumWideBanner extends StatelessWidget {
55
54
  children: [
56
55
  Text(
57
56
  smallText.toUpperCase(),
58
- style: GoogleFonts.albertSans(
59
- fontSize: 12,
60
- fontWeight: FontWeight.w700,
57
+ style: context.kasyTextTheme.sectionLabel.copyWith(
61
58
  color: context.colors.premiumBannerText,
62
59
  ),
63
60
  ),
64
61
  const SizedBox(height: KasySpacing.sm),
65
62
  Text(
66
63
  title,
67
- style: GoogleFonts.albertSans(
68
- fontSize: 20,
69
- fontWeight: FontWeight.w400,
64
+ style: context.textTheme.titleLarge?.copyWith(
70
65
  color: context.colors.onPrimary,
71
66
  ),
72
67
  overflow: TextOverflow.clip,