proxybuy-flutter/lib/screens/product/checkout.dart

804 lines
24 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:lba/gen/assets.gen.dart';
import 'package:lba/res/colors.dart';
import 'package:lba/screens/product/map_selection_screen.dart';
import 'package:lba/widgets/add_card_bottom_sheet.dart';
import 'package:lba/widgets/time_selection_bottom_sheet.dart';
class CheckoutPage extends StatefulWidget {
const CheckoutPage({super.key});
@override
State<CheckoutPage> createState() => _CheckoutPageState();
}
class _CheckoutPageState extends State<CheckoutPage>
with TickerProviderStateMixin {
int _selectedTabIndex = 0;
String? _selectedAddress;
String? _selectedTime;
String? _selectedPickupTime;
int _selectedDeliveryOption = 0;
String? _cardNumber;
late AnimationController _deliveryController;
late AnimationController _pickupController;
late Animation<Color?> _deliveryColorAnimation;
late Animation<Color?> _pickupColorAnimation;
late Animation<double> _deliveryScaleAnimation;
late Animation<double> _pickupScaleAnimation;
late Animation<Offset> _deliverySlideAnimation;
late Animation<Offset> _pickupSlideAnimation;
static const Color greyTextMid = Color(0xFF616161);
static const Color greyTextLight = Color(0xFF9E9E9E);
static const Color activeColor = Color(0xFF189CFF);
@override
void initState() {
super.initState();
_deliveryController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
_pickupController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
_deliveryColorAnimation =
ColorTween(begin: greyTextLight, end: activeColor)
.animate(_deliveryController);
_pickupColorAnimation = ColorTween(begin: greyTextLight, end: activeColor)
.animate(_pickupController);
_deliveryScaleAnimation =
Tween<double>(begin: 1.0, end: 1.1).animate(_deliveryController);
_pickupScaleAnimation =
Tween<double>(begin: 1.0, end: 1.1).animate(_pickupController);
_deliverySlideAnimation =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(0, -0.1))
.animate(_deliveryController);
_pickupSlideAnimation =
Tween<Offset>(begin: const Offset(0, 0), end: const Offset(0, -0.1))
.animate(_pickupController);
if (_selectedTabIndex == 0) {
_deliveryController.forward();
} else {
_pickupController.forward();
}
}
@override
void dispose() {
_deliveryController.dispose();
_pickupController.dispose();
super.dispose();
}
void _onTabSelected(int index) {
setState(() {
_selectedTabIndex = index;
});
if (index == 0) {
_deliveryController.forward();
_pickupController.reverse();
} else {
_pickupController.forward();
_deliveryController.reverse();
}
}
Future<void> _selectAddress() async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const MapSelectionScreen()),
);
if (result != null && result.isNotEmpty) {
setState(() {
_selectedAddress = result;
});
}
}
Future<void> _selectTime() async {
final result = await showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const TimeSelectionBottomSheet(),
);
if (result != null && result.isNotEmpty) {
setState(() {
_selectedTime = result;
_selectedDeliveryOption = 2;
});
}
}
Future<void> _selectPickupTime() async {
final result = await showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const TimeSelectionBottomSheet(),
);
if (result != null && result.isNotEmpty) {
setState(() {
_selectedPickupTime = result;
});
}
}
Future<void> _addCard() async {
final result = await showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => const AddCardBottomSheet(),
);
if (result != null && result.isNotEmpty) {
setState(() {
_cardNumber = result;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: Column(
children: [
_buildTabs(),
Container(height: 1, color: LightAppColors.divider, width: 370),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (child, animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: Offset(_selectedTabIndex == 0 ? 0.5 : -0.5, 0),
end: Offset.zero,
).animate(animation),
child: child,
),
);
},
child: _selectedTabIndex == 0
? _buildDeliveryContent()
: _buildPickupContent(),
),
),
],
),
bottomNavigationBar: _payBar(),
);
}
Widget _deliveryTimeCards() {
bool isAsapSelected = _selectedDeliveryOption == 1;
bool isScheduleSelected = _selectedDeliveryOption == 2;
return Column(
children: [
_clickableCard(
onTap: () {
setState(() {
_selectedDeliveryOption = 1;
_selectedTime = null;
});
},
isSelected: isAsapSelected,
customChild: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
"ASAP",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black),
),
SizedBox(height: 4),
Text(
"Delivered directly to you",
style:
TextStyle(fontSize: 13, color: greyTextLight, height: 1.1),
),
],
),
),
const SizedBox(height: 12),
_clickableCard(
onTap: _selectTime,
isSelected: isScheduleSelected,
customChild: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("Schedule",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black)),
const SizedBox(height: 4),
Text(
_selectedTime != null ? _selectedTime! : "Select a time",
style: TextStyle(
fontSize: 13,
color: _selectedTime != null
? Colors.black
: greyTextLight,
height: 1.1),
),
],
),
),
SvgPicture.asset(
Assets.icons.arrowRight.path,
height: 20,
color: Colors.black,
),
],
),
),
],
);
}
Widget _buildDeliveryContent() {
return SingleChildScrollView(
key: const ValueKey('delivery'),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
_sectionHeader("Deliver to:",
trailingAction: "Select address", onAction: _selectAddress),
const SizedBox(height: 10),
_selectAddressRow(),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 20),
_sectionHeader("Delivery time:"),
const SizedBox(height: 12),
_deliveryTimeCards(),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 15),
_sectionHeader("Payment Method:"),
const SizedBox(height: 12),
_clickableCard(
leading: Assets.icons.cardAdd.path,
title: _cardNumber ?? "Add a credit card",
onTap: _addCard,
),
const SizedBox(height: 24),
const DottedDivider(),
const SizedBox(height: 24),
_sectionHeader("promo code:"),
const SizedBox(height: 12),
_promoCodeField(),
const SizedBox(height: 28),
_priceDetails(),
const SizedBox(height: 40),
],
),
);
}
Widget _buildPickupContent() {
return SingleChildScrollView(
key: const ValueKey('pickup'),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
_sectionHeader("Pickup from:",
trailingAction: "Select address", onAction: () {}),
const SizedBox(height: 10),
_pickupAddressRow("Mall of the Emirates"),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 20),
_sectionHeader("Pickup time:",
trailingAction: "Select time", onAction: _selectPickupTime),
const SizedBox(height: 12),
_pickupTimeRow(_selectedPickupTime ?? "Set a time for your Pickup"),
const SizedBox(height: 20),
const DottedDivider(),
const SizedBox(height: 15),
_sectionHeader("Payment Method:"),
const SizedBox(height: 12),
_clickableCard(
leading: Assets.icons.cardAdd.path,
title: _cardNumber ?? "Add a credit card",
onTap: _addCard,
),
const SizedBox(height: 24),
const DottedDivider(),
const SizedBox(height: 24),
_sectionHeader("promo code:"),
const SizedBox(height: 12),
_promoCodeField(),
const SizedBox(height: 28),
_priceDetails(),
const SizedBox(height: 40),
],
),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
elevation: 0,
backgroundColor: Colors.white,
centerTitle: false,
leading: IconButton(
icon: SvgPicture.asset(
Assets.icons.arrowLeft.path,
height: 20,
),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'Checkout',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.normal,
),
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: Container(
color: LightAppColors.divider,
height: 1.0,
),
),
);
}
Widget _buildTabs() {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
children: [
_buildAnimatedTabItem(
index: 0,
selectedIcon: Assets.icons.deliveryOn.path,
unselectedIcon: Assets.icons.deliveryOff.path,
label: 'Delivery',
controller: _deliveryController,
colorAnimation: _deliveryColorAnimation,
scaleAnimation: _deliveryScaleAnimation,
slideAnimation: _deliverySlideAnimation,
),
const SizedBox(width: 16),
_buildAnimatedTabItem(
index: 1,
selectedIcon: Assets.icons.pickupOn.path,
unselectedIcon: Assets.icons.pickUpOff.path,
label: 'Pickup',
controller: _pickupController,
colorAnimation: _pickupColorAnimation,
scaleAnimation: _pickupScaleAnimation,
slideAnimation: _pickupSlideAnimation,
),
],
),
);
}
Widget _buildAnimatedTabItem({
required int index,
required String selectedIcon,
required String unselectedIcon,
required String label,
required AnimationController controller,
required Animation<Color?> colorAnimation,
required Animation<double> scaleAnimation,
required Animation<Offset> slideAnimation,
}) {
final bool selected = _selectedTabIndex == index;
return Expanded(
child: InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () => _onTabSelected(index),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.fromLTRB(6, 8, 6, 0),
child: Column(
children: [
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return SlideTransition(
position: slideAnimation,
child: ScaleTransition(
scale: scaleAnimation,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
selected ? selectedIcon : unselectedIcon,
color: colorAnimation.value,
),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorAnimation.value,
),
),
],
),
),
);
},
),
const SizedBox(height: 6),
AnimatedContainer(
duration: const Duration(milliseconds: 400),
height: 5,
width: selected ? 100 : 0,
child: selected
? SvgPicture.asset(
Assets.icons.shape.path,
color: LightAppColors.primary,
)
: const SizedBox(),
),
],
),
),
),
);
}
Widget _sectionHeader(String title,
{String? trailingAction, VoidCallback? onAction}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
if (trailingAction != null)
GestureDetector(
onTap: onAction,
child: Text(
trailingAction,
style: const TextStyle(
color: LightAppColors.offerTimer,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Widget _selectAddressRow() {
return GestureDetector(
onTap: _selectAddress,
child: Row(
children: [
SvgPicture.asset(
Assets.icons.location.path,
color: const Color.fromARGB(255, 85, 84, 81),
),
const SizedBox(width: 10),
Expanded(
child: Text(
_selectedAddress ?? "Select address",
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: _selectedAddress == null ? greyTextMid : Colors.black,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _pickupAddressRow(String address) {
return Row(
children: [
SvgPicture.asset(
Assets.icons.location.path,
color: const Color.fromARGB(255, 85, 84, 81),
),
const SizedBox(width: 10),
Text(
address,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: greyTextMid,
),
),
],
);
}
Widget _pickupTimeRow(String time) {
bool hasSelectedTime = _selectedPickupTime != null;
return Text(
time,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: hasSelectedTime ? Colors.black : greyTextMid,
),
);
}
Widget _clickableCard({
String? leading,
String? title,
Widget? customChild,
VoidCallback? onTap,
bool isSelected = false,
}) {
return InkWell(
borderRadius: BorderRadius.circular(15),
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color:
isSelected ? LightAppColors.deliverySelectedButton : Colors.white,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: isSelected
? LightAppColors.confirmButton
: LightAppColors.divider,
width: isSelected ? 1.5 : 1.0,
),
),
child: customChild ??
Row(
children: [
if (leading != null) SvgPicture.asset(leading),
if (leading != null) const SizedBox(width: 10),
if (title != null)
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: greyTextMid,
),
),
),
const Icon(Icons.arrow_forward_ios, size: 16),
],
),
),
);
}
Widget _promoCodeField() {
return TextField(
style: const TextStyle(fontSize: 14),
decoration: InputDecoration(
prefixIcon: Padding(
padding: const EdgeInsets.all(15.0),
child: SvgPicture.asset(
Assets.icons.ticketDiscount2.path,
),
),
hintText: 'Enter your promo code here',
hintStyle: const TextStyle(color: greyTextLight, fontSize: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: LightAppColors.divider),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: LightAppColors.divider),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: LightAppColors.divider),
),
fillColor: Colors.white,
filled: true,
isDense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
),
);
}
Widget _priceDetails() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DottedDivider(),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
"Price Details",
style: TextStyle(
color: LightAppColors.textPrice,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
"1 Item",
style: TextStyle(
color: LightAppColors.textPrice,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 12),
const DottedDivider(),
const SizedBox(height: 14),
_priceRow("Subtotal", "27.900"),
const SizedBox(height: 12),
_priceRow("Sales tax", "0.000"),
const SizedBox(height: 14),
const DottedDivider(),
const SizedBox(height: 14),
_priceRow("Total", "27.900", isTotal: true),
],
);
}
Widget _priceRow(String label, String amount, {bool isTotal = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 15,
color:
isTotal ? LightAppColors.textPrice : LightAppColors.popupText,
fontWeight: isTotal ? FontWeight.w600 : FontWeight.normal,
),
),
Text(
"AED $amount",
style: TextStyle(
fontSize: 15,
color:
isTotal ? LightAppColors.textPrice : LightAppColors.popupText,
fontWeight: isTotal ? FontWeight.w700 : FontWeight.w600,
),
),
],
);
}
Widget _payBar() {
return SafeArea(
top: false,
child: Container(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
decoration: const BoxDecoration(color: Colors.white),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: LightAppColors.offerTimer,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
onPressed: () {},
child: const Text(
"Pay AED 27.900",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.2,
),
),
),
),
),
);
}
}
class DottedDivider extends StatelessWidget {
final double height;
final Color color;
final double dashWidth;
final double dashSpace;
const DottedDivider({
super.key,
this.height = 1,
this.color = const Color(0xFFE0E0E0),
this.dashWidth = 4,
this.dashSpace = 3,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
width: double.infinity,
child: CustomPaint(
painter: _DottedLinePainter(
color: color,
dashWidth: dashWidth,
dashSpace: dashSpace,
),
),
);
}
}
class _DottedLinePainter extends CustomPainter {
final Color color;
final double dashWidth;
final double dashSpace;
_DottedLinePainter({
required this.color,
required this.dashWidth,
required this.dashSpace,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = size.height
..style = PaintingStyle.stroke;
double x = 0;
while (x < size.width) {
canvas.drawLine(Offset(x, 0), Offset(x + dashWidth, 0), paint);
x += dashWidth + dashSpace;
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}