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.
- package/lib/scaffold/CHANGELOG.json +23 -0
- package/lib/scaffold/backends/api/patch/README.md +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/api/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +9 -1
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/backends/patch-base-hashes.json +6 -6
- package/lib/scaffold/backends/supabase/edge-functions/admin-list-users/index.ts +3 -1
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +3 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql +62 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +11 -6
- package/lib/scaffold/backends/supabase/patch/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -0
- package/lib/scaffold/shared/generator-utils.js +12 -6
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +23 -23
- package/templates/firebase/AGENTS.md +2 -2
- package/templates/firebase/DESIGN_SYSTEM.md +23 -8
- package/templates/firebase/assets/icons/apple_black.svg +3 -0
- package/templates/firebase/assets/icons/apple_white.svg +4 -0
- package/templates/firebase/assets/icons/facebook.svg +49 -0
- package/templates/firebase/assets/icons/google.svg +1 -0
- package/templates/firebase/functions/src/admin/functions.ts +2 -0
- package/templates/firebase/functions/src/authentication/functions.ts +13 -7
- package/templates/firebase/functions/src/notifications/triggers.ts +6 -2
- package/templates/firebase/lib/components/components.dart +5 -2
- package/templates/firebase/lib/components/kasy_app_bar.dart +325 -15
- package/templates/firebase/lib/components/kasy_card.dart +4 -0
- package/templates/firebase/lib/components/kasy_drop_down.dart +584 -0
- package/templates/firebase/lib/components/kasy_sidebar.dart +18 -6
- package/templates/firebase/lib/components/kasy_tabs.dart +31 -10
- package/templates/firebase/lib/components/kasy_text_field.dart +29 -7
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +27 -18
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +34 -16
- package/templates/firebase/lib/core/chrome/app_bar_config.dart +214 -0
- package/templates/firebase/lib/core/chrome/app_bar_scope.dart +102 -0
- package/templates/firebase/lib/core/data/api/user_api.dart +11 -0
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +55 -15
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +95 -30
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +11 -0
- package/templates/firebase/lib/core/states/logout_action.dart +11 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +28 -1
- package/templates/firebase/lib/core/theme/texts.dart +21 -6
- package/templates/firebase/lib/core/theme/type_scale.dart +34 -15
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +51 -19
- package/templates/firebase/lib/core/web_viewport_scale.dart +66 -36
- package/templates/firebase/lib/core/widgets/kasy_pressable_depth.dart +14 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_composer.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +52 -35
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +1 -1
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +18 -8
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/signup_page.dart +11 -61
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +7 -5
- package/templates/firebase/lib/features/authentication/ui/widgets/social_auth_tile.dart +83 -0
- package/templates/firebase/lib/features/feedbacks/ui/widgets/feature_card.dart +4 -4
- package/templates/firebase/lib/features/home/home_components_page.dart +253 -125
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +263 -59
- package/templates/firebase/lib/features/home/home_feed.dart +2 -2
- package/templates/firebase/lib/features/home/home_image_grid.dart +3 -3
- package/templates/firebase/lib/features/local_reminders/providers/reminder_notifier.dart +8 -1
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +111 -57
- package/templates/firebase/lib/features/notifications/api/device_api.dart +11 -3
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -4
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +7 -56
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +5 -5
- package/templates/firebase/lib/features/notifications/ui/widgets/push_permission_banner.dart +163 -0
- package/templates/firebase/lib/features/notifications/ui/widgets/web_notifications_bell.dart +2 -2
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +9 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +28 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +51 -12
- package/templates/firebase/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +90 -32
- package/templates/firebase/lib/features/settings/settings_page.dart +53 -32
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_home_widgets.dart +4 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +895 -111
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_api.dart +7 -0
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +445 -233
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +171 -41
- package/templates/firebase/lib/features/settings/ui/components/avatar_component.dart +1 -1
- package/templates/firebase/lib/features/settings/ui/components/delete_user_component.dart +9 -1
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +48 -47
- package/templates/firebase/lib/features/settings/ui/widgets/settings_bottom_sheet_option_tile.dart +21 -18
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +25 -10
- package/templates/firebase/lib/i18n/en.i18n.json +753 -712
- package/templates/firebase/lib/i18n/es.i18n.json +753 -712
- package/templates/firebase/lib/i18n/pt.i18n.json +753 -712
- package/templates/firebase/lib/main.dart +20 -7
- package/templates/firebase/lib/router.dart +32 -26
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/test/admin_shell_chrome_test.dart +11 -5
- package/templates/firebase/test/app_bar_config_test.dart +70 -0
- package/templates/firebase/test/components/kasy_text_field_height_test.dart +77 -0
- package/templates/firebase/test/core/web_viewport_scale_test.dart +23 -16
- package/templates/firebase/tool/design_check.dart +9 -0
- package/templates/firebase/assets/icons/apple.png +0 -0
- package/templates/firebase/assets/icons/facebook.png +0 -0
- package/templates/firebase/assets/icons/google.png +0 -0
- package/templates/firebase/assets/icons/google_play_games.png +0 -0
- package/templates/firebase/lib/components/kasy_web_header.dart +0 -218
- package/templates/firebase/lib/core/chrome/web_header_scope.dart +0 -20
- package/templates/firebase/lib/features/authentication/ui/components/apple_signin.dart +0 -19
- package/templates/firebase/lib/features/authentication/ui/components/google_signin.dart +0 -32
- package/templates/firebase/lib/features/authentication/ui/widgets/round_signin.dart +0 -73
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -66
- package/templates/firebase/lib/features/notifications/ui/components/notification_settings_sheet.dart +0 -179
- 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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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,
|
|
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
|
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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++;
|
package/lib/scaffold/backends/supabase/migrations/20240101000012_welcome_decouple_from_push.sql
ADDED
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
}
|
|
@@ -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
|
|
1385
|
-
// in
|
|
1386
|
-
//
|
|
1387
|
-
// label so the Google/Apple tiles are never touched. Runs before dartFix/format,
|
|
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:
|
|
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
|
@@ -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,
|
|
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,
|
|
64
|
-
flutter_service_worker.js,
|
|
65
|
-
assets/FontManifest.json,
|
|
66
|
-
|
|
67
|
-
assets/AssetManifest.bin,
|
|
68
|
-
|
|
69
|
-
assets/
|
|
70
|
-
assets/shaders/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
main.dart.js,
|
|
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
|
|
68
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
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
|
});
|