diff --git a/apps/lib/core/router/app_router.dart b/apps/lib/core/router/app_router.dart index 9c270b5..69948ca 100644 --- a/apps/lib/core/router/app_router.dart +++ b/apps/lib/core/router/app_router.dart @@ -1,5 +1,7 @@ import 'package:go_router/go_router.dart'; - +import '../../features/auth/presentation/bloc/auth_bloc.dart'; +import '../../features/auth/presentation/bloc/auth_state.dart'; +import 'go_router_refresh_stream.dart'; import '../../features/auth/ui/screens/login_email_screen.dart'; import '../../features/auth/ui/screens/login_password_screen.dart'; import '../../features/auth/ui/screens/login_code_screen.dart'; @@ -20,78 +22,114 @@ import '../../features/settings/ui/screens/features_screen.dart'; import '../../features/settings/ui/screens/memory_screen.dart'; import '../../features/settings/ui/screens/account_screen.dart'; -final appRouter = GoRouter( - initialLocation: '/', - routes: [ - GoRoute(path: '/', builder: (context, state) => const LoginEmailScreen()), - GoRoute( - path: '/login/password', - builder: (context, state) => const LoginPasswordScreen(), - ), - GoRoute( - path: '/login/code', - builder: (context, state) => const LoginCodeScreen(), - ), - GoRoute( - path: '/register', - builder: (context, state) => const RegisterScreen(), - ), - GoRoute( - path: '/register/step2', - builder: (context, state) => const RegisterStep2Screen(), - ), - GoRoute(path: '/home', builder: (context, state) => const HomeScreen()), - GoRoute( - path: '/messages/invites', - builder: (context, state) => const MessageInviteListScreen(), - ), - GoRoute( - path: '/messages/invites/:id', - builder: (context, state) => const MessageInviteDetailScreen(), - ), - GoRoute( - path: '/contacts', - builder: (context, state) => const ContactsScreen(), - ), - GoRoute( - path: '/contacts/add', - builder: (context, state) => const AddContactScreen(), - ), - GoRoute( - path: '/calendar/dayweek', - builder: (context, state) => const CalendarDayWeekScreen(), - ), - GoRoute( - path: '/calendar/month', - builder: (context, state) => const CalendarMonthScreen(), - ), - GoRoute( - path: '/calendar/events/:id', - builder: (context, state) => const CalendarEventDetailScreen(), - ), - GoRoute( - path: '/todo', - builder: (context, state) => const TodoQuadrantsScreen(), - ), - GoRoute( - path: '/todo/:id', - builder: (context, state) => const TodoDetailScreen(), - ), - GoRoute( - path: '/settings', - builder: (context, state) => const SettingsScreen(), - ), - GoRoute( - path: '/settings/features', - builder: (context, state) => const FeaturesScreen(), - ), - GoRoute( - path: '/settings/memory', - builder: (context, state) => const MemoryScreen(), - ), - GoRoute( - path: '/settings/account', - builder: (context, state) => const AccountScreen(), - ), - ], -); +final _protectedRoutes = [ + '/home', + '/contacts', + '/contacts/add', + '/calendar/dayweek', + '/calendar/month', + '/calendar/events', + '/todo', + '/settings', + '/settings/features', + '/settings/memory', + '/settings/account', + '/messages/invites', +]; + +GoRouter createAppRouter(AuthBloc authBloc) { + return GoRouter( + initialLocation: '/', + refreshListenable: GoRouterRefreshStream(authBloc.stream), + redirect: (context, state) { + final authState = authBloc.state; + final isAuthenticated = authState is AuthAuthenticated; + final isAuthRoute = + state.matchedLocation.startsWith('/login') || + state.matchedLocation.startsWith('/register'); + final isProtected = _protectedRoutes.any( + (route) => state.matchedLocation.startsWith(route), + ); + + if (!isAuthenticated && isProtected) { + return '/'; + } + if (isAuthenticated && isAuthRoute) { + return '/home'; + } + return null; + }, + routes: [ + GoRoute(path: '/', builder: (context, state) => const LoginEmailScreen()), + GoRoute( + path: '/login/password', + builder: (context, state) => const LoginPasswordScreen(), + ), + GoRoute( + path: '/login/code', + builder: (context, state) => const LoginCodeScreen(), + ), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterScreen(), + ), + GoRoute( + path: '/register/step2', + builder: (context, state) => const RegisterStep2Screen(), + ), + GoRoute(path: '/home', builder: (context, state) => const HomeScreen()), + GoRoute( + path: '/messages/invites', + builder: (context, state) => const MessageInviteListScreen(), + ), + GoRoute( + path: '/messages/invites/:id', + builder: (context, state) => const MessageInviteDetailScreen(), + ), + GoRoute( + path: '/contacts', + builder: (context, state) => const ContactsScreen(), + ), + GoRoute( + path: '/contacts/add', + builder: (context, state) => const AddContactScreen(), + ), + GoRoute( + path: '/calendar/dayweek', + builder: (context, state) => const CalendarDayWeekScreen(), + ), + GoRoute( + path: '/calendar/month', + builder: (context, state) => const CalendarMonthScreen(), + ), + GoRoute( + path: '/calendar/events/:id', + builder: (context, state) => const CalendarEventDetailScreen(), + ), + GoRoute( + path: '/todo', + builder: (context, state) => const TodoQuadrantsScreen(), + ), + GoRoute( + path: '/todo/:id', + builder: (context, state) => const TodoDetailScreen(), + ), + GoRoute( + path: '/settings', + builder: (context, state) => const SettingsScreen(), + ), + GoRoute( + path: '/settings/features', + builder: (context, state) => const FeaturesScreen(), + ), + GoRoute( + path: '/settings/memory', + builder: (context, state) => const MemoryScreen(), + ), + GoRoute( + path: '/settings/account', + builder: (context, state) => const AccountScreen(), + ), + ], + ); +} diff --git a/apps/lib/core/router/go_router_refresh_stream.dart b/apps/lib/core/router/go_router_refresh_stream.dart new file mode 100644 index 0000000..722069c --- /dev/null +++ b/apps/lib/core/router/go_router_refresh_stream.dart @@ -0,0 +1,17 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; + +class GoRouterRefreshStream extends ChangeNotifier { + GoRouterRefreshStream(Stream stream) { + notifyListeners(); + _subscription = stream.listen((_) => notifyListeners()); + } + + late final StreamSubscription _subscription; + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/apps/lib/main.dart b/apps/lib/main.dart index b7bc359..9f9c061 100644 --- a/apps/lib/main.dart +++ b/apps/lib/main.dart @@ -1,21 +1,36 @@ import 'package:flutter/material.dart'; -import 'core/theme/app_theme.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'core/di/injection.dart'; import 'core/router/app_router.dart'; +import 'core/theme/app_theme.dart'; +import 'features/auth/presentation/bloc/auth_bloc.dart'; +import 'features/auth/presentation/bloc/auth_event.dart'; -void main() { - runApp(const LinksyApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await configureDependencies(); + + final authBloc = sl(); + authBloc.add(AuthStarted()); + + runApp(LinksyApp(authBloc: authBloc)); } class LinksyApp extends StatelessWidget { - const LinksyApp({super.key}); + final AuthBloc authBloc; + + const LinksyApp({super.key, required this.authBloc}); @override Widget build(BuildContext context) { - return MaterialApp.router( - title: 'Linksy', - debugShowCheckedModeBanner: false, - theme: AppTheme.light, - routerConfig: appRouter, + return BlocProvider.value( + value: authBloc, + child: MaterialApp.router( + title: 'Linksy', + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + routerConfig: createAppRouter(authBloc), + ), ); } } diff --git a/apps/test/widget_test.dart b/apps/test/widget_test.dart index ee923f4..49c2130 100644 --- a/apps/test/widget_test.dart +++ b/apps/test/widget_test.dart @@ -1,10 +1,27 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:social_app/main.dart'; +import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart'; +import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; + +class MockAuthBloc extends Mock implements AuthBloc {} + +class FakeAuthState extends Fake implements AuthState {} void main() { + setUpAll(() { + registerFallbackValue(FakeAuthState()); + }); + testWidgets('Login screen loads correctly', (WidgetTester tester) async { - await tester.pumpWidget(const LinksyApp()); + final mockAuthBloc = MockAuthBloc(); + when(() => mockAuthBloc.state).thenReturn(AuthInitial()); + when( + () => mockAuthBloc.stream, + ).thenAnswer((_) => Stream.value(AuthInitial())); + + await tester.pumpWidget(LinksyApp(authBloc: mockAuthBloc)); expect(find.text('linksy'), findsOneWidget); expect(find.text('继续'), findsOneWidget); expect(find.text('还没有账号?去注册'), findsOneWidget); @@ -13,7 +30,13 @@ void main() { testWidgets('Main content is vertically centered above footer', ( WidgetTester tester, ) async { - await tester.pumpWidget(const LinksyApp()); + final mockAuthBloc = MockAuthBloc(); + when(() => mockAuthBloc.state).thenReturn(AuthInitial()); + when( + () => mockAuthBloc.stream, + ).thenAnswer((_) => Stream.value(AuthInitial())); + + await tester.pumpWidget(LinksyApp(authBloc: mockAuthBloc)); final safeAreaRect = tester.getRect(find.byType(SafeArea)); final mainRect = tester.getRect(