import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'features/auth/presentation/login_screen.dart'; import 'features/auth/presentation/signup_screen.dart'; import 'features/auth/presentation/approve_qr_screen.dart'; import 'features/auth/presentation/qr_scan_screen.dart'; import 'features/auth/presentation/forgot_password_screen.dart'; import 'features/auth/presentation/reset_password_screen.dart'; import 'features/auth/presentation/error_screen.dart'; import 'features/dashboard/presentation/dashboard_screen.dart'; import 'features/admin/presentation/user_management_screen.dart'; import 'features/profile/presentation/pages/profile_page.dart'; import 'core/services/auth_proxy_service.dart'; import 'core/services/auth_token_store.dart'; import 'core/services/logger_service.dart'; import 'core/notifiers/auth_notifier.dart'; import 'package:logging/logging.dart'; import 'features/auth/presentation/consent_screen.dart'; import 'i18n.dart'; final _log = Logger('Main'); Future _loadBundledFonts() async { const family = 'NotoSansKR'; final loader = FontLoader(family); try { loader.addFont(rootBundle.load('assets/fonts/NotoSansKR-Regular.ttf')); loader.addFont(rootBundle.load('assets/fonts/NotoSansKR-Bold.ttf')); await loader.load(); } catch (e) { _log.warning("Failed to preload bundled fonts: $e"); } } void main() async { WidgetsFlutterBinding.ensureInitialized(); usePathUrlStrategy(); // 1. Global Error Handling FlutterError.onError = (details) { FlutterError.presentError(details); _log.severe("FLUTTER_ERROR", details.exception, details.stack); // Also send to backend if needed AuthProxyService.logError("FLUTTER_ERROR: ${details.exception}\n${details.stack}"); }; PlatformDispatcher.instance.onError = (error, stack) { _log.severe("PLATFORM_ERROR", error, stack); AuthProxyService.logError("PLATFORM_ERROR: $error\n$stack"); return true; }; // .env가 없더라도 초기화 상태를 보장하도록 optional 로딩 try { await dotenv.load(fileName: ".env", isOptional: true); } catch (e) { _log.warning("Warning: .env file load failed: $e"); } // 0. Initialize Logger LoggerService.init(); // 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화 await _loadBundledFonts(); runApp(const ProviderScope(child: BaronSSOApp())); } // Router Configuration final _routerLogger = Logger('Router'); final _router = GoRouter( initialLocation: '/', debugLogDiagnostics: !kReleaseMode, refreshListenable: AuthNotifier.instance, routes: [ GoRoute( path: '/', builder: (context, state) { _routerLogger.info("Navigating to root (DashboardScreen)"); return const DashboardScreen(); }, ), GoRoute( path: '/profile', builder: (context, state) => const ProfilePage(), ), GoRoute( path: '/signin', builder: (context, state) { final loginChallenge = state.uri.queryParameters['login_challenge']; _routerLogger.info("Navigating to /signin with login_challenge: $loginChallenge"); return LoginScreen(key: state.pageKey, loginChallenge: loginChallenge); }, ), GoRoute( path: '/login', builder: (context, state) { _routerLogger.info("Navigating to /login"); return LoginScreen(key: state.pageKey); }, ), GoRoute( path: '/consent', builder: (BuildContext context, GoRouterState state) { final consentChallenge = state.uri.queryParameters['consent_challenge']; if (consentChallenge == null) { _routerLogger.warning("Consent screen loaded without a challenge."); return const Scaffold(body: Center(child: Text('Error: Consent challenge is missing.'))); } _routerLogger.info("Navigating to /consent with challenge."); return ConsentScreen(consentChallenge: consentChallenge); }, ), GoRoute( path: '/signup', builder: (context, state) { _routerLogger.info("Navigating to /signup"); return const SignupScreen(); }, ), GoRoute( path: '/registration', builder: (context, state) { _routerLogger.info("Navigating to /registration"); return const SignupScreen(); }, ), GoRoute( path: '/verify', builder: (context, state) { _routerLogger.info("Navigating to /verify (query)"); return LoginScreen(key: state.pageKey); }, ), GoRoute( path: '/verify/:token', builder: (context, state) { final token = state.pathParameters['token']; _routerLogger.info("Navigating to /verify with token: $token"); return LoginScreen(key: state.pageKey, verificationToken: token); }, ), GoRoute( path: '/verification', builder: (context, state) { _routerLogger.info("Navigating to /verification"); return LoginScreen(key: state.pageKey); }, ), GoRoute( path: '/l/:shortCode', builder: (context, state) { final shortCode = state.pathParameters['shortCode']; _routerLogger.info("Navigating to /l with code: $shortCode"); return LoginScreen(key: state.pageKey); }, ), GoRoute( path: '/forgot-password', builder: (context, state) { _routerLogger.info("Navigating to /forgot-password"); return const ForgotPasswordScreen(); }, ), GoRoute( path: '/recovery', builder: (context, state) { _routerLogger.info("Navigating to /recovery"); return const ForgotPasswordScreen(); }, ), GoRoute( // Supports both /reset-password and /reset-password?token=... path: '/reset-password', builder: (context, state) { // For deep linking, you might pass the token in the path, e.g., /reset-password/:token // final token = state.pathParameters['token']; _routerLogger.info("Navigating to /reset-password"); return const ResetPasswordScreen(); }, ), GoRoute( path: '/error', builder: (context, state) { _routerLogger.info("Navigating to /error"); final params = state.uri.queryParameters; return ErrorScreen( errorId: params['id'], errorCode: params['error'], description: params['error_description'] ?? params['message'], ); }, ), GoRoute( path: '/settings', builder: (context, state) { _routerLogger.info("Navigating to /settings (disabled)"); return ErrorScreen( errorCode: 'settings_disabled', description: tr( 'msg.userfront.settings.disabled', fallback: '현재 계정 설정 화면은 준비 중입니다.', ), ); }, ), GoRoute( path: '/approve', builder: (context, state) { final ref = state.uri.queryParameters['ref']; _routerLogger.info("Navigating to /approve with ref: $ref"); return ApproveQrScreen(pendingRef: ref); }, ), GoRoute( path: '/ql/:ref', builder: (context, state) { final ref = state.pathParameters['ref']; _routerLogger.info("Navigating to /ql with ref: $ref"); return ApproveQrScreen(pendingRef: ref); }, ), GoRoute( path: '/scan', builder: (context, state) { _routerLogger.info("Navigating to /scan"); return const QRScanScreen(); }, ), GoRoute( path: '/admin/users', builder: (context, state) { _routerLogger.info("Navigating to /admin/users"); return const UserManagementScreen(); }, ), ], redirect: (context, state) { final hasStoredToken = AuthTokenStore.getToken() != null; final hasCookieSession = AuthTokenStore.usesCookie(); final isLoggedIn = hasStoredToken || hasCookieSession; final path = state.uri.path; // Public paths that don't require login final isPublicPath = path == '/signin' || path == '/signup' || path == '/login' || path == '/registration' || path == '/verify' || path == '/verification' || path.startsWith('/verify/') || path == '/approve' || path.startsWith('/ql/') || path == '/forgot-password' || path == '/recovery' || path == '/reset-password' || path == '/error' || path == '/settings' || path == '/consent'; // Consent page is public _routerLogger.fine("Redirect check - Path: $path, IsLoggedIn: $isLoggedIn"); // 0. ALWAYS allow public paths to proceed so they can function if (isPublicPath) { return null; } // If not logged in and trying to access a protected page, redirect to /signin if (!isLoggedIn) { _routerLogger.info("Not logged in, redirecting to /signin"); // Preserve OIDC challenge if present final loginChallenge = state.uri.queryParameters['login_challenge']; if (loginChallenge != null) { return '/signin?login_challenge=$loginChallenge'; } return '/signin'; } // If logged in and trying to access login page, redirect to root (dashboard) // This is now implicitly handled by the isPublicPath check, but kept for clarity. // if (isLoggedIn && path == '/signin') { // _routerLogger.info("Logged in, redirecting to /"); // return '/'; // } return null; }, ); class BaronSSOApp extends StatelessWidget { const BaronSSOApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( title: tr('ui.userfront.app_title', fallback: 'Baron 로그인'), theme: ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base brightness: Brightness.light, ), useMaterial3: true, fontFamily: 'NotoSansKR', pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.android: NoTransitionsBuilder(), TargetPlatform.iOS: NoTransitionsBuilder(), TargetPlatform.linux: NoTransitionsBuilder(), TargetPlatform.macOS: NoTransitionsBuilder(), TargetPlatform.windows: NoTransitionsBuilder(), TargetPlatform.fuchsia: NoTransitionsBuilder(), }, ), ), routerConfig: _router, ); } } class NoTransitionsBuilder extends PageTransitionsBuilder { const NoTransitionsBuilder(); @override Widget buildTransitions( PageRoute route, BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) { return child; } }