kasy-cli 1.34.0 → 1.35.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 (141) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +24 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/new.js +11 -9
  5. package/lib/commands/release-version.js +234 -0
  6. package/lib/commands/update.js +27 -0
  7. package/lib/scaffold/CHANGELOG.json +9 -0
  8. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  9. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  10. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  11. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  12. package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
  13. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  14. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  15. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  16. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  17. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
  18. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  19. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  20. package/lib/scaffold/generate.js +53 -4
  21. package/lib/utils/i18n/messages-en.js +23 -0
  22. package/lib/utils/i18n/messages-es.js +23 -0
  23. package/lib/utils/i18n/messages-pt.js +23 -0
  24. package/package.json +5 -2
  25. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  26. package/templates/firebase/AGENTS.md +83 -0
  27. package/templates/firebase/DESIGN_SYSTEM.md +37 -2
  28. package/templates/firebase/docs/auth-setup.en.md +2 -0
  29. package/templates/firebase/docs/auth-setup.es.md +2 -0
  30. package/templates/firebase/docs/auth-setup.pt.md +2 -0
  31. package/templates/firebase/firebase.json +56 -1
  32. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  33. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  34. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  35. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  36. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  37. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  38. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  39. package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
  40. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  41. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  42. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  43. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  44. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  45. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  46. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  47. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  48. package/templates/firebase/lib/components/kasy_toast.dart +107 -41
  49. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  50. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  51. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  52. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  53. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  54. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  55. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  56. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  57. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  58. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  59. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  60. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
  61. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  62. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  63. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  64. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  65. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  66. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  67. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  68. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  69. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  70. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  71. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  72. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  73. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  74. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  75. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  76. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  77. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
  78. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  79. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  80. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  81. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  82. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  83. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
  84. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  85. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  86. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  87. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  88. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  89. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  90. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  91. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  92. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  93. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
  94. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  95. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  96. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  97. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  98. package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
  99. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  100. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +154 -56
  101. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  102. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
  103. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  104. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
  105. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
  106. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  107. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  108. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  109. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  110. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
  111. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  112. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  113. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  114. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  115. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  116. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  117. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  118. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  119. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  120. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  121. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  122. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  123. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  124. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  125. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  126. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  127. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  128. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  129. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  130. package/templates/firebase/lib/i18n/en.i18n.json +49 -3
  131. package/templates/firebase/lib/i18n/es.i18n.json +49 -3
  132. package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
  133. package/templates/firebase/lib/main.dart +11 -2
  134. package/templates/firebase/lib/router.dart +92 -13
  135. package/templates/firebase/pubspec.yaml +1 -1
  136. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  137. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  138. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  139. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  140. package/templates/firebase/web/index.html +162 -14
  141. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
5
5
  import 'package:kasy_kit/components/components.dart';
6
6
  import 'package:kasy_kit/core/chrome/chrome_visibility.dart';
7
7
  import 'package:kasy_kit/core/config/features.dart';
8
+ import 'package:kasy_kit/core/data/models/subscription.dart';
8
9
  import 'package:kasy_kit/core/data/models/user.dart';
9
10
  import 'package:kasy_kit/core/haptics/haptic_feedback_notifier.dart';
10
11
  import 'package:kasy_kit/core/security/biometric_preference_notifier.dart';
@@ -14,7 +15,10 @@ import 'package:kasy_kit/core/states/logout_action.dart';
14
15
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
15
16
  import 'package:kasy_kit/core/theme/theme.dart';
16
17
  import 'package:kasy_kit/core/widgets/kasy_hover.dart';
18
+ import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
19
+ import 'package:kasy_kit/features/authentication/repositories/exceptions/authentication_exceptions.dart';
17
20
  import 'package:kasy_kit/features/settings/ui/components/avatar_component.dart';
21
+ import 'package:kasy_kit/features/settings/ui/components/create_password_sheet.dart';
18
22
  import 'package:kasy_kit/features/settings/ui/components/delete_user_component.dart';
19
23
  import 'package:kasy_kit/features/settings/ui/components/edit_name_sheet.dart';
20
24
  import 'package:kasy_kit/features/settings/ui/components/language_switcher.dart';
@@ -23,6 +27,60 @@ import 'package:kasy_kit/i18n/translations.g.dart';
23
27
  import 'package:package_info_plus/package_info_plus.dart';
24
28
  import 'package:url_launcher/url_launcher.dart';
25
29
 
30
+ /// All providers linked to the current account (google/apple/facebook/email/
31
+ /// phone), for the "Connected with" row and to decide whether to offer "create
32
+ /// password". Empty for guests/unknown.
33
+ final _linkedProvidersProvider = FutureProvider.autoDispose<List<String>>(
34
+ (ref) => ref.read(authRepositoryProvider).getLinkedProviders(),
35
+ );
36
+
37
+ /// Social providers the current user can still link to their account (Firebase).
38
+ /// Empty on backends that link automatically (Supabase) or aren't wired (API).
39
+ final _linkableProvidersProvider = FutureProvider.autoDispose<List<String>>(
40
+ (ref) => ref.read(authRepositoryProvider).linkableSocialProviders(),
41
+ );
42
+
43
+ String _providerDisplayName(String provider) => switch (provider) {
44
+ 'google' => 'Google',
45
+ 'apple' => 'Apple',
46
+ 'facebook' => 'Facebook',
47
+ _ => provider,
48
+ };
49
+
50
+ /// Links a social provider to the current account, then refreshes the lists and
51
+ /// shows a toast. Cancelled flows are silently ignored.
52
+ Future<void> _linkSocialProvider(
53
+ WidgetRef ref,
54
+ BuildContext context,
55
+ String provider,
56
+ ) async {
57
+ final tr = context.t.settings;
58
+ try {
59
+ await ref.read(authRepositoryProvider).linkSocialProvider(provider);
60
+ ref.invalidate(_linkableProvidersProvider);
61
+ ref.invalidate(_linkedProvidersProvider);
62
+ await ref.read(userStateNotifierProvider.notifier).refresh();
63
+ } on UserCancelledSignInException {
64
+ return;
65
+ } catch (_) {
66
+ if (context.mounted) {
67
+ showKasyToast(
68
+ context,
69
+ title: tr.link_social_error,
70
+ tone: KasyToastTone.danger,
71
+ );
72
+ }
73
+ return;
74
+ }
75
+ if (context.mounted) {
76
+ showKasyToast(
77
+ context,
78
+ title: tr.link_social_success(provider: _providerDisplayName(provider)),
79
+ tone: KasyToastTone.success,
80
+ );
81
+ }
82
+ }
83
+
26
84
  class SettingsPage extends ConsumerWidget {
27
85
  const SettingsPage({super.key});
28
86
 
@@ -76,6 +134,18 @@ class SettingsPage extends ConsumerWidget {
76
134
  email: displayEmail,
77
135
  isAuthenticated: isAuthenticated,
78
136
  onRegister: () => context.push('/signup'),
137
+ linkedProviders: ref
138
+ .watch(_linkedProvidersProvider)
139
+ .asData
140
+ ?.value ??
141
+ const [],
142
+ linkableProviders: ref
143
+ .watch(_linkableProvidersProvider)
144
+ .asData
145
+ ?.value ??
146
+ const [],
147
+ onLinkProvider: (p) =>
148
+ _linkSocialProvider(ref, context, p),
79
149
  ),
80
150
  const SizedBox(height: KasySpacing.xl),
81
151
  ..._sections(
@@ -204,12 +274,8 @@ List<Widget> _preferenceRows(BuildContext context, {required bool isPhone}) {
204
274
  title: tr.feedback,
205
275
  onTap: () => context.push('/feedback'),
206
276
  ),
207
- if (withRevenuecat)
208
- SettingsTile(
209
- icon: KasyIcons.payment,
210
- title: tr.premium,
211
- onTap: () => context.push('/premium'),
212
- ),
277
+ if (withRevenuecat || withStripe)
278
+ const _BillingTile(),
213
279
  ];
214
280
  }
215
281
 
@@ -241,11 +307,20 @@ List<Widget> _accountFields(
241
307
  required String email,
242
308
  required bool isAuthenticated,
243
309
  required VoidCallback onRegister,
310
+ List<String> linkedProviders = const [],
311
+ List<String> linkableProviders = const [],
312
+ void Function(String provider)? onLinkProvider,
244
313
  }) {
245
314
  final tr = context.t.settings;
246
315
  if (!isAuthenticated) {
247
316
  return [
248
- KasyButton(label: tr.register, expand: true, onPressed: onRegister),
317
+ KasyButton(
318
+ label: tr.register,
319
+ expand: true,
320
+ variant: KasyButtonVariant.ghost,
321
+ foregroundColor: context.colors.primary,
322
+ onPressed: onRegister,
323
+ ),
249
324
  ];
250
325
  }
251
326
  return [
@@ -263,10 +338,72 @@ List<Widget> _accountFields(
263
338
  ),
264
339
  ),
265
340
  _FieldRow(label: tr.email_label, value: email),
341
+ if (linkedProviders.isNotEmpty)
342
+ _FieldRow(
343
+ label: tr.connected_with_label,
344
+ value: linkedProviders
345
+ .map((p) => switch (p) {
346
+ 'google' => 'Google',
347
+ 'apple' => 'Apple',
348
+ 'facebook' => 'Facebook',
349
+ 'phone' => tr.provider_phone,
350
+ _ => tr.provider_email,
351
+ })
352
+ .join(', '),
353
+ ),
354
+ // Social-only accounts can attach a password (same account) so they can
355
+ // also sign in with email + password. Email/phone users already have one.
356
+ if (linkedProviders.isNotEmpty &&
357
+ !linkedProviders.contains('email') &&
358
+ !linkedProviders.contains('phone'))
359
+ _FieldRow(
360
+ label: tr.create_password_title,
361
+ value: '',
362
+ onTap: () => showCreatePasswordSheet(context),
363
+ ),
364
+ // Firebase: link a social provider (Google/Apple) to this account so it
365
+ // can also be used to sign in. Empty/hidden on Supabase (auto-links).
366
+ for (final provider in linkableProviders)
367
+ _FieldRow(
368
+ label: tr.link_social(provider: _providerDisplayName(provider)),
369
+ value: '',
370
+ onTap:
371
+ onLinkProvider == null ? null : () => onLinkProvider(provider),
372
+ ),
266
373
  ]),
267
374
  ];
268
375
  }
269
376
 
377
+ /// Subscription/billing entry in the Preferences section.
378
+ /// - Not subscribed → "Premium" (taps into the paywall).
379
+ /// - Subscribed → "Cobrança" + plan name on the right (taps into billing).
380
+ class _BillingTile extends ConsumerWidget {
381
+ const _BillingTile();
382
+
383
+ @override
384
+ Widget build(BuildContext context, WidgetRef ref) {
385
+ final sub = ref.watch(userStateNotifierProvider).subscription;
386
+ final tr = context.t;
387
+
388
+ if (sub is SubscriptionStateData) {
389
+ final planName =
390
+ sub.activeOffer?.title ?? tr.activePremium.plan_fallback;
391
+ return SettingsTile(
392
+ icon: KasyIcons.payment,
393
+ title: tr.settings.billing,
394
+ trailingLabel: planName,
395
+ onTap: () => context.push('/premium'),
396
+ );
397
+ }
398
+
399
+ return SettingsTile(
400
+ icon: KasyIcons.payment,
401
+ title: tr.settings.premium,
402
+ onTap: () => context.push('/premium'),
403
+ );
404
+ }
405
+ }
406
+
270
407
  class _SectionLabel extends StatelessWidget {
271
408
  final String label;
272
409
  const _SectionLabel(this.label);
@@ -279,7 +416,7 @@ class _SectionLabel extends StatelessWidget {
279
416
  padding: const EdgeInsets.only(left: KasySpacing.xs),
280
417
  child: Text(
281
418
  label,
282
- style: KasyTextTheme.sectionLabel.copyWith(
419
+ style: context.kasyTextTheme.sectionLabel.copyWith(
283
420
  color: context.colors.muted,
284
421
  ),
285
422
  ),
@@ -544,9 +681,8 @@ class _DesktopNav extends StatelessWidget {
544
681
  name,
545
682
  maxLines: 1,
546
683
  overflow: TextOverflow.ellipsis,
547
- style: context.textTheme.titleSmall?.copyWith(
684
+ style: context.textTheme.titleMedium?.copyWith(
548
685
  color: context.colors.onSurface,
549
- fontWeight: FontWeight.w600,
550
686
  ),
551
687
  ),
552
688
  if (email.isNotEmpty)
@@ -664,7 +800,7 @@ class _DesktopDetail extends ConsumerWidget {
664
800
  ),
665
801
  child: Text(
666
802
  title,
667
- style: KasyTextTheme.sectionTitle.copyWith(
803
+ style: context.kasyTextTheme.sectionTitle.copyWith(
668
804
  color: context.colors.onSurface,
669
805
  ),
670
806
  ),
@@ -719,6 +855,11 @@ class _DesktopDetail extends ConsumerWidget {
719
855
  email: email,
720
856
  isAuthenticated: isAuthenticated,
721
857
  onRegister: () => context.push('/signup'),
858
+ linkedProviders:
859
+ ref.watch(_linkedProvidersProvider).asData?.value ?? const [],
860
+ linkableProviders:
861
+ ref.watch(_linkableProvidersProvider).asData?.value ?? const [],
862
+ onLinkProvider: (p) => _linkSocialProvider(ref, context, p),
722
863
  ),
723
864
  if (isAuthenticated) ...[
724
865
  const SizedBox(height: KasySpacing.xl),
@@ -10,6 +10,7 @@ import 'package:kasy_kit/components/kasy_button.dart';
10
10
  import 'package:kasy_kit/components/kasy_status_tag.dart';
11
11
  import 'package:kasy_kit/components/kasy_tabs.dart';
12
12
  import 'package:kasy_kit/components/kasy_text_field.dart';
13
+ import 'package:kasy_kit/core/app_update/update_available_sheet.dart';
13
14
  import 'package:kasy_kit/core/config/features.dart';
14
15
  import 'package:kasy_kit/core/data/models/user.dart';
15
16
  import 'package:kasy_kit/core/dev_inspector/dev_inspector.dart';
@@ -159,6 +160,9 @@ class _TabScroll extends StatelessWidget {
159
160
  @override
160
161
  Widget build(BuildContext context) {
161
162
  return SingleChildScrollView(
163
+ // Scrolling a form means the user is done with the focused field, so
164
+ // dismiss the keyboard to free up the screen.
165
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
162
166
  child: _MaxWidth(
163
167
  child: Padding(
164
168
  padding: EdgeInsets.fromLTRB(
@@ -191,11 +195,8 @@ class _GroupLabel extends StatelessWidget {
191
195
  ),
192
196
  child: Text(
193
197
  label.toUpperCase(),
194
- style: context.textTheme.labelMedium?.copyWith(
195
- fontSize: 12,
198
+ style: context.kasyTextTheme.sectionLabel.copyWith(
196
199
  color: context.colors.muted,
197
- letterSpacing: 1.2,
198
- fontWeight: FontWeight.w700,
199
200
  ),
200
201
  ),
201
202
  );
@@ -489,7 +490,7 @@ class _OverviewTab extends ConsumerWidget {
489
490
  onTap: () {
490
491
  Clipboard.setData(ClipboardData(text: uid));
491
492
  ref.read(toastProvider).alert(
492
- title: '',
493
+ title: t.common.copied,
493
494
  text: t.settings.admin.user_id_copied,
494
495
  );
495
496
  },
@@ -558,9 +559,8 @@ class _InfoRow extends StatelessWidget {
558
559
  Expanded(
559
560
  child: Text(
560
561
  value,
561
- style: context.textTheme.bodyMedium?.copyWith(
562
+ style: context.kasyTextTheme.rowValue.copyWith(
562
563
  color: valueColor ?? context.colors.onSurface,
563
- fontWeight: FontWeight.w600,
564
564
  ),
565
565
  ),
566
566
  ),
@@ -787,7 +787,7 @@ class _RequestCard extends ConsumerWidget {
787
787
  .setActive(req.id!, v);
788
788
  ref.invalidate(_adminRequestsProvider);
789
789
  if (context.mounted) {
790
- ref.read(toastProvider).alert(title: '', text: r.saved);
790
+ ref.read(toastProvider).alert(title: t.common.saved, text: r.saved);
791
791
  }
792
792
  },
793
793
  ),
@@ -910,11 +910,11 @@ class _RequestEditorSheetState extends ConsumerState<_RequestEditorSheet> {
910
910
  ref.invalidate(_adminRequestsProvider);
911
911
  if (!mounted) return;
912
912
  context.pop();
913
- ref.read(toastProvider).alert(title: '', text: r.saved);
913
+ ref.read(toastProvider).alert(title: t.common.saved, text: r.saved);
914
914
  } catch (_) {
915
915
  if (!mounted) return;
916
916
  setState(() => _saving = false);
917
- ref.read(toastProvider).alert(title: '', text: r.error);
917
+ ref.read(toastProvider).alert(title: t.common.error, text: r.error);
918
918
  }
919
919
  }
920
920
 
@@ -927,6 +927,9 @@ class _RequestEditorSheetState extends ConsumerState<_RequestEditorSheet> {
927
927
  body: SizedBox(
928
928
  height: MediaQuery.sizeOf(context).height * 0.5,
929
929
  child: SingleChildScrollView(
930
+ // Scrolling a form means the user is done with the focused field, so
931
+ // dismiss the keyboard to free up the screen.
932
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
930
933
  child: Column(
931
934
  crossAxisAlignment: CrossAxisAlignment.stretch,
932
935
  children: [
@@ -1105,6 +1108,17 @@ class _ToolsTab extends ConsumerWidget {
1105
1108
  version: '0.0.0',
1106
1109
  ),
1107
1110
  ),
1111
+ _ActionCard(
1112
+ icon: KasyIcons.download,
1113
+ title: admin.preview_update_available,
1114
+ // Previews the optional (dismissible) sheet so the design can be
1115
+ // reviewed without TestFlight or Remote Config. The forced variant is
1116
+ // the same layout, blocking — test it via app_min_version.
1117
+ onTap: () => showUpdateAvailableSheet(
1118
+ navigatorKey.currentContext!,
1119
+ forced: false,
1120
+ ),
1121
+ ),
1108
1122
  _ActionCard(
1109
1123
  icon: KasyIcons.payment,
1110
1124
  title: admin.paywalls,
@@ -1123,6 +1137,8 @@ class _ToolsTab extends ConsumerWidget {
1123
1137
  _ActionCard(
1124
1138
  icon: KasyIcons.star,
1125
1139
  title: admin.ask_review,
1140
+ // Has a design (the review dialog), so it's previewable on web too —
1141
+ // only the store action no-ops there.
1126
1142
  onTap: () => showReviewDialog(context, ref, force: true),
1127
1143
  ),
1128
1144
  _ActionCard(
@@ -1140,7 +1156,10 @@ class _ToolsTab extends ConsumerWidget {
1140
1156
  Clipboard.setData(
1141
1157
  ClipboardData(text: userState.user.idOrNull ?? 'no-id (guest)'),
1142
1158
  );
1143
- ref.read(toastProvider).alert(title: '', text: admin.user_id_copied);
1159
+ ref.read(toastProvider).alert(
1160
+ title: t.common.copied,
1161
+ text: admin.user_id_copied,
1162
+ );
1144
1163
  },
1145
1164
  ),
1146
1165
  _ActionCard(
@@ -1148,19 +1167,25 @@ class _ToolsTab extends ConsumerWidget {
1148
1167
  title: admin.copy_fcm_token,
1149
1168
  onTap: () async {
1150
1169
  if (kIsWeb) {
1151
- ref.read(toastProvider).alert(title: '', text: admin.native_only);
1170
+ ref.read(toastProvider).alert(
1171
+ title: t.common.native_only_title,
1172
+ text: admin.native_only,
1173
+ );
1152
1174
  return;
1153
1175
  }
1154
1176
  final token = await FirebaseMessaging.instance.getToken();
1155
1177
  if (token == null) {
1156
1178
  ref.read(toastProvider).alert(
1157
- title: '',
1179
+ title: t.common.unavailable,
1158
1180
  text: admin.fcm_token_unavailable,
1159
1181
  );
1160
1182
  return;
1161
1183
  }
1162
1184
  await Clipboard.setData(ClipboardData(text: token));
1163
- ref.read(toastProvider).alert(title: '', text: admin.fcm_token_copied);
1185
+ ref.read(toastProvider).alert(
1186
+ title: t.common.copied,
1187
+ text: admin.fcm_token_copied,
1188
+ );
1164
1189
  },
1165
1190
  ),
1166
1191
  _ActionCard(
@@ -1168,7 +1193,10 @@ class _ToolsTab extends ConsumerWidget {
1168
1193
  title: admin.ask_notification,
1169
1194
  onTap: () {
1170
1195
  if (kIsWeb) {
1171
- ref.read(toastProvider).alert(title: '', text: admin.native_only);
1196
+ ref.read(toastProvider).alert(
1197
+ title: t.common.native_only_title,
1198
+ text: admin.native_only,
1199
+ );
1172
1200
  return;
1173
1201
  }
1174
1202
  ref.read(notificationsSettingsProvider).askPermission();
@@ -613,11 +613,9 @@ class _HeaderCell extends StatelessWidget {
613
613
  label.toUpperCase(),
614
614
  maxLines: 1,
615
615
  overflow: TextOverflow.ellipsis,
616
- style: context.textTheme.labelSmall?.copyWith(
616
+ style: context.kasyTextTheme.sectionLabel.copyWith(
617
617
  color: color,
618
618
  letterSpacing: 0.8,
619
- fontWeight: FontWeight.w700,
620
- fontSize: 10.5,
621
619
  ),
622
620
  ),
623
621
  ),
@@ -666,6 +664,9 @@ class _TableBody extends StatelessWidget {
666
664
  children: [
667
665
  Expanded(
668
666
  child: ListView.separated(
667
+ // Scrolling the results means the user stopped typing in the search
668
+ // field above, so dismiss the keyboard to show more of the list.
669
+ keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
669
670
  padding: const EdgeInsets.only(bottom: KasySpacing.xs),
670
671
  itemCount: users.length,
671
672
  separatorBuilder: (_, _) => Divider(
@@ -957,15 +958,14 @@ class _UserRow extends StatelessWidget {
957
958
  primaryText,
958
959
  maxLines: 1,
959
960
  overflow: TextOverflow.ellipsis,
960
- style: context.textTheme.bodyMedium?.copyWith(
961
- color: isAnonymous
962
- ? context.colors.muted
963
- : context.colors.onSurface,
964
- fontWeight:
965
- isAnonymous ? FontWeight.w400 : FontWeight.w600,
966
- fontStyle:
967
- isAnonymous ? FontStyle.italic : FontStyle.normal,
968
- ),
961
+ style: isAnonymous
962
+ ? context.textTheme.bodyMedium?.copyWith(
963
+ color: context.colors.muted,
964
+ fontStyle: FontStyle.italic,
965
+ )
966
+ : context.kasyTextTheme.rowTitle.copyWith(
967
+ color: context.colors.onSurface,
968
+ ),
969
969
  ),
970
970
  if (subText != null) ...[
971
971
  const SizedBox(height: 1),
@@ -182,11 +182,8 @@ class _SendPushNotificationPageState
182
182
  ),
183
183
  child: Text(
184
184
  tr.send_push_preview_label.toUpperCase(),
185
- style: context.textTheme.labelMedium?.copyWith(
185
+ style: context.kasyTextTheme.sectionLabel.copyWith(
186
186
  color: context.colors.muted,
187
- letterSpacing: 1.2,
188
- fontWeight: FontWeight.w700,
189
- fontSize: 12,
190
187
  ),
191
188
  ),
192
189
  ),
@@ -0,0 +1,141 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+ import 'package:kasy_kit/components/components.dart';
4
+ import 'package:kasy_kit/core/theme/theme.dart';
5
+ import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
6
+ import 'package:kasy_kit/i18n/translations.g.dart';
7
+
8
+ /// Opens the "create password" bottom sheet. Lets a social-only user (Google,
9
+ /// Apple, ...) set a password so they can also sign in with email + password.
10
+ /// The account stays the same (no duplicate). Shows a success toast on the
11
+ /// calling [context] when done.
12
+ Future<void> showCreatePasswordSheet(BuildContext context) async {
13
+ final bool? saved = await showKasyBottomSheet<bool>(
14
+ context: context,
15
+ isScrollControlled: true,
16
+ builder: (_) => const _CreatePasswordSheet(),
17
+ );
18
+ if (saved == true && context.mounted) {
19
+ showKasyToast(
20
+ context,
21
+ title: context.t.settings.create_password_success,
22
+ tone: KasyToastTone.success,
23
+ );
24
+ }
25
+ }
26
+
27
+ class _CreatePasswordSheet extends ConsumerStatefulWidget {
28
+ const _CreatePasswordSheet();
29
+
30
+ @override
31
+ ConsumerState<_CreatePasswordSheet> createState() =>
32
+ _CreatePasswordSheetState();
33
+ }
34
+
35
+ class _CreatePasswordSheetState extends ConsumerState<_CreatePasswordSheet> {
36
+ final _passwordController = TextEditingController();
37
+ final _confirmController = TextEditingController();
38
+ bool _saving = false;
39
+ String? _error;
40
+
41
+ static const int _minLength = 6;
42
+
43
+ @override
44
+ void dispose() {
45
+ _passwordController.dispose();
46
+ _confirmController.dispose();
47
+ super.dispose();
48
+ }
49
+
50
+ Future<void> _save() async {
51
+ if (_saving) return;
52
+ final tr = context.t.settings;
53
+ final password = _passwordController.text;
54
+ if (password.length < _minLength) {
55
+ setState(() => _error = tr.create_password_too_short);
56
+ return;
57
+ }
58
+ if (password != _confirmController.text) {
59
+ setState(() => _error = tr.create_password_mismatch);
60
+ return;
61
+ }
62
+ setState(() {
63
+ _saving = true;
64
+ _error = null;
65
+ });
66
+ try {
67
+ await ref.read(authRepositoryProvider).setPassword(password);
68
+ if (!mounted) return;
69
+ // Toast is shown by the launcher on the page context (this sheet's context
70
+ // is gone right after the pop).
71
+ Navigator.of(context).pop(true);
72
+ } catch (_) {
73
+ if (!mounted) return;
74
+ setState(() => _saving = false);
75
+ showKasyToast(
76
+ context,
77
+ title: tr.create_password_error,
78
+ tone: KasyToastTone.danger,
79
+ );
80
+ }
81
+ }
82
+
83
+ @override
84
+ Widget build(BuildContext context) {
85
+ final tr = context.t.settings;
86
+ return KasyBottomSheet(
87
+ title: tr.create_password_title,
88
+ addKeyboardInset: true,
89
+ body: Column(
90
+ mainAxisSize: MainAxisSize.min,
91
+ crossAxisAlignment: CrossAxisAlignment.stretch,
92
+ children: [
93
+ Text(
94
+ tr.create_password_subtitle,
95
+ style: context.textTheme.bodyMedium?.copyWith(
96
+ color: context.colors.muted,
97
+ ),
98
+ ),
99
+ const SizedBox(height: KasySpacing.md),
100
+ KasyTextField(
101
+ controller: _passwordController,
102
+ label: tr.create_password_field,
103
+ contentType: KasyTextFieldContentType.password,
104
+ textInputAction: TextInputAction.next,
105
+ ),
106
+ const SizedBox(height: KasySpacing.sm),
107
+ KasyTextField(
108
+ controller: _confirmController,
109
+ label: tr.create_password_confirm_label,
110
+ contentType: KasyTextFieldContentType.password,
111
+ textInputAction: TextInputAction.done,
112
+ onSubmitted: (_) => _save(),
113
+ ),
114
+ if (_error != null) ...[
115
+ const SizedBox(height: KasySpacing.sm),
116
+ Text(
117
+ _error!,
118
+ style: context.textTheme.bodySmall?.copyWith(
119
+ color: context.colors.error,
120
+ ),
121
+ ),
122
+ ],
123
+ ],
124
+ ),
125
+ actions: [
126
+ KasyButton(
127
+ label: tr.edit_name_save,
128
+ expand: true,
129
+ isLoading: _saving,
130
+ onPressed: _save,
131
+ ),
132
+ KasyButton(
133
+ label: tr.edit_name_cancel,
134
+ variant: KasyButtonVariant.ghost,
135
+ expand: true,
136
+ onPressed: _saving ? null : () => Navigator.of(context).pop(),
137
+ ),
138
+ ],
139
+ );
140
+ }
141
+ }
@@ -95,7 +95,6 @@ class LanguageSwitcher extends ConsumerWidget {
95
95
  child: Text(
96
96
  sheetTitle,
97
97
  style: sheetContext.textTheme.titleMedium?.copyWith(
98
- fontWeight: FontWeight.w600,
99
98
  color: sheetContext.colors.onSurface,
100
99
  ),
101
100
  ),