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:easy_localization/easy_localization.dart' hide tr; 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 'core/i18n/locale_gate.dart'; import 'core/i18n/locale_registry.dart'; import 'core/i18n/locale_utils.dart'; import 'core/i18n/toml_asset_loader.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(); await EasyLocalization.ensureInitialized(); await LocaleRegistry.initialize(); // 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( // URL(/en, /ko)이 있으면 우선 적용해서 첫 렌더부터 올바른 언어로 시작합니다. () { final supportedLocaleCodes = LocaleRegistry.supportedLocaleCodes; final supportedLocales = supportedLocaleCodes .map((code) => Locale(code)) .toList(growable: false); final fallbackLocaleCode = LocaleRegistry.fallbackLocaleCode; final initialLocaleCode = extractLocaleFromPath(Uri.base) ?? resolvePreferredLocaleCode(); return EasyLocalization( supportedLocales: supportedLocales, fallbackLocale: Locale(fallbackLocaleCode), startLocale: Locale(initialLocaleCode), saveLocale: false, path: 'assets/translations', assetLoader: const TomlAssetLoader(), child: const ProviderScope(child: BaronSSOApp()), ); }(), ); } // Router Configuration final _routerLogger = Logger('Router'); final _router = GoRouter( initialLocation: '/', debugLogDiagnostics: !kReleaseMode, refreshListenable: AuthNotifier.instance, routes: [ ShellRoute( builder: (context, state, child) { final localeCode = extractLocaleFromPath(state.uri) ?? resolvePreferredLocaleCode(); return LocaleGate(localeCode: localeCode, child: child); }, routes: [ GoRoute( path: '/:locale', builder: (context, state) { _routerLogger.info("Navigating to root (DashboardScreen)"); return const DashboardScreen(); }, routes: [ GoRoute( path: 'profile', builder: (context, state) => const ProfilePage(), ), GoRoute( path: 'signin', builder: (context, state) { final loginChallenge = state.uri.queryParameters['login_challenge']; final redirectUrl = state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_url']; _routerLogger.info( "Navigating to /signin with login_challenge: $loginChallenge, redirect: $redirectUrl", ); return LoginScreen( key: state.pageKey, loginChallenge: loginChallenge, redirectUrl: redirectUrl, ); }, ), GoRoute( path: 'login', builder: (context, state) { final loginChallenge = state.uri.queryParameters['login_challenge']; final redirectUrl = state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_url']; _routerLogger.info( "Navigating to /login with login_challenge: $loginChallenge, redirect: $redirectUrl", ); return LoginScreen( key: state.pageKey, loginChallenge: loginChallenge, redirectUrl: redirectUrl, ); }, ), 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) { _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'), ); }, ), 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 requestedLocale = extractLocaleFromPath(state.uri); final preferredLocale = resolvePreferredLocaleCode(); if (requestedLocale == null) { return buildLocalizedPath(preferredLocale, state.uri); } final hasStoredToken = AuthTokenStore.getToken() != null; final hasCookieSession = AuthTokenStore.usesCookie(); final isLoggedIn = hasStoredToken || hasCookieSession; final path = stripLocalePath(state.uri); // 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"); return buildSigninRedirectPath(requestedLocale, state.uri); } // 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'), localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, 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; } }