payfrit-app/lib/screens/group_order_invite_screen.dart
John Mizerek 2522970078 Version 3.0.0+9: Fix beacon scanning and SnackBar styling
- Restore FOREGROUND_SERVICE permission for beacon scanning
- Remove FOREGROUND_SERVICE_LOCATION (no video required)
- Update all SnackBars to Payfrit green (#90EE90) with black text
- Float SnackBars with 80px bottom margin to avoid buttons
- Add signup screen with OTP verification flow
- Fix build.gradle.kts to use Flutter version system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:16:10 -08:00

305 lines
9.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../app/app_router.dart';
import '../app/app_state.dart';
/// Screen to invite additional Payfrit users to join a group order
/// Shown after selecting Delivery or Takeaway
class GroupOrderInviteScreen extends StatefulWidget {
const GroupOrderInviteScreen({super.key});
@override
State<GroupOrderInviteScreen> createState() => _GroupOrderInviteScreenState();
}
class _GroupOrderInviteScreenState extends State<GroupOrderInviteScreen> {
final List<String> _invitedUsers = [];
final TextEditingController _searchController = TextEditingController();
bool _isSearching = false;
// Mock search results - in production this would come from API
final List<_UserResult> _searchResults = [];
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _searchUsers(String query) {
if (query.isEmpty) {
setState(() {
_searchResults.clear();
_isSearching = false;
});
return;
}
setState(() => _isSearching = true);
// TODO: Replace with actual API call to search users by phone/email/username
// For now, show placeholder
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted && _searchController.text == query) {
setState(() {
_searchResults.clear();
// Mock results - would come from API
if (query.length >= 3) {
_searchResults.addAll([
_UserResult(
userId: 1,
name: 'John D.',
phone: '***-***-${query.substring(0, 4)}',
),
]);
}
_isSearching = false;
});
}
});
}
void _inviteUser(_UserResult user) {
if (!_invitedUsers.contains(user.name)) {
setState(() {
_invitedUsers.add(user.name);
_searchResults.clear();
_searchController.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Invitation sent to ${user.name}', style: const TextStyle(color: Colors.black)),
backgroundColor: const Color(0xFF90EE90),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.only(bottom: 80, left: 16, right: 16),
),
);
}
}
void _removeInvite(String userName) {
setState(() {
_invitedUsers.remove(userName);
});
}
void _continueToRestaurants() {
// Store invited users in app state if needed
final appState = context.read<AppState>();
// TODO: appState.setGroupOrderInvites(_invitedUsers);
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
}
void _skipInvites() {
Navigator.of(context).pushReplacementNamed(AppRoutes.restaurantSelect);
}
@override
Widget build(BuildContext context) {
final appState = context.watch<AppState>();
final orderTypeLabel = appState.isDelivery ? 'Delivery' : 'Takeaway';
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
title: Text('$orderTypeLabel Order'),
elevation: 0,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
const Icon(
Icons.group_add,
color: Colors.white54,
size: 48,
),
const SizedBox(height: 16),
const Text(
"Invite others to join",
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
"Add Payfrit users to share this order.\n${appState.isDelivery ? 'Split the delivery fee between everyone!' : 'Everyone pays for their own items.'}",
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Search field
TextField(
controller: _searchController,
onChanged: _searchUsers,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Search by phone or email...',
hintStyle: const TextStyle(color: Colors.white38),
prefixIcon: const Icon(Icons.search, color: Colors.white54),
suffixIcon: _isSearching
? const Padding(
padding: EdgeInsets.all(12),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white54,
),
),
)
: null,
filled: true,
fillColor: Colors.grey.shade900,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
// Search results
if (_searchResults.isNotEmpty) ...[
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: _searchResults.map((user) {
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.withAlpha(50),
child: Text(
user.name[0].toUpperCase(),
style: const TextStyle(color: Colors.blue),
),
),
title: Text(
user.name,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
user.phone,
style: const TextStyle(color: Colors.white54),
),
trailing: IconButton(
icon: const Icon(Icons.person_add, color: Colors.blue),
onPressed: () => _inviteUser(user),
),
);
}).toList(),
),
),
],
const SizedBox(height: 16),
// Invited users list
if (_invitedUsers.isNotEmpty) ...[
const Text(
'Invited:',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _invitedUsers.map((name) {
return Chip(
avatar: CircleAvatar(
backgroundColor: Colors.green.withAlpha(50),
child: const Icon(
Icons.check,
size: 16,
color: Colors.green,
),
),
label: Text(name),
deleteIcon: const Icon(Icons.close, size: 18),
onDeleted: () => _removeInvite(name),
backgroundColor: Colors.grey.shade800,
labelStyle: const TextStyle(color: Colors.white),
);
}).toList(),
),
],
const Spacer(),
// Continue button
if (_invitedUsers.isNotEmpty)
FilledButton.icon(
onPressed: _continueToRestaurants,
icon: const Icon(Icons.group),
label: Text('Continue with ${_invitedUsers.length + 1} people'),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
)
else
FilledButton(
onPressed: _continueToRestaurants,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text('Continue Alone'),
),
const SizedBox(height: 12),
// Skip button
TextButton(
onPressed: _skipInvites,
child: const Text(
'Skip this step',
style: TextStyle(color: Colors.white54),
),
),
],
),
),
),
);
}
}
class _UserResult {
final int userId;
final String name;
final String phone;
const _UserResult({
required this.userId,
required this.name,
required this.phone,
});
}