kasy-cli 1.36.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/scaffold/CHANGELOG.json +28 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +1 -1
- package/lib/scaffold/backends/patch-base-hashes.json +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +3 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +1 -1
- package/lib/scaffold/shared/generator-utils.js +8 -0
- package/package.json +1 -1
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +24 -22
- package/templates/firebase/firebase.json +1 -1
- package/templates/firebase/lib/components/kasy_sidebar.dart +33 -36
- package/templates/firebase/lib/components/kasy_tabs.dart +25 -3
- package/templates/firebase/lib/core/states/user_state_notifier.dart +18 -2
- package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +13 -0
- package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +17 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +3 -0
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +22 -6
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +58 -36
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +8 -2
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +15 -4
- package/templates/firebase/lib/features/settings/settings_page.dart +0 -1
- package/templates/firebase/lib/main.dart +7 -0
- package/templates/firebase/pubspec.yaml +2 -2
- package/templates/firebase/test/features/notifications/ui/notifications_page_test.dart +2 -2
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.37.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"components": {
|
|
5
|
+
"pt": "Sidebar e abas mais responsivas: a sidebar ganhou um modo de colapso mais consistente (vira rail fino conforme a largura) e teve a busca não usada removida pra ficar mais enxuta; as abas (KasyTabs) agora se medem e se adaptam melhor em telas estreitas, sem cortar nem espremer.",
|
|
6
|
+
"en": "More responsive sidebar and tabs: the sidebar got a more consistent collapse mode (turns into a thin rail by width) and had its unused search removed for a leaner component; tabs (KasyTabs) now measure and adapt better on narrow screens, without clipping or squeezing.",
|
|
7
|
+
"es": "Sidebar y pestañas más responsivas: la sidebar tiene un modo de colapso más consistente (se vuelve un rail fino según el ancho) y se le quitó la búsqueda sin uso para un componente más limpio; las pestañas (KasyTabs) ahora se miden y adaptan mejor en pantallas estrechas, sin recortar ni apretar."
|
|
8
|
+
},
|
|
9
|
+
"web": {
|
|
10
|
+
"pt": "Web mais redonda: login social na web agora trata o fluxo por redirecionamento (redirect), útil quando o popup é bloqueado pelo navegador; e o Firebase Hosting foi ajustado pra publicar o build/web sem descartar arquivos ocultos necessários, deixando o deploy do site mais previsível.",
|
|
11
|
+
"en": "Smoother web: social login on web now handles the redirect flow, useful when the browser blocks the popup; and Firebase Hosting was tuned to publish build/web without dropping needed hidden files, making the site deploy more predictable.",
|
|
12
|
+
"es": "Web más pulida: el inicio de sesión social en web ahora maneja el flujo por redirección (redirect), útil cuando el navegador bloquea el popup; y Firebase Hosting se ajustó para publicar build/web sin descartar archivos ocultos necesarios, dejando el deploy del sitio más predecible."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"1.36.1": {
|
|
17
|
+
"modules": {
|
|
18
|
+
"onboarding": {
|
|
19
|
+
"pt": "Onboarding mais resistente a falhas: se a escrita no backend falhar (por exemplo, o documento do usuário ainda não foi criado logo após o cadastro), o loader não trava mais. O onboarding sempre conclui e o estado fica salvo localmente, e uma resposta que não salvou é registrada no log sem interromper a experiência.",
|
|
20
|
+
"en": "More resilient onboarding: if a backend write fails (e.g. the user document isn't created yet right after sign-up), the loader no longer gets stuck. Onboarding always completes with the state saved locally, and an answer that failed to save is logged without interrupting the flow.",
|
|
21
|
+
"es": "Onboarding más resistente a fallos: si una escritura en el backend falla (por ejemplo, el documento del usuario aún no se creó justo tras el registro), el loader ya no se queda atascado. El onboarding siempre concluye con el estado guardado localmente, y una respuesta que no se guardó se registra en el log sin interrumpir la experiencia."
|
|
22
|
+
},
|
|
23
|
+
"components": {
|
|
24
|
+
"pt": "Sidebar: a variante \"collapsed rail\" agora fica fina de verdade em tela estreita (mobile) no showcase, em vez de abrir larga. Só a gaveta explícita (isDrawer) força a abertura larga. O app real não muda (a sidebar só aparece de tablet pra cima).",
|
|
25
|
+
"en": "Sidebar: the \"collapsed rail\" variant now actually stays thin on narrow (mobile) widths in the showcase, instead of opening wide. Only the explicit drawer (isDrawer) forces the wide open. The real app is unchanged (the sidebar only shows from tablet up).",
|
|
26
|
+
"es": "Sidebar: la variante \"collapsed rail\" ahora se mantiene fina de verdad en anchos estrechos (móvil) en el showcase, en vez de abrirse ancha. Solo el cajón explícito (isDrawer) fuerza la apertura ancha. La app real no cambia (la sidebar solo aparece de tablet en adelante)."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
2
30
|
"1.36.0": {
|
|
3
31
|
"modules": {
|
|
4
32
|
"components": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"api/lib/features/ai_chat/api/ai_chat_message_entity.dart": "5aca4fdb5b1c38df04c9d377c6e6eeb6930ef85b2a32689fe6403e7cc7645373",
|
|
12
12
|
"api/lib/features/ai_chat/providers/ai_chat_notifier.dart": "f16ea3f3266e63a5591ceff9bc1900845f0b96e9bbbc5463ec6ab894f878eae3",
|
|
13
13
|
"api/lib/features/authentication/api/authentication_api_interface.dart": "ef9219237babd73e4b3ac148a24ebbbdf39ab98166d21f578c359074b528e5da",
|
|
14
|
-
"api/lib/features/authentication/api/authentication_api.dart": "
|
|
14
|
+
"api/lib/features/authentication/api/authentication_api.dart": "4ee563f9fd81b9738b71928edc146846e6226fb3b8a6974cf1dc3f3cd8813d4d",
|
|
15
15
|
"api/lib/features/authentication/repositories/authentication_repository.dart": "8e98ca0d39df3aac1dab159e6bdfe8d2bbb8e43931459984eb4ecfb676698ca2",
|
|
16
16
|
"api/lib/features/feedbacks/api/entities/feature_request_entity.dart": "e72c683c7321bf1f3c19fc1c6d54c6964bd99970e87c9ff31d8918efa93f0090",
|
|
17
17
|
"api/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"supabase/lib/features/ai_chat/api/ai_chat_conversation_entity.dart": "1c4e4223e802d7fe8e93d9e7a09fcc6be81a41792d722e8b7e697837cde4737f",
|
|
43
43
|
"supabase/lib/features/ai_chat/api/ai_chat_message_entity.dart": "5aca4fdb5b1c38df04c9d377c6e6eeb6930ef85b2a32689fe6403e7cc7645373",
|
|
44
44
|
"supabase/lib/features/ai_chat/providers/ai_chat_notifier.dart": "f16ea3f3266e63a5591ceff9bc1900845f0b96e9bbbc5463ec6ab894f878eae3",
|
|
45
|
-
"supabase/lib/features/authentication/api/authentication_api.dart": "
|
|
45
|
+
"supabase/lib/features/authentication/api/authentication_api.dart": "4ee563f9fd81b9738b71928edc146846e6226fb3b8a6974cf1dc3f3cd8813d4d",
|
|
46
46
|
"supabase/lib/features/authentication/repositories/authentication_repository.dart": "8e98ca0d39df3aac1dab159e6bdfe8d2bbb8e43931459984eb4ecfb676698ca2",
|
|
47
47
|
"supabase/lib/features/feedbacks/api/entities/feature_request_entity.dart": "e72c683c7321bf1f3c19fc1c6d54c6964bd99970e87c9ff31d8918efa93f0090",
|
|
48
48
|
"supabase/lib/features/feedbacks/api/entities/feature_vote_entity.dart": "a13ccfc493d8f5277beffc8e28f77ec322ab0c0a56358c3669353206b7c6756b",
|
package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart
CHANGED
|
@@ -67,6 +67,9 @@ class SupabaseAuthenticationApi implements AuthenticationApi {
|
|
|
67
67
|
/// — the caller stays in its loading state until the browser leaves.
|
|
68
68
|
/// See [isMobileWebBrowser].
|
|
69
69
|
Future<Credentials> _googleSignInWithRedirectWeb() async {
|
|
70
|
+
// Flag the login so startup lands on Home, not the stale tab URL the
|
|
71
|
+
// redirect returns to (linking is a no-op on Supabase, so only login here).
|
|
72
|
+
markWebRedirectLogin();
|
|
70
73
|
await fb_auth.FirebaseAuth.instance.signInWithRedirect(
|
|
71
74
|
fb_auth.GoogleAuthProvider(),
|
|
72
75
|
);
|
|
@@ -975,6 +975,8 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
|
|
|
975
975
|
lines.push(`import 'package:${pkg}/firebase_options_dev.dart' as firebase_dev;`);
|
|
976
976
|
}
|
|
977
977
|
lines.push(`import 'package:${pkg}/i18n/translations.g.dart';`);
|
|
978
|
+
lines.push(`import 'package:${pkg}/features/authentication/api/auth_web_support.dart'`);
|
|
979
|
+
lines.push(` if (dart.library.js_interop) 'package:${pkg}/features/authentication/api/auth_web_support_web.dart';`);
|
|
978
980
|
lines.push(`import 'package:${pkg}/features/authentication/api/authentication_api.dart';`);
|
|
979
981
|
lines.push(`import 'package:${pkg}/features/notifications/api/local_notifier.dart';`);
|
|
980
982
|
if (withFirebase) {
|
|
@@ -1063,6 +1065,12 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
|
|
|
1063
1065
|
lines.push('');
|
|
1064
1066
|
}
|
|
1065
1067
|
|
|
1068
|
+
lines.push(` // Web: if we just returned from a social-login redirect, reset the address`);
|
|
1069
|
+
lines.push(` // bar to '/' BEFORE the router reads the initial URL, so the app boots on Home`);
|
|
1070
|
+
lines.push(` // instead of the stale tab URL (e.g. /settings) the redirect returned to.`);
|
|
1071
|
+
lines.push(` resetUrlAfterRedirectLogin();`);
|
|
1072
|
+
lines.push('');
|
|
1073
|
+
|
|
1066
1074
|
if (withSentry) {
|
|
1067
1075
|
lines.push(` // initialize sentry for error reporting in production only`);
|
|
1068
1076
|
lines.push(` // run the app with Sentry for production environment`);
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
stripe_success.html,1781195415353,677b2fb430839866f32a33a751aae85b4c4819123be820f3f5b2f689b8eed00e
|
|
2
2
|
manifest.json,1779810782774,1b929ac7267ff738d05646106a0fac3591082b6f365051928a62518af30b3fd5
|
|
3
3
|
local_notifications.js,1772398909034,0b44b177c4b8a705ad1b0e3d7532537aad7b230e9472f93c820742b7548797b8
|
|
4
|
+
index.html,1781388256310,bb8142eb84e9e44049957c3edf86006e1ae5f194c397a8326ce505b68fdf58f4
|
|
5
|
+
flutter_bootstrap.js,1781388256056,25f49550b20f234b84194d9e067725abdc33590343251086f6c7d147325dfb41
|
|
4
6
|
flutter.js,1777393608000,b42e549a02f5c1428b0ad85dbdc663d400a6d2db10cb8aefcc0fddd592fd6ceb
|
|
5
7
|
firebase-messaging-sw.js,1772484605561,dba3c939f7c240e62f63d06408d5f70e45f1168d00df98726e6a7a20cbd3b0df
|
|
6
8
|
favicon.png,1779810782545,c967afd3c3d4dd165ef2e5cf3f203c3982dfff6a95881859ed163d9c38d75512
|
|
9
|
+
.last_build_id,1781388166126,fb2a0536078d7023bcc9d41eb14cc00d74532f9465fac7aa9b88dae42e4a249c
|
|
7
10
|
splash/img/light-4x.png,1780546027751,abf0e55345e532a4645fe14b01c2948b07c78df50cc6da23648837dfb6271c74
|
|
8
11
|
splash/img/light-3x.png,1780546027699,5f02c94032cb7053807ce89603b109f14ea2740eaa3145dc76c924cbe1d534a9
|
|
9
12
|
splash/img/light-2x.png,1780546027647,3c073b9da154ac8be530f8beefa3579246f223acd819e856a2db000fdabcb67e
|
|
@@ -31,16 +34,8 @@ canvaskit/canvaskit.js,1777393746000,df5aa63d90b0491908ad19d64fd18f29db1dc416548
|
|
|
31
34
|
canvaskit/chromium/canvaskit.wasm,1777393830000,44d29356b9e2c903751a8aa583716275e165d1fb56eea46963c95c7c43f7ca34
|
|
32
35
|
canvaskit/chromium/canvaskit.js.symbols,1777393830000,60722421e56edacbeb674cc3ef0f6152969067e74b51a960c80cd36e75b75a54
|
|
33
36
|
canvaskit/chromium/canvaskit.js,1777393830000,10ea3badcf26e29aba851699c03e09bc35fea65cdc440a035bac263d6d02d665
|
|
37
|
+
assets/.env,1781370223435,777e85eb4de226724b28db8664404d0b73ae37f63fd66c955ac90c89cd491e7e
|
|
34
38
|
assets/packages/mixpanel_flutter/assets/mixpanel.js,1779806094332,1915455df317dd1a9f42c6f3ea5bbe3ee357614f89d994f157a2e821f37956c3
|
|
35
|
-
assets/packages/lucide_icons_flutter/assets/lucide.ttf,1779806149220,bfca5b519d438d7194550cbc60a4e7c10f53a36ac96d48c7e17503a45b6b7b0b
|
|
36
|
-
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w600.ttf,1779806149210,cf2218879264e413687602c192e43b86000404ff85e2bca933d9a67020299ed4
|
|
37
|
-
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w500.ttf,1779806149217,587ce31612aa88e6c019210a18625531557eda111f5f8e838a298277efc9e308
|
|
38
|
-
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w400.ttf,1779806149212,644f7ccdf1b74bdb898210c4171327a25d7476690c7ade2d3082dd473bc24fe1
|
|
39
|
-
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w300.ttf,1779806149214,5dd834d2d84e5a1524e027fa21d87c6e8aaaf6421f201be2ec61e57c0b7bce72
|
|
40
|
-
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w200.ttf,1779806149208,d929819511975bf5030c0f359e8c16d21ad14b20ac24d5f2ff238462114b1d9b
|
|
41
|
-
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w100.ttf,1779806149216,1e9e6ea39a38b874c40242d02533adac9dd3d09c8119dae55458e65c6c3c16e6
|
|
42
|
-
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779806141382,010d647b6cab4e1a9b4398957353f43589903ce8dc760d05d894944b664e29bd
|
|
43
|
-
assets/fonts/MaterialIcons-Regular.otf,1662569016000,3a631b0761788d7cf2a71e8732a5b31288d6e7aa996c671c70d53a251bbd3848
|
|
44
39
|
assets/assets/images/splash_logo_light_android12.png,1779867497377,da2caee4f16230230dbb04d84913f0a9df2eae6bcb8f9d1ce83ed556cee6d094
|
|
45
40
|
assets/assets/images/splash_logo_light.png,1779867497286,643a1f45d6ef9458c4c0eed9f4889cc2adbdf1db616913215be66673a05a0066
|
|
46
41
|
assets/assets/images/splash_logo_dark_android12.png,1779867497464,b237d9f9035d419525a10b32889c30d44c706d62990b30bdc6bef911fe9352d6
|
|
@@ -58,16 +53,23 @@ assets/assets/icons/google_play_games.png,1772398653083,533c1d87d7be8690ab173ef4
|
|
|
58
53
|
assets/assets/icons/google.png,1772398653083,f423e7e7be1e06008d45617d07f095f04da7fdcab9a56523f9e0633828e464e0
|
|
59
54
|
assets/assets/icons/facebook.png,1772398653083,79ac67e449c1db63319d43329ca91f682b4e0bc6a0883b0dfb2a849e13ae6eb6
|
|
60
55
|
assets/assets/icons/apple.png,1772398653083,2a737d8801ca81452b2978bb2e1ac72136acf1a4c338f2de2f77b65e9f6de1fa
|
|
61
|
-
version.json,
|
|
62
|
-
flutter_service_worker.js,
|
|
63
|
-
assets/
|
|
64
|
-
assets/AssetManifest.bin.json,
|
|
65
|
-
assets/
|
|
66
|
-
assets/shaders/ink_sparkle.frag,
|
|
67
|
-
assets/shaders/stretch_effect.frag,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
main.dart.
|
|
71
|
-
main.dart.
|
|
72
|
-
assets/
|
|
73
|
-
|
|
56
|
+
version.json,1781388383201,55f3aab1f3d71610bf791902e59e92cd77db8963f8290193f573c4172fa6fd32
|
|
57
|
+
flutter_service_worker.js,1781388386040,baeeaf9f4b8e6f40d3b0549429ceb70ca01f120db6a7ef2f60d5809233dc2205
|
|
58
|
+
assets/AssetManifest.bin,1781388383326,78bccb08a36307a400711a1d7e6868bd5f89a24e63439fb5b6747660323e4475
|
|
59
|
+
assets/AssetManifest.bin.json,1781388383326,50a11b51c3fc4cbed1b5ec3a4f466c6a6b75c481d08d4782c8191f99bdbdba9c
|
|
60
|
+
assets/FontManifest.json,1781388383326,4d84ab517c27984d36f9a3c8be6f2a72788c0c3985c1d5874297fef0a53407ca
|
|
61
|
+
assets/shaders/ink_sparkle.frag,1781388383423,1c8e222328206d1e06754f76fb53947aad38d62180aafad5298a3c6f510b173d
|
|
62
|
+
assets/shaders/stretch_effect.frag,1781388383423,1a7d4ac2be40cf0a459dfb390ef08bcd740f37913ffdee8de3c2ea836a18410e
|
|
63
|
+
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1781388385468,12bc6bf55aad4657f62747b6c1c9b5c120a594ed3540db21729b6db3c847340d
|
|
64
|
+
assets/fonts/MaterialIcons-Regular.otf,1781388385471,89da9939a222284d9e14e9757b4dbdb75965434cb84b0f2969495153f3a018c9
|
|
65
|
+
main.dart.js_3.part.js,1781388360492,6f52d11c8e140736cd046ce6b541ad8d8bf20e6ef709f39d8ef4eb3dddbece8b
|
|
66
|
+
main.dart.js_1.part.js,1781388360481,42136799a40b5acb14b5acf0a8cef39bbef13b4dc99ce808a7bc8a1f8cdd8b09
|
|
67
|
+
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w300.ttf,1781388385457,aebf5e4f6b1e6de1e61a866f813aa5b08efb4cdd108cc2831d40aad71398c88f
|
|
68
|
+
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w400.ttf,1781388385463,edcd01ee13f6ec2285fd18863c0ce0efb0876b290fb427ad5c052f753b3210c8
|
|
69
|
+
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w600.ttf,1781388385468,008f1e974c9ff4e8d40d16221226e0d34d2298a7feb2bdea8c134a9333929c9c
|
|
70
|
+
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w100.ttf,1781388385446,c16d95412fe2600bda1f53c2b8dc4bd82496f60f7c5a6ecfa180b0f866128712
|
|
71
|
+
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w500.ttf,1781388385464,3f68f958d0fa13077968a6767339c38ede8705bcadd0382078db6ec4bac96fa0
|
|
72
|
+
assets/packages/lucide_icons_flutter/assets/lucide.ttf,1781388385448,9b000e47cddf9ada4a6b85fc9577fef7ed2ee261c9279e9a44e4753da28f83d3
|
|
73
|
+
assets/packages/lucide_icons_flutter/assets/build_font/LucideVariable-w200.ttf,1781388385451,1f798186cfc3e2fcd54b15aa7b8695bc7c29b781c5058029a8d80ecdfa615904
|
|
74
|
+
assets/NOTICES,1781388383326,a18efc42c5ed99b56481537d0f229ddd3add671c548a893aaf8766f30c854158
|
|
75
|
+
main.dart.js,1781388361400,b625f1b346dc2f1475ee99ebb1d698654e74183d737c91e1bbac020869096a84
|
|
@@ -192,6 +192,22 @@ const _NavItem _kHelpItem = _NavItem(
|
|
|
192
192
|
/// Controls which edge receives the content-facing hairline + shadow nudge.
|
|
193
193
|
enum KasySidebarSide { left, right }
|
|
194
194
|
|
|
195
|
+
/// How the rail picks its initial width — the standard SaaS sidebar presets.
|
|
196
|
+
enum KasySidebarCollapseMode {
|
|
197
|
+
/// Adapts to the viewport: wide on desktop, auto-collapses to a thin icon
|
|
198
|
+
/// rail on tablet and mobile. The toggle overrides it on any breakpoint. This
|
|
199
|
+
/// is the default and what a typical app navigation uses.
|
|
200
|
+
responsive,
|
|
201
|
+
|
|
202
|
+
/// Starts wide on every breakpoint (the "pinned" sidebar); the user collapses
|
|
203
|
+
/// it to the thin rail via the toggle. Stays wide on mobile too.
|
|
204
|
+
expanded,
|
|
205
|
+
|
|
206
|
+
/// Starts as the thin icon rail (with hover tooltips); the user expands it via
|
|
207
|
+
/// the toggle.
|
|
208
|
+
collapsed,
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
/// A SaaS-style sidebar modelled on the HeroUI Figma kit: brand logo + panel
|
|
196
212
|
/// toggle, a workspace selector, a segmented control, a navigable list with an
|
|
197
213
|
/// active pill, and a pinned ⌘K search row. Collapses to an icon rail (with
|
|
@@ -203,9 +219,8 @@ class KasySidebar extends StatefulWidget {
|
|
|
203
219
|
super.key,
|
|
204
220
|
this.onSettingsTap,
|
|
205
221
|
this.onLogout,
|
|
206
|
-
this.
|
|
222
|
+
this.collapseMode = KasySidebarCollapseMode.responsive,
|
|
207
223
|
this.isDrawer = false,
|
|
208
|
-
this.showSearch = false,
|
|
209
224
|
this.side = KasySidebarSide.left,
|
|
210
225
|
this.routes,
|
|
211
226
|
this.onTapItem,
|
|
@@ -250,19 +265,15 @@ class KasySidebar extends StatefulWidget {
|
|
|
250
265
|
/// (confirm dialog + sign-out). When null, the Logout row does nothing real.
|
|
251
266
|
final VoidCallback? onLogout;
|
|
252
267
|
|
|
253
|
-
///
|
|
254
|
-
|
|
268
|
+
/// How the rail picks its initial width (responsive / pinned-expanded /
|
|
269
|
+
/// collapsed). See [KasySidebarCollapseMode].
|
|
270
|
+
final KasySidebarCollapseMode collapseMode;
|
|
255
271
|
|
|
256
272
|
/// Present the rail as a slide-in drawer (the mobile pattern): it always opens
|
|
257
273
|
/// wide and hides the collapse toggle, regardless of viewport width. Use when
|
|
258
274
|
/// opening the sidebar from an app-bar menu button on a phone.
|
|
259
275
|
final bool isDrawer;
|
|
260
276
|
|
|
261
|
-
/// Whether to pin a ⌘K search row above the footer in connected mode. Opt-in
|
|
262
|
-
/// (off by default) so the live app navigation stays lean; flip it on when the
|
|
263
|
-
/// sidebar should double as a command/search entry point.
|
|
264
|
-
final bool showSearch;
|
|
265
|
-
|
|
266
277
|
/// The screen edge this sidebar is anchored to.
|
|
267
278
|
final KasySidebarSide side;
|
|
268
279
|
|
|
@@ -302,18 +313,6 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
302
313
|
/// Viewport width below which the sidebar auto-collapses (tablet breakpoint).
|
|
303
314
|
static const double _kBreakpoint = 1024.0;
|
|
304
315
|
|
|
305
|
-
/// Below this (mobile), the rail is meant to be a wide drawer: it always opens
|
|
306
|
-
/// full width and hides the collapse toggle (thinning makes no sense on a
|
|
307
|
-
/// phone-width sheet). The collapse affordance only exists from tablet up.
|
|
308
|
-
static const double _kMobileBreakpoint = 768.0;
|
|
309
|
-
|
|
310
|
-
bool _isMobile(BuildContext context) =>
|
|
311
|
-
MediaQuery.sizeOf(context).width < _kMobileBreakpoint;
|
|
312
|
-
|
|
313
|
-
/// Drawer presentation — always wide, no collapse toggle. True on a phone-
|
|
314
|
-
/// width viewport or when explicitly opened as a drawer ([isDrawer]).
|
|
315
|
-
bool _wideDrawer(BuildContext context) =>
|
|
316
|
-
widget.isDrawer || _isMobile(context);
|
|
317
316
|
|
|
318
317
|
/// True when wired to Bart's navigation (real, tappable screens).
|
|
319
318
|
bool get _connected =>
|
|
@@ -325,7 +324,11 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
325
324
|
@override
|
|
326
325
|
void initState() {
|
|
327
326
|
super.initState();
|
|
328
|
-
_collapsePreference = widget.
|
|
327
|
+
_collapsePreference = switch (widget.collapseMode) {
|
|
328
|
+
KasySidebarCollapseMode.responsive => null,
|
|
329
|
+
KasySidebarCollapseMode.expanded => false,
|
|
330
|
+
KasySidebarCollapseMode.collapsed => true,
|
|
331
|
+
};
|
|
329
332
|
// Connected mode follows Bart's currentItem (empty highlight here); the
|
|
330
333
|
// showcase defaults to the active layer from the Figma reference.
|
|
331
334
|
_activeItemId = _connected ? '' : 'object2';
|
|
@@ -382,9 +385,10 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
382
385
|
|
|
383
386
|
@override
|
|
384
387
|
Widget build(BuildContext context) {
|
|
385
|
-
//
|
|
386
|
-
// explicit choice, falling back to the
|
|
387
|
-
|
|
388
|
+
// Only an explicit drawer ([isDrawer]) forces wide; everything else honours
|
|
389
|
+
// the user's explicit choice, falling back to the auto-collapse on narrow
|
|
390
|
+
// viewports. Mobile is NOT forced wide — a collapsed config stays thin there.
|
|
391
|
+
_collapsed = !widget.isDrawer &&
|
|
388
392
|
(_collapsePreference ?? _isViewportNarrow(context));
|
|
389
393
|
|
|
390
394
|
final c = _colors;
|
|
@@ -580,15 +584,6 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
580
584
|
child: Column(
|
|
581
585
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
582
586
|
children: [
|
|
583
|
-
if (widget.showSearch)
|
|
584
|
-
_buildItemRow(
|
|
585
|
-
c,
|
|
586
|
-
icon: KasyIcons.search,
|
|
587
|
-
label: 'Search',
|
|
588
|
-
isActive: false,
|
|
589
|
-
onTap: () {},
|
|
590
|
-
trailing: [_buildKbd(c)],
|
|
591
|
-
),
|
|
592
587
|
_buildNavItem(context, _kHelpItem, c),
|
|
593
588
|
_buildItemRow(
|
|
594
589
|
c,
|
|
@@ -622,8 +617,10 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
622
617
|
MediaQuery.sizeOf(context).width >= _kBreakpoint
|
|
623
618
|
? _kTopBandHeight
|
|
624
619
|
: kasyAppBarBodyTopOverlap(context);
|
|
625
|
-
//
|
|
626
|
-
|
|
620
|
+
// The collapse toggle is available on every breakpoint so any config can be
|
|
621
|
+
// switched thin↔wide — except a drawer, which is a dismissible overlay you
|
|
622
|
+
// close whole rather than collapse in place.
|
|
623
|
+
final bool showToggle = !widget.isDrawer;
|
|
627
624
|
final bool anchoredLeft = widget.side == KasySidebarSide.left;
|
|
628
625
|
// Brand wordmark — same artwork as the splash screen.
|
|
629
626
|
final Widget logo = Image.asset(
|
|
@@ -125,6 +125,14 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
125
125
|
// Whether we have a valid measurement yet.
|
|
126
126
|
bool _measured = false;
|
|
127
127
|
|
|
128
|
+
// Last layout width seen. The pill/underline geometry is measured from the
|
|
129
|
+
// laid-out tab boxes, so it must be recomputed whenever the bar's own width
|
|
130
|
+
// changes — e.g. when a collapsing sidebar animates the tabs open, the first
|
|
131
|
+
// measure happens at the narrow start width and would otherwise stay stuck in
|
|
132
|
+
// the corner until the next selection. Re-measuring per resize lets the
|
|
133
|
+
// indicator track the animation smoothly.
|
|
134
|
+
double? _lastLayoutWidth;
|
|
135
|
+
|
|
128
136
|
@override
|
|
129
137
|
void initState() {
|
|
130
138
|
super.initState();
|
|
@@ -241,9 +249,23 @@ class _KasyTabsState extends State<KasyTabs> {
|
|
|
241
249
|
Widget build(BuildContext context) {
|
|
242
250
|
if (widget.items.isEmpty) return const SizedBox.shrink();
|
|
243
251
|
|
|
244
|
-
return
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
return LayoutBuilder(
|
|
253
|
+
builder: (context, constraints) {
|
|
254
|
+
final double width = constraints.maxWidth;
|
|
255
|
+
// Re-measure on every width change (e.g. a sidebar animating open) so
|
|
256
|
+
// the indicator follows instead of sticking at its first-frame spot.
|
|
257
|
+
if (_lastLayoutWidth == null ||
|
|
258
|
+
(_lastLayoutWidth! - width).abs() > 0.5) {
|
|
259
|
+
_lastLayoutWidth = width;
|
|
260
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
261
|
+
if (mounted) _measure();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return widget.variant == KasyTabsVariant.primary
|
|
265
|
+
? _buildPrimary(context)
|
|
266
|
+
: _buildSecondary(context);
|
|
267
|
+
},
|
|
268
|
+
);
|
|
247
269
|
}
|
|
248
270
|
|
|
249
271
|
// ── Primary (pill indicator) ───────────────────────────────────────────────
|
|
@@ -108,8 +108,24 @@ class UserStateNotifier extends _$UserStateNotifier implements OnStartService {
|
|
|
108
108
|
state = state.copyWith(user: const User.anonymous(onboarded: true));
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
try {
|
|
112
|
+
final newUser = await _userRepository.setOnboarded(state.user);
|
|
113
|
+
state = state.copyWith(user: newUser);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// The user document may not exist yet: the backend's onUserRegistration
|
|
116
|
+
// trigger runs asynchronously and can lag a just-created anonymous account
|
|
117
|
+
// (Firestore update() throws on a missing doc). Onboarding is already
|
|
118
|
+
// remembered locally (flag above), so reflect it in state and move on
|
|
119
|
+
// instead of letting this bubble up and trap the onboarding loader.
|
|
120
|
+
_logger.w('setOnboarded failed, keeping local onboarded state: $e');
|
|
121
|
+
state = state.copyWith(
|
|
122
|
+
user: switch (state.user) {
|
|
123
|
+
final AuthenticatedUserData u => u.copyWith(onboarded: true),
|
|
124
|
+
final AnonymousUserData u => u.copyWith(onboarded: true),
|
|
125
|
+
final LoadingUserData _ => state.user,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
}
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
/// Finish onboarding (or "continue as guest" from the sign-in screen) by
|
|
@@ -10,3 +10,16 @@
|
|
|
10
10
|
/// Non-web stub: always false. The real implementation lives in
|
|
11
11
|
/// `auth_web_support_web.dart` and is selected via conditional import on web.
|
|
12
12
|
bool isMobileWebBrowser() => false;
|
|
13
|
+
|
|
14
|
+
/// Marks that a full-page redirect *sign-in* (not provider linking) is starting.
|
|
15
|
+
/// Persisted across the redirect's page reload so [resetUrlAfterRedirectLogin]
|
|
16
|
+
/// can tell, at startup, that we returned from a login (and must land on Home)
|
|
17
|
+
/// rather than from a link (which stays on the originating screen, e.g. Settings).
|
|
18
|
+
/// Non-web stub: no-op.
|
|
19
|
+
void markWebRedirectLogin() {}
|
|
20
|
+
|
|
21
|
+
/// If we just returned from a redirect sign-in (see [markWebRedirectLogin]),
|
|
22
|
+
/// clear the marker and reset the address bar to `/` so the app boots on Home
|
|
23
|
+
/// instead of whatever stale tab URL the redirect returned to. MUST run before
|
|
24
|
+
/// the router reads the initial URL. Non-web stub: no-op.
|
|
25
|
+
void resetUrlAfterRedirectLogin() {}
|
|
@@ -23,3 +23,20 @@ bool isMobileWebBrowser() {
|
|
|
23
23
|
// iPadOS 13+ masquerades as desktop Safari but still exposes touch points.
|
|
24
24
|
return ua.contains('macintosh') && web.window.navigator.maxTouchPoints > 1;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
/// localStorage key flagging that a redirect *sign-in* is in flight. localStorage
|
|
28
|
+
/// (not in-memory) because the redirect reloads the whole page.
|
|
29
|
+
const String _redirectLoginKey = 'kasy_redirect_login';
|
|
30
|
+
|
|
31
|
+
void markWebRedirectLogin() {
|
|
32
|
+
web.window.localStorage.setItem(_redirectLoginKey, '1');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void resetUrlAfterRedirectLogin() {
|
|
36
|
+
if (web.window.localStorage.getItem(_redirectLoginKey) == null) return;
|
|
37
|
+
web.window.localStorage.removeItem(_redirectLoginKey);
|
|
38
|
+
// Absolute path: land on Home, dropping the stale tab path (e.g. /settings)
|
|
39
|
+
// the redirect returned to. replaceState (not pushState) so no bogus history
|
|
40
|
+
// entry is left behind.
|
|
41
|
+
web.window.history.replaceState(null, '', '/');
|
|
42
|
+
}
|
|
@@ -209,6 +209,9 @@ class FirebaseAuthenticationApi implements AuthenticationApi {
|
|
|
209
209
|
/// we sign in. The linked/signed-in user is materialised at startup by
|
|
210
210
|
/// [getRedirectResult].
|
|
211
211
|
Future<Credentials> _signInWithRedirectWeb(AuthProvider provider) async {
|
|
212
|
+
// Flag the login so startup lands on Home, not the stale tab URL the
|
|
213
|
+
// redirect returns to (linking does NOT flag — it stays put, e.g. Settings).
|
|
214
|
+
markWebRedirectLogin();
|
|
212
215
|
final current = _auth.currentUser;
|
|
213
216
|
if (current != null && current.isAnonymous) {
|
|
214
217
|
await current.linkWithRedirect(provider);
|
|
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|
|
5
5
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
6
6
|
import 'package:go_router/go_router.dart';
|
|
7
7
|
import 'package:kasy_kit/components/components.dart';
|
|
8
|
+
import 'package:kasy_kit/core/bottom_menu/web_url.dart';
|
|
8
9
|
import 'package:kasy_kit/core/config/features.dart';
|
|
9
10
|
import 'package:kasy_kit/core/data/models/user.dart';
|
|
10
11
|
import 'package:kasy_kit/core/haptics/kasy_haptics.dart';
|
|
@@ -41,6 +42,14 @@ class SigninPage extends ConsumerWidget {
|
|
|
41
42
|
body: SizedBox.shrink(),
|
|
42
43
|
);
|
|
43
44
|
}
|
|
45
|
+
// Web: the sign-in screen is reached via the router redirect (e.g. after a
|
|
46
|
+
// logout), where Bart may have left a stale tab path in the address bar
|
|
47
|
+
// (it writes tab URLs directly via pushState). Pin the URL to '/signin' so
|
|
48
|
+
// it matches the screen actually shown. No-op on native.
|
|
49
|
+
if (kIsWeb) {
|
|
50
|
+
WidgetsBinding.instance
|
|
51
|
+
.addPostFrameCallback((_) => syncBrowserUrl('/signin'));
|
|
52
|
+
}
|
|
44
53
|
final state = ref.watch(signinStateProvider);
|
|
45
54
|
final isSending = state is SigninStateSending;
|
|
46
55
|
final showBack = shouldShowAuthBackButton(context);
|
|
@@ -238,9 +247,9 @@ class _SignupPrompt extends StatelessWidget {
|
|
|
238
247
|
}
|
|
239
248
|
}
|
|
240
249
|
|
|
241
|
-
/// "Continue as guest" — creates the anonymous account on demand and
|
|
242
|
-
///
|
|
243
|
-
///
|
|
250
|
+
/// "Continue as guest" — creates the anonymous account on demand and navigates
|
|
251
|
+
/// into the app. Shown only on native in anonymous mode (see the build
|
|
252
|
+
/// condition in [SigninPage]).
|
|
244
253
|
class _GuestContinueButton extends ConsumerStatefulWidget {
|
|
245
254
|
const _GuestContinueButton();
|
|
246
255
|
|
|
@@ -256,8 +265,13 @@ class _GuestContinueButtonState extends ConsumerState<_GuestContinueButton> {
|
|
|
256
265
|
setState(() => _loading = true);
|
|
257
266
|
try {
|
|
258
267
|
await ref.read(userStateNotifierProvider.notifier).continueAsGuest();
|
|
259
|
-
|
|
260
|
-
//
|
|
268
|
+
if (!mounted) return;
|
|
269
|
+
// Navigate explicitly: the user is now an anonymous guest (has an id but
|
|
270
|
+
// is NOT an AuthenticatedUserData), and the router redirect intentionally
|
|
271
|
+
// only bounces *authenticated* users off the auth routes — so a guest can
|
|
272
|
+
// still open sign-in to upgrade. That means it won't carry us home on its
|
|
273
|
+
// own here; we do it ourselves.
|
|
274
|
+
context.go('/');
|
|
261
275
|
} catch (_) {
|
|
262
276
|
if (!mounted) return;
|
|
263
277
|
setState(() => _loading = false);
|
|
@@ -273,7 +287,9 @@ class _GuestContinueButtonState extends ConsumerState<_GuestContinueButton> {
|
|
|
273
287
|
Widget build(BuildContext context) {
|
|
274
288
|
return KasyButton(
|
|
275
289
|
label: t.auth.signin.continue_without,
|
|
276
|
-
variant:
|
|
290
|
+
// Most subtle variant in the button preview: it's a tertiary action that
|
|
291
|
+
// should sit quietly below sign-in / social, not compete with them.
|
|
292
|
+
variant: KasyButtonVariant.ghost,
|
|
277
293
|
expand: true,
|
|
278
294
|
isLoading: _loading,
|
|
279
295
|
onPressed: _loading ? null : _continue,
|
|
@@ -559,16 +559,16 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
559
559
|
builder: _buildSidebarAppNav,
|
|
560
560
|
),
|
|
561
561
|
ComponentPreviewVariant(
|
|
562
|
-
label: '
|
|
563
|
-
builder:
|
|
562
|
+
label: 'Mobile drawer',
|
|
563
|
+
builder: _buildSidebarMobileDrawer,
|
|
564
564
|
),
|
|
565
565
|
ComponentPreviewVariant(
|
|
566
566
|
label: 'Collapsed rail',
|
|
567
567
|
builder: _buildSidebarCollapsed,
|
|
568
568
|
),
|
|
569
569
|
ComponentPreviewVariant(
|
|
570
|
-
label: '
|
|
571
|
-
builder:
|
|
570
|
+
label: 'Pinned (always wide)',
|
|
571
|
+
builder: _buildSidebarPinned,
|
|
572
572
|
),
|
|
573
573
|
ComponentPreviewVariant(
|
|
574
574
|
label: 'Workspace showcase',
|
|
@@ -629,56 +629,70 @@ ComponentPreviewDefinition? getComponentPreviewDefinition(
|
|
|
629
629
|
// Sidebar — interactive preview
|
|
630
630
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
631
631
|
|
|
632
|
-
// The live Home configuration (real pages, highlight-only nav). The default
|
|
633
|
-
//
|
|
634
|
-
Widget _buildSidebarAppNav(BuildContext context) => const _SidebarPreview(
|
|
632
|
+
// The live Home configuration (real pages, highlight-only nav). The default —
|
|
633
|
+
// adapts to the breakpoint, which is what actually ships in the app.
|
|
634
|
+
Widget _buildSidebarAppNav(BuildContext context) => const _SidebarPreview(
|
|
635
|
+
caption: 'The default app navigation, wired to the live Home pages. '
|
|
636
|
+
'Adapts to the breakpoint: wide on desktop, a thin icon rail on '
|
|
637
|
+
'tablet and mobile — the toggle expands it on any screen.',
|
|
638
|
+
);
|
|
635
639
|
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
640
|
+
// The mobile pattern: always opens wide as a slide-in drawer, no collapse
|
|
641
|
+
// toggle (the sheet you open from an app-bar menu button on a phone).
|
|
642
|
+
Widget _buildSidebarMobileDrawer(BuildContext context) => const _SidebarPreview(
|
|
643
|
+
isDrawer: true,
|
|
644
|
+
caption: 'The phone pattern: always opens wide as a slide-in drawer, with '
|
|
645
|
+
'the collapse toggle hidden. This is what an app-bar menu button opens.',
|
|
646
|
+
);
|
|
639
647
|
|
|
640
|
-
//
|
|
641
|
-
Widget _buildSidebarCollapsed(BuildContext context) =>
|
|
642
|
-
|
|
648
|
+
// Starts as the narrow icon rail (tooltips on hover), at any breakpoint.
|
|
649
|
+
Widget _buildSidebarCollapsed(BuildContext context) => const _SidebarPreview(
|
|
650
|
+
collapseMode: KasySidebarCollapseMode.collapsed,
|
|
651
|
+
caption: 'Starts collapsed to the thin icon rail (labels show as hover '
|
|
652
|
+
'tooltips). Expand it with the toggle on any breakpoint.',
|
|
653
|
+
);
|
|
643
654
|
|
|
644
|
-
//
|
|
645
|
-
Widget
|
|
646
|
-
|
|
655
|
+
// Pinned wide on every breakpoint; the user collapses it themselves.
|
|
656
|
+
Widget _buildSidebarPinned(BuildContext context) => const _SidebarPreview(
|
|
657
|
+
collapseMode: KasySidebarCollapseMode.expanded,
|
|
658
|
+
caption: 'Pinned wide on every breakpoint instead of auto-collapsing; the '
|
|
659
|
+
'user narrows it with the toggle when they want the room.',
|
|
660
|
+
);
|
|
647
661
|
|
|
648
|
-
// The rich SaaS showcase (workspace selector, KasyTabs segment,
|
|
649
|
-
Widget _buildSidebarShowcase(BuildContext context) =>
|
|
650
|
-
|
|
662
|
+
// The rich SaaS showcase (workspace selector, KasyTabs segment, ⌘K search).
|
|
663
|
+
Widget _buildSidebarShowcase(BuildContext context) => const _SidebarPreview(
|
|
664
|
+
showcase: true,
|
|
665
|
+
caption: 'The rich SaaS layout: workspace switcher, a KasyTabs segment '
|
|
666
|
+
'(Layers / Assets) and a pinned ⌘K search row.',
|
|
667
|
+
);
|
|
651
668
|
|
|
652
669
|
/// Launches a [KasySidebar] as a full-height drawer (left + right buttons) so
|
|
653
|
-
/// the full-bleed chrome can be inspected without crushing the preview card.
|
|
670
|
+
/// the full-bleed chrome can be inspected without crushing the preview card. A
|
|
671
|
+
/// short [caption] (Kasy typography) explains what each configuration is.
|
|
654
672
|
/// Connected variants mirror the live Home; [showcase] swaps to the HeroUI
|
|
655
673
|
/// workspace demo.
|
|
656
674
|
class _SidebarPreview extends StatelessWidget {
|
|
657
675
|
const _SidebarPreview({
|
|
676
|
+
required this.caption,
|
|
658
677
|
this.showcase = false,
|
|
659
|
-
this.
|
|
678
|
+
this.collapseMode = KasySidebarCollapseMode.responsive,
|
|
660
679
|
this.isDrawer = false,
|
|
661
|
-
this.showSearch = false,
|
|
662
680
|
});
|
|
663
681
|
|
|
682
|
+
final String caption;
|
|
664
683
|
final bool showcase;
|
|
665
|
-
final
|
|
684
|
+
final KasySidebarCollapseMode collapseMode;
|
|
666
685
|
final bool isDrawer;
|
|
667
|
-
final bool showSearch;
|
|
668
686
|
|
|
669
687
|
Widget _sidebar({required bool fromEnd}) {
|
|
670
688
|
final KasySidebarSide side =
|
|
671
689
|
fromEnd ? KasySidebarSide.right : KasySidebarSide.left;
|
|
672
690
|
if (showcase) {
|
|
673
|
-
return KasySidebar(
|
|
674
|
-
initiallyCollapsed: initiallyCollapsed,
|
|
675
|
-
side: side,
|
|
676
|
-
);
|
|
691
|
+
return KasySidebar(collapseMode: collapseMode, side: side);
|
|
677
692
|
}
|
|
678
693
|
return _DemoSidebar(
|
|
679
694
|
isDrawer: isDrawer,
|
|
680
|
-
|
|
681
|
-
showSearch: showSearch,
|
|
695
|
+
collapseMode: collapseMode,
|
|
682
696
|
side: side,
|
|
683
697
|
);
|
|
684
698
|
}
|
|
@@ -696,6 +710,17 @@ class _SidebarPreview extends StatelessWidget {
|
|
|
696
710
|
mainAxisSize: MainAxisSize.min,
|
|
697
711
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
698
712
|
children: [
|
|
713
|
+
Padding(
|
|
714
|
+
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
715
|
+
child: Text(
|
|
716
|
+
caption,
|
|
717
|
+
textAlign: TextAlign.center,
|
|
718
|
+
style: context.textTheme.bodySmall?.copyWith(
|
|
719
|
+
color: context.colors.muted,
|
|
720
|
+
),
|
|
721
|
+
),
|
|
722
|
+
),
|
|
723
|
+
const SizedBox(height: KasySpacing.md),
|
|
699
724
|
Padding(
|
|
700
725
|
padding: const EdgeInsets.symmetric(horizontal: KasySpacing.md),
|
|
701
726
|
child: _previewIntrinsicLaunch(
|
|
@@ -728,14 +753,12 @@ class _SidebarPreview extends StatelessWidget {
|
|
|
728
753
|
class _DemoSidebar extends StatefulWidget {
|
|
729
754
|
const _DemoSidebar({
|
|
730
755
|
this.isDrawer = false,
|
|
731
|
-
this.
|
|
732
|
-
this.showSearch = false,
|
|
756
|
+
this.collapseMode = KasySidebarCollapseMode.responsive,
|
|
733
757
|
this.side = KasySidebarSide.left,
|
|
734
758
|
});
|
|
735
759
|
|
|
736
760
|
final bool isDrawer;
|
|
737
|
-
final
|
|
738
|
-
final bool showSearch;
|
|
761
|
+
final KasySidebarCollapseMode collapseMode;
|
|
739
762
|
final KasySidebarSide side;
|
|
740
763
|
|
|
741
764
|
@override
|
|
@@ -783,8 +806,7 @@ class _DemoSidebarState extends State<_DemoSidebar> {
|
|
|
783
806
|
currentItem: _current,
|
|
784
807
|
onTapItem: (i) => _current.value = i, // highlight only
|
|
785
808
|
isDrawer: widget.isDrawer,
|
|
786
|
-
|
|
787
|
-
showSearch: widget.showSearch,
|
|
809
|
+
collapseMode: widget.collapseMode,
|
|
788
810
|
side: widget.side,
|
|
789
811
|
);
|
|
790
812
|
}
|
|
@@ -8,6 +8,7 @@ import 'package:kasy_kit/features/notifications/repositories/notifications_repos
|
|
|
8
8
|
import 'package:kasy_kit/features/onboarding/models/user_info.dart';
|
|
9
9
|
import 'package:kasy_kit/features/onboarding/providers/onboarding_model.dart';
|
|
10
10
|
import 'package:kasy_kit/features/onboarding/repositories/user_infos_repository.dart';
|
|
11
|
+
import 'package:logger/logger.dart';
|
|
11
12
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
12
13
|
|
|
13
14
|
part 'onboarding_provider.g.dart';
|
|
@@ -83,13 +84,18 @@ class OnboardingNotifier extends _$OnboardingNotifier {
|
|
|
83
84
|
await userStateNotifier.continueAsGuest();
|
|
84
85
|
|
|
85
86
|
// Now that the account exists, flush the answers collected during the
|
|
86
|
-
// questions (gender, age, …) that had nowhere to go before.
|
|
87
|
+
// questions (gender, age, …) that had nowhere to go before. Best-effort:
|
|
88
|
+
// a failed profile write must never block onboarding from finishing.
|
|
87
89
|
final userId = ref.read(userStateNotifierProvider).user.idOrNull;
|
|
88
90
|
final pending = state.pendingUserInfo;
|
|
89
91
|
if (userId != null && pending.isNotEmpty) {
|
|
90
92
|
final repository = ref.read(userInfosRepositoryProvider);
|
|
91
93
|
for (final info in pending) {
|
|
92
|
-
|
|
94
|
+
try {
|
|
95
|
+
await repository.save(userId, info);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
Logger().w('Failed to save onboarding answer: $e');
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
state = state.copyWith(pendingUserInfo: const []);
|
|
95
101
|
}
|
|
@@ -27,10 +27,21 @@ class _OnboardingJournalLoaderState extends ConsumerState<OnboardingLoader> {
|
|
|
27
27
|
// Run the real work (which now lazily creates the guest account) while the
|
|
28
28
|
// loader animation plays, so its network latency is hidden under the
|
|
29
29
|
// minimum display time instead of being added on top of it.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
//
|
|
31
|
+
// The completion call MUST always run, even if a backend write fails (e.g.
|
|
32
|
+
// setting the onboarded flag on a user document the backend trigger has not
|
|
33
|
+
// created yet). Otherwise the user is trapped on this loader forever. The
|
|
34
|
+
// account already exists and onboarding is remembered locally, so moving
|
|
35
|
+
// on is safe.
|
|
36
|
+
final minimumDisplay = Future<void>.delayed(
|
|
37
|
+
const Duration(milliseconds: 3500),
|
|
38
|
+
);
|
|
39
|
+
try {
|
|
40
|
+
await ref.onboardingNotifier.onOnboardingCompleted();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
debugPrint('OnboardingLoader: completion error (continuing anyway): $e');
|
|
43
|
+
}
|
|
44
|
+
await minimumDisplay;
|
|
34
45
|
if (!mounted) return;
|
|
35
46
|
widget.onCompleted();
|
|
36
47
|
}
|
|
@@ -24,6 +24,8 @@ import 'package:kasy_kit/core/web_device_preview/web_device_preview.dart';
|
|
|
24
24
|
import 'package:kasy_kit/core/web_viewport_scale.dart';
|
|
25
25
|
import 'package:kasy_kit/core/widgets/focus_visibility.dart';
|
|
26
26
|
import 'package:kasy_kit/environments.dart';
|
|
27
|
+
import 'package:kasy_kit/features/authentication/api/auth_web_support.dart'
|
|
28
|
+
if (dart.library.js_interop) 'package:kasy_kit/features/authentication/api/auth_web_support_web.dart';
|
|
27
29
|
import 'package:kasy_kit/features/authentication/api/authentication_api.dart';
|
|
28
30
|
import 'package:kasy_kit/features/notifications/api/local_notifier.dart';
|
|
29
31
|
import 'package:kasy_kit/features/notifications/repositories/background_notification_handler.dart';
|
|
@@ -88,6 +90,11 @@ void main() async {
|
|
|
88
90
|
// MUST be registered at the top-level BEFORE runApp().
|
|
89
91
|
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
|
|
90
92
|
|
|
93
|
+
// Web: if we just returned from a social-login redirect, reset the address bar
|
|
94
|
+
// to '/' BEFORE the router reads the initial URL, so the app boots on Home
|
|
95
|
+
// instead of the stale tab URL (e.g. /settings) the redirect returned to.
|
|
96
|
+
resetUrlAfterRedirectLogin();
|
|
97
|
+
|
|
91
98
|
// initialize sentry for error reporting in production only
|
|
92
99
|
if (env is DevEnvironment) {
|
|
93
100
|
run(sharedPrefs);
|
|
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|
|
16
16
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
|
17
17
|
# In Windows, build-name is used as the major, minor, and patch parts
|
|
18
18
|
# of the product and file versions while build-number is used as the build suffix.
|
|
19
|
-
version: 1.0.0+
|
|
19
|
+
version: 1.0.0+57
|
|
20
20
|
|
|
21
21
|
environment:
|
|
22
22
|
sdk: ^3.11.0
|
|
@@ -73,7 +73,7 @@ dependencies:
|
|
|
73
73
|
json_annotation: ^4.9.0
|
|
74
74
|
local_auth: ^3.0.1
|
|
75
75
|
logger: ^2.6.2
|
|
76
|
-
lucide_icons_flutter: ^3.1.
|
|
76
|
+
lucide_icons_flutter: ^3.1.14
|
|
77
77
|
mixpanel_flutter: ^2.5.0
|
|
78
78
|
open_filex: ^4.7.0
|
|
79
79
|
package_info_plus: ^8.3.0
|
|
@@ -81,10 +81,10 @@ void main() {
|
|
|
81
81
|
|
|
82
82
|
// Confirm dialog appears with destructive action.
|
|
83
83
|
expect(find.text('Delete all notifications?'), findsOneWidget);
|
|
84
|
-
expect(find.text('
|
|
84
|
+
expect(find.text('Yes, delete'), findsOneWidget);
|
|
85
85
|
expect(find.text('Cancel'), findsOneWidget);
|
|
86
86
|
|
|
87
|
-
await tester.tap(find.text('
|
|
87
|
+
await tester.tap(find.text('Yes, delete'));
|
|
88
88
|
await tester.pumpAndSettle();
|
|
89
89
|
|
|
90
90
|
// After deletion, no notification tiles remain.
|