kasy-cli 1.31.9 → 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 -16
- package/lib/scaffold/catalog.js +24 -0
- 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 +22 -33
- 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/features/home/home_feed.dart +7 -1
- 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
|
@@ -6,6 +6,7 @@ import 'package:kasy_kit/components/components.dart';
|
|
|
6
6
|
import 'package:kasy_kit/core/animations/movefade_anim.dart';
|
|
7
7
|
import 'package:kasy_kit/core/data/models/subscription.dart';
|
|
8
8
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
9
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
9
10
|
import 'package:kasy_kit/features/subscriptions/ui/component/premium_page_factory.dart';
|
|
10
11
|
import 'package:kasy_kit/features/subscriptions/ui/premium_page.dart';
|
|
11
12
|
import 'package:kasy_kit/features/subscriptions/ui/widgets/comparison_table.dart';
|
|
@@ -26,7 +27,7 @@ class PaywallRow extends ConsumerStatefulWidget {
|
|
|
26
27
|
final OnTap? onSkip;
|
|
27
28
|
final PremiumPageArgs? args;
|
|
28
29
|
final bool showCoupon;
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
|
|
31
32
|
const PaywallRow({
|
|
32
33
|
super.key,
|
|
@@ -47,6 +48,8 @@ class PaywallRow extends ConsumerStatefulWidget {
|
|
|
47
48
|
class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
|
|
48
49
|
final ValueNotifier<bool> showCloseBtnNotifier = ValueNotifier(false);
|
|
49
50
|
|
|
51
|
+
static const double _contentMaxWidth = 640;
|
|
52
|
+
|
|
50
53
|
@override
|
|
51
54
|
void initState() {
|
|
52
55
|
super.initState();
|
|
@@ -78,221 +81,239 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
|
|
|
78
81
|
body: SafeArea(
|
|
79
82
|
bottom: false,
|
|
80
83
|
top: false,
|
|
81
|
-
child:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
SliverAppBar(
|
|
96
|
-
backgroundColor: context.colors.background,
|
|
97
|
-
elevation: 0,
|
|
98
|
-
expandedHeight: 130,
|
|
99
|
-
flexibleSpace: FlexibleSpaceBar(
|
|
100
|
-
background: Image.asset(
|
|
101
|
-
'assets/images/premium-switch-header.png',
|
|
102
|
-
fit: BoxFit.fitWidth,
|
|
103
|
-
alignment: Alignment.bottomCenter,
|
|
104
|
-
),
|
|
105
|
-
),
|
|
106
|
-
),
|
|
107
|
-
const SliverToBoxAdapter(child: SizedBox(height: KasySpacing.smd)),
|
|
108
|
-
SliverToBoxAdapter(
|
|
109
|
-
child: MoveFadeAnim(
|
|
110
|
-
child: Text(
|
|
111
|
-
translations.title_1,
|
|
112
|
-
textAlign: TextAlign.center,
|
|
113
|
-
style: context.textTheme.headlineLarge?.copyWith(
|
|
114
|
-
fontWeight: FontWeight.w700,
|
|
115
|
-
color: context.colors.onBackground,
|
|
116
|
-
letterSpacing: -0.8,
|
|
117
|
-
),
|
|
118
|
-
),
|
|
84
|
+
child: LayoutBuilder(
|
|
85
|
+
builder: (context, constraints) {
|
|
86
|
+
final isSmall = DeviceType.fromWidth(constraints.maxWidth) == DeviceType.small;
|
|
87
|
+
final maxWidth = isSmall ? constraints.maxWidth : _contentMaxWidth;
|
|
88
|
+
final sideInset = isSmall ? 0.0 : ((constraints.maxWidth - _contentMaxWidth) / 2).clamp(0.0, double.infinity);
|
|
89
|
+
|
|
90
|
+
return Stack(
|
|
91
|
+
children: [
|
|
92
|
+
Positioned.fill(
|
|
93
|
+
child: Opacity(
|
|
94
|
+
opacity: 0.3,
|
|
95
|
+
child: Image.asset(
|
|
96
|
+
'assets/images/premium-bg.jpg',
|
|
97
|
+
fit: BoxFit.cover,
|
|
119
98
|
),
|
|
120
99
|
),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
items: widget.offers
|
|
139
|
-
.map(
|
|
140
|
-
(el) => SelectableOption(
|
|
141
|
-
sublabel: switch (el.durationType) {
|
|
142
|
-
DurationType.week => Translations.of(context).premium.duration_weekly,
|
|
143
|
-
DurationType.year => Translations.of(context).premium.duration_annual,
|
|
144
|
-
DurationType.month => Translations.of(context).premium.duration_monthly,
|
|
145
|
-
DurationType.lifetime => Translations.of(context).premium.duration_lifetime,
|
|
146
|
-
_ => '--',
|
|
147
|
-
},
|
|
148
|
-
label: switch(defaultTargetPlatform) {
|
|
149
|
-
TargetPlatform.iOS => el.title ?? '',
|
|
150
|
-
TargetPlatform.android when el.durationType == DurationType.lifetime =>
|
|
151
|
-
Translations.of(context).premium.duration_lifetime.toUpperCase(),
|
|
152
|
-
TargetPlatform.android when el.durationType == DurationType.year =>
|
|
153
|
-
Translations.of(context).premium.duration_recuring_label_annual.toUpperCase(),
|
|
154
|
-
TargetPlatform.android when el.durationType == DurationType.month =>
|
|
155
|
-
Translations.of(context).premium.duration_recuring_label_monthly.toUpperCase(),
|
|
156
|
-
TargetPlatform.android when el.durationType == DurationType.week =>
|
|
157
|
-
Translations.of(context).premium.duration_recuring_label_weekly.toUpperCase(),
|
|
158
|
-
_ => el.title ?? '',
|
|
159
|
-
},
|
|
160
|
-
price: el.priceString,
|
|
161
|
-
icon: switch (el.durationType) {
|
|
162
|
-
DurationType.lifetime => KasyIcons.book,
|
|
163
|
-
_ => null,
|
|
164
|
-
},
|
|
165
|
-
trial: switch(el.trialDays) {
|
|
166
|
-
null => null,
|
|
167
|
-
_ => Translations.of(context).paywallWithSwitch.withTrial.trial_switch_title(days: el.trialDays!),
|
|
168
|
-
},
|
|
169
|
-
promotion: switch (el.durationType) {
|
|
170
|
-
DurationType.year => "best deal",
|
|
171
|
-
_ => null,
|
|
172
|
-
},
|
|
173
|
-
data: el,
|
|
100
|
+
),
|
|
101
|
+
Positioned.fill(
|
|
102
|
+
child: Align(
|
|
103
|
+
alignment: Alignment.topCenter,
|
|
104
|
+
child: ConstrainedBox(
|
|
105
|
+
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
106
|
+
child: CustomScrollView(
|
|
107
|
+
slivers: [
|
|
108
|
+
SliverAppBar(
|
|
109
|
+
backgroundColor: context.colors.background,
|
|
110
|
+
elevation: 0,
|
|
111
|
+
expandedHeight: 130,
|
|
112
|
+
flexibleSpace: FlexibleSpaceBar(
|
|
113
|
+
background: Image.asset(
|
|
114
|
+
'assets/images/premium-switch-header.png',
|
|
115
|
+
fit: BoxFit.fitWidth,
|
|
116
|
+
alignment: Alignment.bottomCenter,
|
|
174
117
|
),
|
|
175
|
-
)
|
|
176
|
-
.toList(),
|
|
177
|
-
onSelectItem: widget.onSelectItem,
|
|
178
|
-
initialSelectedIndex: widget.selectedOffer != null
|
|
179
|
-
? widget.offers.indexOf(widget.selectedOffer!)
|
|
180
|
-
: 0,
|
|
181
|
-
),
|
|
182
|
-
),
|
|
183
|
-
),
|
|
184
|
-
if (widget.showCoupon)
|
|
185
|
-
SliverToBoxAdapter(
|
|
186
|
-
child: Align(
|
|
187
|
-
child: Padding(
|
|
188
|
-
padding: const EdgeInsets.fromLTRB(KasySpacing.md, KasySpacing.md, KasySpacing.lg, 0),
|
|
189
|
-
child: KasyButton(
|
|
190
|
-
variant: KasyButtonVariant.link,
|
|
191
|
-
label: translations.coupon_title,
|
|
192
|
-
size: KasyButtonSize.small,
|
|
193
|
-
foregroundColor: context.colors.background,
|
|
194
|
-
fontWeight: FontWeight.w400,
|
|
195
|
-
onPressed: () => debugPrint("coupon"),
|
|
196
|
-
// onPressed: () => askForCoupon(ref),
|
|
118
|
+
),
|
|
197
119
|
),
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
120
|
+
const SliverToBoxAdapter(child: SizedBox(height: KasySpacing.smd)),
|
|
121
|
+
SliverToBoxAdapter(
|
|
122
|
+
child: MoveFadeAnim(
|
|
123
|
+
child: Text(
|
|
124
|
+
translations.title_1,
|
|
125
|
+
textAlign: TextAlign.center,
|
|
126
|
+
style: context.textTheme.headlineLarge?.copyWith(
|
|
127
|
+
fontWeight: FontWeight.w700,
|
|
128
|
+
color: context.colors.onBackground,
|
|
129
|
+
letterSpacing: -0.8,
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
const SliverToBoxAdapter(child: SizedBox(height: KasySpacing.smd)),
|
|
135
|
+
for (var i = 0; i < features.length; i++)
|
|
136
|
+
SliverToBoxAdapter(
|
|
137
|
+
child: Padding(
|
|
138
|
+
padding: hPadding,
|
|
139
|
+
child: FeatureLine(
|
|
140
|
+
title: features[i],
|
|
141
|
+
topPadding: i > 0 ? 4 : 0,
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
const SliverToBoxAdapter(child: SizedBox(height: KasySpacing.xl)),
|
|
146
|
+
SliverToBoxAdapter(
|
|
147
|
+
child: Padding(
|
|
148
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
149
|
+
child: SelectableColGroup<SubscriptionProduct>(
|
|
150
|
+
brightness: Brightness.light,
|
|
151
|
+
items: widget.offers
|
|
152
|
+
.map(
|
|
153
|
+
(el) => SelectableOption(
|
|
154
|
+
sublabel: switch (el.durationType) {
|
|
155
|
+
DurationType.week => Translations.of(context).premium.duration_weekly,
|
|
156
|
+
DurationType.year => Translations.of(context).premium.duration_annual,
|
|
157
|
+
DurationType.month => Translations.of(context).premium.duration_monthly,
|
|
158
|
+
DurationType.lifetime => Translations.of(context).premium.duration_lifetime,
|
|
159
|
+
_ => '--',
|
|
160
|
+
},
|
|
161
|
+
label: switch(defaultTargetPlatform) {
|
|
162
|
+
TargetPlatform.iOS => el.title ?? '',
|
|
163
|
+
TargetPlatform.android when el.durationType == DurationType.lifetime =>
|
|
164
|
+
Translations.of(context).premium.duration_lifetime.toUpperCase(),
|
|
165
|
+
TargetPlatform.android when el.durationType == DurationType.year =>
|
|
166
|
+
Translations.of(context).premium.duration_recuring_label_annual.toUpperCase(),
|
|
167
|
+
TargetPlatform.android when el.durationType == DurationType.month =>
|
|
168
|
+
Translations.of(context).premium.duration_recuring_label_monthly.toUpperCase(),
|
|
169
|
+
TargetPlatform.android when el.durationType == DurationType.week =>
|
|
170
|
+
Translations.of(context).premium.duration_recuring_label_weekly.toUpperCase(),
|
|
171
|
+
_ => el.title ?? '',
|
|
172
|
+
},
|
|
173
|
+
price: el.priceString,
|
|
174
|
+
icon: switch (el.durationType) {
|
|
175
|
+
DurationType.lifetime => KasyIcons.book,
|
|
176
|
+
_ => null,
|
|
177
|
+
},
|
|
178
|
+
trial: switch(el.trialDays) {
|
|
179
|
+
null => null,
|
|
180
|
+
_ => Translations.of(context).paywallWithSwitch.withTrial.trial_switch_title(days: el.trialDays!),
|
|
181
|
+
},
|
|
182
|
+
promotion: switch (el.durationType) {
|
|
183
|
+
DurationType.year => "best deal",
|
|
184
|
+
_ => null,
|
|
185
|
+
},
|
|
186
|
+
data: el,
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
.toList(),
|
|
190
|
+
onSelectItem: widget.onSelectItem,
|
|
191
|
+
initialSelectedIndex: widget.selectedOffer != null
|
|
192
|
+
? widget.offers.indexOf(widget.selectedOffer!)
|
|
193
|
+
: 0,
|
|
194
|
+
),
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
if (widget.showCoupon)
|
|
198
|
+
SliverToBoxAdapter(
|
|
199
|
+
child: Align(
|
|
200
|
+
child: Padding(
|
|
201
|
+
padding: const EdgeInsets.fromLTRB(KasySpacing.md, KasySpacing.md, KasySpacing.lg, 0),
|
|
202
|
+
child: KasyButton(
|
|
203
|
+
variant: KasyButtonVariant.link,
|
|
204
|
+
label: translations.coupon_title,
|
|
205
|
+
size: KasyButtonSize.small,
|
|
206
|
+
foregroundColor: context.colors.background,
|
|
207
|
+
fontWeight: FontWeight.w400,
|
|
208
|
+
onPressed: () => debugPrint("coupon"),
|
|
209
|
+
),
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
const SliverToBoxAdapter(child: SizedBox(height: KasySpacing.lg)),
|
|
214
|
+
SliverToBoxAdapter(
|
|
215
|
+
child: Text(Translations.of(context).premium.comparison.title,
|
|
216
|
+
textAlign: TextAlign.center,
|
|
217
|
+
style: context.textTheme.headlineSmall?.copyWith(
|
|
218
|
+
fontWeight: FontWeight.w600,
|
|
219
|
+
color: context.colors.onBackground,
|
|
220
|
+
),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
const SliverToBoxAdapter(child: SizedBox(height: KasySpacing.smd)),
|
|
224
|
+
SliverToBoxAdapter(
|
|
225
|
+
child: Padding(
|
|
226
|
+
padding: hPadding,
|
|
227
|
+
child: const ComparisonTableComponent(),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
const SliverToBoxAdapter(child: SizedBox(height: 300)),
|
|
231
|
+
],
|
|
208
232
|
),
|
|
209
233
|
),
|
|
210
234
|
),
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
235
|
+
),
|
|
236
|
+
Positioned(
|
|
237
|
+
top: defaultTargetPlatform == TargetPlatform.android ? KasySpacing.sm : 0,
|
|
238
|
+
right: sideInset + KasySpacing.lg,
|
|
239
|
+
child: SafeArea(
|
|
240
|
+
child: AppCloseButtonComponent(
|
|
241
|
+
showCloseBtn: showCloseBtnNotifier,
|
|
242
|
+
onTap: () => widget.onSkip?.call(),
|
|
216
243
|
),
|
|
217
244
|
),
|
|
218
|
-
const SliverToBoxAdapter(child: SizedBox(height: 300)),
|
|
219
|
-
],
|
|
220
|
-
),
|
|
221
|
-
),
|
|
222
|
-
Positioned(
|
|
223
|
-
top: defaultTargetPlatform == TargetPlatform.android ? KasySpacing.sm : 0,
|
|
224
|
-
right: KasySpacing.lg,
|
|
225
|
-
child: SafeArea(
|
|
226
|
-
child: AppCloseButtonComponent(
|
|
227
|
-
showCloseBtn: showCloseBtnNotifier,
|
|
228
|
-
onTap: () => widget.onSkip?.call(),
|
|
229
245
|
),
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const SizedBox(height: KasySpacing.smd),
|
|
243
|
-
switch((widget.selectedOffer?.trialDays, widget.selectedOffer?.durationType)) {
|
|
244
|
-
(_, DurationType.lifetime) => Padding(
|
|
245
|
-
padding: hPadding,
|
|
246
|
-
child: const SizedBox.shrink(),
|
|
247
|
-
),
|
|
248
|
-
(null, _) => Padding(
|
|
249
|
-
padding: hPadding,
|
|
250
|
-
child: Text(
|
|
251
|
-
translations.payment_cancel_reassurance,
|
|
252
|
-
textAlign: TextAlign.center,
|
|
253
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
254
|
-
fontWeight: FontWeight.w400,
|
|
255
|
-
color: context.colors.background.withValues(alpha: .9),
|
|
256
|
-
),
|
|
257
|
-
),
|
|
258
|
-
),
|
|
259
|
-
(_, _) => Padding(
|
|
260
|
-
padding: hPadding,
|
|
261
|
-
child: Row(
|
|
262
|
-
mainAxisAlignment: MainAxisAlignment.center,
|
|
246
|
+
Positioned(
|
|
247
|
+
bottom: 0,
|
|
248
|
+
left: 0,
|
|
249
|
+
right: 0,
|
|
250
|
+
child: ColoredBox(
|
|
251
|
+
color: context.colors.onBackground.withValues(alpha: .9),
|
|
252
|
+
child: Align(
|
|
253
|
+
child: ConstrainedBox(
|
|
254
|
+
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
255
|
+
child: Column(
|
|
256
|
+
mainAxisAlignment: MainAxisAlignment.end,
|
|
257
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
263
258
|
children: [
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
259
|
+
const SizedBox(height: KasySpacing.smd),
|
|
260
|
+
switch((widget.selectedOffer?.trialDays, widget.selectedOffer?.durationType)) {
|
|
261
|
+
(_, DurationType.lifetime) => Padding(
|
|
262
|
+
padding: hPadding,
|
|
263
|
+
child: const SizedBox.shrink(),
|
|
264
|
+
),
|
|
265
|
+
(null, _) => Padding(
|
|
266
|
+
padding: hPadding,
|
|
267
|
+
child: Text(
|
|
268
|
+
translations.payment_cancel_reassurance,
|
|
269
|
+
textAlign: TextAlign.center,
|
|
270
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
271
|
+
fontWeight: FontWeight.w400,
|
|
272
|
+
color: context.colors.background.withValues(alpha: .9),
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
(_, _) => Padding(
|
|
277
|
+
padding: hPadding,
|
|
278
|
+
child: Row(
|
|
279
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
280
|
+
children: [
|
|
281
|
+
Icon(
|
|
282
|
+
KasyIcons.security,
|
|
283
|
+
size: 14,
|
|
284
|
+
color: context.colors.primary,
|
|
285
|
+
),
|
|
286
|
+
const SizedBox(width: KasySpacing.xs),
|
|
287
|
+
Text(
|
|
288
|
+
translations.payment_cancel_reassurance_free_trial,
|
|
289
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
290
|
+
fontWeight: FontWeight.w400,
|
|
291
|
+
color: context.colors.background.withValues(alpha: .9),
|
|
292
|
+
),
|
|
293
|
+
),
|
|
294
|
+
],
|
|
295
|
+
),
|
|
296
|
+
),
|
|
297
|
+
},
|
|
298
|
+
const SizedBox(height: KasySpacing.smd),
|
|
299
|
+
_buildButton(context, ref),
|
|
300
|
+
Padding(
|
|
301
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.sm),
|
|
302
|
+
child: BottomPremiumMenu(
|
|
303
|
+
textColor: context.colors.onPrimary,
|
|
304
|
+
onTapRestore: widget.onTapRestore,
|
|
275
305
|
),
|
|
276
306
|
),
|
|
307
|
+
const SizedBox(height: KasySpacing.md),
|
|
277
308
|
],
|
|
278
309
|
),
|
|
279
310
|
),
|
|
280
|
-
},
|
|
281
|
-
const SizedBox(height: KasySpacing.smd),
|
|
282
|
-
_buildButton(context, ref),
|
|
283
|
-
Padding(
|
|
284
|
-
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.sm),
|
|
285
|
-
child: BottomPremiumMenu(
|
|
286
|
-
textColor: context.colors.onPrimary,
|
|
287
|
-
onTapRestore: widget.onTapRestore,
|
|
288
|
-
),
|
|
289
311
|
),
|
|
290
|
-
|
|
291
|
-
],
|
|
312
|
+
),
|
|
292
313
|
),
|
|
293
|
-
|
|
294
|
-
)
|
|
295
|
-
|
|
314
|
+
],
|
|
315
|
+
);
|
|
316
|
+
},
|
|
296
317
|
),
|
|
297
318
|
),
|
|
298
319
|
);
|
|
@@ -309,7 +330,6 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
|
|
|
309
330
|
ScaleEffect(
|
|
310
331
|
delay: Duration(milliseconds: 300),
|
|
311
332
|
duration: Duration(milliseconds: 1000),
|
|
312
|
-
curve: Curves.elasticInOut,
|
|
313
333
|
),
|
|
314
334
|
],
|
|
315
335
|
child: Padding(
|
|
@@ -326,7 +346,4 @@ class _BasicPaywallRowState extends ConsumerState<PaywallRow> {
|
|
|
326
346
|
),
|
|
327
347
|
);
|
|
328
348
|
}
|
|
329
|
-
|
|
330
349
|
}
|
|
331
|
-
|
|
332
|
-
|
|
@@ -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/premium_background_gradient.dart';
|
|
9
10
|
import 'package:kasy_kit/features/subscriptions/ui/widgets/premium_bottom_menu.dart';
|
|
@@ -17,7 +18,7 @@ import 'package:kasy_kit/i18n/translations.g.dart';
|
|
|
17
18
|
/// The paywall with switch is a paywall with a switch to toggle the trial offer
|
|
18
19
|
/// (You have to have 1 offer with a trial period and 1 offer without a trial period)
|
|
19
20
|
/// Once the user toggles the switch, the selected offer is updated
|
|
20
|
-
/// - Offer with trial period is most of the time more expensive
|
|
21
|
+
/// - Offer with trial period is most of the time more expensive
|
|
21
22
|
/// - try different pricing strategies to see which one converts the best
|
|
22
23
|
class PaywallWithSwitch extends StatelessWidget {
|
|
23
24
|
final List<SubscriptionProduct> offers;
|
|
@@ -43,6 +44,8 @@ class PaywallWithSwitch extends StatelessWidget {
|
|
|
43
44
|
this.onTapTerms,
|
|
44
45
|
});
|
|
45
46
|
|
|
47
|
+
static const double _maxContentWidth = 460;
|
|
48
|
+
|
|
46
49
|
@override
|
|
47
50
|
Widget build(BuildContext context) {
|
|
48
51
|
var detailedPrice = "";
|
|
@@ -75,33 +78,31 @@ class PaywallWithSwitch extends StatelessWidget {
|
|
|
75
78
|
backgroundColor: context.colors.primary,
|
|
76
79
|
body: PremiumBackgroundGradient(
|
|
77
80
|
child: SafeArea(
|
|
78
|
-
child:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"assets/images/premium-switch-header.png",
|
|
90
|
-
fit: BoxFit.contain,
|
|
91
|
-
),
|
|
92
|
-
),
|
|
93
|
-
Positioned(
|
|
94
|
-
top: 8,
|
|
95
|
-
left: 0,
|
|
96
|
-
child: AppCloseButton(
|
|
97
|
-
onTap: () {
|
|
98
|
-
onSkip?.call();
|
|
99
|
-
},
|
|
100
|
-
),
|
|
101
|
-
),
|
|
102
|
-
],
|
|
81
|
+
child: LayoutBuilder(
|
|
82
|
+
builder: (context, constraints) {
|
|
83
|
+
final isSmall = DeviceType.fromWidth(constraints.maxWidth) == DeviceType.small;
|
|
84
|
+
|
|
85
|
+
Widget buildImageSection() => Stack(
|
|
86
|
+
children: [
|
|
87
|
+
Positioned.fill(
|
|
88
|
+
child: Image.asset(
|
|
89
|
+
"assets/images/premium-switch-header.png",
|
|
90
|
+
fit: BoxFit.contain,
|
|
91
|
+
),
|
|
103
92
|
),
|
|
104
|
-
|
|
93
|
+
Positioned(
|
|
94
|
+
top: 8,
|
|
95
|
+
left: 0,
|
|
96
|
+
child: AppCloseButton(
|
|
97
|
+
onTap: () {
|
|
98
|
+
onSkip?.call();
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
final sharedContent = <Widget>[
|
|
105
106
|
const SizedBox(height: KasySpacing.smd),
|
|
106
107
|
Animate(
|
|
107
108
|
effects: const [
|
|
@@ -197,11 +198,47 @@ class PaywallWithSwitch extends StatelessWidget {
|
|
|
197
198
|
onTapRestore: onTapRestore,
|
|
198
199
|
),
|
|
199
200
|
const SizedBox(height: KasySpacing.sm),
|
|
200
|
-
]
|
|
201
|
-
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
if (isSmall) {
|
|
204
|
+
return Padding(
|
|
205
|
+
padding: const EdgeInsets.symmetric(
|
|
206
|
+
horizontal: KasySpacing.pageHorizontalGutter,
|
|
207
|
+
),
|
|
208
|
+
child: Column(
|
|
209
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
210
|
+
children: [
|
|
211
|
+
Expanded(flex: 2, child: buildImageSection()),
|
|
212
|
+
...sharedContent,
|
|
213
|
+
],
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return SingleChildScrollView(
|
|
219
|
+
child: Center(
|
|
220
|
+
child: ConstrainedBox(
|
|
221
|
+
constraints: const BoxConstraints(maxWidth: _maxContentWidth),
|
|
222
|
+
child: Padding(
|
|
223
|
+
padding: const EdgeInsets.symmetric(
|
|
224
|
+
horizontal: KasySpacing.pageHorizontalGutter,
|
|
225
|
+
vertical: KasySpacing.xl,
|
|
226
|
+
),
|
|
227
|
+
child: Column(
|
|
228
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
229
|
+
children: [
|
|
230
|
+
SizedBox(height: 220, child: buildImageSection()),
|
|
231
|
+
...sharedContent,
|
|
232
|
+
],
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
},
|
|
202
239
|
),
|
|
203
240
|
),
|
|
204
241
|
),
|
|
205
242
|
);
|
|
206
243
|
}
|
|
207
|
-
}
|
|
244
|
+
}
|
|
@@ -50,11 +50,19 @@ class BasicPaywall extends StatelessWidget {
|
|
|
50
50
|
toolbarHeight: 32,
|
|
51
51
|
),
|
|
52
52
|
body: SafeArea(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
// Centered, max-width column so the card doesn't stretch edge-to-edge
|
|
54
|
+
// on tablet/desktop. On phones the maxWidth exceeds the screen, so the
|
|
55
|
+
// layout is unchanged. Bottom-aligned to keep the CTA reachable.
|
|
56
|
+
child: Align(
|
|
57
|
+
alignment: Alignment.bottomCenter,
|
|
58
|
+
child: ConstrainedBox(
|
|
59
|
+
constraints: const BoxConstraints(maxWidth: 480),
|
|
60
|
+
child: Column(
|
|
61
|
+
mainAxisSize: MainAxisSize.min,
|
|
62
|
+
mainAxisAlignment: MainAxisAlignment.end,
|
|
63
|
+
children: [
|
|
64
|
+
PremiumCard(
|
|
65
|
+
bgColor: Colors.transparent,
|
|
58
66
|
child: Column(
|
|
59
67
|
mainAxisSize: MainAxisSize.min,
|
|
60
68
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
@@ -148,7 +156,9 @@ class BasicPaywall extends StatelessWidget {
|
|
|
148
156
|
onTapRestore: onTapRestore,
|
|
149
157
|
)
|
|
150
158
|
),
|
|
151
|
-
|
|
159
|
+
],
|
|
160
|
+
),
|
|
161
|
+
),
|
|
152
162
|
),
|
|
153
163
|
),
|
|
154
164
|
),
|
|
@@ -441,6 +441,7 @@
|
|
|
441
441
|
"theme_option_light": "Light",
|
|
442
442
|
"theme_option_dark": "Dark",
|
|
443
443
|
"haptic_feedback_title": "Haptic feedback",
|
|
444
|
+
"hide_chrome_on_scroll_title": "Hide bars when scrolling",
|
|
444
445
|
"section_preferences_label": "PREFERENCES",
|
|
445
446
|
"section_security_label": "SECURITY",
|
|
446
447
|
"section_support_label": "SUPPORT",
|
|
@@ -441,6 +441,7 @@
|
|
|
441
441
|
"theme_option_light": "Claro",
|
|
442
442
|
"theme_option_dark": "Oscuro",
|
|
443
443
|
"haptic_feedback_title": "Feedback háptico",
|
|
444
|
+
"hide_chrome_on_scroll_title": "Ocultar barras al desplazar",
|
|
444
445
|
"section_preferences_label": "PREFERENCIAS",
|
|
445
446
|
"section_security_label": "SEGURIDAD",
|
|
446
447
|
"section_support_label": "SOPORTE",
|