Files
Eventify-frontend/lib/screens/checkout_screen.dart

393 lines
14 KiB
Dart
Raw Normal View History

// lib/screens/checkout_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../features/booking/providers/checkout_provider.dart';
import '../features/booking/services/payment_service.dart';
import '../features/booking/models/booking_models.dart';
import '../core/utils/error_utils.dart';
import 'tickets_booked_screen.dart';
class CheckoutScreen extends StatefulWidget {
final int eventId;
final String eventName;
final String? eventImage;
const CheckoutScreen({
Key? key,
required this.eventId,
required this.eventName,
this.eventImage,
}) : super(key: key);
@override
State<CheckoutScreen> createState() => _CheckoutScreenState();
}
class _CheckoutScreenState extends State<CheckoutScreen> {
late final PaymentService _paymentService;
final _formKey = GlobalKey<FormState>();
final _nameCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _phoneCtrl = TextEditingController();
@override
void initState() {
super.initState();
_paymentService = PaymentService();
_paymentService.initialize(
onSuccess: _onPaymentSuccess,
onError: _onPaymentError,
);
_prefillUserData();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<CheckoutProvider>().initForEvent(widget.eventId, widget.eventName);
});
}
Future<void> _prefillUserData() async {
final prefs = await SharedPreferences.getInstance();
_emailCtrl.text = prefs.getString('email') ?? '';
_nameCtrl.text = prefs.getString('display_name') ?? '';
_phoneCtrl.text = prefs.getString('phone_number') ?? '';
}
void _onPaymentSuccess(dynamic response) {
final provider = context.read<CheckoutProvider>();
provider.markPaymentSuccess(response.paymentId ?? 'success');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const TicketsBookedScreen()),
);
}
void _onPaymentError(dynamic response) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Payment failed: ${response.message ?? "Please try again"}'),
backgroundColor: Colors.red,
),
);
}
@override
void dispose() {
_paymentService.dispose();
_nameCtrl.dispose();
_emailCtrl.dispose();
_phoneCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Checkout', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
body: Consumer<CheckoutProvider>(
builder: (ctx, provider, _) {
if (provider.loading && provider.availableTickets.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
if (provider.error != null && provider.availableTickets.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(provider.error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => provider.initForEvent(widget.eventId, widget.eventName),
child: const Text('Retry'),
),
],
),
);
}
return Column(
children: [
_buildStepIndicator(provider),
Expanded(child: _buildCurrentStep(provider)),
_buildBottomBar(provider),
],
);
},
),
);
}
Widget _buildStepIndicator(CheckoutProvider provider) {
final steps = ['Tickets', 'Details', 'Payment'];
final currentIdx = provider.currentStep.index;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
color: Colors.white,
child: Row(
children: List.generate(steps.length, (i) {
final isActive = i <= currentIdx;
return Expanded(
child: Row(
children: [
Container(
width: 28, height: 28,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive ? const Color(0xFF0B63D6) : Colors.grey.shade300,
),
child: Center(
child: Text('${i + 1}', style: TextStyle(
color: isActive ? Colors.white : Colors.grey,
fontSize: 13, fontWeight: FontWeight.w600,
)),
),
),
const SizedBox(width: 6),
Text(steps[i], style: TextStyle(
fontSize: 13,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
color: isActive ? Colors.black : Colors.grey,
)),
if (i < steps.length - 1) Expanded(
child: Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 8),
color: i < currentIdx ? const Color(0xFF0B63D6) : Colors.grey.shade300,
),
),
],
),
);
}),
),
);
}
Widget _buildCurrentStep(CheckoutProvider provider) {
switch (provider.currentStep) {
case CheckoutStep.tickets:
return _buildTicketSelection(provider);
case CheckoutStep.details:
return _buildDetailsForm(provider);
case CheckoutStep.payment:
return _buildPaymentReview(provider);
case CheckoutStep.confirmation:
return const Center(child: Text('Booking confirmed!'));
}
}
Widget _buildTicketSelection(CheckoutProvider provider) {
if (provider.availableTickets.isEmpty) {
return const Center(child: Text('No tickets available for this event.'));
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text(widget.eventName, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700)),
const SizedBox(height: 20),
...provider.availableTickets.map((ticket) {
final cartMatches = provider.cart.where((c) => c.ticket.id == ticket.id);
final cartItem = cartMatches.isNotEmpty ? cartMatches.first : null;
final qty = cartItem?.quantity ?? 0;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: qty > 0 ? const Color(0xFF0B63D6) : Colors.grey.shade200),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(ticket.ticketType, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 16)),
const SizedBox(height: 4),
Text('Rs ${ticket.price.toStringAsFixed(0)}', style: const TextStyle(color: Color(0xFF0B63D6), fontWeight: FontWeight.w700, fontSize: 18)),
if (ticket.description != null) ...[
const SizedBox(height: 4),
Text(ticket.description!, style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
],
],
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: qty > 0 ? () => provider.setTicketQuantity(ticket, qty - 1) : null,
),
Text('$qty', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
IconButton(
icon: const Icon(Icons.add_circle_outline, color: Color(0xFF0B63D6)),
onPressed: qty < ticket.availableQuantity
? () => provider.setTicketQuantity(ticket, qty + 1)
: null,
),
],
),
],
),
);
}),
],
);
}
Widget _buildDetailsForm(CheckoutProvider provider) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Contact Details', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const SizedBox(height: 16),
_field('Full Name', _nameCtrl, validator: (v) => v!.isEmpty ? 'Required' : null),
_field('Email', _emailCtrl, type: TextInputType.emailAddress, validator: (v) => v!.isEmpty ? 'Required' : null),
_field('Phone', _phoneCtrl, type: TextInputType.phone, validator: (v) => v!.isEmpty ? 'Required' : null),
],
),
),
);
}
Widget _field(String label, TextEditingController ctrl, {TextInputType? type, String? Function(String?)? validator}) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextFormField(
controller: ctrl,
keyboardType: type,
validator: validator,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
filled: true,
fillColor: Colors.grey.shade50,
),
),
);
}
Widget _buildPaymentReview(CheckoutProvider provider) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
const Text('Order Summary', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
const SizedBox(height: 16),
...provider.cart.map((item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${item.ticket.ticketType} x${item.quantity}'),
Text('Rs ${item.subtotal.toStringAsFixed(0)}', style: const TextStyle(fontWeight: FontWeight.w600)),
],
),
)),
const Divider(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)),
Text('Rs ${provider.total.toStringAsFixed(0)}', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Color(0xFF0B63D6))),
],
),
const SizedBox(height: 24),
if (_nameCtrl.text.isNotEmpty) ...[
Text('Name: ${_nameCtrl.text}', style: TextStyle(color: Colors.grey.shade700)),
Text('Email: ${_emailCtrl.text}', style: TextStyle(color: Colors.grey.shade700)),
Text('Phone: ${_phoneCtrl.text}', style: TextStyle(color: Colors.grey.shade700)),
],
],
);
}
Widget _buildBottomBar(CheckoutProvider provider) {
return Container(
padding: EdgeInsets.fromLTRB(16, 12, 16, MediaQuery.of(context).padding.bottom + 12),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 12, offset: const Offset(0, -4))],
),
child: Row(
children: [
if (provider.currentStep != CheckoutStep.tickets)
TextButton(
onPressed: provider.previousStep,
child: const Text('Back'),
),
const Spacer(),
if (provider.currentStep == CheckoutStep.tickets)
ElevatedButton(
onPressed: provider.hasItems ? provider.nextStep : null,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0B63D6),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text('Continue Rs ${provider.subtotal.toStringAsFixed(0)}'),
)
else if (provider.currentStep == CheckoutStep.details)
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
provider.setShipping(ShippingDetails(
name: _nameCtrl.text,
email: _emailCtrl.text,
phone: _phoneCtrl.text,
));
provider.nextStep();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0B63D6),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Review Order'),
)
else if (provider.currentStep == CheckoutStep.payment)
ElevatedButton(
onPressed: provider.loading ? null : () => _processPayment(provider),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0B63D6),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: provider.loading
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: Text('Pay Rs ${provider.total.toStringAsFixed(0)}'),
),
],
),
);
}
Future<void> _processPayment(CheckoutProvider provider) async {
try {
await provider.processCheckout();
_paymentService.openPayment(
amount: provider.total,
email: _emailCtrl.text,
phone: _phoneCtrl.text,
eventName: widget.eventName,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(userFriendlyError(e)), backgroundColor: Colors.red),
);
}
}
}