kasy-cli 1.31.14 → 1.34.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/bin/kasy.js +42 -0
- package/lib/commands/apple-web.js +222 -0
- package/lib/commands/configure.js +3 -91
- package/lib/commands/doctor.js +20 -0
- package/lib/commands/facebook.js +189 -0
- package/lib/commands/new.js +65 -3
- package/lib/scaffold/CHANGELOG.json +27 -0
- package/lib/scaffold/backends/api/patch/README.md +87 -2
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +34 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +186 -0
- package/lib/scaffold/backends/supabase/deploy.js +92 -0
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/README.md +26 -22
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -11
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-checkout-session/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/stripe-create-portal-session/index.ts +60 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +22 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +3 -2
- package/lib/scaffold/generate.js +1 -1
- package/lib/scaffold/shared/generator-utils.js +34 -3
- package/lib/utils/apple-web.js +147 -0
- package/lib/utils/facebook.js +162 -0
- package/lib/utils/i18n/messages-en.js +64 -0
- package/lib/utils/i18n/messages-es.js +64 -0
- package/lib/utils/i18n/messages-pt.js +64 -0
- package/package.json +2 -2
- package/templates/firebase/AGENTS.md +87 -0
- package/templates/firebase/CLAUDE.md +16 -0
- package/templates/firebase/DESIGN_SYSTEM.md +234 -0
- package/templates/firebase/docs/auth-setup.en.md +7 -1
- package/templates/firebase/docs/auth-setup.es.md +7 -1
- package/templates/firebase/docs/auth-setup.pt.md +7 -1
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +79 -5
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_accordion.dart +2 -2
- package/templates/firebase/lib/components/kasy_alert.dart +1 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +7 -4
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_chip.dart +1 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +26 -21
- package/templates/firebase/lib/components/kasy_dialog.dart +2 -2
- package/templates/firebase/lib/components/kasy_screen.dart +114 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +2 -2
- package/templates/firebase/lib/components/kasy_tabs.dart +2 -2
- package/templates/firebase/lib/components/kasy_text_area.dart +37 -5
- package/templates/firebase/lib/components/kasy_text_field.dart +77 -16
- package/templates/firebase/lib/components/kasy_toast.dart +39 -70
- package/templates/firebase/lib/components/kasy_web_header.dart +4 -3
- package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
- package/templates/firebase/lib/core/config/features.dart +18 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +21 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +1 -1
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +46 -124
- package/templates/firebase/lib/core/theme/icon_sizes.dart +47 -0
- package/templates/firebase/lib/core/theme/shadows.dart +13 -0
- package/templates/firebase/lib/core/theme/texts.dart +32 -0
- package/templates/firebase/lib/core/theme/theme.dart +2 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +23 -4
- package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -7
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -1
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +61 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +1 -1
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -1
- package/templates/firebase/lib/features/authentication/ui/recover_password_page.dart +3 -1
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +57 -29
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +47 -25
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/feedback_page.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +1 -1
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +2 -3
- package/templates/firebase/lib/features/home/home_components_page.dart +7 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +54 -3
- package/templates/firebase/lib/features/home/home_image_grid.dart +1 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +165 -209
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -6
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +6 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +104 -156
- package/templates/firebase/lib/features/notifications/ui/widgets/permission_request_view.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +1 -1
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_module_mockups.dart +3 -3
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +3 -2
- package/templates/firebase/lib/features/settings/settings_page.dart +264 -307
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +17 -8
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +4 -4
- package/templates/firebase/lib/features/settings/ui/components/edit_name_sheet.dart +115 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +2 -2
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +13 -5
- package/templates/firebase/lib/features/subscriptions/api/stripe_backend_api.dart +12 -2
- package/templates/firebase/lib/features/subscriptions/api/stripe_payment_api.dart +7 -1
- package/templates/firebase/lib/features/subscriptions/providers/premium_page_provider.dart +11 -3
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/comparison_table.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +2 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_close_button.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +1 -1
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +3 -3
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +1 -1
- package/templates/firebase/lib/i18n/en.i18n.json +13 -4
- package/templates/firebase/lib/i18n/es.i18n.json +13 -4
- package/templates/firebase/lib/i18n/pt.i18n.json +13 -4
- package/templates/firebase/lib/router.dart +2 -0
- package/templates/firebase/pubspec.yaml +1 -2
- package/templates/firebase/tool/design_check.dart +152 -0
- package/templates/firebase/web/stripe_success.html +64 -26
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/animations/movefade_anim.dart +0 -34
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -67
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -183
- package/templates/firebase/assets/images/review.png +0 -0
- package/templates/firebase/assets/images/update.png +0 -0
- package/templates/firebase/lib/features/authentication/ui/components/facebook_signin.dart +0 -19
- package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
- package/templates/firebase/login-redesign-preview.png +0 -0
|
@@ -52,29 +52,36 @@ class KasyToast extends StatelessWidget {
|
|
|
52
52
|
color: c.surface,
|
|
53
53
|
borderRadius: KasyRadius.lgBorderRadius,
|
|
54
54
|
border: Border.all(
|
|
55
|
-
color: c.outline.withValues(alpha: isDark ? 0.
|
|
55
|
+
color: c.outline.withValues(alpha: isDark ? 0.20 : 0.20),
|
|
56
56
|
),
|
|
57
57
|
boxShadow: [
|
|
58
58
|
BoxShadow(
|
|
59
|
-
color: Colors.black.withValues(alpha: isDark ? 0.
|
|
60
|
-
blurRadius:
|
|
61
|
-
offset: const Offset(0,
|
|
59
|
+
color: Colors.black.withValues(alpha: isDark ? 0.22 : 0.06),
|
|
60
|
+
blurRadius: 18,
|
|
61
|
+
offset: const Offset(0, 6),
|
|
62
62
|
),
|
|
63
63
|
],
|
|
64
64
|
),
|
|
65
65
|
child: Padding(
|
|
66
|
-
padding
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// Tighter vertical rhythm and less right padding (the close affordance
|
|
67
|
+
// is a light icon, not a filled button) for a slimmer, refined bar.
|
|
68
|
+
padding: const EdgeInsets.fromLTRB(
|
|
69
|
+
KasySpacing.md,
|
|
70
|
+
KasySpacing.smd,
|
|
71
|
+
KasySpacing.sm,
|
|
72
|
+
KasySpacing.smd,
|
|
69
73
|
),
|
|
70
74
|
child: Row(
|
|
71
75
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
72
76
|
children: [
|
|
73
77
|
Padding(
|
|
74
|
-
padding: const EdgeInsets.only(top:
|
|
75
|
-
|
|
78
|
+
padding: const EdgeInsets.only(top: 1),
|
|
79
|
+
// Only the icon carries the tone colour; the title stays neutral
|
|
80
|
+
// so the toast reads calm instead of shouting.
|
|
81
|
+
child: icon ??
|
|
82
|
+
Icon(pal.icon, size: KasyIconSize.lg, color: pal.accent),
|
|
76
83
|
),
|
|
77
|
-
const SizedBox(width: KasySpacing.
|
|
84
|
+
const SizedBox(width: KasySpacing.sm),
|
|
78
85
|
Expanded(
|
|
79
86
|
child: Column(
|
|
80
87
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
@@ -82,38 +89,34 @@ class KasyToast extends StatelessWidget {
|
|
|
82
89
|
children: [
|
|
83
90
|
Text(
|
|
84
91
|
title,
|
|
85
|
-
style: context.textTheme.
|
|
86
|
-
color:
|
|
87
|
-
fontWeight: FontWeight.
|
|
92
|
+
style: context.textTheme.titleSmall?.copyWith(
|
|
93
|
+
color: c.onSurface,
|
|
94
|
+
fontWeight: FontWeight.w600,
|
|
88
95
|
),
|
|
89
96
|
),
|
|
90
97
|
if (message != null) ...[
|
|
91
|
-
const SizedBox(height:
|
|
98
|
+
const SizedBox(height: 2),
|
|
92
99
|
Text(
|
|
93
100
|
message!,
|
|
94
101
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
c.muted,
|
|
98
|
-
isDark ? Colors.white : Colors.black,
|
|
99
|
-
0.18,
|
|
100
|
-
),
|
|
102
|
+
color: c.muted,
|
|
103
|
+
height: 1.35,
|
|
101
104
|
),
|
|
102
105
|
),
|
|
103
106
|
],
|
|
104
107
|
],
|
|
105
108
|
),
|
|
106
109
|
),
|
|
107
|
-
const SizedBox(width: KasySpacing.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
),
|
|
110
|
+
const SizedBox(width: KasySpacing.xs),
|
|
111
|
+
KasyButton.iconOnly(
|
|
112
|
+
icon: KasyIcons.close,
|
|
113
|
+
variant: KasyButtonVariant.ghost,
|
|
114
|
+
foregroundColor: c.muted,
|
|
115
|
+
size: KasyButtonSize.small,
|
|
116
|
+
iconOnlyLayoutExtent: 28,
|
|
117
|
+
iconGlyphSize: 16,
|
|
118
|
+
onPressed: onClose,
|
|
119
|
+
semanticLabel: MaterialLocalizations.of(context).closeButtonTooltip,
|
|
117
120
|
),
|
|
118
121
|
],
|
|
119
122
|
),
|
|
@@ -706,63 +709,29 @@ class _AnimatedToastState extends State<_AnimatedToast>
|
|
|
706
709
|
// ---------------------------------------------------------------------------
|
|
707
710
|
|
|
708
711
|
class _Palette {
|
|
709
|
-
const _Palette({
|
|
710
|
-
required this.accent,
|
|
711
|
-
required this.icon,
|
|
712
|
-
required this.buttonBg,
|
|
713
|
-
required this.buttonFg,
|
|
714
|
-
});
|
|
712
|
+
const _Palette({required this.accent, required this.icon});
|
|
715
713
|
|
|
716
714
|
final Color accent;
|
|
717
715
|
final IconData icon;
|
|
718
|
-
final Color buttonBg;
|
|
719
|
-
final Color buttonFg;
|
|
720
716
|
}
|
|
721
717
|
|
|
722
718
|
_Palette _palette(KasyColors c, KasyToastTone tone) {
|
|
723
719
|
switch (tone) {
|
|
724
720
|
case KasyToastTone.neutral:
|
|
725
|
-
return _Palette(
|
|
726
|
-
accent: c.onSurface,
|
|
727
|
-
icon: KasyIcons.notification,
|
|
728
|
-
buttonBg: c.surfaceNeutralSoft,
|
|
729
|
-
buttonFg: c.onSurface,
|
|
730
|
-
);
|
|
721
|
+
return _Palette(accent: c.onSurface, icon: KasyIcons.notification);
|
|
731
722
|
case KasyToastTone.accent:
|
|
732
|
-
return _Palette(
|
|
733
|
-
accent: c.primary,
|
|
734
|
-
icon: KasyIcons.info,
|
|
735
|
-
buttonBg: c.primary,
|
|
736
|
-
// The Close button sits on a solid, vivid tone color — keep its label
|
|
737
|
-
// white for reliable contrast regardless of the brand "on" token.
|
|
738
|
-
buttonFg: Colors.white,
|
|
739
|
-
);
|
|
723
|
+
return _Palette(accent: c.primary, icon: KasyIcons.info);
|
|
740
724
|
case KasyToastTone.success:
|
|
741
725
|
final Color successDark = HSLColor.fromColor(c.success)
|
|
742
726
|
.withLightness(
|
|
743
727
|
(HSLColor.fromColor(c.success).lightness - 0.09).clamp(0.0, 1.0),
|
|
744
728
|
)
|
|
745
729
|
.toColor();
|
|
746
|
-
return _Palette(
|
|
747
|
-
accent: successDark,
|
|
748
|
-
icon: KasyIcons.checkCircle,
|
|
749
|
-
buttonBg: successDark,
|
|
750
|
-
buttonFg: Colors.white,
|
|
751
|
-
);
|
|
730
|
+
return _Palette(accent: successDark, icon: KasyIcons.checkCircle);
|
|
752
731
|
case KasyToastTone.warning:
|
|
753
|
-
return _Palette(
|
|
754
|
-
accent: c.warning,
|
|
755
|
-
icon: KasyIcons.privacy,
|
|
756
|
-
buttonBg: c.warning,
|
|
757
|
-
buttonFg: Colors.white,
|
|
758
|
-
);
|
|
732
|
+
return _Palette(accent: c.warning, icon: KasyIcons.privacy);
|
|
759
733
|
case KasyToastTone.danger:
|
|
760
|
-
return _Palette(
|
|
761
|
-
accent: c.error,
|
|
762
|
-
icon: KasyIcons.error,
|
|
763
|
-
buttonBg: c.error,
|
|
764
|
-
buttonFg: Colors.white,
|
|
765
|
-
);
|
|
734
|
+
return _Palette(accent: c.error, icon: KasyIcons.error);
|
|
766
735
|
}
|
|
767
736
|
}
|
|
768
737
|
|
|
@@ -148,13 +148,14 @@ class KasyWebHeader extends StatelessWidget {
|
|
|
148
148
|
|
|
149
149
|
Widget _buildSearch(BuildContext context) {
|
|
150
150
|
return KasyTextField(
|
|
151
|
+
variant: KasyTextFieldVariant.flat,
|
|
151
152
|
controller: searchController,
|
|
152
153
|
hint: searchHint,
|
|
153
154
|
onChanged: onSearchChanged,
|
|
154
155
|
onSubmitted: onSearchSubmitted,
|
|
155
156
|
prefix: Icon(
|
|
156
157
|
KasyIcons.search,
|
|
157
|
-
size:
|
|
158
|
+
size: KasyIconSize.md,
|
|
158
159
|
color: context.colors.muted,
|
|
159
160
|
),
|
|
160
161
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
@@ -168,7 +169,7 @@ class KasyWebHeader extends StatelessWidget {
|
|
|
168
169
|
variant: KasyButtonVariant.ghost,
|
|
169
170
|
size: KasyButtonSize.small,
|
|
170
171
|
iconOnlyLayoutExtent: 36,
|
|
171
|
-
iconGlyphSize:
|
|
172
|
+
iconGlyphSize: KasyIconSize.md,
|
|
172
173
|
onPressed: onToggleTheme,
|
|
173
174
|
semanticLabel: isDark ? 'Light mode' : 'Dark mode',
|
|
174
175
|
);
|
|
@@ -181,7 +182,7 @@ class KasyWebHeader extends StatelessWidget {
|
|
|
181
182
|
variant: KasyButtonVariant.ghost,
|
|
182
183
|
size: KasyButtonSize.small,
|
|
183
184
|
iconOnlyLayoutExtent: 36,
|
|
184
|
-
iconGlyphSize:
|
|
185
|
+
iconGlyphSize: KasyIconSize.md,
|
|
185
186
|
onPressed: onNotifications,
|
|
186
187
|
semanticLabel: 'Notifications',
|
|
187
188
|
);
|
|
@@ -71,6 +71,28 @@ class KasyChromeVisibility {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/// Brings the chrome back whenever a route is pushed onto or popped from the
|
|
75
|
+
/// root navigator. Tab switches already reset via [BartScaffold.onRouteChanged],
|
|
76
|
+
/// but detail screens (feedback, reminders, …) are pushed on the root navigator
|
|
77
|
+
/// and bypass that — so without this, returning to a screen that had scrolled
|
|
78
|
+
/// its chrome away would leave the app bar and bottom menu stuck hidden.
|
|
79
|
+
///
|
|
80
|
+
/// Only the chrome is restored; the destination screen keeps its scroll position
|
|
81
|
+
/// (the framework preserves it), matching how large apps handle back navigation.
|
|
82
|
+
class KasyChromeVisibilityObserver extends NavigatorObserver {
|
|
83
|
+
void _reset() => KasyChromeVisibility.instance.resetShown();
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => _reset();
|
|
87
|
+
|
|
88
|
+
@override
|
|
89
|
+
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => _reset();
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
|
|
93
|
+
_reset();
|
|
94
|
+
}
|
|
95
|
+
|
|
74
96
|
// ---------------------------------------------------------------------------
|
|
75
97
|
// Configuration — edit these to change the experience your app ships with.
|
|
76
98
|
// ---------------------------------------------------------------------------
|
|
@@ -6,6 +6,17 @@ const bool withAiChat = true;
|
|
|
6
6
|
const bool withFeedback = true;
|
|
7
7
|
const bool withRevenuecat = true;
|
|
8
8
|
const bool withLocalReminders = true;
|
|
9
|
+
/// When true, the Apple sign-in button is shown on WEB. Apple-on-web needs a paid
|
|
10
|
+
/// Apple Service ID + manual setup (see docs/auth-setup): Firebase can do it
|
|
11
|
+
/// (signInWithPopup), so this ships `true`; Supabase/API can't (no web secret), so
|
|
12
|
+
/// the CLI generates them as `false`. Apple shows on iOS/macOS regardless; it is
|
|
13
|
+
/// hidden on Android (also needs a paid Service ID, and the native flow throws).
|
|
14
|
+
const bool withAppleWebSignin = true;
|
|
15
|
+
/// When true, the Facebook sign-in button is shown on WEB. Facebook-on-web works
|
|
16
|
+
/// on the Firebase backend (signInWithPopup) after `kasy facebook`; on Supabase the
|
|
17
|
+
/// web flow isn't wired yet (roadmap), so the CLI ships this `false` there. Native
|
|
18
|
+
/// (iOS/Android) always shows the Facebook button regardless of this flag.
|
|
19
|
+
const bool withFacebookWebSignin = false;
|
|
9
20
|
/// When true, the app includes web support:
|
|
10
21
|
/// - anonymous sign-up is disabled on web (user is redirected to /signin)
|
|
11
22
|
/// - onboarding is skipped on web
|
|
@@ -14,3 +25,10 @@ const bool withWeb = true;
|
|
|
14
25
|
/// When true, the Stripe web-subscription module is included (web checkout +
|
|
15
26
|
/// customer portal). Independent from RevenueCat (which stays mobile-only).
|
|
16
27
|
const bool withStripe = true;
|
|
28
|
+
/// When true, Stripe Checkout shows a promo-code / coupon field.
|
|
29
|
+
/// Set to false if you have no promotions strategy yet.
|
|
30
|
+
const bool withStripePromoCodes = true;
|
|
31
|
+
/// When true, the Stripe Customer Portal lets subscribers switch plans
|
|
32
|
+
/// (upgrade / downgrade). Requires at least two recurring prices on the same
|
|
33
|
+
/// product. The portal configuration is created automatically on first use.
|
|
34
|
+
const bool withStripePlanSwitching = true;
|
|
@@ -36,6 +36,13 @@ final ValueNotifier<bool> devInspectorEnabledNotifier =
|
|
|
36
36
|
final ValueNotifier<bool> devInspectorCopyTriggerNotifier =
|
|
37
37
|
ValueNotifier<bool>(false);
|
|
38
38
|
|
|
39
|
+
/// Set to true to clear the current selection WITHOUT deactivating the
|
|
40
|
+
/// inspector. The Web Device Preview toggle fires this when entering/leaving
|
|
41
|
+
/// the device frame so a stale highlight doesn't linger across the transition.
|
|
42
|
+
/// [DevInspector] clears the selection and resets this to false.
|
|
43
|
+
final ValueNotifier<bool> devInspectorClearSelectionTriggerNotifier =
|
|
44
|
+
ValueNotifier<bool>(false);
|
|
45
|
+
|
|
39
46
|
/// Runtime active state of the inspector. Mirrors [devInspectorEnabledNotifier]
|
|
40
47
|
/// — the Web Device Preview pill, the admin toggle and the Esc shortcut all
|
|
41
48
|
/// flip the persisted notifier, and this one follows.
|
|
@@ -127,6 +134,8 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
127
134
|
devInspectorActiveNotifier.addListener(_handleActiveChanged);
|
|
128
135
|
devInspectorEnabledNotifier.addListener(_handleEnabledChanged);
|
|
129
136
|
devInspectorCopyTriggerNotifier.addListener(_onCopyTriggered);
|
|
137
|
+
devInspectorClearSelectionTriggerNotifier
|
|
138
|
+
.addListener(_onClearSelectionTriggered);
|
|
130
139
|
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
|
131
140
|
unawaited(_bootstrapEnabledPreference());
|
|
132
141
|
}
|
|
@@ -139,6 +148,8 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
139
148
|
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
|
140
149
|
devInspectorEnabledNotifier.removeListener(_handleEnabledChanged);
|
|
141
150
|
devInspectorCopyTriggerNotifier.removeListener(_onCopyTriggered);
|
|
151
|
+
devInspectorClearSelectionTriggerNotifier
|
|
152
|
+
.removeListener(_onClearSelectionTriggered);
|
|
142
153
|
devInspectorActiveNotifier.removeListener(_handleActiveChanged);
|
|
143
154
|
if (devInspectorActiveNotifier.value) {
|
|
144
155
|
devInspectorActiveNotifier.value = false;
|
|
@@ -312,6 +323,16 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
312
323
|
unawaited(_copySelection());
|
|
313
324
|
}
|
|
314
325
|
|
|
326
|
+
/// Drops the current selection while keeping the inspector active, so a stale
|
|
327
|
+
/// highlight doesn't carry over when the Web Device Preview is toggled.
|
|
328
|
+
void _onClearSelectionTriggered() {
|
|
329
|
+
if (!devInspectorClearSelectionTriggerNotifier.value) return;
|
|
330
|
+
devInspectorClearSelectionTriggerNotifier.value = false;
|
|
331
|
+
if (!mounted) return;
|
|
332
|
+
if (_selectedInfo == null && _selectedRender == null) return;
|
|
333
|
+
setState(_clearSelection);
|
|
334
|
+
}
|
|
335
|
+
|
|
315
336
|
void _handleActiveChanged() {
|
|
316
337
|
final bool active = devInspectorActiveNotifier.value;
|
|
317
338
|
if (_active == active) return;
|
|
@@ -127,7 +127,7 @@ class RateBannerWidget extends StatelessWidget {
|
|
|
127
127
|
color: cs.primary.withValues(alpha: 0.12),
|
|
128
128
|
shape: BoxShape.circle,
|
|
129
129
|
),
|
|
130
|
-
child: Icon(KasyIcons.star, color: cs.primary, size:
|
|
130
|
+
child: Icon(KasyIcons.star, color: cs.primary, size: KasyIconSize.xxl),
|
|
131
131
|
),
|
|
132
132
|
const SizedBox(height: KasySpacing.md),
|
|
133
133
|
Text(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
1
2
|
import 'package:flutter/material.dart';
|
|
2
|
-
import 'package:flutter_animate/flutter_animate.dart';
|
|
3
3
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
4
4
|
import 'package:go_router/go_router.dart';
|
|
5
5
|
import 'package:kasy_kit/components/components.dart';
|
|
@@ -7,17 +7,25 @@ import 'package:kasy_kit/core/data/api/analytics_api.dart';
|
|
|
7
7
|
import 'package:kasy_kit/core/rating/providers/rating_repository.dart';
|
|
8
8
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
9
9
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
10
|
-
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
11
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
12
11
|
import 'package:logger/logger.dart';
|
|
13
12
|
|
|
14
13
|
/// Shows the in-app review dialog. Prefer a stable [context] (not a sheet that
|
|
15
14
|
/// will be popped before the async work finishes).
|
|
15
|
+
///
|
|
16
|
+
/// A clean [KasyDialog]: a title, a short message and two stacked actions
|
|
17
|
+
/// (write a review / suggest improvements). Dismissing via the dialog's own
|
|
18
|
+
/// close button just defers the next ask and keeps the user where they are.
|
|
16
19
|
Future<bool> showReviewDialog(
|
|
17
20
|
BuildContext context,
|
|
18
21
|
WidgetRef ref, {
|
|
19
22
|
bool force = false,
|
|
20
23
|
}) async {
|
|
24
|
+
// Store reviews are a native-only concept (App Store / Play Store). On web
|
|
25
|
+
// there is nowhere to send the user, so the prompt never shows there.
|
|
26
|
+
if (kIsWeb) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
21
29
|
if (!context.mounted) {
|
|
22
30
|
return false;
|
|
23
31
|
}
|
|
@@ -41,97 +49,42 @@ Future<bool> showReviewDialog(
|
|
|
41
49
|
barrierDismissible: false,
|
|
42
50
|
builder: (dialogContext) {
|
|
43
51
|
ratingRepository.delay();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
'assets/images/review.png',
|
|
81
|
-
fit: BoxFit.fitWidth,
|
|
82
|
-
width: maxWidth,
|
|
83
|
-
),
|
|
84
|
-
),
|
|
85
|
-
Positioned(
|
|
86
|
-
top: KasySpacing.sm,
|
|
87
|
-
left: KasySpacing.sm,
|
|
88
|
-
child: CloseIcon(
|
|
89
|
-
onExit: () {
|
|
90
|
-
analytics.logEvent('rating_popup_close', {});
|
|
91
|
-
rating.delay().then((_) {
|
|
92
|
-
if (!dialogContext.mounted) return;
|
|
93
|
-
Navigator.of(dialogContext).pop();
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
),
|
|
97
|
-
),
|
|
98
|
-
],
|
|
99
|
-
),
|
|
100
|
-
const SizedBox(height: KasySpacing.md),
|
|
101
|
-
Text(
|
|
102
|
-
translations.description,
|
|
103
|
-
textAlign: TextAlign.center,
|
|
104
|
-
style: Theme.of(dialogContext).textTheme.bodyMedium,
|
|
105
|
-
),
|
|
106
|
-
],
|
|
107
|
-
),
|
|
108
|
-
footer: Column(
|
|
109
|
-
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
110
|
-
children: [
|
|
111
|
-
KasyButton(
|
|
112
|
-
label: translations.rate_button,
|
|
113
|
-
expand: true,
|
|
114
|
-
onPressed: () {
|
|
115
|
-
analytics.logEvent('rating_popup_show', {});
|
|
116
|
-
ratingRepository.rate().then((_) => rating.review()).then(
|
|
117
|
-
(_) {
|
|
118
|
-
if (!dialogContext.mounted) return;
|
|
119
|
-
Navigator.of(dialogContext).pop();
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
),
|
|
124
|
-
const SizedBox(height: KasySpacing.sm),
|
|
125
|
-
KasyButton(
|
|
126
|
-
label: translations.cancel_button,
|
|
127
|
-
variant: KasyButtonVariant.soft,
|
|
128
|
-
expand: true,
|
|
129
|
-
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
130
|
-
),
|
|
131
|
-
],
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
},
|
|
52
|
+
final translations = Translations.of(dialogContext).review_popup;
|
|
53
|
+
return KasyDialog(
|
|
54
|
+
leadingIcon: KasyIcons.star,
|
|
55
|
+
iconTone: KasyDialogIconTone.info,
|
|
56
|
+
title: translations.title,
|
|
57
|
+
titleCentered: true,
|
|
58
|
+
message: translations.description,
|
|
59
|
+
onClose: () {
|
|
60
|
+
analytics.logEvent('rating_popup_close', {});
|
|
61
|
+
rating.delay();
|
|
62
|
+
Navigator.of(dialogContext).pop();
|
|
63
|
+
},
|
|
64
|
+
footer: Column(
|
|
65
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
66
|
+
children: [
|
|
67
|
+
KasyButton(
|
|
68
|
+
label: translations.rate_button,
|
|
69
|
+
expand: true,
|
|
70
|
+
onPressed: () {
|
|
71
|
+
analytics.logEvent('rating_popup_show', {});
|
|
72
|
+
ratingRepository.rate().then((_) => rating.review()).then(
|
|
73
|
+
(_) {
|
|
74
|
+
if (!dialogContext.mounted) return;
|
|
75
|
+
Navigator.of(dialogContext).pop();
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
const SizedBox(height: KasySpacing.sm),
|
|
81
|
+
KasyButton(
|
|
82
|
+
label: translations.cancel_button,
|
|
83
|
+
variant: KasyButtonVariant.soft,
|
|
84
|
+
expand: true,
|
|
85
|
+
onPressed: () => Navigator.of(dialogContext).pop(true),
|
|
86
|
+
),
|
|
87
|
+
],
|
|
135
88
|
),
|
|
136
89
|
);
|
|
137
90
|
},
|
|
@@ -142,34 +95,3 @@ Future<bool> showReviewDialog(
|
|
|
142
95
|
}
|
|
143
96
|
return true;
|
|
144
97
|
}
|
|
145
|
-
|
|
146
|
-
class CloseIcon extends StatelessWidget {
|
|
147
|
-
final VoidCallback onExit;
|
|
148
|
-
|
|
149
|
-
const CloseIcon({super.key, required this.onExit});
|
|
150
|
-
|
|
151
|
-
@override
|
|
152
|
-
Widget build(BuildContext context) {
|
|
153
|
-
return ClipOval(
|
|
154
|
-
child: Material(
|
|
155
|
-
color: Colors.transparent,
|
|
156
|
-
child: InkWell(
|
|
157
|
-
onTap: () => onExit.call(),
|
|
158
|
-
child: Ink(
|
|
159
|
-
width: 32,
|
|
160
|
-
height: 32,
|
|
161
|
-
decoration: BoxDecoration(
|
|
162
|
-
color: context.colors.background,
|
|
163
|
-
shape: BoxShape.circle,
|
|
164
|
-
),
|
|
165
|
-
child: Icon(
|
|
166
|
-
KasyIcons.close,
|
|
167
|
-
color: context.colors.onBackground,
|
|
168
|
-
size: 21,
|
|
169
|
-
),
|
|
170
|
-
),
|
|
171
|
-
),
|
|
172
|
-
),
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/// Kasy Design System — Icon Size Tokens
|
|
2
|
+
///
|
|
3
|
+
/// Single source of truth for every icon dimension in the app. Screens and
|
|
4
|
+
/// components must read from here instead of hardcoding `size: 20` etc., so the
|
|
5
|
+
/// whole product can be re-scaled from one place.
|
|
6
|
+
///
|
|
7
|
+
/// Usage: `Icon(KasyIcons.person, size: KasyIconSize.rowLeading)`
|
|
8
|
+
///
|
|
9
|
+
/// Scale reference:
|
|
10
|
+
/// xxs → 12 micro glyphs inside small badges
|
|
11
|
+
/// xs → 14 tiny, dense inline glyphs
|
|
12
|
+
/// sm → 16 secondary / trailing (chevrons, value adornments)
|
|
13
|
+
/// md → 18 inline-with-text (chips, accordions, alerts)
|
|
14
|
+
/// lg → 20 default — list-row leading icons, chrome actions
|
|
15
|
+
/// xl → 24 prominent / large chrome
|
|
16
|
+
/// xxl → 28 feature highlights
|
|
17
|
+
/// display → 36 illustrative glyphs (empty states, brand badges)
|
|
18
|
+
/// hero → 72 hero glyph at the top of a focused screen (auth, onboarding)
|
|
19
|
+
class KasyIconSize {
|
|
20
|
+
KasyIconSize._();
|
|
21
|
+
|
|
22
|
+
static const double xxs = 12;
|
|
23
|
+
static const double xs = 14;
|
|
24
|
+
static const double sm = 16;
|
|
25
|
+
static const double md = 18;
|
|
26
|
+
static const double lg = 20;
|
|
27
|
+
static const double xl = 24;
|
|
28
|
+
static const double xxl = 28;
|
|
29
|
+
static const double display = 36;
|
|
30
|
+
static const double hero = 72;
|
|
31
|
+
|
|
32
|
+
// ── Semantic aliases ─────────────────────────────────────────────────────
|
|
33
|
+
// Prefer these in screens so intent is explicit and a single edit re-scales
|
|
34
|
+
// every row/chrome at once.
|
|
35
|
+
|
|
36
|
+
/// Leading icon in a list / settings row (the standard list glyph).
|
|
37
|
+
static const double rowLeading = lg; // 20
|
|
38
|
+
|
|
39
|
+
/// Trailing affordance in a row — chevron, small value adornment.
|
|
40
|
+
static const double rowTrailing = sm; // 16
|
|
41
|
+
|
|
42
|
+
/// Action icons in the top/bottom chrome (app bar orbs, nav).
|
|
43
|
+
static const double chrome = lg; // 20
|
|
44
|
+
|
|
45
|
+
/// Glyph sitting inline next to body text (chips, inline status).
|
|
46
|
+
static const double inline = md; // 18
|
|
47
|
+
}
|
|
@@ -38,6 +38,19 @@ class KasyShadows {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/// Resting border for the FLAT [KasyTextField] / [KasyTextArea] variant.
|
|
42
|
+
///
|
|
43
|
+
/// Flat fields share their container's fill (no fill contrast) and cast no
|
|
44
|
+
/// shadow, so the border alone separates them from the surface behind. It is
|
|
45
|
+
/// derived from `onSurface` (not the fixed-hue `outline` token) so it reads
|
|
46
|
+
/// clearly in BOTH modes: a soft dark hairline on light, a soft light
|
|
47
|
+
/// hairline on dark — where the regular hairline would vanish.
|
|
48
|
+
static Color inputFieldFlatBorder(BuildContext context) {
|
|
49
|
+
return context.colors.onSurface.withValues(
|
|
50
|
+
alpha: context.isDark ? 0.16 : 0.12,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
/// Standard shadow for input surfaces ([KasyTextField] and [KasyTextArea]).
|
|
42
55
|
///
|
|
43
56
|
/// Slightly tighter and softer than [component] — native only (callers skip on web).
|
|
@@ -54,6 +54,38 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
|
|
|
54
54
|
static TextStyle get buttonBase => _inter(FontWeight.w500, 16, 24);
|
|
55
55
|
static TextStyle get buttonSm => _inter(FontWeight.w500, 14, 20);
|
|
56
56
|
|
|
57
|
+
// -----------------------------------------------------------------------
|
|
58
|
+
// Semantic app roles — name the recurring UI text roles so screens stop
|
|
59
|
+
// hand-tuning slots with copyWith(fontSize/fontWeight/...). Each composes a
|
|
60
|
+
// scale role above; restyle a role app-wide by editing it here once.
|
|
61
|
+
// Apply color at the call site (.copyWith(color: ...)); the scale is colorless.
|
|
62
|
+
// -----------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/// Top-level page title (auth, paywall, full-screen flows). 24 / w700.
|
|
65
|
+
static TextStyle get pageTitle => heading2;
|
|
66
|
+
|
|
67
|
+
/// Section or master-detail pane title (Settings detail, grouped areas). 16 / w600.
|
|
68
|
+
static TextStyle get sectionTitle => heading4;
|
|
69
|
+
|
|
70
|
+
/// Small header above a grouped list (e.g. "PREFERENCES"). 12 / w600, lightly tracked.
|
|
71
|
+
static TextStyle get sectionLabel =>
|
|
72
|
+
_inter(FontWeight.w600, 12, 16).copyWith(letterSpacing: 0.5);
|
|
73
|
+
|
|
74
|
+
/// Primary text of a list / settings row. 14 / w500.
|
|
75
|
+
static TextStyle get rowTitle => bodySmMedium;
|
|
76
|
+
|
|
77
|
+
/// Secondary / value text in a row (apply a muted color at the call site). 14 / w400.
|
|
78
|
+
static TextStyle get rowValue => bodySm;
|
|
79
|
+
|
|
80
|
+
/// Card title. 14 / w500.
|
|
81
|
+
static TextStyle get cardTitle => bodySmMedium;
|
|
82
|
+
|
|
83
|
+
/// Card subtitle / supporting line. 12 / w400.
|
|
84
|
+
static TextStyle get cardSubtitle => bodyXs;
|
|
85
|
+
|
|
86
|
+
/// Caption, hint, version label, footnote. 12 / w400.
|
|
87
|
+
static TextStyle get caption => bodyXs;
|
|
88
|
+
|
|
57
89
|
// -----------------------------------------------------------------------
|
|
58
90
|
// Display — large hero text (extends the HeroUI scale upward in Inter)
|
|
59
91
|
// -----------------------------------------------------------------------
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
/// KasyTextTheme → context.textTheme.bodyMedium / .titleLarge / etc.
|
|
10
10
|
/// KasySpacing → KasySpacing.md (16) / .lg (24) / etc.
|
|
11
11
|
/// KasyRadius → KasyRadius.smBorderRadius / .lgBorderRadius / etc.
|
|
12
|
+
/// KasyIconSize → KasyIconSize.rowLeading (20) / .rowTrailing (16) / etc.
|
|
12
13
|
library;
|
|
13
14
|
|
|
14
15
|
export '../icons/kasy_icons.dart';
|
|
15
16
|
export 'colors.dart';
|
|
16
17
|
export 'extensions/theme_extension.dart';
|
|
18
|
+
export 'icon_sizes.dart';
|
|
17
19
|
export 'providers/theme_provider.dart';
|
|
18
20
|
export 'radius.dart';
|
|
19
21
|
export 'shadows.dart';
|
|
@@ -189,6 +189,9 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
189
189
|
// the DevInspector's in-app status pill while the preview is on.
|
|
190
190
|
devInspectorSuppressStatusPillNotifier.value =
|
|
191
191
|
webDevicePreviewEnabledNotifier.value;
|
|
192
|
+
// Entering or leaving the device frame: drop any lingering inspector
|
|
193
|
+
// selection so an old highlight doesn't carry across the transition.
|
|
194
|
+
devInspectorClearSelectionTriggerNotifier.value = true;
|
|
192
195
|
if (webDevicePreviewEnabledNotifier.value) {
|
|
193
196
|
_controlsTimer?.cancel();
|
|
194
197
|
_controlsTimer = Timer(const Duration(milliseconds: 800), () {
|