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_permissions.dart"; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @override State createState() => _SplashScreenState(); } class _SplashScreenState extends State with TickerProviderStateMixin { // 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 Map _uuidToBeaconId = {}; final Map> _beaconRssiSamples = {}; final Map _beaconDetectionCount = {}; bool _scanComplete = false; BeaconResult? _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; setState(() { _x += _dx; _y += _dy; // Bounce off edges and change color if (_x <= 0 || _x >= size.width - logoWidth) { _dx = -_dx; _changeColor(); } if (_y <= 0 || _y >= size.height - logoHeight) { _dy = -_dy; _changeColor(); } // Keep in bounds _x = _x.clamp(0, size.width - logoWidth); _y = _y.clamp(0, size.height - logoHeight); }); } void _changeColor() { final newColor = _colors[_random.nextInt(_colors.length)]; if (newColor != _logoColor) { _logoColor = newColor; } } Future _initializeApp() async { // 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}'); Api.setAuthToken(credentials.token); final appState = context.read(); appState.setUserId(credentials.userId); } // Start beacon scanning in background await _performBeaconScan(); // Navigate based on results if (!mounted) return; _navigateToNextScreen(); } Future _performBeaconScan() async { print('[Splash] 📡 Starting beacon scan...'); // Request permissions final granted = await BeaconPermissions.requestPermissions(); if (!granted) { print('[Splash] ❌ Permissions denied'); _scanComplete = true; return; } // 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'); // Fetch beacon list from server try { _uuidToBeaconId = await Api.listAllBeacons(); print('[Splash] Loaded ${_uuidToBeaconId.length} beacons from database'); } catch (e) { print('[Splash] Error loading beacons: $e'); _scanComplete = true; return; } if (_uuidToBeaconId.isEmpty) { print('[Splash] No beacons in database'); _scanComplete = true; return; } // Initialize beacon scanning try { await flutterBeacon.initializeScanning; await Future.delayed(const Duration(milliseconds: 500)); // Create regions for all known UUIDs final regions = _uuidToBeaconId.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(); // Perform scan cycles for (int scanCycle = 1; scanCycle <= 5; scanCycle++) { print('[Splash] ----- Scan cycle $scanCycle/5 -----'); StreamSubscription? subscription; subscription = flutterBeacon.ranging(regions).listen((result) { for (var beacon in result.beacons) { final uuid = beacon.proximityUUID.toUpperCase().replaceAll('-', ''); final rssi = beacon.rssi; if (_uuidToBeaconId.containsKey(uuid)) { _beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi); _beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1; print('[Splash] ✅ Beacon ${_uuidToBeaconId[uuid]}, RSSI=$rssi'); } } }); await Future.delayed(const Duration(seconds: 2)); await subscription.cancel(); // Check for early exit after 3 cycles if (scanCycle >= 3 && _beaconRssiSamples.isNotEmpty && _canExitEarly()) { print('[Splash] ⚡ Early exit - stable readings'); break; } if (scanCycle < 5) { await Future.delayed(const Duration(milliseconds: 200)); } } // Find best beacon _bestBeacon = _findBestBeacon(); print('[Splash] 🎯 Best beacon: ${_bestBeacon?.beaconId ?? "none"}'); } catch (e) { print('[Splash] Scan error: $e'); } _scanComplete = true; } bool _canExitEarly() { if (_beaconRssiSamples.isEmpty) return false; bool hasEnoughSamples = _beaconRssiSamples.values.any((s) => s.length >= 3); if (!hasEnoughSamples) return false; for (final entry in _beaconRssiSamples.entries) { final samples = entry.value; if (samples.length < 3) continue; final avg = samples.reduce((a, b) => a + b) / samples.length; final variance = samples.map((r) => (r - avg) * (r - avg)).reduce((a, b) => a + b) / samples.length; if (variance > 50) return false; } if (_beaconRssiSamples.length > 1) { final avgRssis = {}; for (final entry in _beaconRssiSamples.entries) { if (entry.value.isNotEmpty) { avgRssis[entry.key] = entry.value.reduce((a, b) => a + b) / entry.value.length; } } final sorted = avgRssis.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); if (sorted.length >= 2 && (sorted[0].value - sorted[1].value) < 5) { return false; } } return true; } BeaconResult? _findBestBeacon() { if (_beaconRssiSamples.isEmpty) return null; String? bestUuid; double bestAvgRssi = -999; for (final entry in _beaconRssiSamples.entries) { final samples = entry.value; final detections = _beaconDetectionCount[entry.key] ?? 0; if (detections < 3) continue; final avgRssi = samples.reduce((a, b) => a + b) / samples.length; if (avgRssi > bestAvgRssi && avgRssi >= -85) { bestAvgRssi = avgRssi; bestUuid = entry.key; } } if (bestUuid != null) { return BeaconResult( uuid: bestUuid, beaconId: _uuidToBeaconId[bestUuid]!, avgRssi: bestAvgRssi, ); } // Fall back to strongest signal even if doesn't meet threshold if (_beaconRssiSamples.isNotEmpty) { for (final entry in _beaconRssiSamples.entries) { final samples = entry.value; if (samples.isEmpty) continue; final avgRssi = samples.reduce((a, b) => a + b) / samples.length; if (avgRssi > bestAvgRssi) { bestAvgRssi = avgRssi; bestUuid = entry.key; } } if (bestUuid != null) { return BeaconResult( uuid: bestUuid, beaconId: _uuidToBeaconId[bestUuid]!, avgRssi: bestAvgRssi, ); } } return null; } Future _navigateToNextScreen() async { if (!mounted) return; if (_bestBeacon != null) { // Auto-select business from beacon try { final mapping = await Api.getBusinessFromBeacon(beaconId: _bestBeacon!.beaconId); if (!mounted) return; final appState = context.read(); appState.setBusinessAndServicePoint( mapping.businessId, mapping.servicePointId, businessName: mapping.businessName, servicePointName: mapping.servicePointName, ); Api.setBusinessId(mapping.businessId); print('[Splash] 🎉 Auto-selected: ${mapping.businessName}'); Navigator.of(context).pushReplacementNamed( AppRoutes.menuBrowse, arguments: { 'businessId': mapping.businessId, 'servicePointId': mapping.servicePointId, }, ); return; } catch (e) { print('[Splash] Error mapping beacon to business: $e'); } } // 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, ), ), ), ], ), ); } } class BeaconResult { final String uuid; final int beaconId; final double avgRssi; const BeaconResult({ required this.uuid, required this.beaconId, required this.avgRssi, }); }