import 'dart:async'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'api/support_api.dart'; import 'app_scope.dart'; import 'inbox.dart'; import 'push.dart'; import 'screens/inbox_screen.dart'; import 'screens/login_screen.dart'; import 'screens/thread_screen.dart'; import 'session.dart'; class KickKingsApp extends StatefulWidget { const KickKingsApp({ super.key, required this.firebaseReady, this.session, this.inbox, this.push, this.api, }); final bool firebaseReady; final SessionController? session; final InboxController? inbox; final PushService? push; final SupportApi? api; @override State createState() => _KickKingsAppState(); } class _KickKingsAppState extends State { final _navigatorKey = GlobalKey(); final _messengerKey = GlobalKey(); late final RouteObserver> _routeObserver; late final SupportApi _api; late final SessionController _session; late final InboxController _inbox; late final PushService _push; var _wasSignedIn = false; var _openScheduled = false; @override void initState() { super.initState(); _routeObserver = RouteObserver>(); _api = widget.api ?? SupportApi(); _session = widget.session ?? SessionController(store: SessionStore(), api: _api); _inbox = widget.inbox ?? InboxController(api: _api); _push = widget.push ?? PushService(firebaseReady: widget.firebaseReady); _api.onUnauthorized = _session.handleUnauthorized; _session.addListener(_onSession); unawaited(_boot()); } Future _boot() async { await _session.restore(); await _push.listen( onOpenTicket: _rememberTicket, onForegroundMessage: _onForegroundMessage, onToken: _registerToken, isSignedIn: () => _session.isSignedIn, ); if (_session.isSignedIn) { await _registerCurrentDevice(); } _openPendingTicket(); } void _onSession() { final signedIn = _session.isSignedIn; if (signedIn && !_wasSignedIn) { unawaited(_registerCurrentDevice()); _openPendingTicket(); } if (!signedIn && _wasSignedIn) { _inbox.reset(); WidgetsBinding.instance.addPostFrameCallback((_) { _navigatorKey.currentState?.popUntil((route) => route.isFirst); }); } _wasSignedIn = signedIn; } Future _registerCurrentDevice() async { if (!_session.isSignedIn) { return; } try { await _push.register(_registerToken); } on Object { // A denied permission or a missing Firebase config leaves the inbox usable. } } Future _registerToken(String token, String platform) async { if (!_session.isSignedIn) { return; } await _api.registerDevice(token: token, platform: platform); await _session.saveFcmToken(token); } void _onForegroundMessage(RemoteMessage message) { if (!_session.isSignedIn) { return; } unawaited(_inbox.refreshList()); final ticketId = message.data['ticketId']; if (ticketId is String && ticketId.isNotEmpty && ticketId == _inbox.activeTicketId) { unawaited(_inbox.refreshThread()); } final title = message.notification?.title; if (title != null && title.isNotEmpty) { _messengerKey.currentState?.showSnackBar(SnackBar(content: Text(title))); } } void _rememberTicket(String ticketId) { _session.pendingTicketId = ticketId; _openPendingTicket(); } void _openPendingTicket() { if (_openScheduled) { return; } final ticketId = _session.pendingTicketId; if (ticketId == null || !_session.isSignedIn) { return; } _openScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _openScheduled = false; if (!mounted || _session.pendingTicketId != ticketId || !_session.isSignedIn) { return; } if (_inbox.activeTicketId == ticketId) { _session.pendingTicketId = null; unawaited(_inbox.refreshThread()); return; } _session.pendingTicketId = null; _navigatorKey.currentState?.push( MaterialPageRoute( builder: (_) => ThreadScreen(ticketId: ticketId), ), ); }); } @override void dispose() { _session.removeListener(_onSession); super.dispose(); } @override Widget build(BuildContext context) { return AppScope( session: _session, inbox: _inbox, routeObserver: _routeObserver, child: MaterialApp( title: 'Kick Kings', navigatorKey: _navigatorKey, scaffoldMessengerKey: _messengerKey, navigatorObservers: [_routeObserver], theme: _theme(Brightness.light), darkTheme: _theme(Brightness.dark), home: ListenableBuilder( listenable: _session, builder: (context, _) { if (!_session.ready) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } if (!_session.isSignedIn) { return const LoginScreen(); } return const InboxScreen(); }, ), ), ); } } ThemeData _theme(Brightness brightness) { final scheme = ColorScheme.fromSeed( seedColor: const Color(0xFF0F6B4C), brightness: brightness, ); return ThemeData( colorScheme: scheme, useMaterial3: true, appBarTheme: AppBarTheme( backgroundColor: scheme.surface, foregroundColor: scheme.onSurface, ), ); }