import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:beacons_plugin/beacons_plugin.dart'; import '../app/app_router.dart'; import '../app/app_state.dart'; import '../services/beacon_permissions.dart'; import '../services/api.dart'; class BeaconScanScreen extends StatefulWidget { const BeaconScanScreen({super.key}); @override State createState() => _BeaconScanScreenState(); } class _BeaconScanScreenState extends State { String _status = 'Initializing...'; bool _scanning = false; bool _permissionsGranted = false; // Track beacons by UUID -> (BeaconID, RSSI) final Map> _detectedBeacons = {}; Map _uuidToBeaconId = {}; @override void initState() { super.initState(); _startScanFlow(); } Future _startScanFlow() async { // Step 1: Request permissions setState(() => _status = 'Requesting permissions...'); final granted = await BeaconPermissions.requestPermissions(); if (!granted) { setState(() { _status = 'Permissions denied'; _permissionsGranted = false; }); return; } setState(() => _permissionsGranted = true); // Step 2: Fetch all active beacons from server setState(() => _status = 'Loading beacon data...'); try { _uuidToBeaconId = await Api.listAllBeacons(); } catch (e) { debugPrint('[BeaconScan] Error loading beacons: $e'); _uuidToBeaconId = {}; } if (_uuidToBeaconId.isEmpty) { // No beacons in database, skip scan and go straight to manual selection debugPrint('[BeaconScan] No beacons in database, going to restaurant select'); if (mounted) _navigateToRestaurantSelect(); return; } // Step 3: Perform initial scan setState(() { _status = 'Scanning for nearby beacons...'; _scanning = true; }); await _performInitialScan(); } Future _performInitialScan() async { try { // Setup beacon monitoring await BeaconsPlugin.listenToBeacons( _beaconDataReceived, ); // Start beacon monitoring await BeaconsPlugin.addRegion( "PayfritBeacons", "00000000-0000-0000-0000-000000000000", // Placeholder UUID ); await BeaconsPlugin.startMonitoring(); // Scan for 3 seconds await Future.delayed(const Duration(seconds: 3)); // Stop scanning await BeaconsPlugin.stopMonitoring(); if (!mounted) return; if (_detectedBeacons.isEmpty) { // No beacons found setState(() => _status = 'No beacons nearby'); await Future.delayed(const Duration(milliseconds: 800)); if (mounted) _navigateToRestaurantSelect(); } else { // Find beacon with highest RSSI final best = _findBestBeacon(); if (best != null) { setState(() => _status = 'Beacon detected! Loading menu...'); await _autoSelectBusinessFromBeacon(best); } else { _navigateToRestaurantSelect(); } } } catch (e) { debugPrint('[BeaconScan] Error during scan: $e'); if (mounted) { setState(() => _status = 'Scan error: ${e.toString()}'); await Future.delayed(const Duration(seconds: 2)); if (mounted) _navigateToRestaurantSelect(); } } } void _beaconDataReceived(dynamic result) { if (result is Map) { try { final uuid = (result["uuid"] ?? "").toString().trim().toUpperCase().replaceAll('-', ''); final rssi = int.tryParse((result["rssi"] ?? "-100").toString()) ?? -100; if (uuid.isNotEmpty && _uuidToBeaconId.containsKey(uuid)) { final beaconId = _uuidToBeaconId[uuid]!; // Update if this is a new beacon or better RSSI if (!_detectedBeacons.containsKey(uuid) || _detectedBeacons[uuid]!.value < rssi) { setState(() { _detectedBeacons[uuid] = MapEntry(beaconId, rssi); }); debugPrint('[BeaconScan] Detected: UUID=$uuid, BeaconID=$beaconId, RSSI=$rssi'); } } } catch (e) { debugPrint('[BeaconScan] Error parsing beacon data: $e'); } } } MapEntry? _findBestBeacon() { if (_detectedBeacons.isEmpty) return null; String? bestUUID; int bestRSSI = -200; for (final entry in _detectedBeacons.entries) { if (entry.value.value > bestRSSI) { bestRSSI = entry.value.value; bestUUID = entry.key; } } if (bestUUID != null) { final beaconId = _detectedBeacons[bestUUID]!.key; return MapEntry(bestUUID, beaconId); } return null; } Future _autoSelectBusinessFromBeacon(MapEntry beacon) async { final beaconId = beacon.value; debugPrint('[BeaconScan] Found beacon! BeaconID=$beaconId, UUID=${beacon.key}'); // TODO: Fetch Business + ServicePoint info from BeaconID // For now, navigate to restaurant select _navigateToRestaurantSelect(); } void _navigateToRestaurantSelect() { Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); } void _retryPermissions() async { await BeaconPermissions.openSettings(); } @override void dispose() { BeaconsPlugin.stopMonitoring(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_scanning) const CircularProgressIndicator(color: Colors.white) else if (!_permissionsGranted) const Icon(Icons.bluetooth_disabled, color: Colors.white70, size: 64) else const Icon(Icons.bluetooth_searching, color: Colors.white70, size: 64), const SizedBox(height: 24), Text( _status, style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), if (_detectedBeacons.isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Found ${_detectedBeacons.length} beacon(s)', style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], if (!_permissionsGranted && _status.contains('denied')) ...[ const SizedBox(height: 24), FilledButton( onPressed: _retryPermissions, child: const Text('Open Settings'), ), const SizedBox(height: 12), TextButton( onPressed: _navigateToRestaurantSelect, style: TextButton.styleFrom(foregroundColor: Colors.white70), child: const Text('Skip and select manually'), ), ], ], ), ), ); } }