import "dart:async"; import "dart:math"; import "package:flutter/material.dart"; import "package:provider/provider.dart"; import "package:dchs_flutter_beacon/dchs_flutter_beacon.dart"; import "../app/app_router.dart"; import "../app/app_state.dart"; import "../services/api.dart"; import "../services/auth_storage.dart"; import "../services/beacon_cache.dart"; import "../services/beacon_permissions.dart"; import "../services/preload_cache.dart"; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @override State createState() => _SplashScreenState(); } class _SplashScreenState extends State with TickerProviderStateMixin { // Track if permissions were freshly granted (needs Bluetooth warmup delay) bool _permissionsWereFreshlyGranted = false; // Bouncing logo animation late AnimationController _bounceController; double _x = 100; double _y = 100; double _dx = 2.5; double _dy = 2.0; Color _logoColor = Colors.white; final Random _random = Random(); // Rotating status text Timer? _statusTimer; int _statusIndex = 0; static const List _statusPhrases = [ "scanning...", "listening...", "searching...", "locating...", "analyzing...", "connecting...", ]; // Beacon scanning state - new approach: scan all, then lookup final Map> _beaconRssiSamples = {}; final Map _beaconDetectionCount = {}; bool _scanComplete = false; BeaconLookupResult? _bestBeacon; static const List _colors = [ Colors.white, Colors.red, Colors.green, Colors.blue, Colors.yellow, Colors.purple, Colors.cyan, Colors.orange, ]; @override void initState() { super.initState(); print('[Splash] 🚀 Starting with bouncing logo + beacon scan'); // Start bouncing animation _bounceController = AnimationController( vsync: this, duration: const Duration(milliseconds: 16), // ~60fps )..addListener(_updatePosition)..repeat(); // Start rotating status text (randomized) _statusTimer = Timer.periodic(const Duration(milliseconds: 1600), (_) { if (mounted) { setState(() { int newIndex; do { newIndex = _random.nextInt(_statusPhrases.length); } while (newIndex == _statusIndex && _statusPhrases.length > 1); _statusIndex = newIndex; }); } }); // Start the initialization flow _initializeApp(); } void _updatePosition() { if (!mounted) return; final size = MediaQuery.of(context).size; const logoWidth = 180.0; const logoHeight = 60.0; // Skip if screen size not yet available if (size.width <= logoWidth || size.height <= logoHeight) return; final maxX = size.width - logoWidth; final maxY = size.height - logoHeight; setState(() { _x += _dx; _y += _dy; // Bounce off edges and change color if (_x <= 0 || _x >= maxX) { _dx = -_dx; _changeColor(); } if (_y <= 0 || _y >= maxY) { _dy = -_dy; _changeColor(); } // Keep in bounds _x = _x.clamp(0.0, maxX); _y = _y.clamp(0.0, maxY); }); } void _changeColor() { final newColor = _colors[_random.nextInt(_colors.length)]; if (newColor != _logoColor) { _logoColor = newColor; } } Future _initializeApp() async { // Run auth check and preloading in parallel for faster startup print('[Splash] 🚀 Starting parallel initialization...'); // Start preloading data in background (fire and forget for non-critical data) PreloadCache.preloadAll(); // Check for saved auth credentials print('[Splash] 🔐 Checking for saved auth credentials...'); final credentials = await AuthStorage.loadAuth(); if (credentials != null && mounted) { print('[Splash] ✅ Found saved credentials: UserID=${credentials.userId}, token=${credentials.token.substring(0, 8)}...'); Api.setAuthToken(credentials.token); // Validate token is still valid by calling profile endpoint print('[Splash] 🔍 Validating token with server...'); final isValid = await _validateToken(); if (isValid && mounted) { print('[Splash] ✅ Token is valid'); final appState = context.read(); appState.setUserId(credentials.userId); } else { print('[Splash] ❌ Token is invalid or expired, clearing saved auth'); await AuthStorage.clearAuth(); Api.clearAuthToken(); } } else { print('[Splash] ❌ No saved credentials found'); } // Start beacon scanning in background await _performBeaconScan(); // Navigate based on results if (!mounted) return; _navigateToNextScreen(); } /// Validates the stored token by making a profile API call Future _validateToken() async { try { final profile = await Api.getProfile(); return profile.userId > 0; } catch (e) { print('[Splash] Token validation failed: $e'); return false; } } Future _performBeaconScan() async { print('[Splash] 📡 Starting beacon scan...'); // Check if permissions are already granted BEFORE requesting final alreadyHadPermissions = await BeaconPermissions.checkPermissions(); // Request permissions (will be instant if already granted) final granted = await BeaconPermissions.requestPermissions(); if (!granted) { print('[Splash] ❌ Permissions denied'); _scanComplete = true; return; } // If permissions were just granted (not already had), Bluetooth needs warmup _permissionsWereFreshlyGranted = !alreadyHadPermissions; if (_permissionsWereFreshlyGranted) { print('[Splash] 🆕 Permissions freshly granted - will add warmup delay'); } // Check if Bluetooth is ON print('[Splash] 📶 Checking Bluetooth state...'); final bluetoothOn = await BeaconPermissions.ensureBluetoothEnabled(); if (!bluetoothOn) { print('[Splash] ❌ Bluetooth is OFF - cannot scan for beacons'); _scanComplete = true; return; } print('[Splash] ✅ Bluetooth is ON'); // Step 1: Try to load beacon list from cache first, then fetch from server print('[Splash] 📥 Loading beacon list...'); Map knownBeacons = {}; // Try cache first final cached = await BeaconCache.load(); if (cached != null && cached.isNotEmpty) { print('[Splash] ✅ Got ${cached.length} beacon UUIDs from cache'); knownBeacons = cached; // Refresh cache in background (fire and forget) Api.listAllBeacons().then((fresh) { BeaconCache.save(fresh); print('[Splash] 🔄 Background refresh: saved ${fresh.length} beacons to cache'); }).catchError((e) { print('[Splash] ⚠️ Background refresh failed: $e'); }); } else { // No cache - must fetch from server try { knownBeacons = await Api.listAllBeacons(); print('[Splash] ✅ Got ${knownBeacons.length} beacon UUIDs from server'); // Save to cache await BeaconCache.save(knownBeacons); } catch (e) { print('[Splash] ❌ Failed to fetch beacons: $e'); _scanComplete = true; return; } } if (knownBeacons.isEmpty) { print('[Splash] ⚠️ No beacons configured'); _scanComplete = true; return; } // Initialize beacon scanning try { await flutterBeacon.initializeScanning; // Only add delay if permissions were freshly granted (Bluetooth subsystem needs warmup) if (_permissionsWereFreshlyGranted) { print('[Splash] 🔄 Fresh permissions - adding Bluetooth warmup delay'); await Future.delayed(const Duration(milliseconds: 1500)); } // Create regions for all known UUIDs final regions = knownBeacons.keys.map((uuid) { final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}'; return Region(identifier: uuid, proximityUUID: formattedUUID); }).toList(); // Single scan - collect samples for 2 seconds print('[Splash] 🔍 Scanning...'); StreamSubscription? subscription; subscription = flutterBeacon.ranging(regions).listen((result) { for (var beacon in result.beacons) { final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', ''); final rssi = beacon.rssi; _beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi); _beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1; print('[Splash] 📶 Found $uuid RSSI=$rssi'); } }); await Future.delayed(const Duration(milliseconds: 2000)); await subscription.cancel(); // Now lookup business info for found beacons if (_beaconRssiSamples.isNotEmpty) { print('[Splash] 🔍 Looking up ${_beaconRssiSamples.length} beacons...'); final uuids = _beaconRssiSamples.keys.toList(); try { final lookupResults = await Api.lookupBeacons(uuids); print('[Splash] 📋 Server returned ${lookupResults.length} registered beacons'); // Find the best registered beacon based on RSSI _bestBeacon = _findBestRegisteredBeacon(lookupResults); } catch (e) { print('[Splash] Error looking up beacons: $e'); } } print('[Splash] 🎯 Best beacon: ${_bestBeacon?.beaconName ?? "none"}'); } catch (e) { print('[Splash] Scan error: $e'); } _scanComplete = true; } /// Find the best registered beacon from lookup results based on RSSI BeaconLookupResult? _findBestRegisteredBeacon(List registeredBeacons) { if (registeredBeacons.isEmpty) return null; BeaconLookupResult? best; double bestAvgRssi = -999; for (final beacon in registeredBeacons) { final samples = _beaconRssiSamples[beacon.uuid]; if (samples == null || samples.isEmpty) continue; final detections = _beaconDetectionCount[beacon.uuid] ?? 0; if (detections < 2) continue; // Need at least 2 detections final avgRssi = samples.reduce((a, b) => a + b) / samples.length; if (avgRssi > bestAvgRssi && avgRssi >= -85) { bestAvgRssi = avgRssi; best = beacon; } } // Fall back to strongest registered beacon if none meet threshold if (best == null) { for (final beacon in registeredBeacons) { final samples = _beaconRssiSamples[beacon.uuid]; if (samples == null || samples.isEmpty) continue; final avgRssi = samples.reduce((a, b) => a + b) / samples.length; if (avgRssi > bestAvgRssi) { bestAvgRssi = avgRssi; best = beacon; } } } return best; } Future _navigateToNextScreen() async { if (!mounted) return; if (_bestBeacon != null) { final beacon = _bestBeacon!; print('[Splash] 📊 Best beacon: ${beacon.businessName}, hasChildren=${beacon.hasChildren}, parent=${beacon.parentBusinessName}'); // Check if this business has child businesses (food court scenario) if (beacon.hasChildren) { print('[Splash] 🏢 Business has children - showing selector'); // Need to fetch children and show selector try { final children = await Api.getChildBusinesses(businessId: beacon.businessId); if (!mounted) return; if (children.isNotEmpty) { Navigator.of(context).pushReplacementNamed( AppRoutes.businessSelector, arguments: { "parentBusinessId": beacon.businessId, "parentBusinessName": beacon.businessName, "servicePointId": beacon.servicePointId, "servicePointName": beacon.servicePointName, "children": children, }, ); return; } } catch (e) { print('[Splash] Error fetching children: $e'); } } // Single business - go directly to menu final appState = context.read(); appState.setBusinessAndServicePoint( beacon.businessId, beacon.servicePointId, businessName: beacon.businessName, servicePointName: beacon.servicePointName, parentBusinessId: beacon.hasParent ? beacon.parentBusinessId : null, parentBusinessName: beacon.hasParent ? beacon.parentBusinessName : null, ); // Beacon detected = dine-in at a table appState.setOrderType(OrderType.dineIn); Api.setBusinessId(beacon.businessId); print('[Splash] 🎉 Auto-selected: ${beacon.businessName}'); Navigator.of(context).pushReplacementNamed( AppRoutes.menuBrowse, arguments: { 'businessId': beacon.businessId, 'servicePointId': beacon.servicePointId, }, ); return; } // No beacon or error - go to restaurant select print('[Splash] Going to restaurant select'); Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); } @override void dispose() { _bounceController.dispose(); _statusTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // Centered static status text Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( "site survey", style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w300, letterSpacing: 1, ), ), const SizedBox(height: 6), Text( _statusPhrases[_statusIndex], style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w300, letterSpacing: 1, ), ), ], ), ), // Bouncing logo Positioned( left: _x, top: _y, child: Text( "PAYFRIT", style: TextStyle( color: _logoColor, fontSize: 38, fontWeight: FontWeight.w800, letterSpacing: 3, ), ), ), ], ), ); } }