payfrit-app/lib/screens/restaurant_select_screen.dart
John Mizerek 7c366d5a9c 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>
2025-12-31 12:04:50 -08:00

234 lines
7.6 KiB
Dart

// lib/screens/restaurant_select_screen.dart
import "package:flutter/material.dart";
import "package:provider/provider.dart";
import "../app/app_router.dart";
import "../app/app_state.dart";
import "../models/restaurant.dart";
import "../models/service_point.dart";
import "../services/api.dart";
class RestaurantSelectScreen extends StatefulWidget {
const RestaurantSelectScreen({super.key});
@override
State<RestaurantSelectScreen> createState() => _RestaurantSelectScreenState();
}
class _RestaurantSelectScreenState extends State<RestaurantSelectScreen> {
late Future<List<Restaurant>> _future;
String? _debugLastRaw;
int? _debugLastStatus;
@override
void initState() {
super.initState();
_future = _load();
}
Future<List<Restaurant>> _load() async {
final raw = await Api.listRestaurantsRaw();
_debugLastRaw = raw.rawBody;
_debugLastStatus = raw.statusCode;
return Api.listRestaurants();
}
Future<void> _selectBusinessAndContinue(Restaurant r) async {
final appState = context.read<AppState>();
// Set selected business
appState.setBusiness(r.businessId);
// Go pick service point, and WAIT for a selection.
final sp = await Navigator.of(context).pushNamed(
AppRoutes.servicePointSelect,
arguments: {
"BusinessID": r.businessId,
},
);
if (!mounted) return;
if (sp is ServicePoint) {
// Store selection in AppState
appState.setServicePoint(sp.servicePointId);
// Navigate to Menu Browse - user can browse anonymously
Navigator.of(context).pushNamed(
AppRoutes.menuBrowse,
arguments: {
"BusinessID": r.businessId,
"ServicePointID": sp.servicePointId,
},
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Select Business"),
),
body: FutureBuilder<List<Restaurant>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return _ErrorPane(
title: "Businesses Load Failed",
message: snapshot.error.toString(),
statusCode: _debugLastStatus,
raw: _debugLastRaw,
onRetry: () => setState(() => _future = _load()),
);
}
final items = snapshot.data ?? const <Restaurant>[];
if (items.isEmpty) {
return _ErrorPane(
title: "No Businesses Returned",
message: "The API returned an empty list.",
statusCode: _debugLastStatus,
raw: _debugLastRaw,
onRetry: () => setState(() => _future = _load()),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, i) {
final r = items[i];
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(15),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Material(
borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.surface,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _selectBusinessAndContinue(r),
child: Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.store,
size: 32,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
r.name,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
"Tap to view menu",
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
color: Theme.of(context).colorScheme.onSurfaceVariant,
size: 20,
),
],
),
),
),
),
);
},
);
},
),
);
}
}
class _ErrorPane extends StatelessWidget {
final String title;
final String message;
final int? statusCode;
final String? raw;
final VoidCallback onRetry;
const _ErrorPane({
required this.title,
required this.message,
required this.statusCode,
required this.raw,
required this.onRetry,
});
@override
Widget build(BuildContext context) {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(18),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 720),
child: Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 10),
Text(message),
const SizedBox(height: 14),
if (statusCode != null) Text("HTTP: $statusCode"),
if (raw != null && raw!.trim().isNotEmpty) ...[
const SizedBox(height: 10),
const Text("Raw response:"),
const SizedBox(height: 6),
Text(raw!),
],
const SizedBox(height: 14),
FilledButton(
onPressed: onRetry,
child: const Text("Retry"),
),
],
),
),
),
),
),
);
}
}