From 09df205fdbeb5df7e01476561156759b0fa333cd Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Tue, 19 May 2026 13:37:41 +0200 Subject: [PATCH] Start writing the web app Signed-off-by: Nikolai Rodionov --- .gitattributes | 1 + .tool-versions | 2 +- assets/fonts/inter/Inter-Black.ttf | 3 + assets/fonts/inter/Inter-BlackItalic.ttf | 3 + assets/fonts/inter/Inter-Bold.ttf | 3 + assets/fonts/inter/Inter-BoldItalic.ttf | 3 + assets/fonts/inter/Inter-ExtraBold.ttf | 3 + assets/fonts/inter/Inter-ExtraBoldItalic.ttf | 3 + assets/fonts/inter/Inter-ExtraLight.ttf | 3 + assets/fonts/inter/Inter-ExtraLightItalic.ttf | 3 + assets/fonts/inter/Inter-Italic.ttf | 3 + assets/fonts/inter/Inter-Light.ttf | 3 + assets/fonts/inter/Inter-LightItalic.ttf | 3 + assets/fonts/inter/Inter-Medium.ttf | 3 + assets/fonts/inter/Inter-MediumItalic.ttf | 3 + assets/fonts/inter/Inter-Regular.ttf | 3 + assets/fonts/inter/Inter-SemiBold.ttf | 3 + assets/fonts/inter/Inter-SemiBoldItalic.ttf | 3 + assets/fonts/inter/Inter-Thin.ttf | 3 + assets/fonts/inter/Inter-ThinItalic.ttf | 3 + assets/fonts/inter/InterDisplay-Black.ttf | 3 + .../fonts/inter/InterDisplay-BlackItalic.ttf | 3 + assets/fonts/inter/InterDisplay-Bold.ttf | 3 + .../fonts/inter/InterDisplay-BoldItalic.ttf | 3 + assets/fonts/inter/InterDisplay-ExtraBold.ttf | 3 + .../inter/InterDisplay-ExtraBoldItalic.ttf | 3 + .../fonts/inter/InterDisplay-ExtraLight.ttf | 3 + .../inter/InterDisplay-ExtraLightItalic.ttf | 3 + assets/fonts/inter/InterDisplay-Italic.ttf | 3 + assets/fonts/inter/InterDisplay-Light.ttf | 3 + .../fonts/inter/InterDisplay-LightItalic.ttf | 3 + assets/fonts/inter/InterDisplay-Medium.ttf | 3 + .../fonts/inter/InterDisplay-MediumItalic.ttf | 3 + assets/fonts/inter/InterDisplay-Regular.ttf | 3 + assets/fonts/inter/InterDisplay-SemiBold.ttf | 3 + .../inter/InterDisplay-SemiBoldItalic.ttf | 3 + assets/fonts/inter/InterDisplay-Thin.ttf | 3 + .../fonts/inter/InterDisplay-ThinItalic.ttf | 3 + assets/fonts/inter/InterVariable-Italic.ttf | 3 + assets/fonts/inter/InterVariable.ttf | 3 + assets/fonts/syne/Syne-Bold.ttf | 3 + assets/fonts/syne/Syne-ExtraBold.ttf | 3 + assets/fonts/syne/Syne-Medium.ttf | 3 + assets/fonts/syne/Syne-Regular.ttf | 3 + assets/fonts/syne/Syne-SemiBold.ttf | 3 + assets/fonts/syne/SyneMono-Regular.ttf | 3 + assets/fonts/syne/SyneTactile-Regular.ttf | 3 + lib/api/grpc/test_service.dart | 45 -- lib/core/api/v1/accounts.dart | 14 + lib/core/api/v1/public_accounts.dart | 35 ++ lib/core/api/v1/refresh_session.dart | 28 + lib/core/api/v1/test.dart | 22 + lib/core/grpc/grpc_auth_interceptor.dart | 71 +++ lib/core/grpc/grpc_channel_provider.dart | 16 + lib/core/grpc/grpc_client.dart | 54 ++ lib/core/router/app_router.dart | 30 ++ .../application/tokens_application.dart | 167 ++++++ .../tokens/data/token_storage_repository.dart | 42 ++ .../authorization_application.dart | 90 ++++ .../application/sign_in_data.dart | 12 + .../application/sign_up_data.dart | 23 + .../.conform.8331486.decoration.dart | 68 +++ .../presentation/authorization_page.dart | 57 +++ .../presentation/decoration.dart | 89 ++++ .../presentation/login_form.dart | 144 ++++++ .../presentation/register_form.dart | 105 ++++ .../application/dashboard_application.dart | 30 ++ .../presentation/dashboard_page.dart | 31 ++ .../test/application/test_controller.dart | 28 + lib/features/test/presentation/ping_page.dart | 30 ++ lib/main.dart | 80 +-- lib/shared/ui/colors/dark_mode.dart | 8 + lib/shared/ui/colors/light_mode.dart | 21 + lib/shared/ui/theme/app_theme.dart | 109 ++++ pubspec.lock | 477 +++++++++++++++++- pubspec.yaml | 14 +- 76 files changed, 1961 insertions(+), 117 deletions(-) create mode 100644 .gitattributes create mode 100644 assets/fonts/inter/Inter-Black.ttf create mode 100644 assets/fonts/inter/Inter-BlackItalic.ttf create mode 100644 assets/fonts/inter/Inter-Bold.ttf create mode 100644 assets/fonts/inter/Inter-BoldItalic.ttf create mode 100644 assets/fonts/inter/Inter-ExtraBold.ttf create mode 100644 assets/fonts/inter/Inter-ExtraBoldItalic.ttf create mode 100644 assets/fonts/inter/Inter-ExtraLight.ttf create mode 100644 assets/fonts/inter/Inter-ExtraLightItalic.ttf create mode 100644 assets/fonts/inter/Inter-Italic.ttf create mode 100644 assets/fonts/inter/Inter-Light.ttf create mode 100644 assets/fonts/inter/Inter-LightItalic.ttf create mode 100644 assets/fonts/inter/Inter-Medium.ttf create mode 100644 assets/fonts/inter/Inter-MediumItalic.ttf create mode 100644 assets/fonts/inter/Inter-Regular.ttf create mode 100644 assets/fonts/inter/Inter-SemiBold.ttf create mode 100644 assets/fonts/inter/Inter-SemiBoldItalic.ttf create mode 100644 assets/fonts/inter/Inter-Thin.ttf create mode 100644 assets/fonts/inter/Inter-ThinItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Black.ttf create mode 100644 assets/fonts/inter/InterDisplay-BlackItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Bold.ttf create mode 100644 assets/fonts/inter/InterDisplay-BoldItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-ExtraBold.ttf create mode 100644 assets/fonts/inter/InterDisplay-ExtraBoldItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-ExtraLight.ttf create mode 100644 assets/fonts/inter/InterDisplay-ExtraLightItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Italic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Light.ttf create mode 100644 assets/fonts/inter/InterDisplay-LightItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Medium.ttf create mode 100644 assets/fonts/inter/InterDisplay-MediumItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Regular.ttf create mode 100644 assets/fonts/inter/InterDisplay-SemiBold.ttf create mode 100644 assets/fonts/inter/InterDisplay-SemiBoldItalic.ttf create mode 100644 assets/fonts/inter/InterDisplay-Thin.ttf create mode 100644 assets/fonts/inter/InterDisplay-ThinItalic.ttf create mode 100644 assets/fonts/inter/InterVariable-Italic.ttf create mode 100644 assets/fonts/inter/InterVariable.ttf create mode 100644 assets/fonts/syne/Syne-Bold.ttf create mode 100644 assets/fonts/syne/Syne-ExtraBold.ttf create mode 100644 assets/fonts/syne/Syne-Medium.ttf create mode 100644 assets/fonts/syne/Syne-Regular.ttf create mode 100644 assets/fonts/syne/Syne-SemiBold.ttf create mode 100644 assets/fonts/syne/SyneMono-Regular.ttf create mode 100644 assets/fonts/syne/SyneTactile-Regular.ttf delete mode 100644 lib/api/grpc/test_service.dart create mode 100644 lib/core/api/v1/accounts.dart create mode 100644 lib/core/api/v1/public_accounts.dart create mode 100644 lib/core/api/v1/refresh_session.dart create mode 100644 lib/core/api/v1/test.dart create mode 100644 lib/core/grpc/grpc_auth_interceptor.dart create mode 100644 lib/core/grpc/grpc_channel_provider.dart create mode 100644 lib/core/grpc/grpc_client.dart create mode 100644 lib/core/router/app_router.dart create mode 100644 lib/core/tokens/application/tokens_application.dart create mode 100644 lib/core/tokens/data/token_storage_repository.dart create mode 100644 lib/features/authorization/application/authorization_application.dart create mode 100644 lib/features/authorization/application/sign_in_data.dart create mode 100644 lib/features/authorization/application/sign_up_data.dart create mode 100755 lib/features/authorization/presentation/.conform.8331486.decoration.dart create mode 100644 lib/features/authorization/presentation/authorization_page.dart create mode 100644 lib/features/authorization/presentation/decoration.dart create mode 100644 lib/features/authorization/presentation/login_form.dart create mode 100644 lib/features/authorization/presentation/register_form.dart create mode 100644 lib/features/dashboard/application/dashboard_application.dart create mode 100644 lib/features/dashboard/presentation/dashboard_page.dart create mode 100644 lib/features/test/application/test_controller.dart create mode 100644 lib/features/test/presentation/ping_page.dart create mode 100644 lib/shared/ui/colors/dark_mode.dart create mode 100644 lib/shared/ui/colors/light_mode.dart create mode 100644 lib/shared/ui/theme/app_theme.dart diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..287f081 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +assets/** filter=lfs diff=lfs merge=lfs -text diff --git a/.tool-versions b/.tool-versions index 4e8f256..abc7132 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -flutter 3.41.9-stable +flutter 3.44.0-stable diff --git a/assets/fonts/inter/Inter-Black.ttf b/assets/fonts/inter/Inter-Black.ttf new file mode 100644 index 0000000..bbce4dc --- /dev/null +++ b/assets/fonts/inter/Inter-Black.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6342d3ea6dc088b43867f615e807d898adf100c93edb978b8e52c5eb71a264da +size 419968 diff --git a/assets/fonts/inter/Inter-BlackItalic.ttf b/assets/fonts/inter/Inter-BlackItalic.ttf new file mode 100644 index 0000000..3844957 --- /dev/null +++ b/assets/fonts/inter/Inter-BlackItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1737f7d5b391520e9adcc3ea8c730a0854fe6fdd9b9c93c4100c89beb215a931 +size 428036 diff --git a/assets/fonts/inter/Inter-Bold.ttf b/assets/fonts/inter/Inter-Bold.ttf new file mode 100644 index 0000000..29f7b0c --- /dev/null +++ b/assets/fonts/inter/Inter-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:288316099b1e0a47a4716d159098005eef7c0066921f34e3200393dbdb01947f +size 420428 diff --git a/assets/fonts/inter/Inter-BoldItalic.ttf b/assets/fonts/inter/Inter-BoldItalic.ttf new file mode 100644 index 0000000..25e4eae --- /dev/null +++ b/assets/fonts/inter/Inter-BoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:948405a16cdc62701da5f4005ed068ca5f4d27061d98f7974ccfc37831d9581d +size 425296 diff --git a/assets/fonts/inter/Inter-ExtraBold.ttf b/assets/fonts/inter/Inter-ExtraBold.ttf new file mode 100644 index 0000000..1f02a67 --- /dev/null +++ b/assets/fonts/inter/Inter-ExtraBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6756ad5690b77606aa62249a7b420d9902d45cae4b0048a24911fd4324b0a22 +size 421796 diff --git a/assets/fonts/inter/Inter-ExtraBoldItalic.ttf b/assets/fonts/inter/Inter-ExtraBoldItalic.ttf new file mode 100644 index 0000000..3bbfa8c --- /dev/null +++ b/assets/fonts/inter/Inter-ExtraBoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31b58a00cb9e8d00cb936057285282310e81d2f834461264c543c96983af64a6 +size 428284 diff --git a/assets/fonts/inter/Inter-ExtraLight.ttf b/assets/fonts/inter/Inter-ExtraLight.ttf new file mode 100644 index 0000000..fa4d19c --- /dev/null +++ b/assets/fonts/inter/Inter-ExtraLight.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3c7079d9eb4a799251a844acea9eb55f99094479306e2ac1bee875dd9f7ae80 +size 414676 diff --git a/assets/fonts/inter/Inter-ExtraLightItalic.ttf b/assets/fonts/inter/Inter-ExtraLightItalic.ttf new file mode 100644 index 0000000..395ec77 --- /dev/null +++ b/assets/fonts/inter/Inter-ExtraLightItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22fca887d9a9336e8a29601b19683947a57b53911c74e251c3657ee798635275 +size 420260 diff --git a/assets/fonts/inter/Inter-Italic.ttf b/assets/fonts/inter/Inter-Italic.ttf new file mode 100644 index 0000000..0232c38 --- /dev/null +++ b/assets/fonts/inter/Inter-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbc051dd204b5019a1aa0bc0ae2aa8a05ab13e7a3f979fa357631dc7feb6833a +size 417388 diff --git a/assets/fonts/inter/Inter-Light.ttf b/assets/fonts/inter/Inter-Light.ttf new file mode 100644 index 0000000..c542fb9 --- /dev/null +++ b/assets/fonts/inter/Inter-Light.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:164414f0aacbe98a7e64addc43f7b3bfd2e32f7b90e101feeab227f14c371bda +size 412844 diff --git a/assets/fonts/inter/Inter-LightItalic.ttf b/assets/fonts/inter/Inter-LightItalic.ttf new file mode 100644 index 0000000..704c119 --- /dev/null +++ b/assets/fonts/inter/Inter-LightItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3f9efa776957eefaeac8a2991a990fd1bba6cb928dbaeab7abd0655f3a7693c +size 419468 diff --git a/assets/fonts/inter/Inter-Medium.ttf b/assets/fonts/inter/Inter-Medium.ttf new file mode 100644 index 0000000..c537669 --- /dev/null +++ b/assets/fonts/inter/Inter-Medium.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97ad806f526e41546d46365bb3a393145f75b7b1568913db74549ad8b8dba872 +size 417300 diff --git a/assets/fonts/inter/Inter-MediumItalic.ttf b/assets/fonts/inter/Inter-MediumItalic.ttf new file mode 100644 index 0000000..598219a --- /dev/null +++ b/assets/fonts/inter/Inter-MediumItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51c2c8d7c36f7c26e6e2678b5c3069b329bde9a081154553b0f5bc2d4fc14075 +size 423452 diff --git a/assets/fonts/inter/Inter-Regular.ttf b/assets/fonts/inter/Inter-Regular.ttf new file mode 100644 index 0000000..96df725 --- /dev/null +++ b/assets/fonts/inter/Inter-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40d692fce188e4471e2b3cba937be967878f631ad3ebbbdcd587687c7ebe0c82 +size 411640 diff --git a/assets/fonts/inter/Inter-SemiBold.ttf b/assets/fonts/inter/Inter-SemiBold.ttf new file mode 100644 index 0000000..2dbc6a7 --- /dev/null +++ b/assets/fonts/inter/Inter-SemiBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78a843fade9d4612a5567302fb595b56976eb5fcebf4fea5a5912d638bafcde3 +size 419744 diff --git a/assets/fonts/inter/Inter-SemiBoldItalic.ttf b/assets/fonts/inter/Inter-SemiBoldItalic.ttf new file mode 100644 index 0000000..b9c2452 --- /dev/null +++ b/assets/fonts/inter/Inter-SemiBoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eff2930c3d3b35d3fcf5f76252b6baef4c3e907d9d2fde1d16cf5d417f8deef4 +size 423976 diff --git a/assets/fonts/inter/Inter-Thin.ttf b/assets/fonts/inter/Inter-Thin.ttf new file mode 100644 index 0000000..66d2670 --- /dev/null +++ b/assets/fonts/inter/Inter-Thin.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22453f995345e5618d539315a8501f4c74ca41d1898ce44e7d1b8206f0d097d4 +size 406736 diff --git a/assets/fonts/inter/Inter-ThinItalic.ttf b/assets/fonts/inter/Inter-ThinItalic.ttf new file mode 100644 index 0000000..cb7455c --- /dev/null +++ b/assets/fonts/inter/Inter-ThinItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d89ffb2849ef91246283de40a929f671b6e0c8ad1c235701f9362fb0406b4cce +size 413532 diff --git a/assets/fonts/inter/InterDisplay-Black.ttf b/assets/fonts/inter/InterDisplay-Black.ttf new file mode 100644 index 0000000..965f872 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Black.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25460b0d5b3afd9764d63cf838f94145af624f8d4c9d20e0f74116be42878b32 +size 417540 diff --git a/assets/fonts/inter/InterDisplay-BlackItalic.ttf b/assets/fonts/inter/InterDisplay-BlackItalic.ttf new file mode 100644 index 0000000..952f713 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-BlackItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4f3426d62f37f4960352201d109b07f4d50e689e321938770af1ca2d4729c67 +size 424796 diff --git a/assets/fonts/inter/InterDisplay-Bold.ttf b/assets/fonts/inter/InterDisplay-Bold.ttf new file mode 100644 index 0000000..74e3619 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b74c8e0dd744b3347faca4c96bc7b2e32f7d6f62300a79b1d1a99331e44a5bc4 +size 418856 diff --git a/assets/fonts/inter/InterDisplay-BoldItalic.ttf b/assets/fonts/inter/InterDisplay-BoldItalic.ttf new file mode 100644 index 0000000..372b689 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-BoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71e7d3709238b1c21b107ba6433d3a87c6377041a507afd9cf7de3c36ab585f2 +size 424108 diff --git a/assets/fonts/inter/InterDisplay-ExtraBold.ttf b/assets/fonts/inter/InterDisplay-ExtraBold.ttf new file mode 100644 index 0000000..2f43bc9 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-ExtraBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:776d88313defcd42d33bc43bddcba6fc1e93ee3eaeca1c1ded2eacf2baec9d24 +size 419716 diff --git a/assets/fonts/inter/InterDisplay-ExtraBoldItalic.ttf b/assets/fonts/inter/InterDisplay-ExtraBoldItalic.ttf new file mode 100644 index 0000000..18cc1bc --- /dev/null +++ b/assets/fonts/inter/InterDisplay-ExtraBoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:857cf06880d9fe99f9d588a64386b875c2d7001c43624e086d0b8424b55bf415 +size 426808 diff --git a/assets/fonts/inter/InterDisplay-ExtraLight.ttf b/assets/fonts/inter/InterDisplay-ExtraLight.ttf new file mode 100644 index 0000000..887355b --- /dev/null +++ b/assets/fonts/inter/InterDisplay-ExtraLight.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d6606f81e4e5e98255e5194138229056380a74fcdebd779d4616d9ebc2819ed +size 407212 diff --git a/assets/fonts/inter/InterDisplay-ExtraLightItalic.ttf b/assets/fonts/inter/InterDisplay-ExtraLightItalic.ttf new file mode 100644 index 0000000..326039f --- /dev/null +++ b/assets/fonts/inter/InterDisplay-ExtraLightItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d055889c083f1f8869a2a9ea1440da9168e996f589ae43a36415a27c857a37b +size 413420 diff --git a/assets/fonts/inter/InterDisplay-Italic.ttf b/assets/fonts/inter/InterDisplay-Italic.ttf new file mode 100644 index 0000000..3c386a9 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20cf47556669f80d12966d2f5eac5054b7486ce5d076feb744dbe24842e2593b +size 414908 diff --git a/assets/fonts/inter/InterDisplay-Light.ttf b/assets/fonts/inter/InterDisplay-Light.ttf new file mode 100644 index 0000000..1b27304 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Light.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d6072bc7807fa7bf08a6d273368f04ac68e77438258f32975057a61c8772811 +size 413164 diff --git a/assets/fonts/inter/InterDisplay-LightItalic.ttf b/assets/fonts/inter/InterDisplay-LightItalic.ttf new file mode 100644 index 0000000..2e31ca3 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-LightItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07d4f953666f6e60022fc2fd6e62c48fe616bff7f2bf7eeaaeebc8febd01bfc9 +size 418544 diff --git a/assets/fonts/inter/InterDisplay-Medium.ttf b/assets/fonts/inter/InterDisplay-Medium.ttf new file mode 100644 index 0000000..2521790 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Medium.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99665c256585aeaab2d06950c2d7715cca8fd7883c2d0e7a421b8b287e1f17de +size 416668 diff --git a/assets/fonts/inter/InterDisplay-MediumItalic.ttf b/assets/fonts/inter/InterDisplay-MediumItalic.ttf new file mode 100644 index 0000000..0e6a2d1 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-MediumItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:443cfbc6d8013dd0c6f4f2a3952ee02a3b84a1ba44fd1456419b7e4f96f8c2a2 +size 422252 diff --git a/assets/fonts/inter/InterDisplay-Regular.ttf b/assets/fonts/inter/InterDisplay-Regular.ttf new file mode 100644 index 0000000..879adff --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99614bda7ff423aaf470990692dd93613a5971ab4446e4a6d5a83b3d74865074 +size 408972 diff --git a/assets/fonts/inter/InterDisplay-SemiBold.ttf b/assets/fonts/inter/InterDisplay-SemiBold.ttf new file mode 100644 index 0000000..21e3c77 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-SemiBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0310d7a325896129730c6c8cf9a6e0f81ee258bedf77b1ff059b2a7b75f74e02 +size 418540 diff --git a/assets/fonts/inter/InterDisplay-SemiBoldItalic.ttf b/assets/fonts/inter/InterDisplay-SemiBoldItalic.ttf new file mode 100644 index 0000000..80fd4c7 --- /dev/null +++ b/assets/fonts/inter/InterDisplay-SemiBoldItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ff566a2fb8e811f6c846050444c3638b7d370fa3aabd08758fb8a0a2012b1ee +size 424544 diff --git a/assets/fonts/inter/InterDisplay-Thin.ttf b/assets/fonts/inter/InterDisplay-Thin.ttf new file mode 100644 index 0000000..07106fd --- /dev/null +++ b/assets/fonts/inter/InterDisplay-Thin.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52762c933467ae114fd5b862c8cffdc289c8f93ef4073782fec1238d3d4a66df +size 402828 diff --git a/assets/fonts/inter/InterDisplay-ThinItalic.ttf b/assets/fonts/inter/InterDisplay-ThinItalic.ttf new file mode 100644 index 0000000..515fc6d --- /dev/null +++ b/assets/fonts/inter/InterDisplay-ThinItalic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3fc28a382edd767041e5ce3cf669cf8b09a150cffbe2086f798e8c5097fd257 +size 408668 diff --git a/assets/fonts/inter/InterVariable-Italic.ttf b/assets/fonts/inter/InterVariable-Italic.ttf new file mode 100644 index 0000000..f29e852 --- /dev/null +++ b/assets/fonts/inter/InterVariable-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6f1f6a172d9e588438db9f986fd5cfad7b30f644374080a8a9d4d91e344586f +size 910252 diff --git a/assets/fonts/inter/InterVariable.ttf b/assets/fonts/inter/InterVariable.ttf new file mode 100644 index 0000000..c4b8e9a --- /dev/null +++ b/assets/fonts/inter/InterVariable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4989b125924991b90d05b2d16e0e388c48f7d5bb8b30539bbf9c755278d0ccaf +size 879708 diff --git a/assets/fonts/syne/Syne-Bold.ttf b/assets/fonts/syne/Syne-Bold.ttf new file mode 100644 index 0000000..769182e --- /dev/null +++ b/assets/fonts/syne/Syne-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d52dbaf7cb6fe7d96f038f0fcce8d51dc4c0b76b98487091e67174ed3742e43 +size 94632 diff --git a/assets/fonts/syne/Syne-ExtraBold.ttf b/assets/fonts/syne/Syne-ExtraBold.ttf new file mode 100644 index 0000000..9ed2423 --- /dev/null +++ b/assets/fonts/syne/Syne-ExtraBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66644411993b76545fb41a411c4347d173e644e0e49143c2cdff545103e2a0bc +size 99672 diff --git a/assets/fonts/syne/Syne-Medium.ttf b/assets/fonts/syne/Syne-Medium.ttf new file mode 100644 index 0000000..4b8550e --- /dev/null +++ b/assets/fonts/syne/Syne-Medium.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10fbd3a0b17f779dc233ae360cf5bb51ac86fdd3bd477b5b5d09f087b78da25d +size 91468 diff --git a/assets/fonts/syne/Syne-Regular.ttf b/assets/fonts/syne/Syne-Regular.ttf new file mode 100644 index 0000000..b19e42b --- /dev/null +++ b/assets/fonts/syne/Syne-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f55db8ac1b3d44200d84d3397f8ec587e40e27dbe7da774029f2a99226e0e2fc +size 90028 diff --git a/assets/fonts/syne/Syne-SemiBold.ttf b/assets/fonts/syne/Syne-SemiBold.ttf new file mode 100644 index 0000000..3c1c638 --- /dev/null +++ b/assets/fonts/syne/Syne-SemiBold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9906697e3f312310d4974fe0b806dd8da32895d0ee3364fbe020416ac8dc9233 +size 92820 diff --git a/assets/fonts/syne/SyneMono-Regular.ttf b/assets/fonts/syne/SyneMono-Regular.ttf new file mode 100644 index 0000000..596f38e --- /dev/null +++ b/assets/fonts/syne/SyneMono-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bccf151f1b5ed87f2c04c5eb679ae8538fa49a5b691eabb9a89fe61c92f4cb38 +size 72240 diff --git a/assets/fonts/syne/SyneTactile-Regular.ttf b/assets/fonts/syne/SyneTactile-Regular.ttf new file mode 100644 index 0000000..126204e --- /dev/null +++ b/assets/fonts/syne/SyneTactile-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:508682f03d6df5d22b9478bf5e4213f4009cad0e3b1b6adc6c1c1f454b0c84b9 +size 129828 diff --git a/lib/api/grpc/test_service.dart b/lib/api/grpc/test_service.dart deleted file mode 100644 index ba237f3..0000000 --- a/lib/api/grpc/test_service.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'dart:developer'; - -import 'package:grpc/grpc_web.dart'; -import 'package:softplayer_dart_proto/src/test/v1/test_v1.pb.dart'; -import 'package:softplayer_dart_proto/src/test/v1/test_v1.pbgrpc.dart'; - -class TestAuthGrpc { - final GrpcWebClientChannel channel; - late TestServiceClient serviceStub; - TestAuthGrpc({required this.channel}); - - void init() { - serviceStub = TestServiceClient(channel); - } - - Future pong() async { - final request = PongRequest(); - try { - final response = await serviceStub.pong(request); - log(response.toString()); - } catch (e) { - rethrow; - } - } -} - -class TestNoAuthGrpc { - final GrpcWebClientChannel channel; - late PublicTestServiceClient serviceStub; - TestNoAuthGrpc({required this.channel}); - - void init() { - serviceStub = PublicTestServiceClient(channel); - } - - Future ping() async { - final request = PingRequest(); - try { - final response = await serviceStub.ping(request); - log(response.toString()); - } catch (e) { - rethrow; - } - } -} diff --git a/lib/core/api/v1/accounts.dart b/lib/core/api/v1/accounts.dart new file mode 100644 index 0000000..4e19696 --- /dev/null +++ b/lib/core/api/v1/accounts.dart @@ -0,0 +1,14 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart'; +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart'; +import 'package:softplayer_web/core/grpc/grpc_client.dart'; + +final accountsGrpcProvider = Provider((ref) { + return AccountsGrpcRepository(ref.watch(accountsServiceClientProvider)); +}); + +class AccountsGrpcRepository { + AccountsGrpcRepository(this._client); + final AccountsServiceClient _client; +} diff --git a/lib/core/api/v1/public_accounts.dart b/lib/core/api/v1/public_accounts.dart new file mode 100644 index 0000000..9364571 --- /dev/null +++ b/lib/core/api/v1/public_accounts.dart @@ -0,0 +1,35 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart'; +import 'package:softplayer_web/core/grpc/grpc_client.dart'; + +final publicAccountsGrpcProvider = Provider(( + ref, +) { + return PublicAccountsGrpcRepository( + ref.watch(publicAccountsServiceClientProvider), + ); +}); + +class PublicAccountsGrpcRepository { + PublicAccountsGrpcRepository(this._client); + final PublicAccountsServiceClient _client; + + ResponseFuture signIn(SignInRequest req) { + try { + final response = _client.signIn(req); + return response; + } catch (error) { + rethrow; + } + } + + ResponseFuture signUp(SignUpRequest req) { + try { + final response = _client.signUp(req); + return response; + } catch (error) { + rethrow; + } + } +} diff --git a/lib/core/api/v1/refresh_session.dart b/lib/core/api/v1/refresh_session.dart new file mode 100644 index 0000000..9050803 --- /dev/null +++ b/lib/core/api/v1/refresh_session.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart'; +import 'package:softplayer_web/core/grpc/grpc_client.dart'; + +final refreshSessionGrpcProvider = Provider(( + ref, +) { + return RefreshSessionGrpcRepository( + ref.watch(refreshSessionServiceClientProvider), + ); +}); + +class RefreshSessionGrpcRepository { + final RefreshSessionServiceClient _client; + RefreshSessionGrpcRepository(this._client); + + ResponseFuture refreshSession(String refreshToken) { + try { + final response = _client.refreshSession( + RefreshSessionRequest(refreshToken: refreshToken), + ); + return response; + } catch (error) { + rethrow; + } + } +} diff --git a/lib/core/api/v1/test.dart b/lib/core/api/v1/test.dart new file mode 100644 index 0000000..18c0624 --- /dev/null +++ b/lib/core/api/v1/test.dart @@ -0,0 +1,22 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:grpc/grpc_web.dart'; +import 'package:softplayer_dart_proto/test/v1/test_v1.pbgrpc.dart'; +import 'package:softplayer_web/core/grpc/grpc_client.dart'; + +final testGrpcProvider = Provider((ref) { + return TestGrpcRepository(ref.watch(testServiceClientProvider)); +}); + +class TestGrpcRepository { + TestGrpcRepository(this._client); + final TestServiceClient _client; + + ResponseFuture pong() { + try { + final response = _client.pong(PongRequest()); + return response; + } catch (error) { + rethrow; + } + } +} diff --git a/lib/core/grpc/grpc_auth_interceptor.dart b/lib/core/grpc/grpc_auth_interceptor.dart new file mode 100644 index 0000000..e5883dd --- /dev/null +++ b/lib/core/grpc/grpc_auth_interceptor.dart @@ -0,0 +1,71 @@ +import 'dart:developer'; + +import 'package:grpc/grpc.dart'; + +class AuthInterceptor extends ClientInterceptor { + final String Function() getAccessToken; + final String Function() getRefreshToken; + + AuthInterceptor({ + required this.getAccessToken, + required this.getRefreshToken, + }); + + bool _isPublicEndpoint(String path) { + return path.contains('/Public'); + } + + bool _isRefreshEndpoint(String path) { + return path.contains('/RefreshToken'); + } + + Map _buildMetadata(String path) { + final metadata = {}; + + // Public endpoints → no token + if (_isPublicEndpoint(path)) { + log("Public endpoint, no tokens needed"); + return metadata; + } + + String? token; + + // Refresh endpoint → use refresh token + if (_isRefreshEndpoint(path)) { + log("Adding a refresh token to the metadata"); + token = getRefreshToken(); + } else { + log("Adding an acces token to the metadata"); + token = getAccessToken(); + } + + if (token.isNotEmpty) { + metadata['authorization'] = 'Bearer $token'; + } + return metadata; + } + + @override + ResponseStream interceptStreaming( + ClientMethod method, + Stream requests, + CallOptions options, + ClientStreamingInvoker invoker, + ) { + // TODO: implement interceptStreaming + throw UnimplementedError(); + } + + @override + ResponseFuture interceptUnary( + ClientMethod method, + Q request, + CallOptions options, + ClientUnaryInvoker invoker, + ) { + final modifiedOptions = options.mergedWith( + CallOptions(metadata: _buildMetadata(method.path)), + ); + return super.interceptUnary(method, request, modifiedOptions, invoker); + } +} diff --git a/lib/core/grpc/grpc_channel_provider.dart b/lib/core/grpc/grpc_channel_provider.dart new file mode 100644 index 0000000..88df092 --- /dev/null +++ b/lib/core/grpc/grpc_channel_provider.dart @@ -0,0 +1,16 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:grpc/grpc_web.dart'; + +final grpcChannelProvider = Provider((ref) { + String backendURL = dotenv.env['SOFTPLAYER_BACKEND_URL']!; + final GrpcWebClientChannel channel = GrpcWebClientChannel.xhr( + Uri.parse(backendURL), + ); + + ref.onDispose(() async { + await channel.shutdown(); + }); + + return channel; +}); diff --git a/lib/core/grpc/grpc_client.dart b/lib/core/grpc/grpc_client.dart new file mode 100644 index 0000000..6f1e55c --- /dev/null +++ b/lib/core/grpc/grpc_client.dart @@ -0,0 +1,54 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart'; +import 'package:softplayer_dart_proto/test/v1/test_v1.pbgrpc.dart'; +import 'package:softplayer_web/core/grpc/grpc_auth_interceptor.dart'; +import 'package:softplayer_web/core/tokens/application/tokens_application.dart'; + +import 'grpc_channel_provider.dart'; + +final testServiceClientProvider = Provider((ref) { + final channel = ref.watch(grpcChannelProvider); + final tokenState = ref.read(tokensControllerProvider).value; + if (tokenState == null) { + throw Exception("Token state is not initialized"); + } + + return TestServiceClient( + channel, + interceptors: [ + AuthInterceptor( + getAccessToken: tokenState.getAccessToken, + getRefreshToken: tokenState.getRefreshToken, + ), + ], + ); +}); + +final accountsServiceClientProvider = Provider((ref) { + final channel = ref.watch(grpcChannelProvider); + final tokenState = ref.read(tokensControllerProvider).value; + if (tokenState == null) { + throw Exception("Token state is not initialized"); + } + return AccountsServiceClient( + channel, + interceptors: [ + AuthInterceptor( + getAccessToken: tokenState.getAccessToken, + getRefreshToken: tokenState.getRefreshToken, + ), + ], + ); +}); + +final publicAccountsServiceClientProvider = + Provider((ref) { + final channel = ref.watch(grpcChannelProvider); + return PublicAccountsServiceClient(channel); + }); + +final refreshSessionServiceClientProvider = + Provider((ref) { + final channel = ref.watch(grpcChannelProvider); + return RefreshSessionServiceClient(channel); + }); diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart new file mode 100644 index 0000000..d345c82 --- /dev/null +++ b/lib/core/router/app_router.dart @@ -0,0 +1,30 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:softplayer_web/core/tokens/application/tokens_application.dart'; +import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; +import 'package:softplayer_web/features/authorization/presentation/authorization_page.dart'; +import 'package:softplayer_web/features/dashboard/presentation/dashboard_page.dart'; + +final goRouterProvider = Provider((ref) { + final authState = ref.watch(authorizationControllerProvider); + final _tokenCtrl = ref.watch(tokensControllerProvider); + + // If not authorized, redirect to the auth page, otherwise dashboard + return GoRouter( + initialLocation: '/', + redirect: (context, state) { + final isAuthorized = authState.requireValue.isAuthorized; + if (!isAuthorized) { + return "/auth"; + } + return "/"; + }, + routes: [ + GoRoute(path: '/', builder: (context, state) => const DashboardPage()), + GoRoute( + path: '/auth', + builder: (context, state) => const AuthorizationPage(), + ), + ], + ); +}); diff --git a/lib/core/tokens/application/tokens_application.dart b/lib/core/tokens/application/tokens_application.dart new file mode 100644 index 0000000..8b89d6a --- /dev/null +++ b/lib/core/tokens/application/tokens_application.dart @@ -0,0 +1,167 @@ +import 'dart:developer'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:jwt_decoder/jwt_decoder.dart'; +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pb.dart'; +import 'package:softplayer_web/core/api/v1/accounts.dart'; +import 'package:softplayer_web/core/api/v1/refresh_session.dart'; +import 'package:softplayer_web/core/grpc/grpc_client.dart'; +import 'package:softplayer_web/core/tokens/data/token_storage_repository.dart'; +import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; + +class TokenState { + final String? accessToken; + final String? refreshToken; + const TokenState({this.accessToken, this.refreshToken}); + + TokenState copyWith({String? accessToken, String? refreshToken}) { + return TokenState( + refreshToken: refreshToken ?? this.refreshToken, + accessToken: accessToken ?? this.accessToken, + ); + } + + // Get an access token from the state + String getAccessToken() { + return accessToken ?? ""; + } + + // Get a refresh token from the state + String getRefreshToken() { + return refreshToken ?? ""; + } +} + +const accessTokenHeader = "x-access-token"; +const refreshTokenHeader = "x-refresh-token"; + +class Tokens { + final String accessToken; + final String refreshToken; + + const Tokens({required this.accessToken, required this.refreshToken}); + + factory Tokens.fromProto(TokenPair pair) { + return Tokens( + accessToken: pair.accessToken, + refreshToken: pair.refreshToken, + ); + } +} + +final tokensControllerProvider = + AsyncNotifierProvider(TokensController.new); + +class TokensController extends AsyncNotifier { + @override + Future build() async { + final tokenRepo = ref.read(tokenStorageRepositoryProvider); + final accessToken = await tokenRepo.getAccessToken(); + final refreshToken = await tokenRepo.getRefreshToken(); + + // If refresh token is not valid, we just drop the session, + // even if there is an active access token, and return + // an empty state + if (!verifyJWT(refreshToken)) { + log("Refresh token is not valid, logging out"); + await tokenRepo.clearStorage(); + return TokenState(); + } + + // If access token is not valid, refresh it using the refresh token + if (verifyJWT(accessToken)) { + log("Access token is valid"); + return TokenState(refreshToken: refreshToken, accessToken: accessToken); + } + log("Only refresh token is valid"); + return TokenState(refreshToken: refreshToken); + } + + Future resetTokens() async { + final tokenRepo = ref.read(tokenStorageRepositoryProvider); + await tokenRepo.clearStorage(); + state = await AsyncValue.guard(() async { + return TokenState(); + }); + } + + // Store an access token to the storage and save it to the state + Future writeAccessToken(String token) async { + final tokenRepo = ref.read(tokenStorageRepositoryProvider); + await tokenRepo.storeAccessToken(token); + state = await AsyncValue.guard(() async { + return state.value!.copyWith(accessToken: token); + }); + } + + // Store a pair of tokens to the storage and refresh the state + Future writeTokenPair(Tokens pair) async { + final tokenRepo = ref.read(tokenStorageRepositoryProvider); + log("Writing tokens"); + await tokenRepo.storeRefreshToken(pair.refreshToken); + await tokenRepo.storeAccessToken(pair.accessToken); + state = await AsyncValue.guard(() async { + return state.value!.copyWith( + accessToken: pair.accessToken, + refreshToken: pair.refreshToken, + ); + }); + } + + Future checkTokens() async { + final currentState = state.value; + if (currentState == null || + currentState.accessToken == null || + currentState.refreshToken == null) { + log("Trying to get tokens from the storage"); + state = await AsyncValue.guard(() async { + final tokenRepo = ref.read(tokenStorageRepositoryProvider); + return TokenState( + accessToken: await tokenRepo.getAccessToken(), + refreshToken: await tokenRepo.getRefreshToken(), + ); + }); + } + log("checking tokens"); + + final inMemAccessToken = state.value!.getAccessToken(); + // If we have a valid token in memory, nothing must be done + // TODO: Store the expiration in memory, to make it cheaper to check + if (verifyJWT(inMemAccessToken)) { + return; + } else { + log("Access it not valid"); + final inMemRefreshToken = state.value!.getRefreshToken(); + if (verifyJWT(inMemRefreshToken)) { + log("Trying to refresh"); + try { + final refreshSessionGrpc = ref.read(refreshSessionGrpcProvider); + log("Trying to refresh"); + final response = await refreshSessionGrpc.refreshSession( + inMemRefreshToken, + ); + await writeTokenPair(Tokens.fromProto(response.tokenPair)); + return; + } catch (e) { + log(e.toString()); + } + } + } + log("Refrsh it not valid"); + await resetTokens(); + ref.read(authorizationControllerProvider.notifier).logout(); + } + + bool verifyJWT(String? token) { + if (token == null || token.isEmpty) { + return false; + } + + try { + bool isExpired = JwtDecoder.isExpired(token); + return !isExpired; + } catch (_) { + rethrow; + } + } +} diff --git a/lib/core/tokens/data/token_storage_repository.dart b/lib/core/tokens/data/token_storage_repository.dart new file mode 100644 index 0000000..cebb4c5 --- /dev/null +++ b/lib/core/tokens/data/token_storage_repository.dart @@ -0,0 +1,42 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const accessTokenKey = "access_token"; +const refreshTokenKey = "refresh_token"; + +final tokenStorageRepositoryProvider = Provider((ref) { + return TokenStorageRepository(); +}); + +// Secure storage backend for storing tokens in the browser +class TokenStorageRepository { + static const _storage = FlutterSecureStorage(); + TokenStorageRepository(); + + // Store the refresh token in the storage + Future storeRefreshToken(String token) async { + await _storage.write(key: refreshTokenKey, value: token); + } + + // Store the access token in the storage + Future storeAccessToken(String token) async { + await _storage.write(key: accessTokenKey, value: token); + } + + // Get the acccess token from the storage + Future getAccessToken() async { + final token = await _storage.read(key: accessTokenKey); + return token; + } + + // Get the refresh token from the storage + Future getRefreshToken() async { + final token = await _storage.read(key: refreshTokenKey); + return token; + } + + Future clearStorage() async { + await _storage.delete(key: refreshTokenKey); + await _storage.delete(key: accessTokenKey); + } +} diff --git a/lib/features/authorization/application/authorization_application.dart b/lib/features/authorization/application/authorization_application.dart new file mode 100644 index 0000000..d94a68b --- /dev/null +++ b/lib/features/authorization/application/authorization_application.dart @@ -0,0 +1,90 @@ +import 'dart:developer'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/core/api/v1/public_accounts.dart'; +import 'package:softplayer_web/core/tokens/application/tokens_application.dart'; +import 'package:softplayer_web/features/authorization/application/sign_in_data.dart'; +import 'package:softplayer_web/features/authorization/application/sign_up_data.dart'; + +class AuthState { + final AuthMode mode; + final bool isAuthorized; + + const AuthState({this.mode = AuthMode.login, this.isAuthorized = false}); + AuthState copyWith({AuthMode? mode, String? status, bool? isAuthorized}) { + return AuthState( + mode: mode ?? this.mode, + isAuthorized: isAuthorized ?? this.isAuthorized, + ); + } +} + +enum AuthMode { login, signup } + +final authorizationControllerProvider = + AsyncNotifierProvider( + AuthorizationController.new, + ); + +class AuthorizationController extends AsyncNotifier { + @override + Future build() async { + // Use is considered authorized if tokens are set in the memory. + // In case tokens are not valid, it will be discovered by the first + // api call. + final tokenState = await ref.watch(tokensControllerProvider.future); + if (tokenState.getAccessToken().isEmpty && + tokenState.getRefreshToken().isNotEmpty) { + final tokenCtrl = ref.read(tokensControllerProvider.notifier); + await tokenCtrl.checkTokens(); + } + final isAuthorized = + tokenState.getAccessToken().isNotEmpty && + tokenState.getRefreshToken().isNotEmpty; + return AuthState(isAuthorized: isAuthorized); + } + + AuthMode authMode = AuthMode.login; + + void toggleAuthMode() { + state = AsyncData( + state.value!.copyWith( + mode: state.value!.mode == AuthMode.login + ? AuthMode.signup + : AuthMode.login, + ), + ); + } + + Future signin(SignInData form) async { + state = const AsyncLoading(); + + state = await AsyncValue.guard(() async { + final accountsGrpc = ref.read(publicAccountsGrpcProvider); + final tokenCtrl = ref.read(tokensControllerProvider.notifier); + + final response = await accountsGrpc.signIn(form.toProto()); + await tokenCtrl.writeTokenPair(Tokens.fromProto(response.tokenPair)); + return state.value!.copyWith(isAuthorized: true); + }); + } + + Future signup(SignUpData form) async { + state = const AsyncLoading(); + + state = await AsyncValue.guard(() async { + final accountsGrpc = ref.read(publicAccountsGrpcProvider); + final tokenCtrl = ref.read(tokensControllerProvider.notifier); + + final response = await accountsGrpc.signUp(form.toProto()); + await tokenCtrl.writeTokenPair(Tokens.fromProto(response.tokenPair)); + return state.value!.copyWith(isAuthorized: true); + }); + } + + Future logout() async { + state = await AsyncValue.guard(() async { + return state.value!.copyWith(isAuthorized: false); + }); + } +} diff --git a/lib/features/authorization/application/sign_in_data.dart b/lib/features/authorization/application/sign_in_data.dart new file mode 100644 index 0000000..062a764 --- /dev/null +++ b/lib/features/authorization/application/sign_in_data.dart @@ -0,0 +1,12 @@ +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart'; + +class SignInData { + final String email; + final String password; + + const SignInData({required this.email, required this.password}); + + SignInRequest toProto() { + return SignInRequest(email: email, password: password); + } +} diff --git a/lib/features/authorization/application/sign_up_data.dart b/lib/features/authorization/application/sign_up_data.dart new file mode 100644 index 0000000..1058a73 --- /dev/null +++ b/lib/features/authorization/application/sign_up_data.dart @@ -0,0 +1,23 @@ +import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pbgrpc.dart'; + +class SignUpData { + final String email; + final String password; + final String name; + final String surname; + + const SignUpData({ + required this.email, + required this.password, + required this.name, + required this.surname, + }); + + SignUpRequest toProto() { + return SignUpRequest( + email: email, + password: password, + personalData: PersonalData(name: name, surname: surname), + ); + } +} diff --git a/lib/features/authorization/presentation/.conform.8331486.decoration.dart b/lib/features/authorization/presentation/.conform.8331486.decoration.dart new file mode 100755 index 0000000..0fb49f4 --- /dev/null +++ b/lib/features/authorization/presentation/.conform.8331486.decoration.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +class AuthDecoration extends StatelessWidget { + final double minSquareSize; + final double spacing; + final Color color; + + const AuthDecoration({ + super.key, + this.minSquareSize = 40, + this.spacing = 24, // ✅ default 24px gap + this.color = Colors.blue, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth; + + // Step 1: decide number of columns (max 4) + int columns = 4; + + double calcSize(int cols) { + final totalSpacing = spacing * (cols - 1); + return (width - totalSpacing) / cols; + } + + while (columns > 1 && calcSize(columns) < minSquareSize) { + columns--; + } + + final squareSize = calcSize(columns); + + // Step 2: compute how many rows are needed to fill screen height + final height = constraints.maxHeight; + final rows = (height / (squareSize + spacing)).ceil(); + + return Column( + children: List.generate(rows, (row) { + return Padding( + padding: EdgeInsets.all(spacing), + child: Row( + children: List.generate(columns, (col) { + return Padding( + padding: EdgeInsets.only( + right: col == columns - 1 ? 0 : spacing, + ), + child: SizedBox( + width: squareSize, + height: squareSize, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(6), + ), + ), + ), + ); + }), + ), + ); + }), + ); + }, + ); + } +} diff --git a/lib/features/authorization/presentation/authorization_page.dart b/lib/features/authorization/presentation/authorization_page.dart new file mode 100644 index 0000000..9a2593a --- /dev/null +++ b/lib/features/authorization/presentation/authorization_page.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; +import 'package:softplayer_web/features/authorization/presentation/decoration.dart'; +import 'package:softplayer_web/features/authorization/presentation/login_form.dart'; +import 'package:softplayer_web/features/authorization/presentation/register_form.dart'; + +class AuthorizationPage extends ConsumerStatefulWidget { + const AuthorizationPage({super.key}); + + @override + ConsumerState createState() => _AuthorizationPage(); +} + +class _AuthorizationPage extends ConsumerState { + @override + Widget build(BuildContext context) { + final state = ref.watch(authorizationControllerProvider); + + return state.when( + data: (value) { + return LayoutBuilder( + builder: (context, constraints) { + final showDecoration = constraints.maxWidth > 1000; + + return Scaffold( + body: Row( + children: [ + Expanded( + flex: showDecoration ? 7 : 1, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (value.mode == AuthMode.login) + LoginForm() + else + RegisterForm(), + ], + ), + ), + ), + + // ✅ only show decoration if width > 400 + if (showDecoration) + Expanded(flex: 3, child: AuthDecoration()), + ], + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center(child: Text('$e')), + ); + } +} diff --git a/lib/features/authorization/presentation/decoration.dart b/lib/features/authorization/presentation/decoration.dart new file mode 100644 index 0000000..d350e6f --- /dev/null +++ b/lib/features/authorization/presentation/decoration.dart @@ -0,0 +1,89 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:softplayer_web/shared/ui/colors/light_mode.dart'; + +class AuthDecoration extends StatelessWidget { + final double spacing; + final Color color; + + const AuthDecoration({ + super.key, + this.spacing = 24, + this.color = Colors.blue, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + return Container( + color: LightModeColors.backgroundElevated, + child: FractionallySizedBox( + widthFactor: 0.3, + alignment: Alignment.centerLeft, + child: CustomPaint( + painter: _GridPainter(spacing: spacing, color: color), + child: const SizedBox.expand(), + ), + ), + ); + }, + ); + } +} + +// todo: Some AI generated shite, that doesn't work +class _GridPainter extends CustomPainter { + final double spacing; + final Color color; + + _GridPainter({required this.spacing, required this.color}); + + static const double maxSquare = 85; + static const double minSquare = 48; // 2x gap + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + + // ✅ outer padding included in layout math + final innerWidth = size.width - spacing * 2; + final innerHeight = size.height - spacing * 2; + + int columns = 10; + + double calcSize(int cols) { + return (innerWidth - (cols - 1) * spacing) / cols; + } + + // choose columns safely + while (columns > 1 && calcSize(columns) < minSquare) { + columns--; + } + + double square = calcSize(columns); + square = math.min(square, maxSquare); + + final rows = (innerHeight / (square + spacing)).ceil(); + + for (int row = 0; row < rows; row++) { + final y = spacing + row * (square + spacing); + + for (int col = 0; col < columns; col++) { + final x = spacing + col * (square + spacing); + + final rect = Rect.fromLTWH(x, y, square, square); + + canvas.drawRRect( + RRect.fromRectAndRadius(rect, const Radius.circular(6)), + paint, + ); + } + } + } + + @override + bool shouldRepaint(covariant _GridPainter oldDelegate) { + return oldDelegate.spacing != spacing || oldDelegate.color != color; + } +} diff --git a/lib/features/authorization/presentation/login_form.dart b/lib/features/authorization/presentation/login_form.dart new file mode 100644 index 0000000..27493dc --- /dev/null +++ b/lib/features/authorization/presentation/login_form.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; +import 'package:softplayer_web/features/authorization/application/sign_in_data.dart'; + +class LoginForm extends ConsumerStatefulWidget { + const LoginForm({super.key}); + + @override + ConsumerState createState() => _LoginForm(); +} + +class _LoginForm extends ConsumerState { + final _formKey = GlobalKey(); + + final TextEditingController emailCtrl = TextEditingController(); + final TextEditingController passwordCtrl = TextEditingController(); + + void _submitForm() { + if (_formKey.currentState!.validate()) { + // If valid, you can use the values + final email = emailCtrl.text; + final password = passwordCtrl.text; + final form = SignInData(email: email, password: password); + ref.read(authorizationControllerProvider.notifier).signin(form); + } + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(authorizationControllerProvider); + final controller = ref.read(authorizationControllerProvider.notifier); + return SizedBox( + width: 400, + child: Form( + key: _formKey, + child: Column( + children: [ + Container( + alignment: Alignment.topLeft, + child: SelectableText( + "Welcome back!", + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + SizedBox(height: 12), + Container( + alignment: Alignment.topLeft, + child: Row( + children: [ + Text( + "Don't have an account yet? ", + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: controller.toggleAuthMode, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text( + "Sign up now", + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + ], + ), + ), + SizedBox(height: 36), + TextFormField( + onFieldSubmitted: (_) => _submitForm(), + controller: emailCtrl, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Email is required'; + } + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value.trim())) { + return 'Enter a valid email address'; + } + return null; + }, + decoration: InputDecoration(hintText: "Email address"), + ), + SizedBox(height: 16), + TextFormField( + onFieldSubmitted: (_) => _submitForm(), + controller: passwordCtrl, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Password is required'; + } + + if (value.length < 12) { + return 'Password must be at least 12 characters long'; + } + + final hasNumber = RegExp(r'\d'); + final hasSpecialChar = RegExp( + r'[!@#$%^&*(),.?":{}|<>_\-\\[\]\\/+=~`]', + ); + + if (!hasNumber.hasMatch(value)) { + return 'Password must contain at least one number'; + } + + if (!hasSpecialChar.hasMatch(value)) { + return 'Password must contain at least one special character'; + } + + return null; + }, + obscureText: true, + decoration: InputDecoration(hintText: "Password"), + ), + SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: Text("Forgot password?", textAlign: TextAlign.left), + ), + ElevatedButton( + onPressed: _submitForm, + child: const Text('Log in'), + // ) { + // if (_formKey.currentState!.validate()) { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar(content: Text('Processing Data')), + // ); + // } + // _ + // }, + ), + state.when( + loading: () => const CircularProgressIndicator(), + data: (_) => const SizedBox.shrink(), + error: (_, _) => const SizedBox.shrink(), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/authorization/presentation/register_form.dart b/lib/features/authorization/presentation/register_form.dart new file mode 100644 index 0000000..21d28b9 --- /dev/null +++ b/lib/features/authorization/presentation/register_form.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/features/authorization/application/authorization_application.dart'; +import 'package:softplayer_web/features/authorization/application/sign_up_data.dart'; + +class RegisterForm extends ConsumerStatefulWidget { + const RegisterForm({super.key}); + + @override + ConsumerState createState() => _RegisterForm(); +} + +class _RegisterForm extends ConsumerState { + final emailCtrl = TextEditingController(); + final nameCtrl = TextEditingController(); + final surnameCtrl = TextEditingController(); + final passwordCtrl = TextEditingController(); + final repeatPasswordCtrl = TextEditingController(); + + @override + Widget build(BuildContext context) { + final controller = ref.read(authorizationControllerProvider.notifier); + return SizedBox( + width: 400, + child: Column( + children: [ + Container( + alignment: Alignment.topLeft, + child: SelectableText( + "Welcome!", + style: Theme.of(context).textTheme.headlineLarge, + ), + ), + SizedBox(height: 12), + Container( + alignment: Alignment.topLeft, + child: Row( + children: [ + Text( + "Already have an account? ", + style: Theme.of(context).textTheme.bodyMedium, + ), + TextButton( + onPressed: controller.toggleAuthMode, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text( + "Sign in", + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + ), + ), + ), + ], + ), + ), + SizedBox(height: 36), + TextField( + controller: nameCtrl, + decoration: InputDecoration(hintText: "Name"), + ), + SizedBox(height: 16), + TextField( + controller: surnameCtrl, + decoration: InputDecoration(hintText: "Surname"), + ), + SizedBox(height: 16), + TextField( + controller: emailCtrl, + decoration: InputDecoration(hintText: "Email address"), + ), + SizedBox(height: 16), + TextField( + controller: passwordCtrl, + obscureText: true, + decoration: InputDecoration(hintText: "Password"), + ), + SizedBox(height: 16), + TextField( + controller: repeatPasswordCtrl, + obscureText: true, + decoration: InputDecoration(hintText: "Repeat the password"), + ), + TextButton( + onPressed: () { + final form = SignUpData( + email: emailCtrl.text, + password: passwordCtrl.text, + name: nameCtrl.text, + surname: surnameCtrl.text, + ); + + ref.read(authorizationControllerProvider.notifier).signup(form); + }, + child: const Text('Sign up'), + ), + ], + ), + ); + } +} diff --git a/lib/features/dashboard/application/dashboard_application.dart b/lib/features/dashboard/application/dashboard_application.dart new file mode 100644 index 0000000..c1c9998 --- /dev/null +++ b/lib/features/dashboard/application/dashboard_application.dart @@ -0,0 +1,30 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:softplayer_dart_proto/src/accounts/v1/accounts_v1.pbgrpc.dart'; +import 'package:softplayer_web/features/authorization/data/public_accounts_grpc_repository.dart'; + +class DashboardState { + final bool authorized; + + const DashboardState({this.authorized = false}); + DashboardState copyWith({bool? authorized}) { + return DashboardState(authorized: authorized ?? this.authorized); + } +} + +final dashboardControllerProvider = + AsyncNotifierProvider( + DashboardController.new, + ); + +class DashboardController extends AsyncNotifier { + static const _storage = FlutterSecureStorage(); + @override + Future build() async { + final accessToken = await _storage.read(key: "x-access-token"); + if (accessToken == null || accessToken.isEmpty) { + return const DashboardState(authorized: false); + } + return const DashboardState(authorized: true); + } +} diff --git a/lib/features/dashboard/presentation/dashboard_page.dart b/lib/features/dashboard/presentation/dashboard_page.dart new file mode 100644 index 0000000..763ba8b --- /dev/null +++ b/lib/features/dashboard/presentation/dashboard_page.dart @@ -0,0 +1,31 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/features/test/application/test_controller.dart'; + +class DashboardPage extends ConsumerStatefulWidget { + const DashboardPage({super.key}); + + @override + ConsumerState createState() => _DashboardPage(); +} + +class _DashboardPage extends ConsumerState { + bool debug = false; + + @override + Widget build(BuildContext context) { + final ctrl = ref.read(pingControllerProvider.notifier); + log("test"); + return Container( + color: Colors.black87, + child: ElevatedButton( + onPressed: () { + ctrl.ping(); + }, + child: Text("pong"), + ), + ); + } +} diff --git a/lib/features/test/application/test_controller.dart b/lib/features/test/application/test_controller.dart new file mode 100644 index 0000000..c20382f --- /dev/null +++ b/lib/features/test/application/test_controller.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/core/api/v1/test.dart'; +import 'package:softplayer_web/core/tokens/application/tokens_application.dart'; + +final pingControllerProvider = AsyncNotifierProvider( + PingController.new, +); + +class PingController extends AsyncNotifier { + @override + Future build() async { + return 'Idle'; + } + + Future ping() async { + state = const AsyncLoading(); + + state = await AsyncValue.guard(() async { + final testGrpc = ref.read(testGrpcProvider); + final tokenCtrl = ref.read(tokensControllerProvider.notifier); + await tokenCtrl.checkTokens(); + + await testGrpc.pong(); + + return 'Ping successful'; + }); + } +} diff --git a/lib/features/test/presentation/ping_page.dart b/lib/features/test/presentation/ping_page.dart new file mode 100644 index 0000000..1f332eb --- /dev/null +++ b/lib/features/test/presentation/ping_page.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../application/test_controller.dart'; + +class PingPage extends ConsumerWidget { + const PingPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(pingControllerProvider); + + return Scaffold( + appBar: AppBar(title: const Text('gRPC Ping')), + body: Center( + child: state.when( + data: (value) => Text(value), + loading: () => const CircularProgressIndicator(), + error: (e, _) => Text('Error: $e'), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + ref.read(pingControllerProvider.notifier).ping(); + }, + child: const Icon(Icons.send), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 7095006..e941eb7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,72 +1,28 @@ -import 'dart:developer'; - -import 'package:softplayer_web/api/grpc/test_service.dart'; -import 'package:web/web.dart' as web; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter/material.dart'; -import 'package:grpc/grpc_web.dart'; -import 'package:flutter/semantics.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:softplayer_web/shared/ui/theme/app_theme.dart'; + +import 'core/router/app_router.dart'; void main() async { - await dotenv.load(fileName: ".env"); - String backendURL = dotenv.env['SOFTPLAYER_BACKEND_URL']!; - GrpcWebClientChannel grpcChannel = GrpcWebClientChannel.xhr( - Uri.parse(backendURL), - ); + WidgetsFlutterBinding.ensureInitialized(); - runApp(MyApp(channel: grpcChannel)); - SemanticsBinding.instance.ensureSemantics(); + await dotenv.load(fileName: '.env'); + runApp(const ProviderScope(child: App())); } -class MyApp extends StatelessWidget { - const MyApp({super.key, required this.channel}); - final GrpcWebClientChannel channel; +class App extends ConsumerWidget { + const App({super.key}); + @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: true, - title: 'Softplayer', - home: RootWidget(channel: channel), - ); - } -} - -class RootWidget extends StatefulWidget { - final GrpcWebClientChannel channel; - const RootWidget({super.key, required this.channel}); - - @override - State createState() => _StateRootWidget(); -} - -class _StateRootWidget extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - height: double.infinity, - child: Column( - children: [ - ElevatedButton( - onPressed: () async { - TestAuthGrpc grpc = TestAuthGrpc(channel: widget.channel); - grpc.init(); - await grpc.pong(); - }, - child: Text("pong"), - ), - ElevatedButton( - onPressed: () async { - TestNoAuthGrpc grpc = TestNoAuthGrpc(channel: widget.channel); - grpc.init(); - await grpc.ping(); - }, - child: Text("ping"), - ), - ], - ), - ), + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(goRouterProvider); + return MaterialApp.router( + routerConfig: router, + theme: AppTheme.light(), + darkTheme: AppTheme.dark(), + themeMode: ThemeMode.light, ); } } diff --git a/lib/shared/ui/colors/dark_mode.dart b/lib/shared/ui/colors/dark_mode.dart new file mode 100644 index 0000000..f2e87ba --- /dev/null +++ b/lib/shared/ui/colors/dark_mode.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class DarkModeColors { + static const darkBackground = Color(0xFF121212); + static const darkSurface = Color(0xFF1E1E1E); + static const darkPrimary = Color(0xFF8B7CFF); + static const darkText = Color(0xFFEDEDED); +} diff --git a/lib/shared/ui/colors/light_mode.dart b/lib/shared/ui/colors/light_mode.dart new file mode 100644 index 0000000..294b9de --- /dev/null +++ b/lib/shared/ui/colors/light_mode.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class LightModeColors { + // Light theme + static const backgroundPrimary = Color(0xFFF6F7F9); + static const backgroundElevated = Color(0xFFEEF0F3); + static const lightSurface = Color(0xFFF5F5F5); + static const lightPrimary = Color(0xFF6C5CE7); + static const lightText = Color(0xFF1A1A1A); + + // Inputs + static const inputBackground = Color(0xFFFFFFFF); + static const inputDefaultBorder = Color(0xFFD9D9D9); + static const inputFocusedBorder = Color(0xFFEFFF1A); + + // Text + static const textPrimary = Color(0xFF14161A); + + // Buttons + static const buttonPrimary = Color(0xFFEFFF1A); +} diff --git a/lib/shared/ui/theme/app_theme.dart b/lib/shared/ui/theme/app_theme.dart new file mode 100644 index 0000000..1401508 --- /dev/null +++ b/lib/shared/ui/theme/app_theme.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import '../colors/dark_mode.dart'; +import '../colors/light_mode.dart'; + +class AppTheme { + static ThemeData light() { + return ThemeData( + brightness: Brightness.light, + scaffoldBackgroundColor: LightModeColors.backgroundPrimary, + colorScheme: const ColorScheme.light( + primary: LightModeColors.lightPrimary, + surface: LightModeColors.backgroundPrimary, + ), + textTheme: const TextTheme( + headlineLarge: TextStyle( + fontSize: 22.0, + fontWeight: FontWeight.w600, + color: LightModeColors.textPrimary, + fontFamily: "Syne", + ), + + headlineMedium: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + bodyMedium: TextStyle( + fontFamily: "Inter", + fontSize: 14, + fontWeight: FontWeight.normal, + color: LightModeColors.textPrimary, + ), + ), + useMaterial3: true, + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + textStyle: TextStyle( + fontSize: 14, + fontFamily: "Inter", + color: LightModeColors.textPrimary, + fontWeight: FontWeight.normal, + ), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + textStyle: TextStyle( + fontSize: 14, + fontFamily: "Inter", + color: LightModeColors.textPrimary, + fontWeight: FontWeight.w600, + ), + foregroundColor: LightModeColors.textPrimary, + backgroundColor: LightModeColors.buttonPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0.0, + ), + ), + + inputDecorationTheme: InputDecorationTheme( + // isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + + // Optional hard height limits + // constraints: BoxConstraints(minHeight: 48, maxHeight: 40), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide( + color: LightModeColors.inputFocusedBorder, + width: 1.0, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderSide: BorderSide( + color: LightModeColors.inputDefaultBorder, + width: 1.0, + ), + ), + fillColor: LightModeColors.inputBackground, + ), + ); + } + + static ThemeData dark() { + return ThemeData( + brightness: Brightness.dark, + scaffoldBackgroundColor: DarkModeColors.darkBackground, + colorScheme: const ColorScheme.dark( + primary: DarkModeColors.darkPrimary, + surface: DarkModeColors.darkSurface, + ), + textTheme: const TextTheme( + bodyMedium: TextStyle(color: DarkModeColors.darkText), + ), + useMaterial3: true, + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.greenAccent, width: 5.0), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: Colors.red, width: 5.0), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 699d294..67ff09e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,30 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + url: "https://pub.dev" + source: hosted + version: "93.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + url: "https://pub.dev" + source: hosted + version: "10.0.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -25,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" clock: dependency: transitive description: @@ -33,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -41,6 +81,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" crypto: dependency: transitive description: @@ -81,6 +137,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -110,11 +190,96 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "6848263f9744072d0977347c383fb8b57d9780319a6bf5238b5a2866a029de62" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "67cd1ff671add31dc13e45194398187a04bb63804b37fa47866afae296d73fcb" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "8f42f359f187a94dce7a3ab2ec5903d013dddfc7127078ebab19fa244c3840e8" + url: "https://pub.dev" + source: hosted + version: "4.2.1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f" + url: "https://pub.dev" + source: hosted + version: "17.2.3" google_cloud: dependency: transitive description: @@ -147,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" http: dependency: "direct main" description: @@ -163,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -179,6 +360,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + jwt_decoder: + dependency: "direct main" + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker: dependency: transitive description: @@ -211,6 +416,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -231,10 +444,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -243,6 +456,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -251,14 +496,110 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - protobuf: + path_provider: dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + protobuf: + dependency: "direct main" description: name: protobuf sha256: "75ec242d22e950bdcc79ee38dd520ce4ee0bc491d7fadc4ea47694604d22bf06" url: "https://pub.dev" source: hosted version: "6.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" + url: "https://pub.dev" + source: hosted + version: "3.2.1" shelf: dependency: transitive description: @@ -267,6 +608,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -276,11 +641,27 @@ packages: dependency: "direct main" description: path: "." - ref: a9a7f05332596f776b37d2757b33f06f6faa92e9 - resolved-ref: a9a7f05332596f776b37d2757b33f06f6faa92e9 + ref: e801928517cee5298eb289f0993eec52e2186d86 + resolved-ref: e801928517cee5298eb289f0993eec52e2186d86 url: "https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git" source: git version: "1.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: @@ -297,6 +678,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -321,14 +710,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" + url: "https://pub.dev" + source: hosted + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" + test_core: + dependency: transitive + description: + name: test_core + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" + url: "https://pub.dev" + source: hosted + version: "0.6.17" typed_data: dependency: transitive description: @@ -353,6 +758,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" web: dependency: "direct main" description: @@ -361,6 +774,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9 + url: "https://pub.dev" + source: hosted + version: "6.2.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.11.5 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 4b2230e..038e2db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,13 +11,18 @@ dependencies: softplayer_dart_proto: git: url: https://gitea.badhouseplants.net/softplayer/softplayer-dart-proto.git - ref: a9a7f05332596f776b37d2757b33f06f6faa92e9 + ref: e801928517cee5298eb289f0993eec52e2186d86 cupertino_icons: ^1.0.9 grpc: 5.1.0 http: ^1.6.0 dio: ^5.9.2 flutter_dotenv: ^6.0.1 web: ^1.1.1 + flutter_riverpod: ^3.3.1 + protobuf: ^6.0.0 + go_router: ^17.2.3 + flutter_secure_storage: ^10.2.0 + jwt_decoder: ^2.0.1 dev_dependencies: flutter_test: sdk: flutter @@ -27,3 +32,10 @@ flutter: assets: - assets/ - .env + fonts: + - family: Syne + fonts: + - asset: assets/fonts/syne/Syne-Regular.ttf + - family: Inter + fonts: + - asset: assets/fonts/inter/Inter-Regular.ttf -- 2.49.1