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
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
2
|
import 'package:flutter/widgets.dart';
|
|
3
3
|
|
|
4
|
-
///
|
|
4
|
+
/// Maximum render scale applied to the app on web (used on wide viewports).
|
|
5
5
|
///
|
|
6
6
|
/// Flutter web tends to render ~10% larger than equivalent HTML apps at the
|
|
7
7
|
/// browser's 100% zoom, so the whole UI feels oversized on desktop. `0.95`
|
|
8
8
|
/// brings it to the proportion the design targets (i.e. what 95% zoom looked
|
|
9
|
-
/// like) without the user having to touch the browser zoom.
|
|
9
|
+
/// like) without the user having to touch the browser zoom. On narrower
|
|
10
|
+
/// viewports the effective scale is reduced further (see [kWebViewportScaleTargetWidth]).
|
|
10
11
|
const double kWebViewportScale = 0.95;
|
|
11
12
|
|
|
13
|
+
/// Design target width (logical px) the desktop shell is laid out against.
|
|
14
|
+
///
|
|
15
|
+
/// A high-DPI display with OS scaling (Windows at 125/150/175%) reports a
|
|
16
|
+
/// smaller logical viewport width than a Mac at the same physical size, so a
|
|
17
|
+
/// fixed [kWebViewportScale] left the shell laid out narrower than the design
|
|
18
|
+
/// target and it looked cropped (the user had to Ctrl-minus). Scaling by
|
|
19
|
+
/// `width / kWebViewportScaleTargetWidth` instead pins the logical layout width
|
|
20
|
+
/// to this target on those displays, so Mac and Windows render the same.
|
|
21
|
+
const double kWebViewportScaleTargetWidth = 1280;
|
|
22
|
+
|
|
12
23
|
/// Minimum real viewport width (logical px) at which the web scale kicks in.
|
|
13
24
|
///
|
|
14
25
|
/// The "oversized" problem only shows up on tablet/desktop layouts. On mobile
|
|
@@ -44,9 +55,17 @@ class WebViewportScale extends StatelessWidget {
|
|
|
44
55
|
// Mobile web (narrow browser) renders at its natural size, just like the
|
|
45
56
|
// native build. The scale only applies from the tablet breakpoint up.
|
|
46
57
|
if (mq.size.width < kWebViewportScaleMinWidth) return child;
|
|
58
|
+
// Width-aware scale: on a wide viewport keep [scale] (0.95); on a high-DPI
|
|
59
|
+
// display with OS scaling the browser reports a smaller logical width, so
|
|
60
|
+
// scale down just enough to lay the shell out at the design target width
|
|
61
|
+
// instead of cropping it. Mac (wide) stays at 0.95; Windows at 125/150/175%
|
|
62
|
+
// shrinks proportionally so both look identical.
|
|
63
|
+
final double effectiveScale =
|
|
64
|
+
(mq.size.width / kWebViewportScaleTargetWidth).clamp(0.5, scale);
|
|
65
|
+
if (effectiveScale == 1.0) return child;
|
|
47
66
|
final Size logicalSize = Size(
|
|
48
|
-
mq.size.width /
|
|
49
|
-
mq.size.height /
|
|
67
|
+
mq.size.width / effectiveScale,
|
|
68
|
+
mq.size.height / effectiveScale,
|
|
50
69
|
);
|
|
51
70
|
return MediaQuery(
|
|
52
71
|
data: mq.copyWith(size: logicalSize),
|
|
@@ -3,17 +3,19 @@ import 'package:kasy_kit/components/components.dart';
|
|
|
3
3
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
4
4
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
5
5
|
|
|
6
|
-
/// Shows
|
|
6
|
+
/// Shows the "what's new" bottom sheet, built on [KasyBottomSheet].
|
|
7
|
+
///
|
|
8
|
+
/// Dismissing it — the Continue button, the drag handle, or tapping the dim
|
|
9
|
+
/// barrier outside — simply closes the sheet and returns the user to the screen
|
|
10
|
+
/// they were on; it never navigates anywhere. The version is already recorded as
|
|
11
|
+
/// seen before this is shown (see `MaybeShowUpdateBottomSheet`), so it won't pop
|
|
12
|
+
/// up again for the same version.
|
|
7
13
|
Future<void> showUpdateBottomSheet({
|
|
8
14
|
required BuildContext context,
|
|
9
15
|
required String version,
|
|
10
|
-
bool useRootNavigator = true,
|
|
11
16
|
}) async {
|
|
12
|
-
await
|
|
17
|
+
await showKasyBottomSheet<void>(
|
|
13
18
|
context: context,
|
|
14
|
-
useSafeArea: true,
|
|
15
|
-
useRootNavigator: useRootNavigator,
|
|
16
|
-
barrierColor: context.colors.background.withValues(alpha: 0.90),
|
|
17
19
|
isScrollControlled: true,
|
|
18
20
|
builder: (context) => _UpdateBottomSheet(version: version),
|
|
19
21
|
);
|
|
@@ -28,130 +30,31 @@ class _UpdateBottomSheet extends StatelessWidget {
|
|
|
28
30
|
Widget build(BuildContext context) {
|
|
29
31
|
final translations = Translations.of(context).update_bottom_sheet;
|
|
30
32
|
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
return KasyBottomSheet(
|
|
34
|
+
icon: KasyIcons.star,
|
|
35
|
+
title: translations.title,
|
|
36
|
+
message: 'Version $version',
|
|
37
|
+
body: ConstrainedBox(
|
|
38
|
+
constraints: BoxConstraints(
|
|
39
|
+
maxHeight: MediaQuery.sizeOf(context).height * 0.5,
|
|
37
40
|
),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
child: ListView.separated(
|
|
42
|
+
shrinkWrap: true,
|
|
43
|
+
padding: EdgeInsets.zero,
|
|
44
|
+
itemBuilder: (context, index) =>
|
|
45
|
+
_UpdateHighlightTile(highlight: translations.highlights[index]),
|
|
46
|
+
separatorBuilder: (context, index) =>
|
|
47
|
+
const SizedBox(height: KasySpacing.sm),
|
|
48
|
+
itemCount: translations.highlights.length,
|
|
42
49
|
),
|
|
43
50
|
),
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
children: [
|
|
50
|
-
Flexible(
|
|
51
|
-
flex: 0,
|
|
52
|
-
child: Align(
|
|
53
|
-
child: ClipRRect(
|
|
54
|
-
borderRadius: const BorderRadius.only(
|
|
55
|
-
topLeft: Radius.circular(KasyRadius.xl),
|
|
56
|
-
topRight: Radius.circular(KasyRadius.xl),
|
|
57
|
-
),
|
|
58
|
-
child: Image.asset(
|
|
59
|
-
'assets/images/update.png',
|
|
60
|
-
fit: BoxFit.cover,
|
|
61
|
-
),
|
|
62
|
-
),
|
|
63
|
-
),
|
|
64
|
-
),
|
|
65
|
-
Padding(
|
|
66
|
-
padding: const EdgeInsets.fromLTRB(
|
|
67
|
-
KasySpacing.lg,
|
|
68
|
-
KasySpacing.lg,
|
|
69
|
-
KasySpacing.md,
|
|
70
|
-
0,
|
|
71
|
-
),
|
|
72
|
-
child: Row(
|
|
73
|
-
children: [
|
|
74
|
-
Container(
|
|
75
|
-
width: 48,
|
|
76
|
-
height: 48,
|
|
77
|
-
decoration: BoxDecoration(
|
|
78
|
-
color: context.colors.primary.withValues(alpha: 0.1),
|
|
79
|
-
borderRadius: KasyRadius.mdBorderRadius,
|
|
80
|
-
),
|
|
81
|
-
child: Icon(
|
|
82
|
-
KasyIcons.star,
|
|
83
|
-
color: context.colors.primary,
|
|
84
|
-
size: 24,
|
|
85
|
-
),
|
|
86
|
-
),
|
|
87
|
-
const SizedBox(width: KasySpacing.md),
|
|
88
|
-
Expanded(
|
|
89
|
-
child: Column(
|
|
90
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
91
|
-
children: [
|
|
92
|
-
Text(
|
|
93
|
-
translations.title,
|
|
94
|
-
style: context.textTheme.headlineSmall?.copyWith(
|
|
95
|
-
color: context.colors.onSurface,
|
|
96
|
-
fontWeight: FontWeight.w700,
|
|
97
|
-
),
|
|
98
|
-
),
|
|
99
|
-
Text(
|
|
100
|
-
"Version $version",
|
|
101
|
-
style: context.textTheme.bodyMedium?.copyWith(
|
|
102
|
-
color: context.colors.muted,
|
|
103
|
-
fontWeight: FontWeight.w500,
|
|
104
|
-
),
|
|
105
|
-
),
|
|
106
|
-
],
|
|
107
|
-
),
|
|
108
|
-
),
|
|
109
|
-
KasyButton.iconOnly(
|
|
110
|
-
icon: KasyIcons.close,
|
|
111
|
-
variant: KasyButtonVariant.ghost,
|
|
112
|
-
foregroundColor: context.colors.muted,
|
|
113
|
-
onPressed: () => Navigator.of(context).pop(),
|
|
114
|
-
semanticLabel: translations.title,
|
|
115
|
-
),
|
|
116
|
-
],
|
|
117
|
-
),
|
|
118
|
-
),
|
|
119
|
-
const SizedBox(height: KasySpacing.lg),
|
|
120
|
-
Flexible(
|
|
121
|
-
child: ConstrainedBox(
|
|
122
|
-
constraints: BoxConstraints(
|
|
123
|
-
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
|
124
|
-
),
|
|
125
|
-
child: ListView.separated(
|
|
126
|
-
shrinkWrap: true,
|
|
127
|
-
padding: const EdgeInsets.symmetric(
|
|
128
|
-
horizontal: KasySpacing.lg,
|
|
129
|
-
),
|
|
130
|
-
itemBuilder: (context, index) {
|
|
131
|
-
final highlight = translations.highlights[index];
|
|
132
|
-
return _UpdateHighlightTile(highlight: highlight);
|
|
133
|
-
},
|
|
134
|
-
separatorBuilder: (context, index) =>
|
|
135
|
-
const SizedBox(height: KasySpacing.sm),
|
|
136
|
-
itemCount: translations.highlights.length,
|
|
137
|
-
),
|
|
138
|
-
),
|
|
139
|
-
),
|
|
140
|
-
const SizedBox(height: KasySpacing.xl),
|
|
141
|
-
Padding(
|
|
142
|
-
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.lg),
|
|
143
|
-
child: SizedBox(
|
|
144
|
-
width: double.infinity,
|
|
145
|
-
child: KasyButton(
|
|
146
|
-
label: translations.continue_button,
|
|
147
|
-
expand: true,
|
|
148
|
-
onPressed: () => Navigator.of(context).pop(),
|
|
149
|
-
),
|
|
150
|
-
),
|
|
151
|
-
),
|
|
152
|
-
],
|
|
51
|
+
actions: [
|
|
52
|
+
KasyButton(
|
|
53
|
+
label: translations.continue_button,
|
|
54
|
+
expand: true,
|
|
55
|
+
onPressed: () => Navigator.of(context).pop(),
|
|
153
56
|
),
|
|
154
|
-
|
|
57
|
+
],
|
|
155
58
|
);
|
|
156
59
|
}
|
|
157
60
|
}
|
|
@@ -115,13 +115,8 @@ class _DetailContainer extends StatelessWidget {
|
|
|
115
115
|
|
|
116
116
|
@override
|
|
117
117
|
Widget build(BuildContext context) {
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
decoration: BoxDecoration(
|
|
121
|
-
color: context.colors.surface,
|
|
122
|
-
borderRadius: BorderRadius.circular(24),
|
|
123
|
-
border: Border.all(color: context.colors.border, width: 0.5),
|
|
124
|
-
),
|
|
118
|
+
return KasyCard(
|
|
119
|
+
borderRadius: BorderRadius.circular(KasyRadius.xl),
|
|
125
120
|
child: child,
|
|
126
121
|
);
|
|
127
122
|
}
|
|
@@ -143,6 +138,15 @@ class _DetailPlaceholder extends StatelessWidget {
|
|
|
143
138
|
showShadow: true,
|
|
144
139
|
),
|
|
145
140
|
const SizedBox(height: KasySpacing.md),
|
|
141
|
+
Text(
|
|
142
|
+
t.home.cards.assistant_title,
|
|
143
|
+
textAlign: TextAlign.center,
|
|
144
|
+
style: context.textTheme.titleMedium?.copyWith(
|
|
145
|
+
fontWeight: FontWeight.w600,
|
|
146
|
+
color: context.colors.onBackground,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
const SizedBox(height: KasySpacing.xs),
|
|
146
150
|
Text(
|
|
147
151
|
t.ai_chat.no_conversation_selected,
|
|
148
152
|
textAlign: TextAlign.center,
|
|
@@ -30,6 +30,12 @@ class AiChatComposer extends StatefulWidget {
|
|
|
30
30
|
class _AiChatComposerState extends State<AiChatComposer> {
|
|
31
31
|
bool _canSend = false;
|
|
32
32
|
|
|
33
|
+
// Owns the field focus so we can intercept hardware Enter: plain Enter sends,
|
|
34
|
+
// Shift+Enter inserts a newline (the standard chat-composer behaviour). On
|
|
35
|
+
// touch keyboards (no hardware key event) the return key keeps inserting a
|
|
36
|
+
// newline and the send orb is used, so mobile is unchanged.
|
|
37
|
+
late final FocusNode _focusNode = FocusNode(onKeyEvent: _onKeyEvent);
|
|
38
|
+
|
|
33
39
|
@override
|
|
34
40
|
void initState() {
|
|
35
41
|
super.initState();
|
|
@@ -40,9 +46,23 @@ class _AiChatComposerState extends State<AiChatComposer> {
|
|
|
40
46
|
@override
|
|
41
47
|
void dispose() {
|
|
42
48
|
widget.controller.removeListener(_onTextChanged);
|
|
49
|
+
_focusNode.dispose();
|
|
43
50
|
super.dispose();
|
|
44
51
|
}
|
|
45
52
|
|
|
53
|
+
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
|
|
54
|
+
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
|
55
|
+
final bool isEnter = event.logicalKey == LogicalKeyboardKey.enter ||
|
|
56
|
+
event.logicalKey == LogicalKeyboardKey.numpadEnter;
|
|
57
|
+
if (!isEnter) return KeyEventResult.ignored;
|
|
58
|
+
// Shift+Enter falls through to the field and inserts a newline.
|
|
59
|
+
if (HardwareKeyboard.instance.isShiftPressed) {
|
|
60
|
+
return KeyEventResult.ignored;
|
|
61
|
+
}
|
|
62
|
+
_handleSend();
|
|
63
|
+
return KeyEventResult.handled;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
bool _hasSendableText(String value) {
|
|
47
67
|
final String trimmed = value.trim();
|
|
48
68
|
return trimmed.isNotEmpty && trimmed.length <= kAiChatMaxMessageLength;
|
|
@@ -88,6 +108,7 @@ class _AiChatComposerState extends State<AiChatComposer> {
|
|
|
88
108
|
Expanded(
|
|
89
109
|
child: KasyTextField(
|
|
90
110
|
controller: widget.controller,
|
|
111
|
+
focusNode: _focusNode,
|
|
91
112
|
enabled: enabled,
|
|
92
113
|
variant: KasyTextFieldVariant.embedded,
|
|
93
114
|
hint: t.ai_chat.hint,
|
|
@@ -208,7 +208,7 @@ class _Header extends StatelessWidget {
|
|
|
208
208
|
maxLines: 1,
|
|
209
209
|
overflow: TextOverflow.ellipsis,
|
|
210
210
|
style: context.textTheme.titleLarge?.copyWith(
|
|
211
|
-
fontWeight: FontWeight.
|
|
211
|
+
fontWeight: FontWeight.w700,
|
|
212
212
|
color: context.colors.onBackground,
|
|
213
213
|
),
|
|
214
214
|
),
|
|
@@ -133,7 +133,7 @@ class _AiConversationTileState extends ConsumerState<AiConversationTile> {
|
|
|
133
133
|
child: InkResponse(
|
|
134
134
|
onTap: widget.onDelete,
|
|
135
135
|
radius: 18,
|
|
136
|
-
child: Icon(KasyIcons.trash, size:
|
|
136
|
+
child: Icon(KasyIcons.trash, size: KasyIconSize.sm, color: context.colors.error),
|
|
137
137
|
),
|
|
138
138
|
);
|
|
139
139
|
}
|
|
@@ -392,6 +392,46 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
392
392
|
/// Falls back to regular sign-in if the Facebook account already exists.
|
|
393
393
|
@override
|
|
394
394
|
Future<Credentials> signupFromAnonymousWithFacebook() async {
|
|
395
|
+
// Web: use the Firebase popup (link to the anonymous user when present), same
|
|
396
|
+
// pattern as Apple/Google on web.
|
|
397
|
+
if (kIsWeb) {
|
|
398
|
+
final facebookProvider = FacebookAuthProvider();
|
|
399
|
+
facebookProvider.addScope('email');
|
|
400
|
+
final currentUser = _auth.currentUser;
|
|
401
|
+
if (currentUser == null) {
|
|
402
|
+
try {
|
|
403
|
+
final result = await _popupOrCancel(
|
|
404
|
+
() => _auth.signInWithPopup(facebookProvider),
|
|
405
|
+
);
|
|
406
|
+
return Credentials(id: result.user!.uid, token: result.credential?.token.toString() ?? '');
|
|
407
|
+
} on FirebaseAuthException catch (e) {
|
|
408
|
+
if (_isUserCancelledPopup(e.code)) throw const UserCancelledSignInException();
|
|
409
|
+
rethrow;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
final result = await _popupOrCancel(() => currentUser.linkWithPopup(facebookProvider));
|
|
414
|
+
return Credentials(id: result.user!.uid, token: result.credential?.token.toString() ?? '');
|
|
415
|
+
} on FirebaseAuthException catch (e) {
|
|
416
|
+
if (_isUserCancelledPopup(e.code)) throw const UserCancelledSignInException();
|
|
417
|
+
if (e.code == 'credential-already-in-use') {
|
|
418
|
+
final anonymousUser = _auth.currentUser;
|
|
419
|
+
if (anonymousUser != null && anonymousUser.isAnonymous) {
|
|
420
|
+
await anonymousUser.delete();
|
|
421
|
+
}
|
|
422
|
+
final cred = e.credential;
|
|
423
|
+
if (cred != null) {
|
|
424
|
+
final result = await _auth.signInWithCredential(cred);
|
|
425
|
+
return Credentials(id: result.user!.uid, token: result.credential?.token.toString() ?? '');
|
|
426
|
+
}
|
|
427
|
+
final result = await _popupOrCancel(
|
|
428
|
+
() => _auth.signInWithPopup(facebookProvider),
|
|
429
|
+
);
|
|
430
|
+
return Credentials(id: result.user!.uid, token: result.credential?.token.toString() ?? '');
|
|
431
|
+
}
|
|
432
|
+
rethrow;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
395
435
|
final loginResult = await FacebookAuth.instance.login();
|
|
396
436
|
if (loginResult.status == LoginStatus.cancelled) throw const UserCancelledSignInException();
|
|
397
437
|
if (loginResult.status != LoginStatus.success || loginResult.accessToken == null) {
|
|
@@ -429,6 +469,27 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
429
469
|
|
|
430
470
|
@override
|
|
431
471
|
Future<Credentials> signinWithFacebook() async {
|
|
472
|
+
// Web has no native Facebook SDK flow: use the Firebase popup, same as Google
|
|
473
|
+
// and Apple on web. Requires the Facebook provider enabled in Firebase
|
|
474
|
+
// (kasy facebook) and the redirect URI registered on Meta.
|
|
475
|
+
if (kIsWeb) {
|
|
476
|
+
final facebookProvider = FacebookAuthProvider();
|
|
477
|
+
facebookProvider.addScope('email');
|
|
478
|
+
try {
|
|
479
|
+
final value = await _popupOrCancel(
|
|
480
|
+
() => _auth.signInWithPopup(facebookProvider),
|
|
481
|
+
);
|
|
482
|
+
return Credentials(
|
|
483
|
+
id: value.user!.uid,
|
|
484
|
+
token: value.credential?.token.toString() ?? '',
|
|
485
|
+
);
|
|
486
|
+
} on FirebaseAuthException catch (e) {
|
|
487
|
+
if (_isUserCancelledPopup(e.code)) {
|
|
488
|
+
throw const UserCancelledSignInException();
|
|
489
|
+
}
|
|
490
|
+
rethrow;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
432
493
|
final LoginResult loginResult = await FacebookAuth.instance.login();
|
|
433
494
|
if (loginResult.status == LoginStatus.cancelled) throw const UserCancelledSignInException();
|
|
434
495
|
if (loginResult.status != LoginStatus.success || loginResult.accessToken == null) {
|
|
@@ -32,7 +32,7 @@ class _OtpVerificationComponentState
|
|
|
32
32
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
33
33
|
children: [
|
|
34
34
|
const SizedBox(height: KasySpacing.lg),
|
|
35
|
-
Icon(KasyIcons.sms, size:
|
|
35
|
+
Icon(KasyIcons.sms, size: KasyIconSize.hero, color: context.colors.primary),
|
|
36
36
|
const SizedBox(height: KasySpacing.lg),
|
|
37
37
|
Text(
|
|
38
38
|
t.phone_auth.verification_code,
|
|
@@ -34,7 +34,7 @@ class _PhoneInputComponentState extends ConsumerState<PhoneInputComponent> {
|
|
|
34
34
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
35
35
|
children: [
|
|
36
36
|
const SizedBox(height: KasySpacing.lg),
|
|
37
|
-
Icon(KasyIcons.phoneAndroid, size:
|
|
37
|
+
Icon(KasyIcons.phoneAndroid, size: KasyIconSize.hero, color: context.colors.primary),
|
|
38
38
|
const SizedBox(height: KasySpacing.lg),
|
|
39
39
|
Text(
|
|
40
40
|
t.phone_auth.subtitle_input,
|
|
@@ -71,6 +71,7 @@ class _PhoneInputComponentState extends ConsumerState<PhoneInputComponent> {
|
|
|
71
71
|
),
|
|
72
72
|
),
|
|
73
73
|
KasyTextField(
|
|
74
|
+
variant: KasyTextFieldVariant.flat,
|
|
74
75
|
controller: _phoneController,
|
|
75
76
|
keyboardType: TextInputType.phone,
|
|
76
77
|
label: t.phone_auth.phone_label,
|
|
@@ -53,6 +53,7 @@ class RecoverPasswordPage extends ConsumerWidget {
|
|
|
53
53
|
subtitle: t.auth.recover.subtitle,
|
|
54
54
|
children: [
|
|
55
55
|
KasyTextField(
|
|
56
|
+
variant: KasyTextFieldVariant.flat,
|
|
56
57
|
key: const Key('email_input'),
|
|
57
58
|
label: t.auth.recover.email_label,
|
|
58
59
|
contentType: KasyTextFieldContentType.email,
|
|
@@ -124,7 +125,8 @@ class _BackToSigninPrompt extends StatelessWidget {
|
|
|
124
125
|
context.go('/signin');
|
|
125
126
|
}
|
|
126
127
|
},
|
|
127
|
-
|
|
128
|
+
// Secondary link: kept out of Tab traversal (focusable defaults to
|
|
129
|
+
// false) so keyboard/next flows email → submit, not this link.
|
|
128
130
|
child: Text(
|
|
129
131
|
t.auth.recover.signin_link,
|
|
130
132
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import 'dart:ui';
|
|
2
2
|
|
|
3
|
+
import 'package:flutter/foundation.dart';
|
|
3
4
|
import 'package:flutter/material.dart';
|
|
4
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
5
6
|
import 'package:go_router/go_router.dart';
|
|
6
7
|
import 'package:kasy_kit/components/components.dart';
|
|
8
|
+
import 'package:kasy_kit/core/config/features.dart';
|
|
7
9
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
8
10
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
9
11
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
@@ -47,7 +49,7 @@ class SigninPage extends ConsumerWidget {
|
|
|
47
49
|
const TextStyle())
|
|
48
50
|
.copyWith(
|
|
49
51
|
color: context.colors.muted,
|
|
50
|
-
fontSize: 13,
|
|
52
|
+
fontSize: 13, // design-check: ignore — small "forgot password" link
|
|
51
53
|
fontWeight: FontWeight.w500,
|
|
52
54
|
);
|
|
53
55
|
return PopScope(
|
|
@@ -64,6 +66,7 @@ class SigninPage extends ConsumerWidget {
|
|
|
64
66
|
subtitle: t.auth.signin.subtitle,
|
|
65
67
|
children: [
|
|
66
68
|
KasyTextField(
|
|
69
|
+
variant: KasyTextFieldVariant.flat,
|
|
67
70
|
key: const Key('email_input'),
|
|
68
71
|
onChanged: (value) => ref
|
|
69
72
|
.read(signinStateProvider.notifier)
|
|
@@ -83,6 +86,7 @@ class SigninPage extends ConsumerWidget {
|
|
|
83
86
|
),
|
|
84
87
|
const SizedBox(height: _authFieldSpacing),
|
|
85
88
|
KasyTextField(
|
|
89
|
+
variant: KasyTextFieldVariant.flat,
|
|
86
90
|
key: const Key('password_input'),
|
|
87
91
|
onChanged: (newValue) => ref
|
|
88
92
|
.read(signinStateProvider.notifier)
|
|
@@ -98,10 +102,15 @@ class SigninPage extends ConsumerWidget {
|
|
|
98
102
|
FocusScope.of(context).unfocus();
|
|
99
103
|
ref.read(signinStateProvider.notifier).signin();
|
|
100
104
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
// Secondary link: intentionally NOT a Tab stop, so
|
|
106
|
+
// Tab/next flows email → password → submit → social
|
|
107
|
+
// buttons. Still tappable by mouse/touch and announced
|
|
108
|
+
// to screen readers as a button.
|
|
109
|
+
labelTrailing: Semantics(
|
|
110
|
+
button: true,
|
|
111
|
+
label: t.auth.signin.forgot_password,
|
|
104
112
|
child: GestureDetector(
|
|
113
|
+
behavior: HitTestBehavior.opaque,
|
|
105
114
|
onTap: () => context.push('/recover_password'),
|
|
106
115
|
child: Text(
|
|
107
116
|
t.auth.signin.forgot_password,
|
|
@@ -195,7 +204,9 @@ class _SignupPrompt extends StatelessWidget {
|
|
|
195
204
|
KasyPressableDepth(
|
|
196
205
|
semanticLabel: t.auth.signin.signup_link,
|
|
197
206
|
onPressed: () => context.pushReplacement('/signup'),
|
|
198
|
-
|
|
207
|
+
// Secondary link: kept out of Tab traversal (focusable defaults to
|
|
208
|
+
// false) so keyboard/next jumps straight to the social sign-in
|
|
209
|
+
// buttons (the most-used path).
|
|
199
210
|
child: Text(
|
|
200
211
|
t.auth.signin.signup_link,
|
|
201
212
|
style: context.textTheme.bodyMedium?.copyWith(
|
|
@@ -218,6 +229,18 @@ class _SocialSigninRow extends ConsumerWidget {
|
|
|
218
229
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
219
230
|
final state = ref.watch(signinStateProvider);
|
|
220
231
|
final isSending = state is SigninStateSending;
|
|
232
|
+
// Apple sign-in is reliable only on Apple-native platforms (iOS/macOS). On web
|
|
233
|
+
// it needs a paid Apple Service ID + manual console setup (see auth docs), gated
|
|
234
|
+
// by withAppleWebSignin (off by default). On Android it needs the same Service ID
|
|
235
|
+
// and the Supabase/API native flow throws, so Apple is hidden there.
|
|
236
|
+
final bool showApple = kIsWeb
|
|
237
|
+
? withAppleWebSignin
|
|
238
|
+
: (defaultTargetPlatform == TargetPlatform.iOS ||
|
|
239
|
+
defaultTargetPlatform == TargetPlatform.macOS);
|
|
240
|
+
// Facebook on web works on the Firebase backend (signInWithPopup); on Supabase
|
|
241
|
+
// the web flow isn't wired yet (roadmap), so withFacebookWebSignin ships false
|
|
242
|
+
// there. Native (iOS/Android) always shows it.
|
|
243
|
+
const bool showFacebook = !kIsWeb || withFacebookWebSignin;
|
|
221
244
|
return Row(
|
|
222
245
|
children: [
|
|
223
246
|
Expanded(
|
|
@@ -230,33 +253,38 @@ class _SocialSigninRow extends ConsumerWidget {
|
|
|
230
253
|
ref.read(signinStateProvider.notifier).signinWithGoogle(),
|
|
231
254
|
),
|
|
232
255
|
),
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
if (showApple) ...[
|
|
257
|
+
const SizedBox(width: KasySpacing.sm),
|
|
258
|
+
Expanded(
|
|
259
|
+
child: _SocialSigninTile(
|
|
260
|
+
label: t.auth.signin.apple,
|
|
261
|
+
icon: Image.asset('assets/icons/apple.png', width: 20, height: 20),
|
|
262
|
+
onPressed: isSending
|
|
263
|
+
? null
|
|
264
|
+
: () => ref
|
|
265
|
+
.read(signinStateProvider.notifier)
|
|
266
|
+
.signinWithApple(),
|
|
267
|
+
),
|
|
242
268
|
),
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
269
|
+
],
|
|
270
|
+
if (showFacebook) ...[
|
|
271
|
+
const SizedBox(width: KasySpacing.sm),
|
|
272
|
+
Expanded(
|
|
273
|
+
child: _SocialSigninTile(
|
|
274
|
+
label: t.auth.signin.facebook,
|
|
275
|
+
icon: Image.asset(
|
|
276
|
+
'assets/icons/facebook.png',
|
|
277
|
+
width: 20,
|
|
278
|
+
height: 20,
|
|
279
|
+
),
|
|
280
|
+
onPressed: isSending
|
|
281
|
+
? null
|
|
282
|
+
: () => ref
|
|
283
|
+
.read(signinStateProvider.notifier)
|
|
284
|
+
.signinWithFacebook(),
|
|
252
285
|
),
|
|
253
|
-
onPressed: isSending
|
|
254
|
-
? null
|
|
255
|
-
: () => ref
|
|
256
|
-
.read(signinStateProvider.notifier)
|
|
257
|
-
.signinWithFacebook(),
|
|
258
286
|
),
|
|
259
|
-
|
|
287
|
+
],
|
|
260
288
|
],
|
|
261
289
|
);
|
|
262
290
|
}
|