kasy-cli 1.38.0 → 1.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/lib/scaffold/CHANGELOG.json +23 -0
  2. package/lib/scaffold/backends/api/patch/README.md +15 -0
  3. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
  4. package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
  5. package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
  6. package/lib/scaffold/backends/patch-base-hashes.json +6 -6
  7. package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
  8. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
  9. package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
  10. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
  11. package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  12. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
  13. package/lib/scaffold/shared/generator-utils.js +12 -6
  14. package/package.json +1 -1
  15. package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
  16. package/templates/firebase/AGENTS.md +2 -2
  17. package/templates/firebase/DESIGN_SYSTEM.md +23 -8
  18. package/templates/firebase/assets/icons/apple_black.svg +3 -0
  19. package/templates/firebase/assets/icons/apple_white.svg +4 -0
  20. package/templates/firebase/assets/icons/facebook.svg +49 -0
  21. package/templates/firebase/assets/icons/google.svg +1 -0
  22. package/templates/firebase/functions/src/admin/functions.ts +2 -0
  23. package/templates/firebase/functions/src/authentication/functions.ts +13 -7
  24. package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
  25. package/templates/firebase/lib/components/components.dart +5 -2
  26. package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
  27. package/templates/firebase/lib/components/kasy_card.dart +4 -0
  28. package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
  29. package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
  30. package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
  31. package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
  32. package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
  33. package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +34 -16
  34. package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
  35. package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
  36. package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
  37. package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
  38. package/templates/firebase/lib/core/rating/widgets/review_popup.dart +95 -30
  39. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
  40. package/templates/firebase/lib/core/states/logout_action.dart +11 -1
  41. package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
  42. package/templates/firebase/lib/core/theme/texts.dart +21 -6
  43. package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
  44. package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
  45. package/templates/firebase/lib/core/web_viewport_scale.dart +66 -36
  46. package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
  47. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
  48. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
  49. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
  50. package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
  51. package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
  52. package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
  53. package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
  54. package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
  55. package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
  56. package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
  57. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +263 -59
  58. package/templates/firebase/lib/features/home/home_feed.dart +2 -2
  59. package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
  60. package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
  61. package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
  62. package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
  63. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
  64. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
  65. package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
  66. package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
  67. package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
  68. package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
  69. package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
  70. package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
  71. package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
  72. package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
  73. package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
  74. package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
  75. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
  76. package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
  77. package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
  78. package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
  79. package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
  80. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
  81. package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
  82. package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
  83. package/templates/firebase/lib/i18n/en.i18n.json +753 -712
  84. package/templates/firebase/lib/i18n/es.i18n.json +753 -712
  85. package/templates/firebase/lib/i18n/pt.i18n.json +753 -712
  86. package/templates/firebase/lib/main.dart +20 -7
  87. package/templates/firebase/lib/router.dart +32 -26
  88. package/templates/firebase/pubspec.yaml +2 -1
  89. package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
  90. package/templates/firebase/test/app_bar_config_test.dart +70 -0
  91. package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
  92. package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
  93. package/templates/firebase/tool/design_check.dart +9 -0
  94. package/templates/firebase/assets/icons/apple.png +0 -0
  95. package/templates/firebase/assets/icons/facebook.png +0 -0
  96. package/templates/firebase/assets/icons/google.png +0 -0
  97. package/templates/firebase/assets/icons/google_play_games.png +0 -0
  98. package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
  99. package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
  100. package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
  101. package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
  102. package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
  103. package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
  104. package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
  105. package/templates/firebase/lib/features/notifications/ui/components/push_notification_switcher.dart +0 -106
@@ -1,4 +1,27 @@
1
1
  {
2
+ "1.39.1": {
3
+ "modules": {
4
+ "core": {
5
+ "pt": "Ajuste fino da proporção da web: a escala passou de 0.95 para 0.93 (a interface fica um pouco menor, mais perto do tamanho do app nativo). É só calibragem do mesmo knob único (kWebViewportScale) em todos os breakpoints; o nativo continua em 1.0 e dá pra desligar tudo numa linha (kWebViewportScaleEnabled).",
6
+ "en": "Fine-tuned the web proportion: the scale went from 0.95 to 0.93 (the interface is a touch smaller, closer to the native app size). It is just calibration of the same single knob (kWebViewportScale) across every breakpoint; native stays at 1.0 and the whole thing can be turned off in one line (kWebViewportScaleEnabled).",
7
+ "es": "Ajuste fino de la proporción web: la escala pasó de 0.95 a 0.93 (la interfaz queda un poco más pequeña, más cerca del tamaño del app nativo). Es solo calibración del mismo knob único (kWebViewportScale) en todos los breakpoints; el nativo se mantiene en 1.0 y todo se puede apagar en una línea (kWebViewportScaleEnabled)."
8
+ }
9
+ }
10
+ },
11
+ "1.39.0": {
12
+ "modules": {
13
+ "core": {
14
+ "pt": "Proporção da interface na web virou um ajuste único e documentado: a web renderiza ~5% menor que o nativo (escala 0.95) em todos os tamanhos de tela, deixando a proporção mais próxima do app nativo. O nativo fica intacto (1.0) e respeitando a fonte do sistema, e dá pra desligar tudo numa linha (kWebViewportScaleEnabled). É uma camada de densidade por cima do design system responsivo, não substitui breakpoints/tipografia.",
15
+ "en": "The web interface proportion is now a single, documented knob: web renders ~5% smaller than native (0.95 scale) at every screen size, bringing it closer to the native app. Native stays untouched (1.0) and respects the system font size, and the whole thing can be turned off in one line (kWebViewportScaleEnabled). It is a density layer on top of the responsive design system, not a replacement for breakpoints/typography.",
16
+ "es": "La proporción de la interfaz en web ahora es un ajuste único y documentado: la web se renderiza ~5% más pequeña que el nativo (escala 0.95) en todos los tamaños de pantalla, acercándola al app nativo. El nativo queda intacto (1.0) y respeta el tamaño de fuente del sistema, y todo se puede apagar en una línea (kWebViewportScaleEnabled). Es una capa de densidad sobre el design system responsivo, no reemplaza breakpoints/tipografía."
17
+ },
18
+ "components": {
19
+ "pt": "Campo de texto com altura idêntica em qualquer renderizador e breakpoint (a linha é travada via strut, então web CanvasKit e nativo batem certinho, sem variar ~1px); e card clicável agora tem destaque ao passar o cursor na web, parecendo um controle interativo de verdade.",
20
+ "en": "Text field with identical height across every renderer and breakpoint (the line is locked via strut, so web CanvasKit and native match exactly, with no ~1px drift); and a tappable card now has a hover highlight on web, feeling like a real interactive control.",
21
+ "es": "Campo de texto con altura idéntica en cualquier renderizador y breakpoint (la línea se fija con strut, así web CanvasKit y nativo coinciden exactamente, sin variar ~1px); y una tarjeta clicable ahora tiene resaltado al pasar el cursor en web, sintiéndose como un control interactivo real."
22
+ }
23
+ }
24
+ },
2
25
  "1.38.0": {
3
26
  "modules": {
4
27
  "components": {
@@ -112,6 +112,7 @@ GET /admin/users
112
112
  "email": "ana@b.com" | null,
113
113
  "name": "Ana" | null,
114
114
  "createdAt": 1700000000000, // epoch em milissegundos | null
115
+ "avatarPath": "https://.../thumb.jpg" | null, // foto do usuario, se tiver
115
116
  "subscriber": true // tem assinatura ativa?
116
117
  }
117
118
  ],
@@ -230,6 +231,20 @@ POST /users/{userId}/devices/cleanup-stale → remove devices anti
230
231
  DELETE /users/{userId}/devices → remove todos os devices do usuário (ex.: no logout)
231
232
  ```
232
233
 
234
+ > **Device sem token de push (importante):** o app registra o device **mesmo sem
235
+ > permissão de push** — nesse caso o `token` vem **vazio** (`""`). É de propósito:
236
+ > rastreia a instalação e dispara a boas-vindas (abaixo) sem depender do push; o
237
+ > token é preenchido depois, via `PUT /devices/{deviceId}`, quando o usuário ativar
238
+ > as notificações. Seu backend deve **aceitar token vazio** e, ao enviar push,
239
+ > **pular** os devices com token vazio (não os trate como inválidos nem os apague).
240
+
241
+ > **Notificação de boas-vindas:** crie-a **uma única vez por conta**, no primeiro
242
+ > registro de device (`POST /users/{userId}/devices`), de forma **independente do
243
+ > push** (vale para conta anônima também). Persista só no banco (sem disparar push) e
244
+ > use o `extra_data.deviceLocale` enviado pelo device para localizar a mensagem
245
+ > (`pt`/`es`/`en`). Referência exata: o trigger `trigger_welcome_notification` do
246
+ > backend Supabase e o `onFirstDeviceRegistered` do Firebase.
247
+
233
248
  ### Endpoints: notifications
234
249
 
235
250
  ```
@@ -118,12 +118,17 @@ class FirebaseDeviceApi implements DeviceApi {
118
118
  await Future.delayed(const Duration(seconds: 1));
119
119
  }
120
120
  }
121
- final token = await _messaging.getToken();
122
- if (token == null) {
123
- throw ApiError(
124
- code: 0,
125
- message: 'FCM token is null check Firebase setup and notification permissions',
126
- );
121
+ // Register the installation even without a push token. On iOS the FCM
122
+ // token stays null until the user grants notification permission (and
123
+ // getToken can even throw before APNS is ready). We still create the
124
+ // device row — so the welcome notification fires and the install is
125
+ // tracked — and the token fills in later via onTokenRefresh once push is
126
+ // enabled.
127
+ String token = '';
128
+ try {
129
+ token = await _messaging.getToken() ?? '';
130
+ } catch (_) {
131
+ token = '';
127
132
  }
128
133
  final os = Platform.isAndroid
129
134
  ? OperatingSystem.android //
@@ -16,6 +16,11 @@ class AdminUser {
16
16
  final String? email;
17
17
  final String? name;
18
18
  final DateTime? createdAt;
19
+
20
+ /// Public photo URL when the user has an avatar (social login import or a
21
+ /// manual upload). Null/empty for users without a picture — the UI then
22
+ /// falls back to initials.
23
+ final String? avatarPath;
19
24
  final bool subscriber;
20
25
 
21
26
  const AdminUser({
@@ -24,6 +29,7 @@ class AdminUser {
24
29
  this.email,
25
30
  this.name,
26
31
  this.createdAt,
32
+ this.avatarPath,
27
33
  });
28
34
 
29
35
  factory AdminUser.fromMap(Map<String, dynamic> m) {
@@ -35,6 +41,7 @@ class AdminUser {
35
41
  createdAt: createdMillis is num
36
42
  ? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
37
43
  : null,
44
+ avatarPath: m['avatarPath'] as String?,
38
45
  subscriber: m['subscriber'] == true,
39
46
  );
40
47
  }
@@ -65,7 +72,8 @@ final adminUsersApiProvider = Provider<AdminUsersApi>(
65
72
  /// Response 200: {
66
73
  /// "users": [
67
74
  /// {"id": "...", "email": "a@b.com"|null, "name": "Ana"|null,
68
- /// "createdAt": 1700000000000 (epoch millis)|null, "subscriber": true}
75
+ /// "createdAt": 1700000000000 (epoch millis)|null,
76
+ /// "avatarPath": "https://.../thumb.jpg"|null, "subscriber": true}
69
77
  /// ],
70
78
  /// "totalUsers": 142, // true size of the collection
71
79
  /// "truncated": false // true when more users exist than returned
@@ -38,6 +38,7 @@ dependencies:
38
38
  flutter_native_splash: ^2.4.7
39
39
  flutter_riverpod: ^3.1.0
40
40
  flutter_secure_storage: ^9.2.4
41
+ flutter_svg: ^2.3.0
41
42
  flutter_timezone: ^5.0.1
42
43
  freezed_annotation: ^3.1.0
43
44
  gal: ^1.3.0
@@ -2,7 +2,7 @@
2
2
  "api/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
3
3
  "api/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
4
4
  "api/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
5
- "api/lib/core/data/api/user_api.dart": "0e7d984dceada025b7e998c217c39da9b781de430fd0f4d2267aaa6439391d5f",
5
+ "api/lib/core/data/api/user_api.dart": "14166f7e3419884c514f93126a64795264f5f2a743b050a600bb37631e0d7e23",
6
6
  "api/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
7
7
  "api/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
8
8
  "api/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
@@ -17,7 +17,7 @@
17
17
  "api/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
18
18
  "api/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
19
19
  "api/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
20
- "api/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
20
+ "api/lib/features/notifications/api/device_api.dart": "cedc21bf8db4eeaf63f9723d539d6afddde01b30332acfa9c70b22ba7de25698",
21
21
  "api/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
22
22
  "api/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
23
23
  "api/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
@@ -25,7 +25,7 @@
25
25
  "api/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
26
26
  "api/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
27
27
  "api/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
28
- "api/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
28
+ "api/lib/features/settings/ui/components/admin/admin_users_api.dart": "ed07e674817099691d6131e8ee7ced1332b9291f0eac207a720f20558e20e64a",
29
29
  "api/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
30
30
  "api/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
31
31
  "api/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
@@ -34,7 +34,7 @@
34
34
  "supabase/android/app/src/main/AndroidManifest.xml": "0579c57068ee50d43837ef9c7bee884b6232e92975301c8305ce389282f68291",
35
35
  "supabase/ios/Runner/AppDelegate.swift": "9f1027a03ad1fef2dab45464fbd7e98364b6a9cb6d2a6abe770ee9c45fa19122",
36
36
  "supabase/lib/core/data/api/storage_api.dart": "d77fecc6923ea9544b6ec20571dcab5c8f3d38be81ec618b0cf4f1b08be7bbae",
37
- "supabase/lib/core/data/api/user_api.dart": "0e7d984dceada025b7e998c217c39da9b781de430fd0f4d2267aaa6439391d5f",
37
+ "supabase/lib/core/data/api/user_api.dart": "14166f7e3419884c514f93126a64795264f5f2a743b050a600bb37631e0d7e23",
38
38
  "supabase/lib/core/data/entities/json_converters.dart": "92a5efd595a03a862fa03e77ab3ca3d556523d0c9e0daca6e296e35d24b760a8",
39
39
  "supabase/lib/core/data/entities/user_entity.dart": "5c1e56e4be3ba2ed95cdcd276112a02b526f2a3fa7ee5bba08a3ec2b51153688",
40
40
  "supabase/lib/environments.dart": "c41f0843a4d627b717579eec5607b7ff5c16369bdd4b2a82e081058a20ffb7a5",
@@ -48,7 +48,7 @@
48
48
  "supabase/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
49
49
  "supabase/lib/features/feedbacks/api/feature_request_api.dart": "a741395509b95e8cae135bc8bdcca1ba0d48785b1e1b201732219659d271346b",
50
50
  "supabase/lib/features/feedbacks/api/feature_vote_api.dart": "89dcaeea29460a7f219c2ede3759707b45066272fe6837c29a40f3d79fade4e9",
51
- "supabase/lib/features/notifications/api/device_api.dart": "0d6bccb6813cceec29c54220daac9d0cd35687c4fa9f10ffde07dad88b204ed3",
51
+ "supabase/lib/features/notifications/api/device_api.dart": "cedc21bf8db4eeaf63f9723d539d6afddde01b30332acfa9c70b22ba7de25698",
52
52
  "supabase/lib/features/notifications/api/entities/device_entity.dart": "54fce6274c5fe60ba663d8693e0c9d88fd6cd6fcd9ec906e021621a2e51176b7",
53
53
  "supabase/lib/features/notifications/api/entities/notifications_entity.dart": "f9ee62111a6f122657e105df317f1f5f803e3a525a96686c0a2728ff02cd8964",
54
54
  "supabase/lib/features/notifications/api/notifications_api.dart": "30cdb1bc52c3249b2cda6b1c7eb7fb6a977db5744da5a9c2980784f41e75d9e7",
@@ -56,7 +56,7 @@
56
56
  "supabase/lib/features/onboarding/api/user_infos_api.dart": "cf728bcef364259912ee1e87151c4f791faee0c0d49232c9af260bb5681bd429",
57
57
  "supabase/lib/features/onboarding/models/user_info.dart": "946fd34a33b630a34a3b6248594cbca717f4fab9d23f669510145b9032bd1321",
58
58
  "supabase/lib/features/onboarding/repositories/user_infos_repository.dart": "487a5177dcaed7f87e613b2b4f04329c4995274e6a52809d4917c037cb3c3c55",
59
- "supabase/lib/features/settings/ui/components/admin/admin_users_api.dart": "0ffb232ffb193154c0366d6f7da0ca499b3e06fed4fd71af572b8dc53ecd844e",
59
+ "supabase/lib/features/settings/ui/components/admin/admin_users_api.dart": "ed07e674817099691d6131e8ee7ced1332b9291f0eac207a720f20558e20e64a",
60
60
  "supabase/lib/features/settings/ui/widgets/avatar_utils.dart": "bb9126409bbbb245f2dec613bd096ac53c208a56bd55f3d2ab2599e43534904f",
61
61
  "supabase/lib/features/subscriptions/api/entities/subscription_entity.dart": "20c1d75ed9d88acb96e94a592dcb4de0f63c792302ea07df53cebbdeb6d0cf7e",
62
62
  "supabase/lib/features/subscriptions/api/stripe_backend_api.dart": "e370bd05211462f2fc94c69af1749eb5330f997e9ec5e73e5c4e119999bf666f",
@@ -39,6 +39,7 @@ interface AdminUser {
39
39
  email: string | null;
40
40
  name: string | null;
41
41
  createdAt: number | null; // epoch millis
42
+ avatarPath: string | null; // public photo URL, when the user has one
42
43
  subscriber: boolean;
43
44
  }
44
45
 
@@ -105,7 +106,7 @@ Deno.serve(async (req: Request) => {
105
106
  // client re-sorts anyway, so this only decides which rows survive the cap.
106
107
  const { data: rows, error: rowsError } = await admin
107
108
  .from("users")
108
- .select("id, email, name, creation_date")
109
+ .select("id, email, name, creation_date, avatar_url")
109
110
  .order("creation_date", { ascending: false, nullsFirst: false })
110
111
  .limit(MAX_SCAN);
111
112
  if (rowsError) throw rowsError;
@@ -132,6 +133,7 @@ Deno.serve(async (req: Request) => {
132
133
  email: (d.email as string) || null,
133
134
  name: (d.name as string) || null,
134
135
  createdAt: d.creation_date ? new Date(d.creation_date).getTime() : null,
136
+ avatarPath: (d.avatar_url as string) || null,
135
137
  subscriber: activeSubscribers.has(d.id),
136
138
  }));
137
139
 
@@ -295,6 +295,9 @@ Deno.serve(async (req: Request) => {
295
295
  const staleTokenIds: string[] = [];
296
296
 
297
297
  for (const device of devices) {
298
+ // Installs without a push token (notifications not enabled yet) aren't
299
+ // sendable and must NOT be treated as stale/deleted — just skip them.
300
+ if (!device.token) continue;
298
301
  const ok = await sendToToken(projectId, accessToken, device.token, notification.title, notification.body, extraData, notification.image_url);
299
302
  if (ok) {
300
303
  sent++;
@@ -0,0 +1,62 @@
1
+ -- Decouple the welcome notification from push permission (parity with Firebase).
2
+ --
3
+ -- Before: the welcome was SKIPPED for anonymous users (no email) and read the
4
+ -- locale from public.users.locale — which is null at signup, so it always fell
5
+ -- back to English. Combined with the fact that a device only registered once a
6
+ -- push token existed (iOS required permission), users who skipped push never got
7
+ -- a welcome at all.
8
+ --
9
+ -- Now: the install registers even without a push token (client change), so this
10
+ -- trigger fires for EVERY account on first device registration, and localises
11
+ -- from the device's own reported locale (device.extra_data.deviceLocale) —
12
+ -- exactly like the Firebase `onFirstDeviceRegistered` trigger. The one-time
13
+ -- `welcome_sent` claim and notify_user = false (DB only, no push) are unchanged.
14
+
15
+ CREATE OR REPLACE FUNCTION public.trigger_welcome_notification()
16
+ RETURNS TRIGGER AS $$
17
+ DECLARE
18
+ v_locale text;
19
+ v_claimed int;
20
+ v_title text;
21
+ v_body text;
22
+ BEGIN
23
+ -- Atomically claim the one-time welcome: flip welcome_sent to true only if it
24
+ -- isn't already. ROW_COUNT is 1 for the very first claim, 0 for any later
25
+ -- device registration — including one re-created after a logout/login cycle.
26
+ UPDATE public.users
27
+ SET welcome_sent = true
28
+ WHERE id = NEW.user_id AND welcome_sent = false;
29
+
30
+ GET DIAGNOSTICS v_claimed = ROW_COUNT;
31
+ IF v_claimed = 0 THEN
32
+ RETURN NEW;
33
+ END IF;
34
+
35
+ -- Locale from the device that just registered (e.g. 'pt_BR' -> 'pt'),
36
+ -- independent of push permission. Falls back to English.
37
+ v_locale := lower(substring(coalesce(NEW.extra_data->>'deviceLocale', 'en') from 1 for 2));
38
+
39
+ IF v_locale = 'pt' THEN
40
+ v_title := 'Bem-vindo!';
41
+ v_body := 'Sua conta está pronta. Aproveite!';
42
+ ELSIF v_locale = 'es' THEN
43
+ v_title := '¡Bienvenido!';
44
+ v_body := 'Tu cuenta está lista. ¡Disfrútala!';
45
+ ELSE
46
+ v_title := 'Welcome!';
47
+ v_body := 'Your account is ready. Enjoy!';
48
+ END IF;
49
+
50
+ -- notify_user = false: user is inside the app, no push needed.
51
+ INSERT INTO public.notifications (user_id, title, body, type, notify_user)
52
+ VALUES (NEW.user_id, v_title, v_body, 'welcome', false);
53
+
54
+ RETURN NEW;
55
+ END;
56
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
57
+
58
+ -- Re-assert the trigger so a database built from these migrations is consistent.
59
+ DROP TRIGGER IF EXISTS on_first_device_registered ON public.devices;
60
+ CREATE TRIGGER on_first_device_registered
61
+ AFTER INSERT ON public.devices
62
+ FOR EACH ROW EXECUTE PROCEDURE public.trigger_welcome_notification();
@@ -98,12 +98,17 @@ class FirebaseDeviceApi implements DeviceApi {
98
98
  await Future.delayed(const Duration(seconds: 1));
99
99
  }
100
100
  }
101
- final token = await _messaging.getToken();
102
- if (token == null) {
103
- throw ApiError(
104
- code: 0,
105
- message: 'FCM token is null check Firebase setup and notification permissions',
106
- );
101
+ // Register the installation even without a push token. On iOS the FCM
102
+ // token stays null until the user grants notification permission (and
103
+ // getToken can even throw before APNS is ready). We still create the
104
+ // device row — so the welcome notification fires and the install is
105
+ // tracked — and the token fills in later via onTokenRefresh once push is
106
+ // enabled.
107
+ String token = '';
108
+ try {
109
+ token = await _messaging.getToken() ?? '';
110
+ } catch (_) {
111
+ token = '';
107
112
  }
108
113
  final os = Platform.isAndroid
109
114
  ? OperatingSystem.android //
@@ -16,6 +16,11 @@ class AdminUser {
16
16
  final String? email;
17
17
  final String? name;
18
18
  final DateTime? createdAt;
19
+
20
+ /// Public photo URL when the user has an avatar (social login import or a
21
+ /// manual upload). Null/empty for users without a picture — the UI then
22
+ /// falls back to initials.
23
+ final String? avatarPath;
19
24
  final bool subscriber;
20
25
 
21
26
  const AdminUser({
@@ -24,6 +29,7 @@ class AdminUser {
24
29
  this.email,
25
30
  this.name,
26
31
  this.createdAt,
32
+ this.avatarPath,
27
33
  });
28
34
 
29
35
  factory AdminUser.fromMap(Map<String, dynamic> m) {
@@ -35,6 +41,7 @@ class AdminUser {
35
41
  createdAt: createdMillis is num
36
42
  ? DateTime.fromMillisecondsSinceEpoch(createdMillis.toInt())
37
43
  : null,
44
+ avatarPath: m['avatarPath'] as String?,
38
45
  subscriber: m['subscriber'] == true,
39
46
  );
40
47
  }
@@ -45,6 +45,7 @@ dependencies:
45
45
  flutter_native_splash: ^2.4.7
46
46
  flutter_riverpod: ^3.1.0
47
47
  flutter_secure_storage: ^9.2.4
48
+ flutter_svg: ^2.3.0
48
49
  flutter_timezone: ^5.0.1
49
50
  freezed_annotation: ^3.1.0
50
51
  gal: ^1.3.0
@@ -1381,13 +1381,19 @@ async function removeFacebookSigninFromAuthPages(projectDir) {
1381
1381
  // Legacy: remove the standalone FacebookSigninComponent + its import, if present.
1382
1382
  content = content.replace(/^import 'package:[^']+\/features\/authentication\/ui\/components\/facebook_signin\.dart';\n/m, '');
1383
1383
  content = content.replace(/[ \t]*const FacebookSigninComponent\(\),\n/g, '');
1384
- // Current UI: the Facebook button is an inline _SocialSigninTile/_SocialSignupTile
1385
- // in the social row (label: t.auth.signin.facebook ... signinWithFacebook()). Strip
1386
- // the whole tile plus the SizedBox spacer that precedes it, anchored on the facebook
1387
- // label so the Google/Apple tiles are never touched. Runs before dartFix/format, so
1388
- // it matches the kit's raw formatting verbatim.
1384
+ // Current UI: the Facebook button is an inline SocialAuthButton in the social
1385
+ // row, wrapped in a collection-if (`if (showFacebook) ...[ ... ]`). Strip the
1386
+ // WHOLE if-block wrapper, SizedBox spacer and tile anchored on the facebook
1387
+ // label so the Google/Apple tiles are never touched. Runs before dartFix/format,
1388
+ // so it matches the kit's raw formatting verbatim.
1389
1389
  content = content.replace(
1390
- /\n[ \t]*const SizedBox\(width: KasySpacing\.sm\),\n[ \t]*Expanded\(\n[ \t]*child: _Social(?:Signin|Signup)Tile\(\n[ \t]*label: t\.auth\.signin\.facebook,[\s\S]*?\.signinWithFacebook\(\),\n[ \t]*\),\n[ \t]*\),/,
1390
+ /\n[ \t]*if \(showFacebook\) \.\.\.\[\n[ \t]*const SizedBox\(width: KasySpacing\.sm\),\n[ \t]*Expanded\(\n[ \t]*child: SocialAuthButton\(\n[ \t]*label: t\.auth\.signin\.facebook,[\s\S]*?\.signinWithFacebook\(\),\n[ \t]*\),\n[ \t]*\),\n[ \t]*\],/,
1391
+ '',
1392
+ );
1393
+ // …then drop the now-unused `showFacebook` declaration (+ its comment), so the
1394
+ // stripped page has no dangling variable / analyzer warning.
1395
+ content = content.replace(
1396
+ /\n[ \t]*\/\/ Facebook on web works[\s\S]*?\n[ \t]*const bool showFacebook = [^;]*;/,
1391
1397
  '',
1392
1398
  );
1393
1399
  await fs.writeFile(p, content, 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.38.0",
3
+ "version": "1.39.1",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -4,7 +4,7 @@ local_notifications.js,1772398909034,0b44b177c4b8a705ad1b0e3d7532537aad7b230e947
4
4
  flutter.js,1777393608000,b42e549a02f5c1428b0ad85dbdc663d400a6d2db10cb8aefcc0fddd592fd6ceb
5
5
  firebase-messaging-sw.js,1772484605561,dba3c939f7c240e62f63d06408d5f70e45f1168d00df98726e6a7a20cbd3b0df
6
6
  favicon.png,1779810782545,c967afd3c3d4dd165ef2e5cf3f203c3982dfff6a95881859ed163d9c38d75512
7
- .last_build_id,1781393235997,3217b88fca927fa7fc5a6d9c24dee562336c5b14ad2c6e8b9b7b1bacff2e4a42
7
+ .last_build_id,1781475668120,4a8e13af757aaae5b1160a761d56555ef4370ca7572732cbfa8d98c989424ab0
8
8
  splash/img/light-4x.png,1780546027751,abf0e55345e532a4645fe14b01c2948b07c78df50cc6da23648837dfb6271c74
9
9
  splash/img/light-3x.png,1780546027699,5f02c94032cb7053807ce89603b109f14ea2740eaa3145dc76c924cbe1d534a9
10
10
  splash/img/light-2x.png,1780546027647,3c073b9da154ac8be530f8beefa3579246f223acd819e856a2db000fdabcb67e
@@ -34,15 +34,6 @@ canvaskit/chromium/canvaskit.js.symbols,1777393830000,60722421e56edacbeb674cc3ef
34
34
  canvaskit/chromium/canvaskit.js,1777393830000,10ea3badcf26e29aba851699c03e09bc35fea65cdc440a035bac263d6d02d665
35
35
  assets/.env,1781370223435,777e85eb4de226724b28db8664404d0b73ae37f63fd66c955ac90c89cd491e7e
36
36
  assets/packages/mixpanel_flutter/assets/mixpanel.js,1779806094332,1915455df317dd1a9f42c6f3ea5bbe3ee357614f89d994f157a2e821f37956c3
37
- assets/packages/lucide_icons_flutter/assets/lucide.ttf,1779809590216,3813e3eb113e8be4c36c2082397dc3397d0ea7cd3a3b43eaecd61100b0256513
38
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w600.ttf,1779809590200,095ae41c43f72ec2997fa59fd91a0c83a36b345915e705f292f414cae6240dab
39
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w500.ttf,1779809590211,59d94414e1a3dd1732ff0b3ea50389c3e8b3915fe5a9b6163ba256a825e1e05b
40
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w400.ttf,1779809590202,9342632d6521314eb2ab7e902a8a6fe6c3d62a110f682bcc123e244e12c4a695
41
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w300.ttf,1779809590205,3bd1b359793f74027dada0e359d7d9dc712afd721db5ef30b095592e2e03e865
42
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w200.ttf,1779809590198,8026f9e7a751a47ace3ec60fdb7c6843a1d8051699fb28eb0a9fb1bd53904a66
43
- assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w100.ttf,1779809590208,1d81fb594eb42c3e8e43a36b89120a0a8a4c2dd858dec1a0c94de0e5b2208388
44
- assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779806141382,010d647b6cab4e1a9b4398957353f43589903ce8dc760d05d894944b664e29bd
45
- assets/fonts/MaterialIcons-Regular.otf,1662569016000,3a631b0761788d7cf2a71e8732a5b31288d6e7aa996c671c70d53a251bbd3848
46
37
  assets/assets/images/splash_logo_light_android12.png,1779867497377,da2caee4f16230230dbb04d84913f0a9df2eae6bcb8f9d1ce83ed556cee6d094
47
38
  assets/assets/images/splash_logo_light.png,1779867497286,643a1f45d6ef9458c4c0eed9f4889cc2adbdf1db616913215be66673a05a0066
48
39
  assets/assets/images/splash_logo_dark_android12.png,1779867497464,b237d9f9035d419525a10b32889c30d44c706d62990b30bdc6bef911fe9352d6
@@ -60,16 +51,25 @@ assets/assets/icons/google_play_games.png,1772398653083,533c1d87d7be8690ab173ef4
60
51
  assets/assets/icons/google.png,1772398653083,f423e7e7be1e06008d45617d07f095f04da7fdcab9a56523f9e0633828e464e0
61
52
  assets/assets/icons/facebook.png,1772398653083,79ac67e449c1db63319d43329ca91f682b4e0bc6a0883b0dfb2a849e13ae6eb6
62
53
  assets/assets/icons/apple.png,1772398653083,2a737d8801ca81452b2978bb2e1ac72136acf1a4c338f2de2f77b65e9f6de1fa
63
- version.json,1781400658272,495754f8591b89616bdad18717ffd8aa9275a8605889167170e6bc98ced40489
64
- flutter_service_worker.js,1781400658740,baeeaf9f4b8e6f40d3b0549429ceb70ca01f120db6a7ef2f60d5809233dc2205
65
- assets/FontManifest.json,1781400658381,4d84ab517c27984d36f9a3c8be6f2a72788c0c3985c1d5874297fef0a53407ca
66
- index.html,1781400619641,bb8142eb84e9e44049957c3edf86006e1ae5f194c397a8326ce505b68fdf58f4
67
- assets/AssetManifest.bin,1781400658381,78bccb08a36307a400711a1d7e6868bd5f89a24e63439fb5b6747660323e4475
68
- assets/AssetManifest.bin.json,1781400658381,50a11b51c3fc4cbed1b5ec3a4f466c6a6b75c481d08d4782c8191f99bdbdba9c
69
- assets/shaders/stretch_effect.frag,1781400658449,1a7d4ac2be40cf0a459dfb390ef08bcd740f37913ffdee8de3c2ea836a18410e
70
- assets/shaders/ink_sparkle.frag,1781400658449,1c8e222328206d1e06754f76fb53947aad38d62180aafad5298a3c6f510b173d
71
- flutter_bootstrap.js,1781400619636,371abfca6d227535d1d7bf43ada40ba5256e04ae0aed40e29ccae33f964fa7f7
72
- main.dart.js_3.part.js,1781400646444,6f52d11c8e140736cd046ce6b541ad8d8bf20e6ef709f39d8ef4eb3dddbece8b
73
- main.dart.js_1.part.js,1781400646439,42136799a40b5acb14b5acf0a8cef39bbef13b4dc99ce808a7bc8a1f8cdd8b09
74
- assets/NOTICES,1781400658382,a18efc42c5ed99b56481537d0f229ddd3add671c548a893aaf8766f30c854158
75
- main.dart.js,1781400646862,c0154549b8162d55ff32593e679d1f3461e6f4ff744d9730cd87953b0244d8c9
54
+ version.json,1781481118464,be3307736a7e600c6e90593352859f1c46bc97666d5cf21a16099effd2a0fed6
55
+ flutter_service_worker.js,1781481120062,baeeaf9f4b8e6f40d3b0549429ceb70ca01f120db6a7ef2f60d5809233dc2205
56
+ assets/FontManifest.json,1781481118640,4d84ab517c27984d36f9a3c8be6f2a72788c0c3985c1d5874297fef0a53407ca
57
+ flutter_bootstrap.js,1781481074600,0955c783148666dcda1406de86f477f1f27ec80a4fc8186ba6cd9bd119d242f9
58
+ assets/AssetManifest.bin,1781481118640,78bccb08a36307a400711a1d7e6868bd5f89a24e63439fb5b6747660323e4475
59
+ index.html,1781481074612,bb8142eb84e9e44049957c3edf86006e1ae5f194c397a8326ce505b68fdf58f4
60
+ assets/AssetManifest.bin.json,1781481118640,50a11b51c3fc4cbed1b5ec3a4f466c6a6b75c481d08d4782c8191f99bdbdba9c
61
+ assets/shaders/stretch_effect.frag,1781481118746,1a7d4ac2be40cf0a459dfb390ef08bcd740f37913ffdee8de3c2ea836a18410e
62
+ assets/shaders/ink_sparkle.frag,1781481118746,1c8e222328206d1e06754f76fb53947aad38d62180aafad5298a3c6f510b173d
63
+ assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1781481119831,12bc6bf55aad4657f62747b6c1c9b5c120a594ed3540db21729b6db3c847340d
64
+ assets/fonts/MaterialIcons-Regular.otf,1781481119828,0d07767255068e38fcf81755fd997bb15d94d6ef83bb81bd0149f974bfe359a0
65
+ main.dart.js_3.part.js,1781481106966,7dd538a2118493eb8ff1a33ec5c2dd6d355a9a61290f1d8d1e6ad7bf8cc0465f
66
+ main.dart.js_1.part.js,1781481106961,a9d49c1a6aff2181d2d352ebe6a64a58c776ece7784ca86546b17be94ffe42b0
67
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w100.ttf,1781481119830,c16d95412fe2600bda1f53c2b8dc4bd82496f60f7c5a6ecfa180b0f866128712
68
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w400.ttf,1781481119832,edcd01ee13f6ec2285fd18863c0ce0efb0876b290fb427ad5c052f753b3210c8
69
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w600.ttf,1781481119834,008f1e974c9ff4e8d40d16221226e0d34d2298a7feb2bdea8c134a9333929c9c
70
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w500.ttf,1781481119830,3f68f958d0fa13077968a6767339c38ede8705bcadd0382078db6ec4bac96fa0
71
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w300.ttf,1781481119832,aebf5e4f6b1e6de1e61a866f813aa5b08efb4cdd108cc2831d40aad71398c88f
72
+ assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w200.ttf,1781481119830,1f798186cfc3e2fcd54b15aa7b8695bc7c29b781c5058029a8d80ecdfa615904
73
+ assets/NOTICES,1781481118640,a18efc42c5ed99b56481537d0f229ddd3add671c548a893aaf8766f30c854158
74
+ assets/packages/lucide_icons_flutter/assets/lucide.ttf,1781481119830,9b000e47cddf9ada4a6b85fc9577fef7ed2ee261c9279e9a44e4753da28f83d3
75
+ main.dart.js,1781481107454,c98fad1aa11ff6a9335379794ac025a501060a6759943e74365f185cc7a5e8ce
@@ -64,8 +64,8 @@ Typography collapses `large`+`xlarge` into one "desktop" tier (same sizes); the
64
64
  - **Native mobile** is straightforward: it's the Mobile breakpoint and never
65
65
  scales the viewport.
66
66
  - **Web render scale** (`WebViewportScale`) is a *separate* concern: Flutter web
67
- renders ~10% large, so **desktop only** scales the whole UI to `0.95`. Tablet
68
- and mobile web render at natural size, like native. This is NOT the typography
67
+ renders ~10% large at every width, so the whole web UI (phone, tablet, desktop)
68
+ is scaled to `0.93`; native renders at natural `1.0`. This is NOT the typography
69
69
  scale — don't conflate the two.
70
70
  - **Screen width follows content type** (two tiers): read/form screens (Settings,
71
71
  Notifications) are contained + centred at `kKasyContentMaxWidth` (600) — set
@@ -103,11 +103,24 @@ whole app, edit the numbers in `KasyTypeScale` — nothing else. The live ramp i
103
103
  visible under **Design System → Typography** (tabs per breakpoint).
104
104
 
105
105
  **Web render scale (`WebViewportScale`, `lib/core/web_viewport_scale.dart`).** A
106
- separate concern from type sizes: Flutter web renders ~10% larger than an
107
- equivalent HTML app, so on **desktop only** (`>= 1024`) the whole UI is scaled to
108
- `0.95` a true viewport scale, so layout, scrolling and hit-testing stay
109
- correct. Tablet and mobile web render at natural size, exactly like the native
110
- build, which never scales.
106
+ separate concern from type sizes, sitting in a layer ON TOP of the responsive
107
+ system it does NOT replace breakpoints, the type ramp or spacing (turn it off
108
+ and the app is still fully responsive, just larger). Flutter web renders ~10%
109
+ larger than an equivalent native/HTML app at 100% zoom, **at every width**. So the
110
+ whole web app — phone, tablet and desktop — is scaled to `0.93` (~7%; a true
111
+ viewport scale, so layout, scrolling and hit-testing stay correct), making web
112
+ read close to the native baseline with **no size jump across breakpoints**. `0.90`
113
+ fully undoes the 10% but reads small on a monitor (native sizes are tuned for a
114
+ phone held close), so `0.93` (a touch past the midpoint, still short of a full
115
+ undo) is the calibrated default. Two knobs:
116
+ `kWebViewportScaleEnabled` (master on/off — `false` makes web render at native
117
+ `1.0`) and `kWebViewportScale` (the value). Two things are never scaled, both at
118
+ native `1.0`: **native** itself (gated on `kIsWeb`, so iOS/Android/macOS/Windows
119
+ respect the system text-size for accessibility) and the **in-app device preview**
120
+ (it simulates a native device, so the scale is skipped while it is on — a
121
+ developer previewing a phone sees the native size). On desktop a high-OS-scale
122
+ screen compensates below the cap, keyed off the screen width (see
123
+ `kWebViewportScaleTargetWidth`).
111
124
 
112
125
  ---
113
126
 
@@ -139,7 +152,7 @@ Never pass a numeric `size:` to `Icon`; use a token so re-scaling is one edit.
139
152
  ## Radius — `KasyRadius` (`lib/core/theme/radius.dart`)
140
153
 
141
154
  Semantic: `xs 4 · sm 8 · md 12 · lg 16 · xl 24 · full 999`.
142
- `KasyCard` defaults to `lg` (16).
155
+ `KasyCard` defaults to `xl` (24).
143
156
 
144
157
  ---
145
158
 
@@ -150,7 +163,9 @@ Key tokens: `primary`, `onPrimary`, `surface`, `onSurface`, `onBackground`,
150
163
  `muted`, `fieldLabel`, `outline`, `error` / `onError`, `success`, `warning`,
151
164
  `surfacePrimarySoft`, `surfaceNeutralSoft`, `surfaceErrorSoft`.
152
165
 
153
- Brand: avocado green `#D2F51E` (accent), deep green `#011820` (dark ground).
166
+ Accent: HeroUI blue `#0485F7` (white foreground) the kit's default theme
167
+ accent. The Kasy brand identity (logo, marketing) uses avocado green `#D2F51E`
168
+ and deep green `#011820`; rebranding is a one-place token swap in `KasyColors`.
154
169
  Disabled state: blend the colour toward the surface (opaque), never raw opacity.
155
170
 
156
171
  ---
@@ -193,7 +208,7 @@ a mouse click never doubles the keyboard focus ring and the border never drops
193
208
  while the calendar is open.
194
209
 
195
210
  ### `KasyCard`
196
- Elevated surface panel, radius `KasyRadius.lg` (16). The base for screen content.
211
+ Elevated surface panel, radius `KasyRadius.xl` (24). The base for screen content.
197
212
 
198
213
  ### `KasyScreen`
199
214
  Opt-in screen scaffold. A new page wrapped in it gets the internal-screen
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="814" height="1000">
2
+ <path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/>
3
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="842.32007" height="1000.0001">
3
+ <path fill="#fff" d="M824.66636 779.30363c-15.12299 34.93724-33.02368 67.09674-53.7638 96.66374-28.27076 40.3074-51.4182 68.2078-69.25717 83.7012-27.65347 25.4313-57.2822 38.4556-89.00964 39.1963-22.77708 0-50.24539-6.4813-82.21973-19.629-32.07926-13.0861-61.55985-19.5673-88.51583-19.5673-28.27075 0-58.59083 6.4812-91.02193 19.5673-32.48053 13.1477-58.64639 19.9994-78.65196 20.6784-30.42501 1.29623-60.75123-12.0985-91.02193-40.2457-19.32039-16.8514-43.48632-45.7394-72.43607-86.6641-31.060778-43.7024-56.597041-94.37983-76.602609-152.15586C10.740416 658.44309 0 598.01283 0 539.50845c0-67.01648 14.481044-124.8172 43.486336-173.25401C66.28194 327.34823 96.60818 296.6578 134.5638 274.1276c37.95566-22.53016 78.96676-34.01129 123.1321-34.74585 24.16591 0 55.85633 7.47508 95.23784 22.166 39.27042 14.74029 64.48571 22.21538 75.54091 22.21538 8.26518 0 36.27668-8.7405 83.7629-26.16587 44.90607-16.16001 82.80614-22.85118 113.85458-20.21546 84.13326 6.78992 147.34122 39.95559 189.37699 99.70686-75.24463 45.59122-112.46573 109.4473-111.72502 191.36456.67899 63.8067 23.82643 116.90384 69.31888 159.06309 20.61664 19.56727 43.64066 34.69027 69.2571 45.4307-5.55531 16.11062-11.41933 31.54225-17.65372 46.35662zM631.70926 20.0057c0 50.01141-18.27108 96.70693-54.6897 139.92782-43.94932 51.38118-97.10817 81.07162-154.75459 76.38659-.73454-5.99983-1.16045-12.31444-1.16045-18.95003 0-48.01091 20.9006-99.39207 58.01678-141.40314 18.53027-21.27094 42.09746-38.95744 70.67685-53.0663C578.3158 9.00229 605.2903 1.31621 630.65988 0c.74076 6.68575 1.04938 13.37191 1.04938 20.00505z"/>
4
+ </svg>
@@ -0,0 +1,49 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+ <svg
5
+ version="1.1"
6
+ id="svg9"
7
+ width="666.66669"
8
+ height="666.66718"
9
+ viewBox="0 0 666.66668 666.66717"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ xmlns:svg="http://www.w3.org/2000/svg">
12
+ <defs
13
+ id="defs13">
14
+ <clipPath
15
+ clipPathUnits="userSpaceOnUse"
16
+ id="clipPath25">
17
+ <path
18
+ d="M 0,700 H 700 V 0 H 0 Z"
19
+ id="path23" />
20
+ </clipPath>
21
+ </defs>
22
+ <g
23
+ id="g17"
24
+ transform="matrix(1.3333333,0,0,-1.3333333,-133.33333,799.99999)">
25
+ <g
26
+ id="g19">
27
+ <g
28
+ id="g21"
29
+ clip-path="url(#clipPath25)">
30
+ <g
31
+ id="g27"
32
+ transform="translate(600,350)">
33
+ <path
34
+ d="m 0,0 c 0,138.071 -111.929,250 -250,250 -138.071,0 -250,-111.929 -250,-250 0,-117.245 80.715,-215.622 189.606,-242.638 v 166.242 h -51.552 V 0 h 51.552 v 32.919 c 0,85.092 38.508,124.532 122.048,124.532 15.838,0 43.167,-3.105 54.347,-6.211 V 81.986 c -5.901,0.621 -16.149,0.932 -28.882,0.932 -40.993,0 -56.832,-15.528 -56.832,-55.9 V 0 h 81.659 l -14.028,-76.396 h -67.631 V -248.169 C -95.927,-233.218 0,-127.818 0,0"
35
+ style="fill:#0866ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
36
+ id="path29" />
37
+ </g>
38
+ <g
39
+ id="g31"
40
+ transform="translate(447.9175,273.6036)">
41
+ <path
42
+ d="M 0,0 14.029,76.396 H -67.63 v 27.019 c 0,40.372 15.838,55.899 56.831,55.899 12.733,0 22.981,-0.31 28.882,-0.931 v 69.253 c -11.18,3.106 -38.509,6.212 -54.347,6.212 -83.539,0 -122.048,-39.441 -122.048,-124.533 V 76.396 h -51.552 V 0 h 51.552 v -166.242 c 19.343,-4.798 39.568,-7.362 60.394,-7.362 10.254,0 20.358,0.632 30.288,1.831 L -67.63,0 Z"
43
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
44
+ id="path33" />
45
+ </g>
46
+ </g>
47
+ </g>
48
+ </g>
49
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/><path d="M1 1h22v22H1z" fill="none"/></svg>
@@ -21,6 +21,7 @@ interface AdminUser {
21
21
  email: string | null;
22
22
  name: string | null;
23
23
  createdAt: number | null; // epoch millis
24
+ avatarPath: string | null; // public photo URL, when the user has one
24
25
  subscriber: boolean;
25
26
  }
26
27
 
@@ -91,6 +92,7 @@ export const listUsers = onCall(async (request) => {
91
92
  email: (d.get("email") as string) || null,
92
93
  name: (d.get("name") as string) || null,
93
94
  createdAt: createdAt ? createdAt.toMillis() : null,
95
+ avatarPath: (d.get("avatarPath") as string) || null,
94
96
  subscriber: activeSubscribers.has(d.id),
95
97
  };
96
98
  });