kasy-cli 1.37.1 → 1.39.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.
Files changed (120) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/core/data/api/user_api.dart +18 -0
  4. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  5. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  6. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  7. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  8. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  9. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  10. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  11. package/lib/scaffold/backends/supabase/patch/lib/core/data/api/user_api.dart +8 -0
  12. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  13. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  14. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  15. package/lib/scaffold/shared/generator-utils.js +12 -6
  16. package/package.json +1 -1
  17. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  18. package/templates/firebase/AGENTS.md +7 -1
  19. package/templates/firebase/DESIGN_SYSTEM.md +35 -8
  20. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  21. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  22. package/templates/firebase/assets/icons/facebook.svg +49 -0
  23. package/templates/firebase/assets/icons/google.svg +1 -0
  24. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  25. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  26. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  27. package/templates/firebase/lib/components/components.dart +1 -1
  28. package/templates/firebase/lib/components/kasy_app_bar.dart +361 -20
  29. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +283 -66
  30. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  31. package/templates/firebase/lib/components/kasy_date_picker.dart +61 -46
  32. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  33. package/templates/firebase/lib/components/kasy_sidebar.dart +412 -31
  34. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  35. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  36. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +29 -231
  37. package/templates/firebase/lib/core/bottom_menu/sidebar_focus.dart +224 -0
  38. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +19 -9
  39. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  40. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  41. package/templates/firebase/lib/core/data/api/user_api.dart +15 -0
  42. package/templates/firebase/lib/core/data/repositories/user_repository.dart +5 -0
  43. package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +525 -65
  44. package/templates/firebase/lib/core/dev_inspector/dev_inspector_info.dart +47 -0
  45. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  46. package/templates/firebase/lib/core/icons/kasy_icons.dart +16 -1
  47. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +18 -35
  48. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  49. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  50. package/templates/firebase/lib/core/states/user_state_notifier.dart +69 -1
  51. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  52. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  53. package/templates/firebase/lib/core/theme/universal_theme.dart +9 -0
  54. package/templates/firebase/lib/core/web_device_preview/png_clipboard.dart +14 -0
  55. package/templates/firebase/lib/core/web_device_preview/png_clipboard_io.dart +9 -0
  56. package/templates/firebase/lib/core/web_device_preview/png_clipboard_web.dart +36 -0
  57. package/templates/firebase/lib/core/web_device_preview/png_export_result.dart +2 -0
  58. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +547 -483
  59. package/templates/firebase/lib/core/web_viewport_scale.dart +64 -35
  60. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  61. package/templates/firebase/lib/core/widgets/responsive_layout.dart +8 -0
  62. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  63. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  64. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  65. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  66. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  67. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  68. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  69. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  70. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  71. package/templates/firebase/lib/features/home/home_components_page.dart +264 -126
  72. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +231 -57
  73. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  74. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  75. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  76. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +118 -57
  77. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  78. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +19 -4
  79. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  80. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  81. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  82. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +262 -0
  83. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  84. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  85. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  86. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  87. package/templates/firebase/lib/features/settings/settings_page.dart +99 -65
  88. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +1379 -422
  89. package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +14 -4
  90. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  91. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  92. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +404 -149
  93. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +24 -31
  94. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  95. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +77 -95
  96. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  97. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  98. package/templates/firebase/lib/i18n/en.i18n.json +749 -698
  99. package/templates/firebase/lib/i18n/es.i18n.json +749 -698
  100. package/templates/firebase/lib/i18n/pt.i18n.json +749 -698
  101. package/templates/firebase/lib/main.dart +20 -7
  102. package/templates/firebase/lib/router.dart +70 -46
  103. package/templates/firebase/pubspec.yaml +2 -1
  104. package/templates/firebase/test/admin_shell_chrome_test.dart +110 -0
  105. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  106. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  107. package/templates/firebase/test/features/authentication/data/api/user_api_fake.dart +3 -0
  108. package/templates/firebase/tool/design_check.dart +9 -0
  109. package/templates/firebase/assets/icons/apple.png +0 -0
  110. package/templates/firebase/assets/icons/facebook.png +0 -0
  111. package/templates/firebase/assets/icons/google.png +0 -0
  112. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  113. package/templates/firebase/lib/components/kasy_web_header.dart +0 -210
  114. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  115. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  116. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  117. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  118. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -169
  119. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
  120. package/templates/firebase/lib/features/settings/ui/components/admin/admin_paywalls.dart +0 -53
@@ -0,0 +1,214 @@
1
+ import 'package:flutter/widgets.dart';
2
+ import 'package:kasy_kit/components/kasy_avatar_presets.dart';
3
+
4
+ /// Declares what the desktop application bar ([KasyAppBar.application]) shows for
5
+ /// a given screen.
6
+ ///
7
+ /// The shell (`WebContentWrapper`) renders one application bar above all content
8
+ /// and seeds it with an app-wide default. A screen overrides that default by
9
+ /// wrapping its body in a [KasyAppBarConfigurator] (or, for screens built on
10
+ /// `KasyOverlayScaffold`, by passing `applicationBar:`). So each screen decides
11
+ /// its own desktop chrome — search here, just notifications there — instead of
12
+ /// every screen sharing one fixed bar.
13
+ ///
14
+ /// Each element has a `showX` flag (presets toggle these) plus the data/callbacks
15
+ /// it needs when shown. A bare [KasyAppBarConfig] mirrors the app default.
16
+ @immutable
17
+ class KasyAppBarConfig {
18
+ // Search ------------------------------------------------------------------
19
+ final bool showSearch;
20
+ final TextEditingController? searchController;
21
+ final String searchHint;
22
+ final ValueChanged<String>? onSearchChanged;
23
+ final ValueChanged<String>? onSearchSubmitted;
24
+
25
+ // Theme toggle ------------------------------------------------------------
26
+ final bool showThemeToggle;
27
+ final VoidCallback? onToggleTheme;
28
+
29
+ // Notifications -----------------------------------------------------------
30
+ final bool showNotifications;
31
+
32
+ /// Custom notifications control (e.g. a data-aware bell). Replaces the built-in
33
+ /// bell when set and [showNotifications] is true.
34
+ final Widget? notifications;
35
+ final VoidCallback? onNotifications;
36
+ final bool showNotificationBadge;
37
+
38
+ // Create ------------------------------------------------------------------
39
+ final bool showCreate;
40
+ final String createLabel;
41
+ final VoidCallback? onCreate;
42
+
43
+ // Profile -----------------------------------------------------------------
44
+ final bool showAvatar;
45
+ final Widget? avatar;
46
+ final KasyAvatarGradientData? avatarGradient;
47
+ final VoidCallback? onAvatarTap;
48
+
49
+ /// Full control: every element shown by default (matches the app shell). Set a
50
+ /// `showX` to false to drop that element, or pass data/callbacks to wire it.
51
+ const KasyAppBarConfig({
52
+ this.showSearch = true,
53
+ this.searchController,
54
+ this.searchHint = 'Search...',
55
+ this.onSearchChanged,
56
+ this.onSearchSubmitted,
57
+ this.showThemeToggle = true,
58
+ this.onToggleTheme,
59
+ this.showNotifications = true,
60
+ this.notifications,
61
+ this.onNotifications,
62
+ this.showNotificationBadge = false,
63
+ this.showCreate = true,
64
+ this.createLabel = 'Create',
65
+ this.onCreate,
66
+ this.showAvatar = false,
67
+ this.avatar,
68
+ this.avatarGradient,
69
+ this.onAvatarTap,
70
+ });
71
+
72
+ /// Preset: only the theme toggle — the quietest bar (settings-style screens).
73
+ const KasyAppBarConfig.minimal({VoidCallback? onToggleTheme})
74
+ : this(
75
+ showSearch: false,
76
+ showThemeToggle: true,
77
+ onToggleTheme: onToggleTheme,
78
+ showNotifications: false,
79
+ showCreate: false,
80
+ showAvatar: false,
81
+ );
82
+
83
+ /// Preset: search + theme, no quick-create (browse / list screens).
84
+ const KasyAppBarConfig.search({
85
+ TextEditingController? controller,
86
+ String hint = 'Search...',
87
+ ValueChanged<String>? onChanged,
88
+ ValueChanged<String>? onSubmitted,
89
+ VoidCallback? onToggleTheme,
90
+ }) : this(
91
+ showSearch: true,
92
+ searchController: controller,
93
+ searchHint: hint,
94
+ onSearchChanged: onChanged,
95
+ onSearchSubmitted: onSubmitted,
96
+ showThemeToggle: true,
97
+ onToggleTheme: onToggleTheme,
98
+ showNotifications: false,
99
+ showCreate: false,
100
+ showAvatar: false,
101
+ );
102
+
103
+ /// Show ONLY the listed elements (everything else hidden), keeping each one's
104
+ /// data and callbacks from the shell default — so a screen can say "just the
105
+ /// notifications and theme here" without rebuilding the bell or theme wiring.
106
+ ///
107
+ /// ```dart
108
+ /// configure: (bar) => bar.only(search: true), // browse screen
109
+ /// configure: (bar) => bar.only(notifications: true, themeToggle: true),
110
+ /// ```
111
+ KasyAppBarConfig only({
112
+ bool search = false,
113
+ bool themeToggle = false,
114
+ bool notifications = false,
115
+ bool create = false,
116
+ bool avatar = false,
117
+ }) {
118
+ return copyWith(
119
+ showSearch: search,
120
+ showThemeToggle: themeToggle,
121
+ showNotifications: notifications,
122
+ showCreate: create,
123
+ showAvatar: avatar,
124
+ );
125
+ }
126
+
127
+ /// Copy with selective overrides — handy for adapting the shell default.
128
+ KasyAppBarConfig copyWith({
129
+ bool? showSearch,
130
+ TextEditingController? searchController,
131
+ String? searchHint,
132
+ ValueChanged<String>? onSearchChanged,
133
+ ValueChanged<String>? onSearchSubmitted,
134
+ bool? showThemeToggle,
135
+ VoidCallback? onToggleTheme,
136
+ bool? showNotifications,
137
+ Widget? notifications,
138
+ VoidCallback? onNotifications,
139
+ bool? showNotificationBadge,
140
+ bool? showCreate,
141
+ String? createLabel,
142
+ VoidCallback? onCreate,
143
+ bool? showAvatar,
144
+ Widget? avatar,
145
+ KasyAvatarGradientData? avatarGradient,
146
+ VoidCallback? onAvatarTap,
147
+ }) {
148
+ return KasyAppBarConfig(
149
+ showSearch: showSearch ?? this.showSearch,
150
+ searchController: searchController ?? this.searchController,
151
+ searchHint: searchHint ?? this.searchHint,
152
+ onSearchChanged: onSearchChanged ?? this.onSearchChanged,
153
+ onSearchSubmitted: onSearchSubmitted ?? this.onSearchSubmitted,
154
+ showThemeToggle: showThemeToggle ?? this.showThemeToggle,
155
+ onToggleTheme: onToggleTheme ?? this.onToggleTheme,
156
+ showNotifications: showNotifications ?? this.showNotifications,
157
+ notifications: notifications ?? this.notifications,
158
+ onNotifications: onNotifications ?? this.onNotifications,
159
+ showNotificationBadge: showNotificationBadge ?? this.showNotificationBadge,
160
+ showCreate: showCreate ?? this.showCreate,
161
+ createLabel: createLabel ?? this.createLabel,
162
+ onCreate: onCreate ?? this.onCreate,
163
+ showAvatar: showAvatar ?? this.showAvatar,
164
+ avatar: avatar ?? this.avatar,
165
+ avatarGradient: avatarGradient ?? this.avatarGradient,
166
+ onAvatarTap: onAvatarTap ?? this.onAvatarTap,
167
+ );
168
+ }
169
+
170
+ @override
171
+ bool operator ==(Object other) {
172
+ return other is KasyAppBarConfig &&
173
+ other.showSearch == showSearch &&
174
+ other.searchController == searchController &&
175
+ other.searchHint == searchHint &&
176
+ other.onSearchChanged == onSearchChanged &&
177
+ other.onSearchSubmitted == onSearchSubmitted &&
178
+ other.showThemeToggle == showThemeToggle &&
179
+ other.onToggleTheme == onToggleTheme &&
180
+ other.showNotifications == showNotifications &&
181
+ other.notifications == notifications &&
182
+ other.onNotifications == onNotifications &&
183
+ other.showNotificationBadge == showNotificationBadge &&
184
+ other.showCreate == showCreate &&
185
+ other.createLabel == createLabel &&
186
+ other.onCreate == onCreate &&
187
+ other.showAvatar == showAvatar &&
188
+ other.avatar == avatar &&
189
+ other.avatarGradient == avatarGradient &&
190
+ other.onAvatarTap == onAvatarTap;
191
+ }
192
+
193
+ @override
194
+ int get hashCode => Object.hashAll(<Object?>[
195
+ showSearch,
196
+ searchController,
197
+ searchHint,
198
+ onSearchChanged,
199
+ onSearchSubmitted,
200
+ showThemeToggle,
201
+ onToggleTheme,
202
+ showNotifications,
203
+ notifications,
204
+ onNotifications,
205
+ showNotificationBadge,
206
+ showCreate,
207
+ createLabel,
208
+ onCreate,
209
+ showAvatar,
210
+ avatar,
211
+ avatarGradient,
212
+ onAvatarTap,
213
+ ]);
214
+ }
@@ -0,0 +1,102 @@
1
+ import 'package:flutter/widgets.dart';
2
+ import 'package:kasy_kit/core/chrome/app_bar_config.dart';
3
+
4
+ /// Transforms the shell's default application-bar config into a screen's version.
5
+ /// Receives the shell default (with its wired bell, theme toggle, etc.) so a
6
+ /// screen only toggles/overrides what it needs and inherits the rest.
7
+ typedef KasyAppBarConfigure = KasyAppBarConfig Function(KasyAppBarConfig base);
8
+
9
+ /// Marks the subtree that sits BELOW the desktop application bar
10
+ /// ([KasyAppBar.application]) — i.e. the shell content area, provided by
11
+ /// [WebContentWrapper] — and carries the config the bar renders from.
12
+ ///
13
+ /// Two jobs:
14
+ /// 1. Presence ([of]) lets the page-level [KasyAppBar] hide on desktop INSIDE the
15
+ /// shell (where the application bar owns the chrome) but stay visible OUTSIDE
16
+ /// it (a full-screen pushed route), so the back button is never lost.
17
+ /// 2. [config] is the live config the bar listens to; [defaultConfig] is the
18
+ /// shell's app-wide default. A [KasyAppBarConfigurator] in the routed page
19
+ /// publishes `configure(defaultConfig)` into [config] to override the bar for
20
+ /// that screen.
21
+ class KasyAppBarScope extends InheritedWidget {
22
+ /// Live config the application bar renders from. The bar listens; screens write.
23
+ final ValueNotifier<KasyAppBarConfig> config;
24
+
25
+ /// The shell's app-wide default — the starting point screen overrides build on.
26
+ final KasyAppBarConfig defaultConfig;
27
+
28
+ const KasyAppBarScope({
29
+ super.key,
30
+ required this.config,
31
+ required this.defaultConfig,
32
+ required super.child,
33
+ });
34
+
35
+ /// True when a [KasyAppBarScope] is an ancestor (no rebuild dependency —
36
+ /// presence is fixed for a given subtree).
37
+ static bool of(BuildContext context) => maybeOf(context) != null;
38
+
39
+ /// The nearest scope, or null outside the shell (e.g. full-screen routes).
40
+ static KasyAppBarScope? maybeOf(BuildContext context) =>
41
+ context.getInheritedWidgetOfExactType<KasyAppBarScope>();
42
+
43
+ @override
44
+ bool updateShouldNotify(KasyAppBarScope oldWidget) =>
45
+ config != oldWidget.config || defaultConfig != oldWidget.defaultConfig;
46
+ }
47
+
48
+ /// Wrap a routed page's body with this to override the desktop application bar
49
+ /// for that screen. On phone/tablet (no scope above) it is a transparent
50
+ /// pass-through, so it is always safe to add.
51
+ ///
52
+ /// ```dart
53
+ /// KasyAppBarConfigurator(
54
+ /// configure: (bar) => bar.only(search: true),
55
+ /// child: MyScreen(),
56
+ /// )
57
+ /// ```
58
+ class KasyAppBarConfigurator extends StatefulWidget {
59
+ /// How this screen adapts the shell default. Return [base] unchanged to keep
60
+ /// the default, or e.g. `base.only(...)` / `base.copyWith(...)`.
61
+ final KasyAppBarConfigure configure;
62
+ final Widget child;
63
+
64
+ const KasyAppBarConfigurator({
65
+ super.key,
66
+ required this.configure,
67
+ required this.child,
68
+ });
69
+
70
+ @override
71
+ State<KasyAppBarConfigurator> createState() => _KasyAppBarConfiguratorState();
72
+ }
73
+
74
+ class _KasyAppBarConfiguratorState extends State<KasyAppBarConfigurator> {
75
+ void _publish() {
76
+ if (!mounted) return;
77
+ final KasyAppBarScope? scope = KasyAppBarScope.maybeOf(context);
78
+ if (scope == null) return; // phone/tablet: no application bar to configure.
79
+ // Always build from the ORIGINAL default (not the current value), so the
80
+ // override is stable across rebuilds. The setter is a no-op when the result
81
+ // is unchanged (==), so an inline-built config does not churn the bar.
82
+ //
83
+ // Published post-frame: the bar (a sibling above this page in the shell
84
+ // Column) has already built this frame, so it picks the override up next.
85
+ scope.config.value = widget.configure(scope.defaultConfig);
86
+ }
87
+
88
+ @override
89
+ void didChangeDependencies() {
90
+ super.didChangeDependencies();
91
+ WidgetsBinding.instance.addPostFrameCallback((_) => _publish());
92
+ }
93
+
94
+ @override
95
+ void didUpdateWidget(covariant KasyAppBarConfigurator oldWidget) {
96
+ super.didUpdateWidget(oldWidget);
97
+ WidgetsBinding.instance.addPostFrameCallback((_) => _publish());
98
+ }
99
+
100
+ @override
101
+ Widget build(BuildContext context) => widget.child;
102
+ }
@@ -53,6 +53,10 @@ class UserApi {
53
53
  );
54
54
  }
55
55
 
56
+ Stream<String?> watchRole(String id) {
57
+ return _collection.doc(id).snapshots().map((snap) => snap.data()?.role);
58
+ }
59
+
56
60
  Future<void> update(UserEntity user) {
57
61
  final data = user.toJson();
58
62
  data.removeWhere((key, value) => value == null);
@@ -74,6 +78,17 @@ class UserApi {
74
78
  'authFunctions-deleteUserAccount',
75
79
  );
76
80
  await callable.call();
81
+ } on FirebaseFunctionsException catch (err, stacktrace) {
82
+ // Surface the REAL cause in the logs instead of a generic message, so a
83
+ // failed deletion is diagnosable at a glance:
84
+ // code 'not-found' → the function isn't deployed yet (run a deploy)
85
+ // code 'unauthenticated'→ the user's token didn't reach the function
86
+ // code 'internal' → the function ran but threw (see its own logs)
87
+ _logger.e(
88
+ "delete user error [${err.code}] ${err.message}",
89
+ stackTrace: stacktrace,
90
+ );
91
+ throw ApiError(message: err.message ?? "Error while deleting user");
77
92
  } catch (err, stacktrace) {
78
93
  _logger.e("delete user error $err", stackTrace: stacktrace);
79
94
  throw ApiError(message: "Error while deleting user");
@@ -59,6 +59,11 @@ class UserRepository {
59
59
  }
60
60
  }
61
61
 
62
+ /// Streams the user's role so a change made in the backend console (e.g.
63
+ /// promoting someone to "admin") is picked up at runtime without a restart.
64
+ /// Delegates to the backend-specific [UserApi].
65
+ Stream<String?> watchRole(String id) => _userApi.watchRole(id);
66
+
62
67
  /// We updates the user avatar
63
68
  /// We convert the image to jpeg and resize it to 300px width
64
69
  /// and 80% quality to reduce the size of the image