kasy-cli 1.31.9 → 1.31.11

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 (36) hide show
  1. package/lib/commands/add.js +31 -0
  2. package/lib/commands/new.js +7 -24
  3. package/lib/scaffold/CHANGELOG.json +9 -0
  4. package/lib/scaffold/backends/supabase/config.toml +39 -2
  5. package/lib/scaffold/backends/supabase/deploy.js +14 -16
  6. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  7. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +27 -9
  8. package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +3 -1
  9. package/lib/scaffold/backends/supabase/edge-functions/stripe-list-prices/index.ts +3 -2
  10. package/lib/scaffold/catalog.js +24 -0
  11. package/lib/scaffold/shared/generator-utils.js +11 -0
  12. package/package.json +2 -2
  13. package/templates/firebase/assets/images/premium-bg.jpg +0 -0
  14. package/templates/firebase/assets/images/premium-switch-header.png +0 -0
  15. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +21 -4
  16. package/templates/firebase/lib/components/kasy_app_bar.dart +109 -1
  17. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +26 -7
  18. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +22 -33
  19. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +119 -0
  20. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +12 -0
  21. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +25 -3
  22. package/templates/firebase/lib/features/home/home_feed.dart +7 -1
  23. package/templates/firebase/lib/features/home/home_page.dart +6 -8
  24. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +1 -0
  25. package/templates/firebase/lib/features/settings/settings_page.dart +26 -0
  26. package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +5 -3
  27. package/templates/firebase/lib/features/subscriptions/api/subscription_payment_api.dart +8 -0
  28. package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +89 -47
  29. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +169 -90
  30. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +219 -202
  31. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +67 -30
  32. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +16 -6
  33. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_bottom_menu.dart +11 -18
  34. package/templates/firebase/lib/i18n/en.i18n.json +3 -0
  35. package/templates/firebase/lib/i18n/es.i18n.json +3 -0
  36. package/templates/firebase/lib/i18n/pt.i18n.json +3 -0
@@ -4,6 +4,7 @@ import 'package:google_fonts/google_fonts.dart';
4
4
  import 'package:kasy_kit/components/components.dart';
5
5
  import 'package:kasy_kit/core/data/models/subscription.dart';
6
6
  import 'package:kasy_kit/core/theme/theme.dart';
7
+ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
7
8
  import 'package:kasy_kit/features/subscriptions/ui/component/premium_page_factory.dart';
8
9
  import 'package:kasy_kit/features/subscriptions/ui/widgets/paywall_empty_state.dart';
9
10
  import 'package:kasy_kit/features/subscriptions/ui/widgets/premium_background_gradient.dart';
@@ -15,6 +16,10 @@ import 'package:kasy_kit/i18n/translations.g.dart';
15
16
 
16
17
  /// Minimal paywall: benefits list + single CTA.
17
18
  /// Best for apps with one plan or when you want a clean, focused layout.
19
+ ///
20
+ /// Responsive: on phones (small) it fills the screen with the CTA pinned to the
21
+ /// bottom. On tablet/desktop (medium+) the content is centered in a max-width
22
+ /// column so it never stretches edge-to-edge or leaves a big empty gap.
18
23
  class PaywallMinimal extends StatelessWidget {
19
24
  final List<SubscriptionProduct> offers;
20
25
  final SubscriptionProduct? selectedOffer;
@@ -23,6 +28,10 @@ class PaywallMinimal extends StatelessWidget {
23
28
  final OnTap? onTapRestore;
24
29
  final OnTap? onSkip;
25
30
 
31
+ /// Max content width on tablet/desktop. Keeps the paywall readable and
32
+ /// centered instead of stretched across a wide viewport.
33
+ static const double _maxContentWidth = 460;
34
+
26
35
  const PaywallMinimal({
27
36
  super.key,
28
37
  required this.offers,
@@ -42,105 +51,175 @@ class PaywallMinimal extends StatelessWidget {
42
51
  return PaywallEmptyState(onTapRestore: onTapRestore, onSkip: onSkip);
43
52
  }
44
53
 
54
+ return Scaffold(
55
+ backgroundColor: context.colors.primary,
56
+ body: PremiumBackgroundGradient(
57
+ child: SafeArea(
58
+ child: LayoutBuilder(
59
+ builder: (context, constraints) {
60
+ final bool compact =
61
+ DeviceType.fromWidth(constraints.maxWidth) == DeviceType.small;
62
+ return compact ? _compactLayout(context) : _wideLayout(context);
63
+ },
64
+ ),
65
+ ),
66
+ ),
67
+ );
68
+ }
69
+
70
+ // --- Phone: full-height column, CTA pinned to the bottom. -----------------
71
+ Widget _compactLayout(BuildContext context) {
72
+ return Padding(
73
+ padding: const EdgeInsets.symmetric(
74
+ horizontal: KasySpacing.pageHorizontalGutter,
75
+ ),
76
+ child: Column(
77
+ crossAxisAlignment: CrossAxisAlignment.stretch,
78
+ children: [
79
+ Align(alignment: Alignment.topRight, child: _closeButton()),
80
+ const SizedBox(height: KasySpacing.lg),
81
+ _title(context),
82
+ const SizedBox(height: KasySpacing.lg),
83
+ ..._features(context),
84
+ const Spacer(),
85
+ _price(context),
86
+ const SizedBox(height: KasySpacing.smd),
87
+ _cta(context),
88
+ const SizedBox(height: KasySpacing.md),
89
+ _bottomMenu(context),
90
+ const SizedBox(height: KasySpacing.lg),
91
+ ],
92
+ ),
93
+ );
94
+ }
95
+
96
+ // --- Tablet/Desktop: centered max-width column, close pinned to corner. ---
97
+ Widget _wideLayout(BuildContext context) {
98
+ return Stack(
99
+ children: [
100
+ Center(
101
+ child: ConstrainedBox(
102
+ constraints: const BoxConstraints(maxWidth: _maxContentWidth),
103
+ child: SingleChildScrollView(
104
+ padding: const EdgeInsets.symmetric(
105
+ horizontal: KasySpacing.lg,
106
+ vertical: KasySpacing.xxl,
107
+ ),
108
+ child: Column(
109
+ crossAxisAlignment: CrossAxisAlignment.stretch,
110
+ mainAxisSize: MainAxisSize.min,
111
+ children: [
112
+ _title(context),
113
+ const SizedBox(height: KasySpacing.xl),
114
+ ..._features(context),
115
+ const SizedBox(height: KasySpacing.xl),
116
+ _price(context),
117
+ const SizedBox(height: KasySpacing.smd),
118
+ _cta(context),
119
+ const SizedBox(height: KasySpacing.md),
120
+ _bottomMenu(context),
121
+ ],
122
+ ),
123
+ ),
124
+ ),
125
+ ),
126
+ Positioned(
127
+ top: KasySpacing.sm,
128
+ right: KasySpacing.md,
129
+ child: _closeButton(),
130
+ ),
131
+ ],
132
+ );
133
+ }
134
+
135
+ // --- Shared content blocks (same widgets across breakpoints). -------------
136
+
137
+ Widget _closeButton() => AppCloseButton(onTap: () => onSkip?.call());
138
+
139
+ Widget _title(BuildContext context) {
140
+ return Animate(
141
+ effects: const [
142
+ FadeEffect(
143
+ delay: Duration(milliseconds: 100),
144
+ duration: Duration(milliseconds: 300),
145
+ ),
146
+ ],
147
+ child: Text(
148
+ Translations.of(context).premium.title_1,
149
+ textAlign: TextAlign.center,
150
+ style: GoogleFonts.albertSans(
151
+ fontSize: 28,
152
+ fontWeight: FontWeight.w700,
153
+ color: context.colors.onPrimary,
154
+ letterSpacing: -0.8,
155
+ ),
156
+ ),
157
+ );
158
+ }
159
+
160
+ List<Widget> _features(BuildContext context) {
45
161
  final translations = Translations.of(context).premium;
46
162
  final features = [
47
163
  translations.feature_1,
48
164
  translations.feature_2,
49
165
  translations.feature_3,
50
166
  ];
167
+ return [
168
+ ...AnimateList(
169
+ interval: 120.ms,
170
+ delay: 300.ms,
171
+ effects: [FadeEffect(duration: 300.ms)],
172
+ children: features.map((text) => PremiumFeature(text: text)).toList(),
173
+ ),
174
+ ];
175
+ }
51
176
 
52
- return Scaffold(
53
- backgroundColor: context.colors.primary,
54
- body: PremiumBackgroundGradient(
55
- child: SafeArea(
56
- child: Padding(
57
- padding: const EdgeInsets.symmetric(
58
- horizontal: KasySpacing.pageHorizontalGutter,
59
- ),
60
- child: Column(
61
- crossAxisAlignment: CrossAxisAlignment.stretch,
62
- children: [
63
- Align(
64
- alignment: Alignment.topRight,
65
- child: AppCloseButton(onTap: () => onSkip?.call()),
66
- ),
67
- const SizedBox(height: KasySpacing.lg),
68
- Animate(
69
- effects: const [
70
- FadeEffect(
71
- delay: Duration(milliseconds: 100),
72
- duration: Duration(milliseconds: 300),
73
- ),
74
- ],
75
- child: Text(
76
- translations.title_1,
77
- textAlign: TextAlign.center,
78
- style: GoogleFonts.albertSans(
79
- fontSize: 28,
80
- fontWeight: FontWeight.w700,
81
- color: context.colors.onPrimary,
82
- letterSpacing: -0.8,
83
- ),
84
- ),
85
- ),
86
- const SizedBox(height: KasySpacing.lg),
87
- ...AnimateList(
88
- interval: 120.ms,
89
- delay: 300.ms,
90
- effects: [FadeEffect(duration: 300.ms)],
91
- children: features
92
- .map((text) => PremiumFeature(text: text))
93
- .toList(),
94
- ),
95
- const Spacer(),
96
- Animate(
97
- effects: const [
98
- FadeEffect(
99
- delay: Duration(milliseconds: 400),
100
- duration: Duration(milliseconds: 300),
101
- ),
102
- ScaleEffect(
103
- delay: Duration(milliseconds: 400),
104
- duration: Duration(milliseconds: 500),
105
- curve: Curves.easeOut,
106
- ),
107
- ],
108
- child: Text(
109
- _effectiveOffer.formattedPrice(context),
110
- textAlign: TextAlign.center,
111
- style: context.textTheme.titleLarge?.copyWith(
112
- fontWeight: FontWeight.w600,
113
- color: context.colors.onPrimary,
114
- ),
115
- ),
116
- ),
117
- const SizedBox(height: KasySpacing.smd),
118
- Animate(
119
- effects: const [
120
- FadeEffect(
121
- delay: Duration(milliseconds: 500),
122
- duration: Duration(milliseconds: 300),
123
- ),
124
- ],
125
- child: KasyButton(
126
- label: translations.action_button,
127
- variant: KasyButtonVariant.inverse,
128
- isLoading: onTap == null,
129
- expand: true,
130
- onPressed: onTap,
131
- ),
132
- ),
133
- const SizedBox(height: KasySpacing.md),
134
- BottomPremiumMenu(
135
- textColor: context.colors.onPrimary,
136
- onTapRestore: onTapRestore,
137
- ),
138
- const SizedBox(height: KasySpacing.lg),
139
- ],
140
- ),
141
- ),
177
+ Widget _price(BuildContext context) {
178
+ return Animate(
179
+ effects: const [
180
+ FadeEffect(
181
+ delay: Duration(milliseconds: 400),
182
+ duration: Duration(milliseconds: 300),
183
+ ),
184
+ ScaleEffect(
185
+ delay: Duration(milliseconds: 400),
186
+ duration: Duration(milliseconds: 500),
187
+ curve: Curves.easeOut,
188
+ ),
189
+ ],
190
+ child: Text(
191
+ _effectiveOffer.formattedPrice(context),
192
+ textAlign: TextAlign.center,
193
+ style: context.textTheme.titleLarge?.copyWith(
194
+ fontWeight: FontWeight.w600,
195
+ color: context.colors.onPrimary,
196
+ ),
197
+ ),
198
+ );
199
+ }
200
+
201
+ Widget _cta(BuildContext context) {
202
+ return Animate(
203
+ effects: const [
204
+ FadeEffect(
205
+ delay: Duration(milliseconds: 500),
206
+ duration: Duration(milliseconds: 300),
142
207
  ),
208
+ ],
209
+ child: KasyButton(
210
+ label: Translations.of(context).premium.action_button,
211
+ variant: KasyButtonVariant.inverse,
212
+ isLoading: onTap == null,
213
+ expand: true,
214
+ onPressed: onTap,
143
215
  ),
144
216
  );
145
217
  }
218
+
219
+ Widget _bottomMenu(BuildContext context) {
220
+ return BottomPremiumMenu(
221
+ textColor: context.colors.onPrimary,
222
+ onTapRestore: onTapRestore,
223
+ );
224
+ }
146
225
  }