kasy-cli 1.34.0 → 1.36.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 +18 -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 +215 -178
  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 +226 -105
  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
@@ -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
  }
@@ -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
- }