import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tatlock_ui/core/auth/auth_provider.dart'; import 'package:tatlock_ui/features/control_room/router.dart'; import 'package:tatlock_ui/features/front_hall/presentation/pages/front_hall_page.dart'; import 'package:tatlock_ui/features/security/router.dart'; import 'package:tatlock_ui/shared/layouts/app_scaffold.dart'; part 'app_router.g.dart'; /// Route paths as constants. abstract class AppRoutes { static const frontHall = '/'; static const parlor = '/parlor'; static const settings = '/settings'; static const callback = '/callback'; } /// Provides the GoRouter instance. @riverpod GoRouter appRouter(Ref ref) { return GoRouter( initialLocation: AppRoutes.frontHall, debugLogDiagnostics: true, routes: [ // OIDC callback route (handles auth code exchange) GoRoute( path: AppRoutes.callback, name: 'callback', builder: (context, state) => _OidcCallbackPage( code: state.uri.queryParameters['code'], callbackState: state.uri.queryParameters['state'], error: state.uri.queryParameters['error'], errorDescription: state.uri.queryParameters['error_description'], ), ), // Main app routes (inside shell with app scaffold) ShellRoute( builder: (context, state, child) => AppScaffold(child: child), routes: [ GoRoute( path: AppRoutes.frontHall, name: 'frontHall', builder: (context, state) => const FrontHallPage(), ), ...controlRoomRoutes(), ...securityRoutes(), GoRoute( path: AppRoutes.parlor, name: 'parlor', builder: (context, state) => const _PlaceholderPage(title: 'Parlor'), ), GoRoute( path: AppRoutes.settings, name: 'settings', builder: (context, state) => const _PlaceholderPage(title: 'Settings'), ), ], ), ], ); } /// Placeholder page for routes not yet implemented. class _PlaceholderPage extends StatelessWidget { const _PlaceholderPage({required this.title}); final String title; @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.construction, size: 64, color: Theme.of(context).colorScheme.outline, ), const SizedBox(height: 16), Text( title, style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 8), Text( 'Coming soon', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.outline, ), ), ], ), ); } } /// OIDC callback page that handles the authorization code exchange. class _OidcCallbackPage extends ConsumerStatefulWidget { const _OidcCallbackPage({ this.code, this.callbackState, this.error, this.errorDescription, }); final String? code; final String? callbackState; final String? error; final String? errorDescription; @override ConsumerState<_OidcCallbackPage> createState() => _OidcCallbackPageState(); } class _OidcCallbackPageState extends ConsumerState<_OidcCallbackPage> { bool _isProcessing = true; String? _error; @override void initState() { super.initState(); // Defer callback processing to avoid Riverpod state modification during build WidgetsBinding.instance.addPostFrameCallback((_) { _processCallback(); }); } Future _processCallback() async { // Check mounted before any async work if (!mounted) return; // Check for error from Authentik if (widget.error != null) { setState(() { _isProcessing = false; _error = widget.errorDescription ?? widget.error; }); return; } // Check for required parameters if (widget.code == null || widget.callbackState == null) { setState(() { _isProcessing = false; _error = 'Invalid callback - missing code or state parameter'; }); return; } // Get notifier reference before async gap to avoid disposed ref errors final authNotifier = ref.read(authProvider.notifier); // Exchange code for tokens try { await authNotifier.handleOidcCallback( widget.code!, widget.callbackState!, ); // Navigate to home on success if (mounted) { context.go(AppRoutes.frontHall); } } catch (e) { if (mounted) { setState(() { _isProcessing = false; _error = e.toString(); }); } } } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Card( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( _error != null ? Icons.error_outline : Icons.home_work_outlined, size: 64, color: _error != null ? colorScheme.error : colorScheme.primary, ), const SizedBox(height: 24), Text( _error != null ? 'Authentication Failed' : 'Signing in...', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), if (_isProcessing) const CircularProgressIndicator() else if (_error != null) ...[ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.errorContainer, borderRadius: BorderRadius.circular(8), ), child: Text( _error!, style: TextStyle(color: colorScheme.onErrorContainer), textAlign: TextAlign.center, ), ), const SizedBox(height: 16), OutlinedButton.icon( onPressed: () => context.go(AppRoutes.frontHall), icon: const Icon(Icons.refresh), label: const Text('Try again'), style: OutlinedButton.styleFrom( minimumSize: const Size(double.infinity, 48), ), ), ], ], ), ), ), ), ), ); } }