payfrit-app/lib/widgets/rescan_button.dart
John Mizerek c4792189dd App Store Version 2: Beacon scanning, preload caching, business selector
Features:
- Beacon scanner service for detecting nearby beacons
- Beacon cache for offline-first beacon resolution
- Preload cache for instant menu display
- Business selector screen for multi-location support
- Rescan button widget for quick beacon refresh
- Sign-in dialog for guest checkout flow
- Task type model for server tasks

Improvements:
- Enhanced menu browsing with category filtering
- Improved cart view with better modifier display
- Order history with detailed order tracking
- Chat screen improvements
- Better error handling in API service

Fixes:
- CashApp payment return crash fix
- Modifier nesting issues resolved
- Auto-expand modifier groups

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 19:51:54 -08:00

239 lines
7.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app/app_router.dart';
import '../app/app_state.dart';
import '../services/api.dart';
import '../services/beacon_scanner_service.dart';
/// A button that triggers a beacon rescan
/// Can be used anywhere in the app - will use the current business if available
class RescanButton extends StatefulWidget {
final bool showLabel;
final Color? iconColor;
const RescanButton({
super.key,
this.showLabel = false,
this.iconColor,
});
@override
State<RescanButton> createState() => _RescanButtonState();
}
class _RescanButtonState extends State<RescanButton> {
final _scanner = BeaconScannerService();
bool _isScanning = false;
Future<void> _performRescan() async {
if (_isScanning) return;
setState(() => _isScanning = true);
final appState = context.read<AppState>();
final currentBusinessId = appState.selectedBusinessId;
// Show scanning indicator
final scaffold = ScaffoldMessenger.of(context);
scaffold.showSnackBar(
const SnackBar(
content: Row(
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
),
SizedBox(width: 12),
Text('Scanning for your table...'),
],
),
duration: Duration(seconds: 3),
),
);
try {
// Use current business for optimized scan if available
final result = await _scanner.scan(businessId: currentBusinessId);
if (!mounted) return;
scaffold.hideCurrentSnackBar();
if (result.foundBeacon) {
final beacon = result.bestBeacon!;
// Check if it's the same business/table or a new one
// Also check if the beacon's business matches our parent (food court scenario)
final isSameLocation = (beacon.businessId == currentBusinessId &&
beacon.servicePointId == appState.selectedServicePointId);
final isSameParentLocation = (appState.hasParentBusiness &&
beacon.businessId == appState.parentBusinessId &&
beacon.servicePointId == appState.selectedServicePointId);
if (isSameLocation || isSameParentLocation) {
// Same location - just confirm
scaffold.showSnackBar(
SnackBar(
content: Text('Still at ${appState.selectedServicePointName ?? beacon.servicePointName}'),
duration: const Duration(seconds: 2),
),
);
} else {
// Different location - ask to switch
_showSwitchDialog(beacon);
}
} else if (result.beaconsFound > 0) {
scaffold.showSnackBar(
SnackBar(
content: Text('Found ${result.beaconsFound} beacon(s) but none registered'),
duration: const Duration(seconds: 2),
),
);
} else {
scaffold.showSnackBar(
const SnackBar(
content: Text('No beacons detected nearby'),
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
scaffold.hideCurrentSnackBar();
scaffold.showSnackBar(
SnackBar(
content: Text('Scan failed: $e'),
duration: const Duration(seconds: 2),
),
);
}
} finally {
if (mounted) {
setState(() => _isScanning = false);
}
}
}
void _showSwitchDialog(BeaconLookupResult beacon) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('New Location Detected'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('You appear to be at:'),
const SizedBox(height: 8),
Text(
beacon.businessName,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
if (beacon.servicePointName.isNotEmpty)
Text(
beacon.servicePointName,
style: TextStyle(color: Colors.grey.shade600),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Stay Here'),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
_switchToBeacon(beacon);
},
child: const Text('Switch'),
),
],
),
);
}
Future<void> _switchToBeacon(BeaconLookupResult beacon) async {
final appState = context.read<AppState>();
// Handle parent business (food court scenario)
if (beacon.hasChildren) {
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) {
debugPrint('[Rescan] Error fetching children: $e');
}
}
// Single business - update state and navigate
appState.setBusinessAndServicePoint(
beacon.businessId,
beacon.servicePointId,
businessName: beacon.businessName,
servicePointName: beacon.servicePointName,
parentBusinessId: beacon.hasParent ? beacon.parentBusinessId : null,
parentBusinessName: beacon.hasParent ? beacon.parentBusinessName : null,
);
appState.setOrderType(OrderType.dineIn);
Api.setBusinessId(beacon.businessId);
if (!mounted) return;
Navigator.of(context).pushReplacementNamed(
AppRoutes.menuBrowse,
arguments: {
'businessId': beacon.businessId,
'servicePointId': beacon.servicePointId,
},
);
}
@override
Widget build(BuildContext context) {
if (widget.showLabel) {
return TextButton.icon(
onPressed: _isScanning ? null : _performRescan,
icon: _isScanning
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(Icons.bluetooth_searching, color: widget.iconColor),
label: Text(
_isScanning ? 'Scanning...' : 'Find My Table',
style: TextStyle(color: widget.iconColor),
),
);
}
return IconButton(
icon: _isScanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(Icons.bluetooth_searching, color: widget.iconColor),
tooltip: 'Find My Table',
onPressed: _isScanning ? null : _performRescan,
);
}
}