kasy-cli 1.31.8 → 1.31.10
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/new.js +7 -10
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/supabase/config.toml +39 -2
- package/lib/scaffold/backends/supabase/deploy.js +14 -2
- package/lib/scaffold/catalog.js +24 -0
- package/lib/scaffold/shared/generator-utils.js +53 -1
- package/package.json +2 -2
- package/templates/firebase/assets/images/premium-bg.jpg +0 -0
- package/templates/firebase/assets/images/premium-switch-header.png +0 -0
- package/templates/firebase/lib/components/kasy_app_bar.dart +107 -1
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +26 -7
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +63 -40
- package/templates/firebase/lib/core/chrome/chrome_visibility.dart +119 -0
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +12 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +25 -3
- package/templates/firebase/lib/core/web_viewport_scale.dart +15 -4
- package/templates/firebase/lib/features/home/home_feed.dart +59 -5
- package/templates/firebase/lib/features/home/home_image_grid.dart +81 -52
- package/templates/firebase/lib/features/home/home_page.dart +6 -8
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +1 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +26 -0
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +169 -90
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +219 -202
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +67 -30
- package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +16 -6
- package/templates/firebase/lib/i18n/en.i18n.json +1 -0
- package/templates/firebase/lib/i18n/es.i18n.json +1 -0
- package/templates/firebase/lib/i18n/pt.i18n.json +1 -0
- package/templates/firebase/pubspec.yaml +1 -1
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
}
|