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:
parent
114401c130
commit
7c366d5a9c
5 changed files with 45 additions and 45 deletions
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue