// ignore_for_file: avoid_print 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/flutter_web_plugins.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 _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', // Note: Removed direct builder here to prevent interference with sub-routes routes: [ GoRoute( path: '', // Matches /:locale builder: (context, state) { return const DashboardScreen(); }, ), 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']; return LoginScreen( key: state.pageKey, loginChallenge: loginChallenge, redirectUrl: redirectUrl, ); }, ), GoRoute( path: 'login', builder: (context, state) { // IMPORTANT: Match signin logic to handle OIDC challenges final loginChallenge = state.uri.queryParameters['login_challenge']; final redirectUrl = state.uri.queryParameters['redirect_uri'] ?? state.uri.queryParameters['redirect_url']; 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) { return const Scaffold( body: Center(child: Text('Error: Consent challenge is missing.')), ); } return ConsentScreen(consentChallenge: consentChallenge); }, ), GoRoute( path: 'signup', builder: (context, state) => const SignupScreen(), ), GoRoute( path: 'registration', builder: (context, state) => const SignupScreen(), ), GoRoute( path: 'verify', builder: (context, state) => LoginScreen(key: state.pageKey), ), GoRoute( path: 'verify/:token', builder: (context, state) { final token = state.pathParameters['token']; return LoginScreen( key: state.pageKey, verificationToken: token, ); }, ), GoRoute( path: 'verification', builder: (context, state) => LoginScreen(key: state.pageKey), ), GoRoute( path: 'l/:shortCode', builder: (context, state) { return LoginScreen(key: state.pageKey); }, ), GoRoute( path: 'forgot-password', builder: (context, state) => const ForgotPasswordScreen(), ), GoRoute( path: 'recovery', builder: (context, state) => const ForgotPasswordScreen(), ), GoRoute( path: 'reset-password', builder: (context, state) => const ResetPasswordScreen(), ), GoRoute( path: 'error', builder: (context, state) { 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) => ErrorScreen( errorCode: 'settings_disabled', description: tr('msg.userfront.settings.disabled'), ), ), GoRoute( path: 'approve', builder: (context, state) => ApproveQrScreen( pendingRef: state.uri.queryParameters['ref'], ), ), GoRoute( path: 'ql/:ref', builder: (context, state) => ApproveQrScreen( pendingRef: state.pathParameters['ref'], ), ), GoRoute( path: 'scan', builder: (context, state) => const QRScanScreen(), ), GoRoute( path: 'admin/users', builder: (context, state) => const UserManagementScreen(), ), ], ), ], ), ], redirect: (context, state) { final uri = state.uri; final requestedLocale = extractLocaleFromPath(uri); final preferredLocale = resolvePreferredLocaleCode(); if (requestedLocale == null) { final localizedPath = buildLocalizedPath(preferredLocale, uri); return localizedPath; } final token = AuthTokenStore.getToken(); final isLoggedIn = (token != null && token.isNotEmpty) || AuthTokenStore.usesCookie(); final path = stripLocalePath(uri); // Precise public path detection 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' || path.startsWith('/consent/') || uri.path.contains('/consent'); if (isPublicPath) { return null; } if (!isLoggedIn) { return buildSigninRedirectPath(requestedLocale, uri); } 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; } }