diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9a2da6b..6cb2d5e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,11 @@ + + + + + + + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/lib/app/app_router.dart b/lib/app/app_router.dart index c66b5ff..7b01076 100644 --- a/lib/app/app_router.dart +++ b/lib/app/app_router.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; +import "../screens/beacon_scan_screen.dart"; import "../screens/cart_view_screen.dart"; import "../screens/login_screen.dart"; import "../screens/menu_browse_screen.dart"; @@ -10,6 +11,7 @@ import "../screens/splash_screen.dart"; class AppRoutes { static const String splash = "/"; static const String login = "/login"; + static const String beaconScan = "/beacon-scan"; static const String restaurantSelect = "/restaurants"; static const String servicePointSelect = "/service-points"; static const String menuBrowse = "/menu"; @@ -18,6 +20,7 @@ class AppRoutes { static Map get routes => { splash: (_) => const SplashScreen(), login: (_) => const LoginScreen(), + beaconScan: (_) => const BeaconScanScreen(), restaurantSelect: (_) => const RestaurantSelectScreen(), servicePointSelect: (_) => const ServicePointSelectScreen(), menuBrowse: (_) => const MenuBrowseScreen(), diff --git a/lib/app/app_state.dart b/lib/app/app_state.dart index f483f93..926974e 100644 --- a/lib/app/app_state.dart +++ b/lib/app/app_state.dart @@ -10,6 +10,7 @@ class AppState extends ChangeNotifier { String? _cartOrderUuid; int _cartItemCount = 0; + int? get selectedBusinessId => _selectedBusinessId; int? get selectedServicePointId => _selectedServicePointId; @@ -20,6 +21,7 @@ class AppState extends ChangeNotifier { String? get cartOrderUuid => _cartOrderUuid; int get cartItemCount => _cartItemCount; + bool get hasLocationSelection => _selectedBusinessId != null && _selectedServicePointId != null; @@ -71,6 +73,7 @@ class AppState extends ChangeNotifier { notifyListeners(); } + void clearAll() { _selectedBusinessId = null; _selectedServicePointId = null; @@ -78,6 +81,6 @@ class AppState extends ChangeNotifier { _cartOrderId = null; _cartOrderUuid = null; - notifyListeners(); + } } diff --git a/lib/app/app_state.dart.bak b/lib/app/app_state.dart.bak new file mode 100644 index 0000000..f483f93 --- /dev/null +++ b/lib/app/app_state.dart.bak @@ -0,0 +1,83 @@ +import "package:flutter/foundation.dart"; + +class AppState extends ChangeNotifier { + int? _selectedBusinessId; + int? _selectedServicePointId; + + int? _userId; + + int? _cartOrderId; + String? _cartOrderUuid; + int _cartItemCount = 0; + + int? get selectedBusinessId => _selectedBusinessId; + int? get selectedServicePointId => _selectedServicePointId; + + int? get userId => _userId; + bool get isLoggedIn => _userId != null && _userId! > 0; + + int? get cartOrderId => _cartOrderId; + String? get cartOrderUuid => _cartOrderUuid; + int get cartItemCount => _cartItemCount; + + bool get hasLocationSelection => + _selectedBusinessId != null && _selectedServicePointId != null; + + void setBusiness(int businessId) { + _selectedBusinessId = businessId; + _selectedServicePointId = null; + + _cartOrderId = null; + _cartOrderUuid = null; + + notifyListeners(); + } + + void setServicePoint(int servicePointId) { + _selectedServicePointId = servicePointId; + + _cartOrderId = null; + _cartOrderUuid = null; + + notifyListeners(); + } + + void setUserId(int userId) { + _userId = userId; + notifyListeners(); + } + + void clearAuth() { + _userId = null; + notifyListeners(); + } + + void setCartOrder({required int orderId, required String orderUuid, int itemCount = 0}) { + _cartOrderId = orderId; + _cartOrderUuid = orderUuid; + _cartItemCount = itemCount; + notifyListeners(); + } + + void updateCartItemCount(int count) { + _cartItemCount = count; + notifyListeners(); + } + + void clearCart() { + _cartOrderId = null; + _cartOrderUuid = null; + _cartItemCount = 0; + notifyListeners(); + } + + void clearAll() { + _selectedBusinessId = null; + _selectedServicePointId = null; + + _cartOrderId = null; + _cartOrderUuid = null; + + notifyListeners(); + } +} diff --git a/lib/screens/beacon_scan_screen.dart b/lib/screens/beacon_scan_screen.dart new file mode 100644 index 0000000..6a15352 --- /dev/null +++ b/lib/screens/beacon_scan_screen.dart @@ -0,0 +1,224 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:dchs_flutter_beacon/dchs_flutter_beacon.dart'; + +import '../app/app_router.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 _permissionsGranted = false; + bool _scanning = false; + + Map _uuidToBeaconId = {}; + final Map> _detectedBeacons = {}; // UUID -> (BeaconID, RSSI) + + @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 - Please enable Location & Bluetooth'; + _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) { + 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 { + // Initialize beacon monitoring + await flutterBeacon.initializeScanning; + + // Create regions for all known UUIDs + final regions = _uuidToBeaconId.keys.map((uuid) { + // Format UUID with dashes for the plugin + 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(); + + if (regions.isEmpty) { + if (mounted) _navigateToRestaurantSelect(); + return; + } + + StreamSubscription? subscription; + + // Start ranging for 3 seconds + 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)) { + final beaconId = _uuidToBeaconId[uuid]!; + + // Update if new 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'); + } + } + } + }); + + // Wait 3 seconds + await Future.delayed(const Duration(seconds: 3)); + await subscription.cancel(); + + if (!mounted) return; + + if (_detectedBeacons.isEmpty) { + 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! BeaconID=${best.value}'); + await Future.delayed(const Duration(milliseconds: 800)); + // TODO: Auto-select business from beacon + if (mounted) _navigateToRestaurantSelect(); + } else { + _navigateToRestaurantSelect(); + } + } + } catch (e) { + debugPrint('[BeaconScan] Error during scan: $e'); + if (mounted) { + setState(() => _status = 'Scan error - continuing to manual selection'); + await Future.delayed(const Duration(seconds: 1)); + if (mounted) _navigateToRestaurantSelect(); + } + } + } + + 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; + } + + void _navigateToRestaurantSelect() { + Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); + } + + void _retryPermissions() async { + await BeaconPermissions.openSettings(); + } + + @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_searching, color: Colors.white70, size: 64) + else + const Icon(Icons.bluetooth_disabled, 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'), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/screens/beacon_scan_screen_broken.dart b/lib/screens/beacon_scan_screen_broken.dart new file mode 100644 index 0000000..2ed20f8 --- /dev/null +++ b/lib/screens/beacon_scan_screen_broken.dart @@ -0,0 +1,243 @@ +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'), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/screens/menu_browse_screen.dart b/lib/screens/menu_browse_screen.dart index 372c9f2..b41513c 100644 --- a/lib/screens/menu_browse_screen.dart +++ b/lib/screens/menu_browse_screen.dart @@ -234,6 +234,10 @@ class _MenuBrowseScreenState extends State { } Future _addToCart(MenuItem item, Set selectedModifierIds) async { + // ignore: avoid_print + print("DEBUG: _addToCart called for item ${item.name} (ItemID=${item.itemId})"); + print("DEBUG: Selected modifier IDs: $selectedModifierIds"); + if (_userId == null || _businessId == null || _servicePointId == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Missing required information")), @@ -282,9 +286,23 @@ class _MenuBrowseScreenState extends State { // ignore: avoid_print print("DEBUG: Added root item, cart now has ${cart.lineItems.length} line items"); + // Find the OrderLineItemID of the root item we just added + // ignore: avoid_print + print("DEBUG: Looking for root item with ItemID=${item.itemId} in ${cart.lineItems.length} line items"); + print("DEBUG: Line items: ${cart.lineItems.map((li) => 'ID=${li.orderLineItemId}, ItemID=${li.itemId}, ParentID=${li.parentOrderLineItemId}').join(', ')}"); + + final rootLineItem = cart.lineItems.lastWhere( + (li) => li.itemId == item.itemId && li.parentOrderLineItemId == 0 && !li.isDeleted, + orElse: () => throw StateError('Root line item not found for ItemID=${item.itemId}'), + ); + + // ignore: avoid_print + print("DEBUG: Root item found - OrderLineItemID=${rootLineItem.orderLineItemId}"); + // Add all selected modifiers recursively await _addModifiersRecursively( cart.orderId, + rootLineItem.orderLineItemId, item.itemId, selectedModifierIds, ); @@ -313,24 +331,52 @@ class _MenuBrowseScreenState extends State { Future _addModifiersRecursively( int orderId, + int parentOrderLineItemId, int parentItemId, Set selectedItemIds, ) async { final children = _itemsByParent[parentItemId] ?? []; + // ignore: avoid_print + print("DEBUG: _addModifiersRecursively called with ParentItemID=$parentItemId, ParentOrderLineItemID=$parentOrderLineItemId"); + print("DEBUG: Found ${children.length} children for ItemID=$parentItemId"); + print("DEBUG: Children ItemIDs: ${children.map((c) => c.itemId).join(', ')}"); + print("DEBUG: Selected ItemIDs: ${selectedItemIds.join(', ')}"); + for (final child in children) { final isSelected = selectedItemIds.contains(child.itemId); - await Api.setLineItem( + // ignore: avoid_print + print("DEBUG: Processing child ItemID=${child.itemId} (${child.name}), isSelected=$isSelected"); + + // Add this modifier with the correct parent OrderLineItemID + final cart = await Api.setLineItem( orderId: orderId, - parentOrderLineItemId: 0, // Will be handled by backend + parentOrderLineItemId: parentOrderLineItemId, itemId: child.itemId, isSelected: isSelected, ); - // Recursively add grandchildren + // ignore: avoid_print + print("DEBUG: setLineItem response: cart has ${cart.lineItems.length} line items"); + + // Recursively add grandchildren if this modifier was selected if (isSelected) { - await _addModifiersRecursively(orderId, child.itemId, selectedItemIds); + // Find the OrderLineItemID of this modifier we just added + final childLineItem = cart.lineItems.lastWhere( + (li) => li.itemId == child.itemId && li.parentOrderLineItemId == parentOrderLineItemId && !li.isDeleted, + orElse: () => throw StateError('Child line item not found for ItemID=${child.itemId}'), + ); + + // ignore: avoid_print + print("DEBUG: Child modifier OrderLineItemID=${childLineItem.orderLineItemId}"); + + await _addModifiersRecursively( + orderId, + childLineItem.orderLineItemId, + child.itemId, + selectedItemIds, + ); } } } diff --git a/lib/screens/splash_screen.dart b/lib/screens/splash_screen.dart index d7b4322..49266fa 100644 --- a/lib/screens/splash_screen.dart +++ b/lib/screens/splash_screen.dart @@ -26,7 +26,7 @@ class _SplashScreenState extends State { // Navigate based on authentication status if (appState.isLoggedIn) { - Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect); + Navigator.of(context).pushReplacementNamed(AppRoutes.beaconScan); } else { Navigator.of(context).pushReplacementNamed(AppRoutes.login); } diff --git a/lib/services/api.dart b/lib/services/api.dart index be131ff..6557630 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -57,11 +57,11 @@ class Api { } static String get baseUrl { - const v = String.fromEnvironment("API_BASE_URL"); + const v = String.fromEnvironment("AALISTS_API_BASE_URL"); if (v.isEmpty) { throw StateError( - "API_BASE_URL is not set. Example (Android emulator): " - "--dart-define=API_BASE_URL=http://10.0.2.2:8888/biz.payfrit.com/api", + "AALISTS_API_BASE_URL is not set. Example (Android emulator): " + "--dart-define=AALISTS_API_BASE_URL=http://10.0.2.2:8888/biz.payfrit.com/api", ); } return v; @@ -401,4 +401,38 @@ class Api { ); } } + + // ------------------------- + // Beacons + // ------------------------- + + static Future> listAllBeacons() async { + final raw = await _getRaw("/beacons/list_all.cfm"); + final j = _requireJson(raw, "ListAllBeacons"); + + if (!_ok(j)) { + throw StateError( + "ListAllBeacons API returned OK=false\nERROR: ${_err(j)}\nHTTP Status: ${raw.statusCode}", + ); + } + + final arr = _pickArray(j, const ["items", "ITEMS"]); + if (arr == null) return {}; + + final Map uuidToBeaconId = {}; + + for (final e in arr) { + if (e is! Map) continue; + final item = e is Map ? e : e.cast(); + + final uuid = (item["BeaconUUID"] ?? item["BEACONUUID"] ?? "").toString().trim(); + final beaconId = item["BeaconID"] ?? item["BEACONID"]; + + if (uuid.isNotEmpty && beaconId is num) { + uuidToBeaconId[uuid] = beaconId.toInt(); + } + } + + return uuidToBeaconId; + } } diff --git a/lib/services/beacon_permissions.dart b/lib/services/beacon_permissions.dart new file mode 100644 index 0000000..59a192b --- /dev/null +++ b/lib/services/beacon_permissions.dart @@ -0,0 +1,47 @@ +import 'package:flutter/foundation.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class BeaconPermissions { + static Future requestPermissions() async { + try { + // Request location permission (required for Bluetooth scanning) + final locationStatus = await Permission.locationWhenInUse.request(); + + // Request Bluetooth permissions (Android 12+) + final bluetoothScan = await Permission.bluetoothScan.request(); + final bluetoothConnect = await Permission.bluetoothConnect.request(); + + final allGranted = locationStatus.isGranted && + bluetoothScan.isGranted && + bluetoothConnect.isGranted; + + if (allGranted) { + debugPrint('[BeaconPermissions] ✅ All permissions granted'); + } else { + debugPrint('[BeaconPermissions] ❌ Permissions denied: ' + 'location=$locationStatus, ' + 'bluetoothScan=$bluetoothScan, ' + 'bluetoothConnect=$bluetoothConnect'); + } + + return allGranted; + } catch (e) { + debugPrint('[BeaconPermissions] Error requesting permissions: $e'); + return false; + } + } + + static Future checkPermissions() async { + final locationStatus = await Permission.locationWhenInUse.status; + final bluetoothScan = await Permission.bluetoothScan.status; + final bluetoothConnect = await Permission.bluetoothConnect.status; + + return locationStatus.isGranted && + bluetoothScan.isGranted && + bluetoothConnect.isGranted; + } + + static Future openSettings() async { + await openAppSettings(); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..724bb2a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 871e5eb..194a722 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + dchs_flutter_beacon: + dependency: "direct main" + description: + name: dchs_flutter_beacon + sha256: "392d56d69585845311fba75493e852fab78eb9b4669812758e6f475f861f9862" + url: "https://pub.dev" + source: hosted + version: "0.6.6" fake_async: dependency: transitive description: @@ -49,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -67,6 +91,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" http: dependency: "direct main" description: @@ -155,6 +184,94 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" provider: dependency: "direct main" description: @@ -163,6 +280,62 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5+1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + url: "https://pub.dev" + source: hosted + version: "2.4.18" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -248,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.8.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 569ecd8..717d754 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,10 @@ dependencies: sdk: flutter http: ^1.2.2 provider: ^6.1.2 + + permission_handler: ^11.3.1 + shared_preferences: ^2.2.3 + dchs_flutter_beacon: ^0.6.6 dev_dependencies: flutter_test: