kasy-cli 1.32.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 (169) hide show
  1. package/README.md +1 -1
  2. package/bin/kasy.js +66 -2
  3. package/docs/cli-reference.md +7 -7
  4. package/lib/commands/apple-web.js +222 -0
  5. package/lib/commands/configure.js +3 -91
  6. package/lib/commands/doctor.js +20 -0
  7. package/lib/commands/facebook.js +189 -0
  8. package/lib/commands/new.js +61 -11
  9. package/lib/commands/release-version.js +234 -0
  10. package/lib/commands/update.js +27 -0
  11. package/lib/scaffold/CHANGELOG.json +27 -0
  12. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
  13. package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
  14. package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
  15. package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  16. package/lib/scaffold/backends/firebase/setup-from-scratch.js +199 -21
  17. package/lib/scaffold/backends/patch-base-hashes.json +66 -0
  18. package/lib/scaffold/backends/supabase/deploy.js +92 -0
  19. package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
  20. package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
  21. package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
  22. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +92 -3
  23. package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
  24. package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
  25. package/lib/scaffold/generate.js +53 -4
  26. package/lib/scaffold/shared/generator-utils.js +18 -6
  27. package/lib/utils/apple-web.js +147 -0
  28. package/lib/utils/facebook.js +162 -0
  29. package/lib/utils/i18n/messages-en.js +85 -0
  30. package/lib/utils/i18n/messages-es.js +85 -0
  31. package/lib/utils/i18n/messages-pt.js +85 -0
  32. package/package.json +5 -2
  33. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
  34. package/templates/firebase/AGENTS.md +170 -0
  35. package/templates/firebase/CLAUDE.md +16 -0
  36. package/templates/firebase/DESIGN_SYSTEM.md +269 -0
  37. package/templates/firebase/docs/auth-setup.en.md +4 -2
  38. package/templates/firebase/docs/auth-setup.es.md +4 -2
  39. package/templates/firebase/docs/auth-setup.pt.md +4 -2
  40. package/templates/firebase/firebase.json +56 -1
  41. package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
  42. package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
  43. package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
  44. package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
  45. package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
  46. package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
  47. package/templates/firebase/lib/components/components.dart +1 -0
  48. package/templates/firebase/lib/components/kasy_alert.dart +0 -1
  49. package/templates/firebase/lib/components/kasy_app_bar.dart +35 -17
  50. package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
  51. package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
  52. package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
  53. package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
  54. package/templates/firebase/lib/components/kasy_screen.dart +114 -0
  55. package/templates/firebase/lib/components/kasy_sidebar.dart +189 -120
  56. package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
  57. package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
  58. package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
  59. package/templates/firebase/lib/components/kasy_toast.dart +108 -73
  60. package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
  61. package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
  62. package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
  63. package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
  64. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
  65. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
  66. package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
  67. package/templates/firebase/lib/core/chrome/chrome_visibility.dart +22 -0
  68. package/templates/firebase/lib/core/config/features.dart +5 -0
  69. package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
  70. package/templates/firebase/lib/core/guards/guard.dart +16 -2
  71. package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
  72. package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
  73. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +48 -124
  74. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
  75. package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
  76. package/templates/firebase/lib/core/states/logout_action.dart +5 -1
  77. package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
  78. package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
  79. package/templates/firebase/lib/core/theme/texts.dart +90 -57
  80. package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
  81. package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
  82. package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
  83. package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
  84. package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
  85. package/templates/firebase/lib/core/web_screen_width.dart +15 -0
  86. package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
  87. package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
  88. package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
  89. package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
  90. package/templates/firebase/lib/core/widgets/update_bottom_sheet.dart +29 -126
  91. package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +11 -8
  92. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +21 -0
  93. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
  94. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
  95. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
  96. package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
  97. package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
  98. package/templates/firebase/lib/features/authentication/api/authentication_api.dart +266 -0
  99. package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
  100. package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
  101. package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
  102. package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
  103. package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
  104. package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
  105. package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
  106. package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
  107. package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
  108. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +80 -15
  109. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +20 -14
  110. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
  111. package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
  112. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
  113. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +1 -2
  114. package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
  115. package/templates/firebase/lib/features/home/home_components_page.dart +8 -1
  116. package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
  117. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +186 -56
  118. package/templates/firebase/lib/features/home/home_page.dart +4 -0
  119. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +169 -208
  120. package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
  121. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +2 -2
  122. package/templates/firebase/lib/features/notifications/ui/components/notification_tile.dart +21 -8
  123. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +2 -2
  124. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -4
  125. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +84 -128
  126. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
  127. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
  128. package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
  129. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +3 -4
  130. package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +2 -4
  131. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +2 -1
  132. package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
  133. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +58 -21
  134. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
  135. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
  136. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +2 -2
  137. package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
  138. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
  139. package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
  140. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
  141. package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
  142. package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
  143. package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
  144. package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
  145. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
  146. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
  147. package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
  148. package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
  149. package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
  150. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
  151. package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
  152. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
  153. package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
  154. package/templates/firebase/lib/i18n/en.i18n.json +54 -7
  155. package/templates/firebase/lib/i18n/es.i18n.json +54 -7
  156. package/templates/firebase/lib/i18n/pt.i18n.json +54 -7
  157. package/templates/firebase/lib/main.dart +11 -2
  158. package/templates/firebase/lib/router.dart +94 -13
  159. package/templates/firebase/pubspec.yaml +1 -1
  160. package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
  161. package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
  162. package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
  163. package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
  164. package/templates/firebase/tool/design_check.dart +152 -0
  165. package/templates/firebase/web/index.html +162 -14
  166. package/templates/firebase/assets/images/review.png +0 -0
  167. package/templates/firebase/assets/images/update.png +0 -0
  168. package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
  169. package/templates/firebase/lib/features/notifications/ui/components/notifications_header.dart +0 -32
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
16
16
  # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17
17
  # In Windows, build-name is used as the major, minor, and patch parts
18
18
  # of the product and file versions while build-number is used as the build suffix.
19
- version: 1.0.0+38
19
+ version: 1.0.0+53
20
20
 
21
21
  environment:
22
22
  sdk: ^3.11.0
@@ -23,6 +23,20 @@ class FakeRemoteConfigApi implements RemoteConfigApi {
23
23
  value: 'Lorem ipsum dolor sit amet',
24
24
  ),
25
25
  );
26
+
27
+ @override
28
+ AppUpdateConfigs get appUpdate => AppUpdateConfigs(
29
+ latestVersion: FakeRemoteConfigData<String>(
30
+ key: 'app_latest_version',
31
+ exists: true,
32
+ value: '0.0.0',
33
+ ),
34
+ minVersion: FakeRemoteConfigData<String>(
35
+ key: 'app_min_version',
36
+ exists: true,
37
+ value: '0.0.0',
38
+ ),
39
+ );
26
40
  }
27
41
 
28
42
  /// This is a fake implementation of RemoteConfigData
@@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
3
3
  import 'package:kasy_kit/core/data/api/http_client.dart';
4
4
  import 'package:kasy_kit/core/data/models/user.dart';
5
5
  import 'package:kasy_kit/core/data/repositories/user_repository.dart';
6
+ import 'package:kasy_kit/core/shared_preferences/shared_preferences.dart';
6
7
  import 'package:kasy_kit/core/states/user_state_notifier.dart';
7
8
  import 'package:kasy_kit/environments.dart';
8
9
  import 'package:kasy_kit/features/authentication/repositories/authentication_repository.dart';
@@ -66,6 +67,9 @@ void main() {
66
67
  ],
67
68
  );
68
69
 
70
+ // The notifier touches SharedPreferences (onboarding flag, biometric
71
+ // reset) during logout/onboarding, so the builder must be initialized.
72
+ await container.read(sharedPreferencesProvider).init();
69
73
  return container;
70
74
  }
71
75
 
@@ -175,11 +179,14 @@ void main() {
175
179
  ],
176
180
  );
177
181
 
182
+ // The notifier touches SharedPreferences (onboarding flag, biometric
183
+ // reset) during logout/onboarding, so the builder must be initialized.
184
+ await container.read(sharedPreferencesProvider).init();
178
185
  return container;
179
186
  }
180
187
 
181
188
  test(
182
- 'Should load user at startup, user is not connected => login user anonymously with id',
189
+ 'Should load user at startup, no account yet => anonymous guest with no id',
183
190
  () async {
184
191
  final testContainer = await initTestContainer();
185
192
  final userStateNotifier = testContainer.read(
@@ -196,9 +203,40 @@ void main() {
196
203
  isA<AnonymousUserData>(),
197
204
  reason: 'user should be in unauthenticated state',
198
205
  );
206
+ expect(
207
+ userStateNotifier.state.user.idOrNull,
208
+ isNull,
209
+ reason:
210
+ 'no anonymous account is created eagerly anymore — it is created '
211
+ 'lazily in continueAsGuest (onboarding end / "continue as guest")',
212
+ );
213
+ },
214
+ );
215
+
216
+ test(
217
+ 'continueAsGuest => creates anonymous account with id and remembers onboarding',
218
+ () async {
219
+ final testContainer = await initTestContainer();
220
+ final userStateNotifier = testContainer.read(
221
+ userStateNotifierProvider.notifier,
222
+ );
223
+ await userStateNotifier.init();
224
+ await userStateNotifier.continueAsGuest();
225
+
226
+ expect(
227
+ userStateNotifier.state.user,
228
+ isA<AnonymousUserData>(),
229
+ reason: 'guest is anonymous',
230
+ );
199
231
  expect(
200
232
  userStateNotifier.state.user.idOrThrow,
201
233
  'fake-user-id-anonymous',
234
+ reason: 'the anonymous account is created on demand here',
235
+ );
236
+ expect(
237
+ testContainer.read(sharedPreferencesProvider).getOnboardingCompleted(),
238
+ isTrue,
239
+ reason: 'onboarding is remembered so it is never shown again',
202
240
  );
203
241
  },
204
242
  );
@@ -222,7 +260,7 @@ void main() {
222
260
  },
223
261
  );
224
262
 
225
- test('on logout -> user state is anonymous with id', () async {
263
+ test('on logout -> anonymous guest with no id (no re-signup)', () async {
226
264
  final testContainer = await initTestContainer();
227
265
  final userStateNotifier = testContainer.read(
228
266
  userStateNotifierProvider.notifier,
@@ -238,7 +276,13 @@ void main() {
238
276
  isA<AnonymousUserData>(),
239
277
  reason: 'user should be anonymous',
240
278
  );
241
- expect(userStateNotifier.state.user.idOrThrow, 'fake-user-id-anonymous');
279
+ expect(
280
+ userStateNotifier.state.user.idOrNull,
281
+ isNull,
282
+ reason:
283
+ 'logout must not recreate an anonymous account (would pile up '
284
+ 'orphan users); the user is sent to the sign-in screen instead',
285
+ );
242
286
  });
243
287
  });
244
288
  }
@@ -0,0 +1,68 @@
1
+ import 'package:flutter_test/flutter_test.dart';
2
+ import 'package:kasy_kit/core/web_viewport_scale.dart';
3
+
4
+ /// Locks the desktop web scaling behaviour so a future change to the constants or
5
+ /// the formula can't silently regress it. The headline rule, and the bug this
6
+ /// guards against: the desktop compensation must key off the SCREEN width (OS
7
+ /// scale), NOT the window width — merely resizing the browser window must not
8
+ /// shrink the UI; the extra shrink happens only when the screen itself is small.
9
+ void main() {
10
+ group('webViewportEffectiveScale', () {
11
+ test('no scaling below the desktop breakpoint (mobile/tablet web == native)',
12
+ () {
13
+ // The window width decides the breakpoint (the layout follows the window),
14
+ // so below it the scale is 1.0 regardless of the screen — same as native
15
+ // and the device preview.
16
+ expect(webViewportEffectiveScale(375), 1.0); // phone
17
+ expect(webViewportEffectiveScale(900), 1.0); // tablet
18
+ expect(webViewportEffectiveScale(900, screenWidth: 1470), 1.0);
19
+ expect(
20
+ webViewportEffectiveScale(kWebViewportScaleDesktopBreakpoint - 1),
21
+ 1.0,
22
+ );
23
+ });
24
+
25
+ test('desktop on a normal/large screen stays at the flat cap (0.95)', () {
26
+ // Unknown screen (null) → flat cap.
27
+ expect(webViewportEffectiveScale(1280), kWebViewportScale);
28
+ expect(webViewportEffectiveScale(1500), kWebViewportScale);
29
+ // Known large screen → still the flat cap, no compensation.
30
+ expect(webViewportEffectiveScale(1100, screenWidth: 1470), kWebViewportScale);
31
+ expect(webViewportEffectiveScale(1024, screenWidth: 1920), kWebViewportScale);
32
+ });
33
+
34
+ test('resizing the window does NOT change the scale (this is the fix)', () {
35
+ // Same screen, different window widths → identical scale. The old formula
36
+ // keyed off the window and shrank here; that was the bug.
37
+ const screen = 1470.0;
38
+ expect(
39
+ webViewportEffectiveScale(1280, screenWidth: screen),
40
+ webViewportEffectiveScale(1050, screenWidth: screen),
41
+ );
42
+ expect(webViewportEffectiveScale(1050, screenWidth: screen), kWebViewportScale);
43
+ });
44
+
45
+ test('high OS scale (small SCREEN) compensates below the cap', () {
46
+ // Drop below 0.95 only because the screen is small — independent of window.
47
+ expect(webViewportEffectiveScale(1024, screenWidth: 1024), closeTo(0.80, 1e-4));
48
+ expect(webViewportEffectiveScale(1097, screenWidth: 1097), closeTo(0.857, 1e-3));
49
+ // Even a wide window on a small (high-scale) screen compensates.
50
+ expect(webViewportEffectiveScale(1280, screenWidth: 1024), closeTo(0.80, 1e-4));
51
+ });
52
+
53
+ test('compensation pins the design width to the target on a small screen', () {
54
+ // scale = screen / target, so screen / scale == target (1280).
55
+ const screen = 1024.0;
56
+ final scale = webViewportEffectiveScale(1100, screenWidth: screen);
57
+ expect(screen / scale, closeTo(kWebViewportScaleTargetWidth, 1e-4));
58
+ });
59
+
60
+ test('the maxScale override is respected as the cap', () {
61
+ expect(webViewportEffectiveScale(1920, screenWidth: 1920, maxScale: 0.9), 0.9);
62
+ });
63
+
64
+ test('an absurdly small screen is clamped to the 0.5 floor', () {
65
+ expect(webViewportEffectiveScale(1024, screenWidth: 300), 0.5);
66
+ });
67
+ });
68
+ }
@@ -166,4 +166,19 @@ class FakeAuthenticationApi implements AuthenticationApi {
166
166
 
167
167
  @override
168
168
  Future<String?> getCurrentUserDisplayName() => Future.value(current != null ? 'Fake User' : null);
169
+
170
+ @override
171
+ Future<String?> getCurrentUserPhotoUrl() => Future.value();
172
+
173
+ @override
174
+ Future<List<String>> getLinkedProviders() => Future.value(const []);
175
+
176
+ @override
177
+ Future<void> setPassword(String password) => Future.value();
178
+
179
+ @override
180
+ Future<List<String>> linkableSocialProviders() => Future.value(const []);
181
+
182
+ @override
183
+ Future<void> linkSocialProvider(String provider) => Future.value();
169
184
  }
@@ -0,0 +1,152 @@
1
+ // Kasy design-system guard-rail.
2
+ //
3
+ // Fails (exit 1) when a FEATURE SCREEN reaches around the design system and
4
+ // hardcodes a value that should come from a token / component. The design
5
+ // primitives themselves (lib/components, lib/core) are exempt — that is where
6
+ // the system is built, so raw values are expected there.
7
+ //
8
+ // This is intentionally a plain script, not a hidden dependency: read it, tune
9
+ // the lists below, or delete it. It enforces the project's *defaults*; it never
10
+ // locks anyone in.
11
+ //
12
+ // Run it locally or in CI:
13
+ // dart run tool/design_check.dart
14
+ //
15
+ // Escape hatch for a deliberate one-off: put `// design-check: ignore` on the
16
+ // offending line (the reason should be obvious from a nearby comment).
17
+ //
18
+ // See DESIGN_SYSTEM.md for the tokens/roles to use instead.
19
+
20
+ import 'dart:io';
21
+
22
+ /// The guard-rail only scans FEATURE SCREENS ([_scanRoot]). The design system
23
+ /// itself (lib/components, lib/core) and the app bootstrap (lib/main.dart) are
24
+ /// the *implementation* of the system, so raw values are expected there and are
25
+ /// out of scope by construction.
26
+ const String _scanRoot = 'lib/features';
27
+
28
+ /// Path fragments that are exempt even inside the feature layer.
29
+ /// - admin: internal admin tooling (kept out of the product design pass).
30
+ /// - generated files: not authored by hand.
31
+ /// - showcase/mockups: deliberately render raw values to demo them.
32
+ /// - subscriptions: the paywall is mid-redesign; re-enable when it lands.
33
+ const List<String> _exemptPathFragments = <String>[
34
+ '/admin/',
35
+ 'admin_card.dart',
36
+ '.g.dart',
37
+ '.freezed.dart',
38
+ 'home_components_preview_registry.dart',
39
+ 'home_components_preview_page.dart',
40
+ 'home_components_page.dart',
41
+ 'design_system_page.dart',
42
+ 'onboarding_module_mockups.dart',
43
+ '/features/subscriptions/',
44
+ ];
45
+
46
+ const String _ignoreMarker = '// design-check: ignore';
47
+
48
+ class _Rule {
49
+ const _Rule(this.id, this.pattern, this.message);
50
+ final String id;
51
+ final RegExp pattern;
52
+ final String message;
53
+
54
+ /// Optional secondary token that must ALSO be on the line for the rule to
55
+ /// fire (used to scope `size:` to Icon lines only).
56
+ bool matches(String line) => pattern.hasMatch(line);
57
+ }
58
+
59
+ final List<_Rule> _rules = <_Rule>[
60
+ _Rule(
61
+ 'raw-material',
62
+ RegExp(r'\b(ElevatedButton|OutlinedButton|TextButton|AlertDialog|SnackBar)\s*\(|(?<![A-Za-z])Card\s*\('),
63
+ 'Use the Kasy component (KasyButton / showKasyConfirmDialog / showKasyToast '
64
+ '/ KasyCard) instead of the raw Material widget.',
65
+ ),
66
+ _Rule(
67
+ 'hardcoded-font-size',
68
+ RegExp(r'fontSize:\s*\d'),
69
+ 'Use a typography role (context.textTheme.* / KasyTextTheme.*) instead of a '
70
+ 'literal fontSize.',
71
+ ),
72
+ _Rule(
73
+ 'raw-color',
74
+ // `Color(0x...)` literals, or `Colors.<named>` — but NOT `KasyColors.` (the
75
+ // token class, hence the lookbehind) and NOT the transparent / white* /
76
+ // black* helpers, which are the legitimate way to build scrims & overlays.
77
+ RegExp(r'Color\(0x|(?<![A-Za-z])Colors\.(?!transparent\b)(?!white)(?!black)[A-Za-z]'),
78
+ 'Use a colour token (context.colors.*) instead of a raw Color/Colors value.',
79
+ ),
80
+ ];
81
+
82
+ // The icon rule needs two signals on the same line, so it is handled separately.
83
+ final RegExp _iconCall = RegExp(r'\bIcon\s*\(');
84
+ final RegExp _numericSize = RegExp(r'size:\s*\d');
85
+
86
+ bool _isExempt(String path) =>
87
+ _exemptPathFragments.any((String frag) => path.contains(frag));
88
+
89
+ void main() {
90
+ final Directory libDir = Directory(_scanRoot);
91
+ if (!libDir.existsSync()) {
92
+ stderr.writeln('design_check: run me from the package root (no $_scanRoot here).');
93
+ exit(2);
94
+ }
95
+
96
+ final List<String> violations = <String>[];
97
+ int scanned = 0;
98
+
99
+ for (final FileSystemEntity entity in libDir.listSync(recursive: true)) {
100
+ if (entity is! File || !entity.path.endsWith('.dart')) continue;
101
+ final String path = entity.path.replaceAll(r'\', '/');
102
+ if (_isExempt('/$path')) continue;
103
+ scanned++;
104
+
105
+ final List<String> lines = entity.readAsLinesSync();
106
+ for (int i = 0; i < lines.length; i++) {
107
+ final String line = lines[i];
108
+ if (line.contains(_ignoreMarker)) continue;
109
+ final String code = _stripComment(line);
110
+ if (code.trim().isEmpty) continue;
111
+
112
+ for (final _Rule rule in _rules) {
113
+ if (rule.matches(code)) {
114
+ violations.add(_format(path, i + 1, rule.id, rule.message, line));
115
+ }
116
+ }
117
+ if (_iconCall.hasMatch(code) && _numericSize.hasMatch(code)) {
118
+ violations.add(_format(
119
+ path,
120
+ i + 1,
121
+ 'hardcoded-icon-size',
122
+ 'Use a KasyIconSize.* token instead of a literal Icon size.',
123
+ line,
124
+ ));
125
+ }
126
+ }
127
+ }
128
+
129
+ if (violations.isEmpty) {
130
+ stdout.writeln('design_check: OK ($scanned feature files clean).');
131
+ exit(0);
132
+ }
133
+
134
+ stdout.writeln('design_check: ${violations.length} violation(s) found.\n');
135
+ stdout.writeln(violations.join('\n\n'));
136
+ stdout.writeln(
137
+ '\nFix by using a token/role/component (see DESIGN_SYSTEM.md), or mark a '
138
+ 'deliberate exception with `$_ignoreMarker` on the line.',
139
+ );
140
+ exit(1);
141
+ }
142
+
143
+ /// Drops an end-of-line `//` comment so rules don't match commented-out code or
144
+ /// doc text (keeps string literals intact enough for these coarse checks).
145
+ String _stripComment(String line) {
146
+ final int idx = line.indexOf('//');
147
+ return idx == -1 ? line : line.substring(0, idx);
148
+ }
149
+
150
+ String _format(String path, int line, String id, String message, String src) {
151
+ return ' $path:$line [$id]\n ${src.trim()}\n → $message';
152
+ }
@@ -24,6 +24,47 @@
24
24
  <meta name="apple-mobile-web-app-title" content="kasy">
25
25
  <link rel="apple-touch-icon" href="icons/Icon-192.png">
26
26
 
27
+ <!-- Browser chrome color (Safari address bar / status bar tint). Uses the
28
+ `surface` token — the SAME colour as the app bar that sits directly below
29
+ the status bar — so there is no seam between the Safari bar and the app bar.
30
+ Two media-query metas are the reliable baseline: iOS Safari honours these at
31
+ load and follows the SYSTEM light/dark automatically. At runtime the app
32
+ appends a higher-priority dynamic meta (web_background_sync_web.dart) so a
33
+ theme FORCED inside the app (different from the system) is reflected too. -->
34
+ <meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
35
+ <meta name="theme-color" content="#18181B" media="(prefers-color-scheme: dark)">
36
+
37
+ <script id="theme-pref-fix">
38
+ // Resolve the theme to apply BEFORE Flutter boots, reading the preference the
39
+ // app saved (shared_preferences -> localStorage under "flutter.themeMode",
40
+ // JSON-encoded e.g. "dark"). This drives the splash background + splash logo
41
+ // via the data-theme attribute (CSS reacts to it reliably). "system" or a
42
+ // missing value falls back to the OS preference.
43
+ //
44
+ // NOTE: this does NOT touch <meta name="theme-color">. iOS Safari ignores
45
+ // theme-color created/changed via JS and only honours the STATIC media-query
46
+ // metas above — so removing them made Safari fall back to the page background
47
+ // (the wrong colour). We keep the static metas as the source of truth for the
48
+ // Safari status-bar tint; the Flutter sync still appends a dynamic meta for
49
+ // Chromium-based browsers, which DO honour it.
50
+ (function () {
51
+ var mode = 'system';
52
+ try {
53
+ var raw = window.localStorage.getItem('flutter.themeMode');
54
+ if (raw) { mode = JSON.parse(raw); }
55
+ } catch (e) {}
56
+ var dark = mode === 'dark'
57
+ ? true
58
+ : mode === 'light'
59
+ ? false
60
+ : !!(window.matchMedia &&
61
+ window.matchMedia('(prefers-color-scheme: dark)').matches);
62
+
63
+ // Drives the splash background + which splash logo shows (see CSS below).
64
+ document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
65
+ })();
66
+ </script>
67
+
27
68
  <!-- Favicon -->
28
69
  <link rel="icon" type="image/png" href="favicon.png">
29
70
 
@@ -31,7 +72,7 @@
31
72
  <link rel="manifest" href="manifest.json">
32
73
 
33
74
 
34
- <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
75
+ <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport">
35
76
 
36
77
 
37
78
 
@@ -51,15 +92,60 @@
51
92
 
52
93
 
53
94
  <style id="splash-screen-style">
95
+ :root { --vh: 1vh; }
96
+
97
+ /* iOS Safari sizes the Flutter canvas to documentElement.clientHeight (the
98
+ <html> box). `height: 100%` mis-resolves there and the canvas renders
99
+ into a smaller middle band (white bars top/bottom). Pin <html> to the
100
+ real visible height: 100dvh on Safari 15.4+, the JS-driven --vh on older. */
101
+ /* IMPORTANT: iOS Safari tints its status bar / address bar with the <body>
102
+ (and <html>) background-color — it ignores <meta name="theme-color"> once
103
+ the Flutter canvas covers the viewport. So we paint <html>/<body> with the
104
+ `surface` token (the app bar colour) to make the Safari bars match the app
105
+ bar. The splash and the area behind the canvas get the page `background`
106
+ via the #splash overlay and the Flutter host elements (set in JS), so only
107
+ the Safari chrome ends up surface-coloured. */
54
108
  html {
55
- height: 100%
109
+ height: 100vh;
110
+ height: 100dvh;
111
+ height: calc(var(--vh, 1vh) * 100);
112
+ background-color: #FFFFFF;
56
113
  }
57
114
 
58
115
  body {
59
116
  margin: 0;
60
- min-height: 100%;
61
- background-color: #FAF9FC;
62
- background-size: 100% 100%;
117
+ height: 100vh;
118
+ height: 100dvh;
119
+ height: calc(var(--vh, 1vh) * 100);
120
+ overflow: hidden;
121
+ overscroll-behavior: none;
122
+ background-color: #FFFFFF;
123
+ }
124
+
125
+ /* Surface follows the APP theme (data-theme set by the script above),
126
+ falling back to the system theme when the attribute is absent (JS off). */
127
+ html[data-theme="dark"], html[data-theme="dark"] body {
128
+ background-color: #18181B;
129
+ }
130
+
131
+ /* Splash overlay keeps the page `background` colour (NOT surface) so the
132
+ loading screen still looks right; it covers the viewport until Flutter
133
+ removes it. */
134
+ #splash {
135
+ position: fixed;
136
+ inset: 0;
137
+ z-index: 2147483647;
138
+ background-color: #F5F5F5;
139
+ }
140
+ html[data-theme="dark"] #splash {
141
+ background-color: #060607;
142
+ }
143
+
144
+ /* Flutter host chain (real tag names for the 3.41.x engine) must fill the
145
+ box so the glass-pane/scene match the now-correct <html> height. */
146
+ flutter-view, flt-glass-pane, flt-scene-host {
147
+ width: 100%;
148
+ height: 100%;
63
149
  }
64
150
 
65
151
  .center {
@@ -108,26 +194,88 @@
108
194
  right: 0;
109
195
  }
110
196
 
197
+ /* Splash logo: show the light or dark asset per the APP theme (data-theme). */
198
+ #splash .splash-dark { display: none; }
199
+ html[data-theme="dark"] #splash .splash-light { display: none; }
200
+ html[data-theme="dark"] #splash .splash-dark { display: block; }
201
+
202
+ /* Fallback when data-theme is absent (JS disabled): follow the system. */
111
203
  @media (prefers-color-scheme: dark) {
112
- body {
113
- background-color: #0C0A14;
114
- }
204
+ html:not([data-theme]), html:not([data-theme]) body {
205
+ background-color: #18181B;
206
+ }
207
+ html:not([data-theme]) #splash { background-color: #060607; }
208
+ html:not([data-theme]) #splash .splash-light { display: none; }
209
+ html:not([data-theme]) #splash .splash-dark { display: block; }
115
210
  }
116
211
  </style>
212
+ <script id="app-height-fix">
213
+ // Flutter engine on iOS reads documentElement.clientHeight (not visualViewport)
214
+ // to size the canvas (full_page_dimensions_provider.dart). On iOS Safari,
215
+ // window.innerHeight is the "large viewport" (address bar hidden) which is
216
+ // LARGER than the visible area — causing white bands top/bottom. The correct
217
+ // value is window.visualViewport.height, which tracks the real visible height.
218
+ (function () {
219
+ function visH() {
220
+ return window.visualViewport ? window.visualViewport.height : window.innerHeight;
221
+ }
222
+ function visW() {
223
+ return window.visualViewport ? window.visualViewport.width : window.innerWidth;
224
+ }
225
+
226
+ // Override clientHeight/Width on the <html> element so the Flutter engine
227
+ // receives the real visible viewport size on every measurement.
228
+ try {
229
+ Object.defineProperty(document.documentElement, 'clientHeight', {
230
+ configurable: true,
231
+ get: visH,
232
+ });
233
+ Object.defineProperty(document.documentElement, 'clientWidth', {
234
+ configurable: true,
235
+ get: visW,
236
+ });
237
+ } catch (e) {
238
+ // If Safari refuses the override, the CSS --vh fallback below takes over.
239
+ }
240
+
241
+ // --vh drives html { height: calc(var(--vh,1vh)*100) } in the CSS above.
242
+ // This is the CSS-level fallback: even if Object.defineProperty is blocked,
243
+ // the html element's rendered height equals visualViewport.height, so
244
+ // clientHeight resolves correctly after layout.
245
+ function setVh() {
246
+ document.documentElement.style.setProperty('--vh', (visH() * 0.01) + 'px');
247
+ }
248
+ setVh();
249
+
250
+ // Keep --vh in sync as the address bar shows/hides.
251
+ if (window.visualViewport) {
252
+ window.visualViewport.addEventListener('resize', setVh);
253
+ }
254
+ window.addEventListener('resize', setVh);
255
+ window.addEventListener('orientationchange', function () {
256
+ // Give the browser 300 ms to settle after rotation before re-measuring.
257
+ setTimeout(setVh, 300);
258
+ });
259
+ })();
260
+ </script>
117
261
  <script id="splash-screen-script">
118
262
  function removeSplashFromWeb() {
119
263
  document.getElementById("splash")?.remove();
120
264
  document.getElementById("splash-branding")?.remove();
121
- document.body.style.background = "transparent";
265
+ // Keep the <body> surface-coloured (do NOT make it transparent): iOS Safari
266
+ // reads the <body> background-color for its status/address bar tint.
122
267
  }
123
268
  </script>
124
269
  </head>
125
270
  <body>
126
- <picture id="splash">
127
- <source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
128
- <source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
129
- <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
130
- </picture>
271
+ <div id="splash">
272
+ <img class="center splash-light" aria-hidden="true" alt=""
273
+ src="splash/img/light-1x.png"
274
+ srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x">
275
+ <img class="center splash-dark" aria-hidden="true" alt=""
276
+ src="splash/img/dark-1x.png"
277
+ srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x">
278
+ </div>
131
279
 
132
280
 
133
281
 
@@ -1,61 +0,0 @@
1
- import 'package:flutter/foundation.dart';
2
- import 'package:flutter/material.dart';
3
- import 'package:flutter_riverpod/flutter_riverpod.dart';
4
- import 'package:kasy_kit/core/config/features.dart';
5
- import 'package:kasy_kit/core/data/models/user.dart';
6
- import 'package:kasy_kit/core/guards/guard.dart';
7
- import 'package:kasy_kit/core/states/user_state_notifier.dart';
8
- import 'package:kasy_kit/core/theme/theme.dart';
9
-
10
- class UserInfosGuard extends ConsumerWidget {
11
- final Widget child;
12
- final String fallbackRoute;
13
-
14
- const UserInfosGuard({
15
- super.key,
16
- required this.child,
17
- required this.fallbackRoute,
18
- });
19
-
20
- @override
21
- Widget build(BuildContext context, WidgetRef ref) {
22
- final authState = ref.watch(userStateNotifierProvider);
23
- if (authState.user.isLoading) {
24
- return Material(
25
- color: context.colors.background,
26
- child: const Center(
27
- child: CircularProgressIndicator.adaptive(),
28
- ),
29
- );
30
- }
31
-
32
- // Web: skip onboarding but require sign-in (or explicit guest choice).
33
- // The /signin page's "Continue without account" button calls onOnboarded()
34
- // which marks isOnboarded=true locally, allowing the guard to pass.
35
- if (kIsWeb && withWeb) {
36
- return Guard(
37
- canActivate: Future.value(
38
- authState.user.idOrNull != null || authState.user.isOnboarded,
39
- ),
40
- fallbackRoute: '/signin',
41
- child: child,
42
- );
43
- }
44
-
45
- // Native: all users (including guests with no ID) go through onboarding.
46
- // Guests are never forced to /signin here; individual features that
47
- // require authentication redirect to /signin on their own.
48
- // Authenticated users skip onboarding: they already committed to an account
49
- // (e.g. returning user signing back in after the anonymous session was
50
- // discarded by `credential-already-in-use`).
51
- return Guard(
52
- canActivate: Future.value(
53
- !withOnboarding ||
54
- authState.user.isOnboarded ||
55
- authState.user is AuthenticatedUserData,
56
- ),
57
- fallbackRoute: fallbackRoute,
58
- child: child,
59
- );
60
- }
61
- }