kasy-cli 1.38.0 → 1.39.1
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/scaffold/CHANGELOG.json +23 -0
- package/lib/scaffold/backends/api/patch/README.md +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/backends/patch-base-hashes.json +6 -6
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/shared/generator-utils.js +12 -6
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +2 -2
- package/templates/firebase/DESIGN_SYSTEM.md +23 -8
- package/templates/firebase/assets/icons/apple_black.svg +3 -0
- package/templates/firebase/assets/icons/apple_white.svg +4 -0
- package/templates/firebase/assets/icons/facebook.svg +49 -0
- package/templates/firebase/assets/icons/google.svg +1 -0
- package/templates/firebase/functions/src/admin/functions.ts +2 -0
- package/templates/firebase/functions/src/authentication/functions.ts +13 -7
- package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
- package/templates/firebase/lib/components/components.dart +5 -2
- package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
- package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
- package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +34 -16
- package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
- package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +95 -30
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
- package/templates/firebase/lib/core/states/logout_action.dart +11 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
- package/templates/firebase/lib/core/theme/texts.dart +21 -6
- package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
- package/templates/firebase/lib/core/web_viewport_scale.dart +66 -36
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
- 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 +18 -8
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
- package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
- package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +263 -59
- package/templates/firebase/lib/features/home/home_feed.dart +2 -2
- package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
- package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
- package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
- package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
- package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
- package/templates/firebase/lib/i18n/en.i18n.json +753 -712
- package/templates/firebase/lib/i18n/es.i18n.json +753 -712
- package/templates/firebase/lib/i18n/pt.i18n.json +753 -712
- package/templates/firebase/lib/main.dart +20 -7
- package/templates/firebase/lib/router.dart +32 -26
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
- package/templates/firebase/test/app_bar_config_test.dart +70 -0
- package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
- package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
- package/templates/firebase/tool/design_check.dart +9 -0
- package/templates/firebase/assets/icons/apple.png +0 -0
- package/templates/firebase/assets/icons/facebook.png +0 -0
- package/templates/firebase/assets/icons/google.png +0 -0
- package/templates/firebase/assets/icons/google_play_games.png +0 -0
- package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
- package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
- package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
- package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
- package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
|
@@ -30,7 +30,25 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
30
30
|
return OnboardingState();
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/// Enter/leave preview mode. Called once when [OnboardingPage] mounts: the
|
|
34
|
+
/// admin Debug "Test onboarding" entry sets `true`, the real flow sets
|
|
35
|
+
/// `false`, so the flag is always correct on every entry.
|
|
36
|
+
void setPreview(bool value) {
|
|
37
|
+
if (state.preview == value) return;
|
|
38
|
+
state = state.copyWith(preview: value);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
Future<void> onAnsweredQuestion(UserInfoDetail value) async {
|
|
42
|
+
// Preview: never touch the admin's real profile. Buffer the answer so the
|
|
43
|
+
// question screens still behave (selection persists across steps), but it's
|
|
44
|
+
// discarded with the provider state once the preview ends.
|
|
45
|
+
if (state.preview) {
|
|
46
|
+
final others = state.pendingUserInfo
|
|
47
|
+
.where((info) => info.runtimeType != value.runtimeType)
|
|
48
|
+
.toList();
|
|
49
|
+
state = state.copyWith(pendingUserInfo: [...others, value]);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
34
52
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
35
53
|
if (userId != null) {
|
|
36
54
|
// Account already exists (e.g. a returning guest re-onboarding): save now.
|
|
@@ -47,6 +65,10 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
Future<void> setupNotifications() async {
|
|
68
|
+
// Preview: don't fire the real OS permission dialog or log analytics — the
|
|
69
|
+
// permission screen is shown for review only.
|
|
70
|
+
if (state.preview) return;
|
|
71
|
+
|
|
50
72
|
final userStateNotifier = ref.read(userStateNotifierProvider.notifier);
|
|
51
73
|
final notificationsRepository = ref.read(notificationRepositoryProvider);
|
|
52
74
|
|
|
@@ -75,6 +97,10 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
Future<void> onOnboardingCompleted() async {
|
|
100
|
+
// Preview: this is the step that would create the anonymous guest account
|
|
101
|
+
// and flush profile writes. Do nothing — the preview just returns to Debug.
|
|
102
|
+
if (state.preview) return;
|
|
103
|
+
|
|
78
104
|
final userStateNotifier = ref.read(userStateNotifierProvider.notifier);
|
|
79
105
|
|
|
80
106
|
// This is the "preparing everything for you" moment (the loader screen):
|
|
@@ -106,6 +132,8 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
106
132
|
/// Skip onboarding: mark as onboarded instantly (optimistic) and navigate
|
|
107
133
|
/// to home. Permissions (push + ATT) are requested on the home screen.
|
|
108
134
|
void skipOnboarding() {
|
|
135
|
+
// Preview: skipping must not mark the real user as onboarded.
|
|
136
|
+
if (state.preview) return;
|
|
109
137
|
ref.read(userStateNotifierProvider.notifier).onSkippedOnboarding();
|
|
110
138
|
}
|
|
111
139
|
}
|
|
@@ -9,22 +9,55 @@ import 'package:kasy_kit/features/onboarding/ui/components/onboarding_features.d
|
|
|
9
9
|
import 'package:kasy_kit/features/onboarding/ui/components/onboarding_loader.dart';
|
|
10
10
|
import 'package:kasy_kit/features/onboarding/ui/components/onboarding_notifications_setup.dart';
|
|
11
11
|
import 'package:kasy_kit/features/onboarding/ui/components/onboarding_questions.dart';
|
|
12
|
+
import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
|
|
12
13
|
import 'package:kasy_kit/features/subscriptions/ui/premium_page.dart';
|
|
13
14
|
import 'package:kasy_kit/router.dart';
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class OnboardingPage extends
|
|
17
|
-
|
|
17
|
+
class OnboardingPage extends ConsumerStatefulWidget {
|
|
18
|
+
/// Preview mode: the flow is opened from the admin Debug screen just to walk
|
|
19
|
+
/// the screens. Real side effects are suppressed (see [OnboardingNotifier])
|
|
20
|
+
/// and the flow returns to Debug instead of Home/paywall.
|
|
21
|
+
final bool preview;
|
|
22
|
+
|
|
23
|
+
const OnboardingPage({super.key, this.preview = false});
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
ConsumerState<OnboardingPage> createState() => _OnboardingPageState();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class _OnboardingPageState extends ConsumerState<OnboardingPage> {
|
|
30
|
+
@override
|
|
31
|
+
void initState() {
|
|
32
|
+
super.initState();
|
|
33
|
+
// Set the flag on every entry (true for the admin preview, false for the
|
|
34
|
+
// real flow) so the notifier's side-effect guards are always correct. The
|
|
35
|
+
// first screen has no side effects, so a post-frame write is safe.
|
|
36
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
37
|
+
if (!mounted) return;
|
|
38
|
+
ref.read(onboardingProvider.notifier).setPreview(widget.preview);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Where the flow lands when finished: back to the admin Debug screen in
|
|
43
|
+
/// preview, Home otherwise.
|
|
44
|
+
void _exit() => ref
|
|
45
|
+
.read(goRouterProvider)
|
|
46
|
+
.go(widget.preview ? adminRouteDebug : '/');
|
|
18
47
|
|
|
19
48
|
@override
|
|
20
|
-
Widget build(BuildContext context
|
|
49
|
+
Widget build(BuildContext context) {
|
|
50
|
+
final bool preview = widget.preview;
|
|
21
51
|
return Navigator(
|
|
22
52
|
initialRoute: 'feature_1',
|
|
53
|
+
// No analytics in preview — walking the flow from Debug must not pollute
|
|
54
|
+
// real onboarding funnels.
|
|
23
55
|
observers: [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
if (!preview)
|
|
57
|
+
AnalyticsObserver(
|
|
58
|
+
prefix: 'userOnboarding/',
|
|
59
|
+
analyticsApi: MixpanelAnalyticsApi.instance(),
|
|
60
|
+
),
|
|
28
61
|
],
|
|
29
62
|
onGenerateRoute: (settings) => switch (settings.name) {
|
|
30
63
|
'feature_1' => OnboardingRouteTransition(
|
|
@@ -33,7 +66,11 @@ class OnboardingPage extends ConsumerWidget {
|
|
|
33
66
|
onSkip: () => Navigator.of(context).pushReplacementNamed(
|
|
34
67
|
'skip_loader',
|
|
35
68
|
),
|
|
36
|
-
|
|
69
|
+
// In preview, leaving to the real sign-in screen would break the
|
|
70
|
+
// "everything just returns to Debug" contract, so go back instead.
|
|
71
|
+
onLogin: () => preview
|
|
72
|
+
? _exit()
|
|
73
|
+
: ref.read(goRouterProvider).go('/signin'),
|
|
37
74
|
),
|
|
38
75
|
settings: settings,
|
|
39
76
|
),
|
|
@@ -79,9 +116,11 @@ class OnboardingPage extends ConsumerWidget {
|
|
|
79
116
|
),
|
|
80
117
|
'loader' => OnboardingRouteTransition(
|
|
81
118
|
builder: (context) => OnboardingLoader(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
119
|
+
// Preview stops here (the paywall has its own admin preview) and
|
|
120
|
+
// returns to Debug; the real flow continues to the paywall.
|
|
121
|
+
onCompleted: () => preview
|
|
122
|
+
? _exit()
|
|
123
|
+
: Navigator.of(context).pushReplacementNamed('paywall'),
|
|
85
124
|
),
|
|
86
125
|
settings: settings,
|
|
87
126
|
),
|
|
@@ -89,7 +128,7 @@ class OnboardingPage extends ConsumerWidget {
|
|
|
89
128
|
builder: (context) => OnboardingLoader(
|
|
90
129
|
onCompleted: () {
|
|
91
130
|
ref.onboardingNotifier.skipOnboarding();
|
|
92
|
-
|
|
131
|
+
_exit();
|
|
93
132
|
},
|
|
94
133
|
),
|
|
95
134
|
settings: settings,
|
|
@@ -73,22 +73,10 @@ class _OnboardingSelectableRowGroupState
|
|
|
73
73
|
return Column(
|
|
74
74
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
75
75
|
children: [
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
_SelectableTileInk(
|
|
77
|
+
onTap: selectRow,
|
|
78
78
|
borderRadius: KasyRadius.lgBorderRadius,
|
|
79
|
-
child:
|
|
80
|
-
color: Colors.transparent,
|
|
81
|
-
borderRadius: KasyRadius.lgBorderRadius,
|
|
82
|
-
child: InkWell(
|
|
83
|
-
canRequestFocus: false,
|
|
84
|
-
borderRadius: KasyRadius.lgBorderRadius,
|
|
85
|
-
onTap: selectRow,
|
|
86
|
-
child: switch (hasOnSelectInfo && _selectedIndex == index) {
|
|
87
|
-
false => widget.options[index],
|
|
88
|
-
true => widget.options[index],
|
|
89
|
-
},
|
|
90
|
-
),
|
|
91
|
-
),
|
|
79
|
+
child: widget.options[index],
|
|
92
80
|
),
|
|
93
81
|
if (hasOnSelectInfo && _selectedIndex == index)
|
|
94
82
|
Padding(
|
|
@@ -150,23 +138,16 @@ class _OnboardingMultiSelectableRowGroupState
|
|
|
150
138
|
setState(() {});
|
|
151
139
|
}
|
|
152
140
|
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
child: SelectableRowTile(
|
|
164
|
-
title: option.title,
|
|
165
|
-
subtitle: option.subtitle,
|
|
166
|
-
selected: _selectedIndex.contains(index),
|
|
167
|
-
emoj: option.emoj,
|
|
168
|
-
),
|
|
169
|
-
),
|
|
141
|
+
return _SelectableTileInk(
|
|
142
|
+
onTap: toggleSelection,
|
|
143
|
+
// Match the tile's own lgBorderRadius so the hover veil and focus ring
|
|
144
|
+
// hug its shape (the old InkWell clipped at md, a mismatch).
|
|
145
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
146
|
+
child: SelectableRowTile(
|
|
147
|
+
title: option.title,
|
|
148
|
+
subtitle: option.subtitle,
|
|
149
|
+
selected: _selectedIndex.contains(index),
|
|
150
|
+
emoj: option.emoj,
|
|
170
151
|
),
|
|
171
152
|
);
|
|
172
153
|
}).toList(),
|
|
@@ -338,6 +319,83 @@ class _SelectableRowTileState extends State<SelectableRowTile>
|
|
|
338
319
|
}
|
|
339
320
|
}
|
|
340
321
|
|
|
322
|
+
/// Adds a web hover highlight, a keyboard focus ring and a click cursor on top
|
|
323
|
+
/// of an opaque selectable tile — no Material ripple.
|
|
324
|
+
///
|
|
325
|
+
/// Because the tile paints its own background, the hover veil is layered ABOVE
|
|
326
|
+
/// the child (clipped to [borderRadius]), the same approach the tappable
|
|
327
|
+
/// [KasyCard] uses; a [KasyHover]-style overlay would sit behind the opaque
|
|
328
|
+
/// surface and never show. The child keeps its own semantics (the option title),
|
|
329
|
+
/// so screen readers still announce the option, not a generic label.
|
|
330
|
+
class _SelectableTileInk extends StatefulWidget {
|
|
331
|
+
const _SelectableTileInk({
|
|
332
|
+
required this.onTap,
|
|
333
|
+
required this.borderRadius,
|
|
334
|
+
required this.child,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
final VoidCallback onTap;
|
|
338
|
+
final BorderRadius borderRadius;
|
|
339
|
+
final Widget child;
|
|
340
|
+
|
|
341
|
+
@override
|
|
342
|
+
State<_SelectableTileInk> createState() => _SelectableTileInkState();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
class _SelectableTileInkState extends State<_SelectableTileInk> {
|
|
346
|
+
bool _hovered = false;
|
|
347
|
+
bool _pressed = false;
|
|
348
|
+
|
|
349
|
+
@override
|
|
350
|
+
Widget build(BuildContext context) {
|
|
351
|
+
final bool dark = context.isDark;
|
|
352
|
+
// Subtle tint that fades in on hover (web/desktop) and deepens briefly on
|
|
353
|
+
// press; invisible at rest. On touch _hovered never becomes true.
|
|
354
|
+
final double alpha = _pressed
|
|
355
|
+
? (dark ? 0.10 : 0.06)
|
|
356
|
+
: (_hovered ? (dark ? 0.06 : 0.04) : 0.0);
|
|
357
|
+
|
|
358
|
+
final Widget veiled = Stack(
|
|
359
|
+
children: <Widget>[
|
|
360
|
+
widget.child,
|
|
361
|
+
Positioned.fill(
|
|
362
|
+
child: IgnorePointer(
|
|
363
|
+
child: AnimatedContainer(
|
|
364
|
+
duration: const Duration(milliseconds: 120),
|
|
365
|
+
curve: Curves.easeOut,
|
|
366
|
+
decoration: BoxDecoration(
|
|
367
|
+
color: context.colors.onSurface.withValues(alpha: alpha),
|
|
368
|
+
borderRadius: widget.borderRadius,
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
],
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return KasyFocusRing(
|
|
377
|
+
onActivate: widget.onTap,
|
|
378
|
+
borderRadius: widget.borderRadius,
|
|
379
|
+
child: MouseRegion(
|
|
380
|
+
cursor: SystemMouseCursors.click,
|
|
381
|
+
onEnter: (_) => setState(() => _hovered = true),
|
|
382
|
+
onExit: (_) => setState(() {
|
|
383
|
+
_hovered = false;
|
|
384
|
+
_pressed = false;
|
|
385
|
+
}),
|
|
386
|
+
child: GestureDetector(
|
|
387
|
+
behavior: HitTestBehavior.opaque,
|
|
388
|
+
onTap: widget.onTap,
|
|
389
|
+
onTapDown: (_) => setState(() => _pressed = true),
|
|
390
|
+
onTapUp: (_) => setState(() => _pressed = false),
|
|
391
|
+
onTapCancel: () => setState(() => _pressed = false),
|
|
392
|
+
child: veiled,
|
|
393
|
+
),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
341
399
|
class RoundRadioBox extends StatelessWidget {
|
|
342
400
|
final Color bgColor;
|
|
343
401
|
final Color borderColor;
|
|
@@ -15,7 +15,6 @@ import 'package:kasy_kit/core/states/logout_action.dart';
|
|
|
15
15
|
import 'package:kasy_kit/core/states/user_state_notifier.dart';
|
|
16
16
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
17
17
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
18
|
-
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
19
18
|
import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
|
|
20
19
|
import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
|
|
21
20
|
import 'package:kasy_kit/features/settings/ui/components/avatar_component.dart';
|
|
@@ -153,6 +152,7 @@ class SettingsPage extends ConsumerWidget {
|
|
|
153
152
|
context,
|
|
154
153
|
ref,
|
|
155
154
|
isAuthenticated: isAuthenticated,
|
|
155
|
+
hasAccount: userId != null,
|
|
156
156
|
isAdmin: user.isAdmin,
|
|
157
157
|
isPhone: isPhone,
|
|
158
158
|
),
|
|
@@ -182,6 +182,7 @@ class SettingsPage extends ConsumerWidget {
|
|
|
182
182
|
BuildContext context,
|
|
183
183
|
WidgetRef ref, {
|
|
184
184
|
required bool isAuthenticated,
|
|
185
|
+
required bool hasAccount,
|
|
185
186
|
required bool isAdmin,
|
|
186
187
|
required bool isPhone,
|
|
187
188
|
}) {
|
|
@@ -215,7 +216,7 @@ class SettingsPage extends ConsumerWidget {
|
|
|
215
216
|
padding: const EdgeInsets.only(left: KasySpacing.xs),
|
|
216
217
|
child: Text(
|
|
217
218
|
t.admin_console.settings_entry.caption,
|
|
218
|
-
style: context.textTheme.
|
|
219
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
219
220
|
color: context.colors.muted,
|
|
220
221
|
),
|
|
221
222
|
),
|
|
@@ -226,8 +227,13 @@ class SettingsPage extends ConsumerWidget {
|
|
|
226
227
|
_settingsGroup([_LogoutRow(onTap: () => confirmLogout(context, ref))]),
|
|
227
228
|
const SizedBox(height: KasySpacing.xl),
|
|
228
229
|
],
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
// Only offer account deletion when there's a real backend account behind
|
|
231
|
+
// it. A guest with no identity has nothing to delete, so the button would
|
|
232
|
+
// just dead-end in an error and trap them on this screen.
|
|
233
|
+
if (hasAccount) ...[
|
|
234
|
+
const DeleteUserButton(),
|
|
235
|
+
const SizedBox(height: KasySpacing.xl),
|
|
236
|
+
],
|
|
231
237
|
const _VersionLabel(),
|
|
232
238
|
];
|
|
233
239
|
}
|
|
@@ -435,10 +441,9 @@ class SettingsContainer extends StatelessWidget {
|
|
|
435
441
|
Widget build(BuildContext context) {
|
|
436
442
|
return KasyCard(
|
|
437
443
|
borderRadius: BorderRadius.circular(KasyRadius.lg),
|
|
438
|
-
padding:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
),
|
|
444
|
+
// No card padding: each row carries its own inset so the press/hover
|
|
445
|
+
// highlight spans the full card width (clipped to the rounded corners),
|
|
446
|
+
// instead of a pill floating inside a white margin.
|
|
442
447
|
child: child,
|
|
443
448
|
);
|
|
444
449
|
}
|
|
@@ -460,7 +465,7 @@ class _FieldRow extends StatelessWidget {
|
|
|
460
465
|
children: [
|
|
461
466
|
Text(
|
|
462
467
|
label,
|
|
463
|
-
style: context.
|
|
468
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
464
469
|
color: context.colors.onSurface,
|
|
465
470
|
),
|
|
466
471
|
),
|
|
@@ -471,7 +476,7 @@ class _FieldRow extends StatelessWidget {
|
|
|
471
476
|
textAlign: TextAlign.right,
|
|
472
477
|
maxLines: 1,
|
|
473
478
|
overflow: TextOverflow.ellipsis,
|
|
474
|
-
style: context.
|
|
479
|
+
style: context.kasyTextTheme.listRowValue.copyWith(
|
|
475
480
|
color: context.colors.muted,
|
|
476
481
|
),
|
|
477
482
|
),
|
|
@@ -484,18 +489,22 @@ class _FieldRow extends StatelessWidget {
|
|
|
484
489
|
);
|
|
485
490
|
if (onTap == null) {
|
|
486
491
|
return Padding(
|
|
487
|
-
padding: const EdgeInsets.symmetric(
|
|
492
|
+
padding: const EdgeInsets.symmetric(
|
|
493
|
+
horizontal: KasySpacing.md,
|
|
494
|
+
vertical: KasySpacing.smd,
|
|
495
|
+
),
|
|
488
496
|
child: row,
|
|
489
497
|
);
|
|
490
498
|
}
|
|
491
499
|
return KasyHover(
|
|
492
500
|
onTap: onTap!,
|
|
493
|
-
hoverEnabled: false,
|
|
494
|
-
pressEnabled: false,
|
|
495
501
|
focusable: true,
|
|
496
|
-
|
|
502
|
+
// Rectangular highlight (default): the card clips the rounded ends.
|
|
497
503
|
semanticLabel: label,
|
|
498
|
-
padding: const EdgeInsets.symmetric(
|
|
504
|
+
padding: const EdgeInsets.symmetric(
|
|
505
|
+
horizontal: KasySpacing.md,
|
|
506
|
+
vertical: KasySpacing.smd,
|
|
507
|
+
),
|
|
499
508
|
child: row,
|
|
500
509
|
);
|
|
501
510
|
}
|
|
@@ -512,7 +521,7 @@ class _AccountAvatarHeader extends StatelessWidget {
|
|
|
512
521
|
return const Center(
|
|
513
522
|
child: Padding(
|
|
514
523
|
padding: EdgeInsets.only(top: KasySpacing.sm),
|
|
515
|
-
child: EditableUserAvatar(diameter:
|
|
524
|
+
child: EditableUserAvatar(diameter: 80),
|
|
516
525
|
),
|
|
517
526
|
);
|
|
518
527
|
}
|
|
@@ -606,9 +615,10 @@ class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
|
|
|
606
615
|
isPhone: widget.isPhone,
|
|
607
616
|
);
|
|
608
617
|
|
|
609
|
-
const double navWidth =
|
|
610
|
-
const double gap = KasySpacing.
|
|
611
|
-
const double
|
|
618
|
+
const double navWidth = 220;
|
|
619
|
+
const double gap = KasySpacing.xl;
|
|
620
|
+
const double detailWidth = 560;
|
|
621
|
+
const double groupWidth = navWidth + gap + detailWidth;
|
|
612
622
|
|
|
613
623
|
return Center(
|
|
614
624
|
child: Padding(
|
|
@@ -641,7 +651,7 @@ class _SettingsDesktopViewState extends ConsumerState<_SettingsDesktopView> {
|
|
|
641
651
|
),
|
|
642
652
|
const SizedBox(width: gap),
|
|
643
653
|
if (fits)
|
|
644
|
-
SizedBox(width:
|
|
654
|
+
SizedBox(width: detailWidth, child: pane)
|
|
645
655
|
else
|
|
646
656
|
Expanded(child: pane),
|
|
647
657
|
],
|
|
@@ -682,7 +692,7 @@ class _DesktopNav extends StatelessWidget {
|
|
|
682
692
|
),
|
|
683
693
|
child: Row(
|
|
684
694
|
children: [
|
|
685
|
-
const EditableUserAvatar(diameter:
|
|
695
|
+
const EditableUserAvatar(diameter: 64),
|
|
686
696
|
const SizedBox(width: KasySpacing.sm),
|
|
687
697
|
Expanded(
|
|
688
698
|
child: Column(
|
|
@@ -702,7 +712,7 @@ class _DesktopNav extends StatelessWidget {
|
|
|
702
712
|
email,
|
|
703
713
|
maxLines: 1,
|
|
704
714
|
overflow: TextOverflow.ellipsis,
|
|
705
|
-
style: context.textTheme.
|
|
715
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
706
716
|
color: context.colors.muted,
|
|
707
717
|
),
|
|
708
718
|
),
|
|
@@ -755,7 +765,7 @@ class _NavTile extends StatelessWidget {
|
|
|
755
765
|
child: Container(
|
|
756
766
|
padding: const EdgeInsets.symmetric(
|
|
757
767
|
horizontal: KasySpacing.smd,
|
|
758
|
-
vertical: KasySpacing.
|
|
768
|
+
vertical: KasySpacing.sm,
|
|
759
769
|
),
|
|
760
770
|
decoration: BoxDecoration(
|
|
761
771
|
color: selected ? c.surfaceNeutralSoft : Colors.transparent,
|
|
@@ -877,8 +887,11 @@ class _DesktopDetail extends ConsumerWidget {
|
|
|
877
887
|
const SizedBox(height: KasySpacing.xl),
|
|
878
888
|
_settingsGroup([_LogoutRow(onTap: () => confirmLogout(context, ref))]),
|
|
879
889
|
],
|
|
880
|
-
|
|
881
|
-
|
|
890
|
+
// Only when there's a real backend account to delete (see mobile layout).
|
|
891
|
+
if (userId != null) ...[
|
|
892
|
+
const SizedBox(height: KasySpacing.xl),
|
|
893
|
+
const DeleteUserButton(),
|
|
894
|
+
],
|
|
882
895
|
const SizedBox(height: KasySpacing.xl),
|
|
883
896
|
const _VersionLabel(),
|
|
884
897
|
];
|
|
@@ -894,13 +907,18 @@ class _LogoutRow extends StatelessWidget {
|
|
|
894
907
|
@override
|
|
895
908
|
Widget build(BuildContext context) {
|
|
896
909
|
return KasyHover(
|
|
897
|
-
|
|
910
|
+
// Match the card radius so the full-bleed press fill hugs the rounded
|
|
911
|
+
// corners exactly (this row is the sole child of its card).
|
|
912
|
+
borderRadius: KasyRadius.lgBorderRadius,
|
|
898
913
|
pressColor: context.colors.error,
|
|
899
914
|
focusable: true,
|
|
900
915
|
focusGapColor: context.colors.surface,
|
|
901
916
|
onTap: onTap,
|
|
902
917
|
child: Padding(
|
|
903
|
-
padding: const EdgeInsets.symmetric(
|
|
918
|
+
padding: const EdgeInsets.symmetric(
|
|
919
|
+
horizontal: KasySpacing.md,
|
|
920
|
+
vertical: KasySpacing.smd,
|
|
921
|
+
),
|
|
904
922
|
child: Row(
|
|
905
923
|
children: [
|
|
906
924
|
Icon(
|
|
@@ -911,7 +929,7 @@ class _LogoutRow extends StatelessWidget {
|
|
|
911
929
|
const SizedBox(width: KasySpacing.sm),
|
|
912
930
|
Text(
|
|
913
931
|
context.t.settings.logout,
|
|
914
|
-
style: context.
|
|
932
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
915
933
|
color: context.colors.error,
|
|
916
934
|
),
|
|
917
935
|
),
|
|
@@ -980,12 +998,12 @@ class BiometricSwitcher extends ConsumerWidget {
|
|
|
980
998
|
),
|
|
981
999
|
Padding(
|
|
982
1000
|
padding: const EdgeInsets.only(
|
|
983
|
-
left: KasyIconSize.rowLeading + KasySpacing.sm,
|
|
1001
|
+
left: KasySpacing.md + KasyIconSize.rowLeading + KasySpacing.sm,
|
|
984
1002
|
bottom: KasySpacing.xs,
|
|
985
1003
|
),
|
|
986
1004
|
child: Text(
|
|
987
1005
|
subtitle,
|
|
988
|
-
style: context.textTheme.
|
|
1006
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
989
1007
|
color: context.colors.muted,
|
|
990
1008
|
),
|
|
991
1009
|
),
|
|
@@ -1114,7 +1132,10 @@ class ThemeSwitcher extends StatelessWidget {
|
|
|
1114
1132
|
),
|
|
1115
1133
|
];
|
|
1116
1134
|
return Padding(
|
|
1117
|
-
padding: const EdgeInsets.symmetric(
|
|
1135
|
+
padding: const EdgeInsets.symmetric(
|
|
1136
|
+
horizontal: KasySpacing.md,
|
|
1137
|
+
vertical: KasySpacing.smd,
|
|
1138
|
+
),
|
|
1118
1139
|
child: Row(
|
|
1119
1140
|
children: [
|
|
1120
1141
|
Icon(
|
|
@@ -1126,7 +1147,7 @@ class ThemeSwitcher extends StatelessWidget {
|
|
|
1126
1147
|
Expanded(
|
|
1127
1148
|
child: Text(
|
|
1128
1149
|
tr.theme_title,
|
|
1129
|
-
style: context.
|
|
1150
|
+
style: context.kasyTextTheme.listRowTitle.copyWith(
|
|
1130
1151
|
color: context.colors.onSurface,
|
|
1131
1152
|
),
|
|
1132
1153
|
),
|
package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart
CHANGED
|
@@ -5,6 +5,7 @@ import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
|
5
5
|
import 'package:kasy_kit/core/home_widgets/home_widget_mywidget_service.dart';
|
|
6
6
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
7
7
|
import 'package:kasy_kit/core/widgets/kasy_scroll_behavior.dart';
|
|
8
|
+
import 'package:kasy_kit/core/widgets/responsive_layout.dart';
|
|
8
9
|
import 'package:kasy_kit/features/settings/ui/widgets/admin_card.dart';
|
|
9
10
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
10
11
|
|
|
@@ -18,6 +19,9 @@ class AdminHomeWidgets extends ConsumerWidget {
|
|
|
18
19
|
child: KasyOverlayScaffold(
|
|
19
20
|
title: t.settings.admin.home_widgets_title,
|
|
20
21
|
onBack: () => context.pop(),
|
|
22
|
+
// Contain + center the single utility card on desktop so it never
|
|
23
|
+
// stretches edge-to-edge, matching Notifications / Reminders.
|
|
24
|
+
maxContentWidth: kKasyContentMaxWidth,
|
|
21
25
|
slivers: [
|
|
22
26
|
SliverList.list(
|
|
23
27
|
children: [
|