feat(app): Flutter-клиент — логин, дашборд, потоки, категории, транзакции, правила
Riverpod + go_router с auth-guard, NavigationRail на широком экране и bottom bar на узком. Токены в flutter_secure_storage, на web access живёт в памяти. Интерцептор подставляет токен и делает ровно один refresh на 401. Деньги приходят строками и форматируются через Decimal: парсить их в double значило бы терять копейки ровно там, где бэкенд их бережёт.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'core/auth/auth_controller.dart';
|
||||
import 'features/accounts/accounts_page.dart';
|
||||
import 'features/cashflow/cashflow_page.dart';
|
||||
import 'features/categories/categories_page.dart';
|
||||
import 'features/home/home_page.dart';
|
||||
import 'features/login/login_page.dart';
|
||||
import 'features/rules/rules_page.dart';
|
||||
import 'features/settings/settings_page.dart';
|
||||
import 'features/shell/app_shell.dart';
|
||||
import 'features/sync/sync_page.dart';
|
||||
import 'features/transactions/transactions_page.dart';
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final auth = ValueNotifier<AuthState>(ref.read(authControllerProvider));
|
||||
ref.listen(authControllerProvider, (_, next) => auth.value = next);
|
||||
ref.onDispose(auth.dispose);
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/',
|
||||
refreshListenable: auth,
|
||||
redirect: (context, state) {
|
||||
final status = auth.value.status;
|
||||
final atLogin = state.matchedLocation == '/login';
|
||||
return switch (status) {
|
||||
AuthStatus.unknown => atLogin ? null : '/login',
|
||||
AuthStatus.signedOut => atLogin ? null : '/login',
|
||||
AuthStatus.signedIn => atLogin ? '/' : null,
|
||||
};
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/login', builder: (_, _) => const LoginPage()),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) =>
|
||||
AppShell(location: state.matchedLocation, child: child),
|
||||
routes: [
|
||||
GoRoute(path: '/', builder: (_, _) => const HomePage()),
|
||||
GoRoute(path: '/accounts', builder: (_, _) => const AccountsPage()),
|
||||
GoRoute(path: '/cashflow', builder: (_, _) => const CashflowPage()),
|
||||
GoRoute(
|
||||
path: '/categories',
|
||||
builder: (_, state) => CategoriesPage(initialMonth: state.uri.queryParameters['month']),
|
||||
),
|
||||
GoRoute(path: '/transactions', builder: (_, _) => const TransactionsPage()),
|
||||
GoRoute(path: '/rules', builder: (_, _) => const RulesPage()),
|
||||
GoRoute(path: '/sync', builder: (_, _) => const SyncPage()),
|
||||
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user