Add production API support and fix login flow

- Configure production API URL (biz.payfrit.com) as default
- Add INTERNET permission to AndroidManifest for API calls
- Remove login requirement from restaurant selection (allow anonymous browsing)
- Add enhanced logging for beacon scanning diagnostics
- Add splash screen logging for auth flow debugging

Technical changes:
- api.dart: Default baseUrl to production instead of throwing error
- restaurant_select_screen.dart: Remove userId requirement for browsing
- beacon_scan_screen.dart: Replace debugPrint with print for better log capture
- splash_screen.dart: Add diagnostic logging for auth restoration
- AndroidManifest.xml: Add INTERNET permission

Known issue:
- Beacon detection not working - app receives beacon data from API (3 beacons)
  but BLE scanning not detecting physical beacons. Needs investigation of:
  * Physical beacon UUID configuration
  * Android BLE permissions at runtime
  * Beacon plugin initialization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2025-12-31 12:04:50 -08:00
parent 114401c130
commit 7c366d5a9c
5 changed files with 45 additions and 45 deletions

View file

@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Internet permission for API calls -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Beacon scanning permissions --> <!-- Beacon scanning permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

View file

@ -49,10 +49,12 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
Future<void> _startScanFlow() async { Future<void> _startScanFlow() async {
// Step 1: Request permissions // Step 1: Request permissions
setState(() => _status = 'Requesting permissions...'); setState(() => _status = 'Requesting permissions...');
print('[BeaconScan] 🔐 Requesting permissions...');
final granted = await BeaconPermissions.requestPermissions(); final granted = await BeaconPermissions.requestPermissions();
if (!granted) { if (!granted) {
print('[BeaconScan] ❌ Permissions DENIED');
setState(() { setState(() {
_status = 'Permissions denied - Please enable Location & Bluetooth'; _status = 'Permissions denied - Please enable Location & Bluetooth';
_permissionsGranted = false; _permissionsGranted = false;
@ -60,6 +62,7 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
return; return;
} }
print('[BeaconScan] ✅ Permissions GRANTED');
setState(() => _permissionsGranted = true); setState(() => _permissionsGranted = true);
// Step 2: Fetch all active beacons from server // Step 2: Fetch all active beacons from server
@ -67,21 +70,21 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
try { try {
_uuidToBeaconId = await Api.listAllBeacons(); _uuidToBeaconId = await Api.listAllBeacons();
debugPrint('[BeaconScan] ========================================'); print('[BeaconScan] ========================================');
debugPrint('[BeaconScan] Loaded ${_uuidToBeaconId.length} beacons from database:'); print('[BeaconScan] Loaded ${_uuidToBeaconId.length} beacons from database:');
_uuidToBeaconId.forEach((uuid, beaconId) { _uuidToBeaconId.forEach((uuid, beaconId) {
debugPrint('[BeaconScan] BeaconID=$beaconId'); print('[BeaconScan] BeaconID=$beaconId');
debugPrint('[BeaconScan] UUID=$uuid'); print('[BeaconScan] UUID=$uuid');
debugPrint('[BeaconScan] ---'); print('[BeaconScan] ---');
}); });
debugPrint('[BeaconScan] ========================================'); print('[BeaconScan] ========================================');
} catch (e) { } catch (e) {
debugPrint('[BeaconScan] Error loading beacons: $e'); debugPrint('[BeaconScan] Error loading beacons: $e');
_uuidToBeaconId = {}; _uuidToBeaconId = {};
} }
if (_uuidToBeaconId.isEmpty) { if (_uuidToBeaconId.isEmpty) {
debugPrint('[BeaconScan] No beacons in database, going to restaurant select'); print('[BeaconScan] ⚠️ No beacons in database, going to restaurant select');
if (mounted) _navigateToRestaurantSelect(); if (mounted) _navigateToRestaurantSelect();
return; return;
} }
@ -104,11 +107,11 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
final regions = _uuidToBeaconId.keys.map((uuid) { final regions = _uuidToBeaconId.keys.map((uuid) {
// Format UUID with dashes for the plugin // 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)}'; final formattedUUID = '${uuid.substring(0, 8)}-${uuid.substring(8, 12)}-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20)}';
debugPrint('[BeaconScan] Creating region for UUID: $uuid -> $formattedUUID'); print('[BeaconScan] 🔍 Creating region for UUID: $uuid -> $formattedUUID');
return Region(identifier: uuid, proximityUUID: formattedUUID); return Region(identifier: uuid, proximityUUID: formattedUUID);
}).toList(); }).toList();
debugPrint('[BeaconScan] Created ${regions.length} regions for scanning'); print('[BeaconScan] 📡 Created ${regions.length} regions for scanning');
if (regions.isEmpty) { if (regions.isEmpty) {
if (mounted) _navigateToRestaurantSelect(); if (mounted) _navigateToRestaurantSelect();
@ -116,30 +119,30 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
} }
// Perform 5 scans of 2 seconds each to increase chance of detecting all beacons // Perform 5 scans of 2 seconds each to increase chance of detecting all beacons
debugPrint('[BeaconScan] Starting 5 scan cycles of 2 seconds each'); print('[BeaconScan] 🔄 Starting 5 scan cycles of 2 seconds each');
for (int scanCycle = 1; scanCycle <= 5; scanCycle++) { for (int scanCycle = 1; scanCycle <= 5; scanCycle++) {
debugPrint('[BeaconScan] ----- Scan cycle $scanCycle/5 -----'); print('[BeaconScan] ----- Scan cycle $scanCycle/5 -----');
StreamSubscription<RangingResult>? subscription; StreamSubscription<RangingResult>? subscription;
subscription = flutterBeacon.ranging(regions).listen((result) { subscription = flutterBeacon.ranging(regions).listen((result) {
debugPrint('[BeaconScan] Ranging result: ${result.beacons.length} beacons in range'); print('[BeaconScan] 📶 Ranging result: ${result.beacons.length} beacons in range');
for (var beacon in result.beacons) { for (var beacon in result.beacons) {
final rawUUID = beacon.proximityUUID; final rawUUID = beacon.proximityUUID;
final uuid = rawUUID.toUpperCase().replaceAll('-', ''); final uuid = rawUUID.toUpperCase().replaceAll('-', '');
final rssi = beacon.rssi; final rssi = beacon.rssi;
debugPrint('[BeaconScan] Raw beacon detected: UUID=$rawUUID (normalized: $uuid), RSSI=$rssi, Major=${beacon.major}, Minor=${beacon.minor}'); print('[BeaconScan] 📍 Raw beacon detected: UUID=$rawUUID (normalized: $uuid), RSSI=$rssi, Major=${beacon.major}, Minor=${beacon.minor}');
if (_uuidToBeaconId.containsKey(uuid)) { if (_uuidToBeaconId.containsKey(uuid)) {
// Collect RSSI samples for averaging // Collect RSSI samples for averaging
_beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi); _beaconRssiSamples.putIfAbsent(uuid, () => []).add(rssi);
_beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1; _beaconDetectionCount[uuid] = (_beaconDetectionCount[uuid] ?? 0) + 1;
debugPrint('[BeaconScan] MATCHED! BeaconID=${_uuidToBeaconId[uuid]}, Sample #${_beaconDetectionCount[uuid]}, RSSI=$rssi'); print('[BeaconScan] ✅ MATCHED! BeaconID=${_uuidToBeaconId[uuid]}, Sample #${_beaconDetectionCount[uuid]}, RSSI=$rssi');
} else { } else {
debugPrint('[BeaconScan] UUID not in database, ignoring'); print('[BeaconScan] ⚠️ UUID not in database, ignoring');
} }
} }
}); });
@ -154,13 +157,13 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
} }
} }
debugPrint('[BeaconScan] All scan cycles complete'); print('[BeaconScan] ✔️ All scan cycles complete');
if (!mounted) return; if (!mounted) return;
// Analyze results and select best beacon // Analyze results and select best beacon
debugPrint('[BeaconScan] ===== SCAN COMPLETE ====='); print('[BeaconScan] ===== SCAN COMPLETE =====');
debugPrint('[BeaconScan] Total beacons detected: ${_beaconRssiSamples.length}'); print('[BeaconScan] 📊 Total beacons detected: ${_beaconRssiSamples.length}');
final beaconScores = <String, BeaconScore>{}; final beaconScores = <String, BeaconScore>{};
@ -187,19 +190,19 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
} }
if (beaconScores.isNotEmpty) { if (beaconScores.isNotEmpty) {
debugPrint('[BeaconScan] Beacon analysis results:'); print('[BeaconScan] Beacon analysis results:');
final sorted = beaconScores.values.toList() final sorted = beaconScores.values.toList()
..sort((a, b) => b.avgRssi.compareTo(a.avgRssi)); // Sort by avg RSSI descending ..sort((a, b) => b.avgRssi.compareTo(a.avgRssi)); // Sort by avg RSSI descending
for (final score in sorted) { for (final score in sorted) {
debugPrint('[BeaconScan] - BeaconID=${score.beaconId}:'); print('[BeaconScan] - BeaconID=${score.beaconId}:');
debugPrint('[BeaconScan] Avg RSSI: ${score.avgRssi.toStringAsFixed(1)}'); print('[BeaconScan] Avg RSSI: ${score.avgRssi.toStringAsFixed(1)}');
debugPrint('[BeaconScan] Range: ${score.minRssi} to ${score.maxRssi}'); print('[BeaconScan] Range: ${score.minRssi} to ${score.maxRssi}');
debugPrint('[BeaconScan] Detections: ${score.detectionCount}'); print('[BeaconScan] Detections: ${score.detectionCount}');
debugPrint('[BeaconScan] Variance: ${score.variance.toStringAsFixed(2)}'); print('[BeaconScan] Variance: ${score.variance.toStringAsFixed(2)}');
} }
} }
debugPrint('[BeaconScan] =========================='); print('[BeaconScan] ==========================');
if (beaconScores.isEmpty) { if (beaconScores.isEmpty) {
setState(() => _status = 'No beacons nearby'); setState(() => _status = 'No beacons nearby');
@ -209,18 +212,19 @@ class _BeaconScanScreenState extends State<BeaconScanScreen> with SingleTickerPr
// Find beacon with highest average RSSI and minimum detections // Find beacon with highest average RSSI and minimum detections
final best = _findBestBeacon(beaconScores); final best = _findBestBeacon(beaconScores);
if (best != null) { if (best != null) {
debugPrint('[BeaconScan] Selected beacon: BeaconID=${best.beaconId} (avg RSSI: ${best.avgRssi.toStringAsFixed(1)})'); print('[BeaconScan] 🎯 Selected beacon: BeaconID=${best.beaconId} (avg RSSI: ${best.avgRssi.toStringAsFixed(1)})');
setState(() => _status = 'Beacon detected! Loading business...'); setState(() => _status = 'Beacon detected! Loading business...');
await _autoSelectBusinessFromBeacon(best.beaconId); await _autoSelectBusinessFromBeacon(best.beaconId);
} else { } else {
debugPrint('[BeaconScan] No beacon met minimum confidence threshold'); print('[BeaconScan] ⚠️ No beacon met minimum confidence threshold');
setState(() => _status = 'No strong beacon signal'); setState(() => _status = 'No strong beacon signal');
await Future.delayed(const Duration(milliseconds: 800)); await Future.delayed(const Duration(milliseconds: 800));
if (mounted) _navigateToRestaurantSelect(); if (mounted) _navigateToRestaurantSelect();
} }
} }
} catch (e) { } catch (e) {
debugPrint('[BeaconScan] Error during scan: $e'); print('[BeaconScan] ❌ ERROR during scan: $e');
print('[BeaconScan] Stack trace: ${StackTrace.current}');
if (mounted) { if (mounted) {
setState(() => _status = 'Scan error - continuing to manual selection'); setState(() => _status = 'Scan error - continuing to manual selection');
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));

View file

@ -37,16 +37,6 @@ class _RestaurantSelectScreenState extends State<RestaurantSelectScreen> {
Future<void> _selectBusinessAndContinue(Restaurant r) async { Future<void> _selectBusinessAndContinue(Restaurant r) async {
final appState = context.read<AppState>(); final appState = context.read<AppState>();
// You MUST have a userId for ordering.
final userId = appState.userId;
if (userId == null || userId <= 0) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Missing UserID (not logged in).")),
);
return;
}
// Set selected business // Set selected business
appState.setBusiness(r.businessId); appState.setBusiness(r.businessId);
@ -55,7 +45,6 @@ class _RestaurantSelectScreenState extends State<RestaurantSelectScreen> {
AppRoutes.servicePointSelect, AppRoutes.servicePointSelect,
arguments: { arguments: {
"BusinessID": r.businessId, "BusinessID": r.businessId,
"UserID": userId,
}, },
); );
@ -65,13 +54,12 @@ class _RestaurantSelectScreenState extends State<RestaurantSelectScreen> {
// Store selection in AppState // Store selection in AppState
appState.setServicePoint(sp.servicePointId); appState.setServicePoint(sp.servicePointId);
// Navigate to Menu Browse // Navigate to Menu Browse - user can browse anonymously
Navigator.of(context).pushNamed( Navigator.of(context).pushNamed(
AppRoutes.menuBrowse, AppRoutes.menuBrowse,
arguments: { arguments: {
"BusinessID": r.businessId, "BusinessID": r.businessId,
"ServicePointID": sp.servicePointId, "ServicePointID": sp.servicePointId,
"UserID": userId,
}, },
); );
} }

View file

@ -20,22 +20,29 @@ class _SplashScreenState extends State<SplashScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
print('[Splash] 🚀 SplashScreen initState called');
_timer = Timer(const Duration(milliseconds: 2400), () async { _timer = Timer(const Duration(milliseconds: 2400), () async {
print('[Splash] ⏰ Timer fired, starting navigation logic');
if (!mounted) return; if (!mounted) return;
// Check for saved authentication credentials // Check for saved authentication credentials
print('[Splash] 🔐 Checking for saved auth credentials...');
final credentials = await AuthStorage.loadAuth(); final credentials = await AuthStorage.loadAuth();
if (credentials != null) { if (credentials != null) {
print('[Splash] ✅ Found saved credentials: UserID=${credentials.userId}');
// Restore authentication state // Restore authentication state
Api.setAuthToken(credentials.token); Api.setAuthToken(credentials.token);
final appState = context.read<AppState>(); final appState = context.read<AppState>();
appState.setUserId(credentials.userId); appState.setUserId(credentials.userId);
} else {
print('[Splash] No saved credentials found');
} }
if (!mounted) return; if (!mounted) return;
// Always go to beacon scan first - allows browsing without login // Always go to beacon scan first - allows browsing without login
print('[Splash] 📡 Navigating to beacon scan screen');
Navigator.of(context).pushReplacementNamed(AppRoutes.beaconScan); Navigator.of(context).pushReplacementNamed(AppRoutes.beaconScan);
}); });
} }

View file

@ -60,10 +60,8 @@ class Api {
static String get baseUrl { static String get baseUrl {
const v = String.fromEnvironment("AALISTS_API_BASE_URL"); const v = String.fromEnvironment("AALISTS_API_BASE_URL");
if (v.isEmpty) { if (v.isEmpty) {
throw StateError( // Default to production API
"AALISTS_API_BASE_URL is not set. Example (Android emulator): " return "https://biz.payfrit.com/api";
"--dart-define=AALISTS_API_BASE_URL=http://10.0.2.2:8888/biz.payfrit.com/api",
);
} }
return v; return v;
} }