fix: order detail screens

This commit is contained in:
mike doornenbal 2024-07-02 09:50:22 +02:00
parent d5309b8198
commit 4e37ec8b5d
21 changed files with 764 additions and 1545 deletions

View file

@ -3,15 +3,6 @@ library flutter_order_details;
export "src/configuration/order_detail_configuration.dart"; export "src/configuration/order_detail_configuration.dart";
export "src/configuration/order_detail_localization.dart"; export "src/configuration/order_detail_localization.dart";
export "src/configuration/order_detail_step.dart";
export "src/configuration/order_detail_title_style.dart"; export "src/configuration/order_detail_title_style.dart";
export "src/models/order_address_input.dart";
export "src/models/order_choice_input.dart";
export "src/models/order_dropdown_input.dart";
export "src/models/order_email_input.dart";
export "src/models/order_input.dart";
export "src/models/order_phone_input.dart";
export "src/models/order_result.dart"; export "src/models/order_result.dart";
export "src/models/order_text_input.dart";
export "src/models/order_time_picker_input.dart";
export "src/widgets/order_detail_screen.dart"; export "src/widgets/order_detail_screen.dart";

View file

@ -1,50 +1,537 @@
import "package:flutter/widgets.dart"; // ignore_for_file: avoid_annotating_with_dynamic
import "package:flutter_order_details/src/configuration/order_detail_localization.dart";
import "package:flutter_order_details/src/configuration/order_detail_step.dart"; import "package:animated_toggle/animated_toggle.dart";
import "package:flutter_order_details/src/models/order_result.dart"; import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Configuration for the order detail screen. /// Configuration for the order detail screen.
class OrderDetailConfiguration { class OrderDetailConfiguration {
/// Constructor for the order detail configuration. /// Constructor for the order detail configuration.
const OrderDetailConfiguration({ OrderDetailConfiguration({
required this.steps,
//
required this.onCompleted, required this.onCompleted,
// this.pages = _defaultPages,
this.progressIndicator = true,
//
this.localization = const OrderDetailLocalization(), this.localization = const OrderDetailLocalization(),
// this.appBar = _defaultAppBar,
this.inputFieldPadding = const EdgeInsets.symmetric( this.nextbuttonBuilder = _defaultNextButtonBuilder,
horizontal: 32,
vertical: 16,
),
this.titlePadding = const EdgeInsets.only(left: 16, right: 16, top: 16),
//
this.appBar,
}); });
/// The different steps that the user has to go through to complete the order. /// The different steps that the user has to go through to complete the order.
/// Each step contains a list of fields that the user has to fill in. /// Each step contains a list of fields that the user has to fill in.
final List<OrderDetailStep> steps; final List<FlutterFormPage> Function(BuildContext context) pages;
/// Callback function that is called when the user has completed the order. /// Callback function that is called when the user has completed the order.
/// The result of the order is passed as an argument to the function. /// The result of the order is passed as an argument to the function.
final Function(OrderResult result) onCompleted; final Function(dynamic value) onCompleted;
/// Whether or not you want to show a progress indicator at
/// the top of the screen.
final bool progressIndicator;
/// Localization for the order detail screen. /// Localization for the order detail screen.
final OrderDetailLocalization localization; final OrderDetailLocalization localization;
/// Padding around the input fields.
final EdgeInsets inputFieldPadding;
/// Padding around the title of the input fields.
final EdgeInsets titlePadding;
/// Optional app bar that you can pass to the order detail screen. /// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget? appBar; final AppBar Function(
BuildContext context,
OrderDetailLocalization localizations,
) appBar;
/// Optional next button builder that you can pass to the order detail screen.
final Widget Function(
int a,
// ignore: avoid_positional_boolean_parameters
bool b,
BuildContext context,
OrderDetailConfiguration configuration,
FlutterFormController controller,
) nextbuttonBuilder;
}
AppBar _defaultAppBar(
BuildContext context,
OrderDetailLocalization localizations,
) {
var theme = Theme.of(context);
return AppBar(
title: Text(
localizations.orderDetailsTitle,
style: theme.textTheme.headlineLarge,
),
);
}
Widget _defaultNextButtonBuilder(
int currentStep,
bool b,
BuildContext context,
OrderDetailConfiguration configuration,
FlutterFormController controller,
) {
var theme = Theme.of(context);
var nextButtonTexts = [
"Choose date and time",
"Next",
"Next",
"Place another order",
];
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 48),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () async {
controller.validateAndSaveCurrentStep();
await controller.autoNextStep();
},
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
nextButtonTexts[currentStep],
style: theme.textTheme.displayLarge,
),
),
),
),
),
);
}
List<FlutterFormPage> _defaultPages(BuildContext context) {
var theme = Theme.of(context);
var morningTimes = <String>[
"09:00",
"09:15",
"09:30",
"09:45",
"10:00",
"10:15",
"10:30",
"10:45",
"11:00",
"11:15",
"11:30",
"11:45",
];
var afternoonTimes = <String>[
"12:00",
"12:15",
"12:30",
"12:45",
"13:00",
"13:15",
"13:30",
"13:45",
"14:00",
"14:15",
"14:30",
"14:45",
"15:00",
"15:15",
"15:30",
"15:45",
"16:00",
"16:15",
"16:30",
"16:45",
"17:00",
];
InputDecoration inputDecoration(String hint) => InputDecoration(
hintStyle: theme.textTheme.bodySmall,
hintText: hint,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
);
InputDecoration dropdownInputDecoration(String hint) => InputDecoration(
hintStyle: theme.textTheme.bodySmall,
hintText: hint,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
);
var switchStatus = ValueNotifier<bool>(false);
var multipleChoiceController = FlutterFormInputMultipleChoiceController(
id: "multipleChoice",
mandatory: true,
);
return [
FlutterFormPage(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"What's your name?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Name"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "name",
mandatory: true,
),
validationMessage: "Please enter your name",
),
const SizedBox(
height: 16,
),
Text(
"What's your address?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Street and number"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "street",
mandatory: true,
),
validationMessage: "Please enter your address",
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a street and house number";
}
var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$");
if (!regex.hasMatch(value)) {
return "Invalid street and house number";
}
return null;
},
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Postal code"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "postalCode",
mandatory: true,
),
validationMessage: "Please enter your postal code",
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a postal code";
}
var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$");
if (!regex.hasMatch(value)) {
return "Invalid postal code format";
}
return null;
},
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("City"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "city",
mandatory: true,
),
validationMessage: "Please enter your city",
),
const SizedBox(
height: 16,
),
Text(
"What's your phone number?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPhone(
numberFieldStyle: theme.textTheme.bodySmall,
textAlignVertical: TextAlignVertical.center,
decoration: inputDecoration("Phone number"),
controller: FlutterFormInputPhoneController(
id: "phone",
mandatory: true,
),
validationMessage: "Please enter your phone number",
validator: (value) {
if (value == null || value.number!.isEmpty) {
return "Please enter a phone number";
}
// Remove any spaces or hyphens from the input
var phoneNumber =
value.number!.replaceAll(RegExp(r"\s+|-"), "");
// Check the length of the remaining digits
if (phoneNumber.length != 10 && phoneNumber.length != 11) {
return "Invalid phone number length";
}
// Check if all remaining characters are digits
if (!phoneNumber.substring(1).contains(RegExp(r"^[0-9]*$"))) {
return "Phone number can only contain digits";
}
// If all checks pass, return null (no error)
return null;
},
),
const SizedBox(
height: 16,
),
Text(
"What's your email address?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputEmail(
style: theme.textTheme.bodySmall,
decoration: inputDecoration("email address"),
controller: FlutterFormInputEmailController(
id: "email",
mandatory: true,
),
validationMessage: "Please fill in a valid email address",
),
const SizedBox(
height: 16,
),
Text(
"Do you have any comments?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Optional"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "comments",
),
validationMessage: "Please enter your email address",
),
const SizedBox(
height: 100,
),
],
),
),
),
),
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
"When and at what time would you like to pick up your order?",
style: theme.textTheme.titleMedium,
),
),
const SizedBox(
height: 4,
),
FlutterFormInputDropdown(
icon: const Icon(
Icons.keyboard_arrow_down,
color: Colors.black,
),
isDense: true,
decoration: dropdownInputDecoration("Select a day"),
validationMessage: "Please select a day",
controller: FlutterFormInputDropdownController(
id: "date",
mandatory: true,
),
items: [
DropdownMenuItem(
value: "Today",
child: Text(
"Today",
style: theme.textTheme.bodySmall,
),
),
DropdownMenuItem(
value: "Tomorrow",
child: Text(
"Tomorrow",
style: theme.textTheme.bodySmall,
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: AnimatedToggle(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 5,
color: theme.colorScheme.primary.withOpacity(0.8),
),
],
color: Colors.white,
borderRadius: BorderRadius.circular(50),
),
width: 280,
toggleColor: theme.colorScheme.primary,
onSwitch: (value) {
switchStatus.value = value;
},
childLeft: Center(
child: ListenableBuilder(
listenable: switchStatus,
builder: (context, widget) => Text(
"Morning",
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? theme.colorScheme.primary
: Colors.white,
),
),
),
),
childRight: Center(
child: ListenableBuilder(
listenable: switchStatus,
builder: (context, widget) => Text(
"Afternoon",
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? Colors.white
: theme.colorScheme.primary,
),
),
),
),
),
),
const SizedBox(
height: 8,
),
ListenableBuilder(
listenable: switchStatus,
builder: (context, widget) => FlutterFormInputMultipleChoice(
validationMessage: "Select a Time",
controller: multipleChoiceController,
options: switchStatus.value ? afternoonTimes : morningTimes,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: MediaQuery.of(context).size.height * 0.6,
builder:
(context, index, selected, controller, options, state) =>
GestureDetector(
onTap: () {
state.didChange(options[index]);
selected.value = index;
controller.onSaved(options[index]);
},
child: Container(
decoration: BoxDecoration(
color: selected.value == index
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(10),
),
height: 40,
child: Center(
child: Text(options[index]),
),
),
),
),
),
],
),
),
),
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Payment method",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Text(
"Choose when you would like to to pay for the order.",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 84,
),
FlutterFormInputMultipleChoice(
crossAxisCount: 1,
mainAxisSpacing: 24,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: 420,
controller: FlutterFormInputMultipleChoiceController(
id: "payment",
mandatory: true,
),
options: const ["PAY NOW", "PAY AT THE CASHIER"],
builder: (context, index, selected, controller, options, state) =>
GestureDetector(
onTap: () {
state.didChange(options[index]);
selected.value = index;
controller.onSaved(options[index]);
},
child: Container(
decoration: BoxDecoration(
color: selected.value == index
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
),
height: 40,
child: Center(child: Text(options[index])),
),
),
validationMessage: "Please select a payment method",
),
],
),
),
),
];
} }

View file

@ -2,17 +2,17 @@
class OrderDetailLocalization { class OrderDetailLocalization {
/// Constructor for the order detail localization. /// Constructor for the order detail localization.
const OrderDetailLocalization({ const OrderDetailLocalization({
this.nextButton = "Next", this.nextButton = "Order",
this.backButton = "Back",
this.completeButton = "Complete", this.completeButton = "Complete",
this.orderDetailsTitle = "Information",
}); });
/// Next button localization. /// Next button localization.
final String nextButton; final String nextButton;
/// Back button localization.
final String backButton;
/// Complete button localization. /// Complete button localization.
final String completeButton; final String completeButton;
/// Title for the order details page.
final String orderDetailsTitle;
} }

View file

@ -1,22 +0,0 @@
import "package:flutter/widgets.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Configuration for the order detail step.
class OrderDetailStep {
/// Constructor for the order detail step.
OrderDetailStep({
required this.formKey,
required this.fields,
this.stepName,
});
/// Optional name for the step.
final String? stepName;
/// Form key for the step.
final GlobalKey<FormState> formKey;
/// List of fields that the user has to fill in.
/// Each field must extend from the `OrderDetailInput` class.
final List<OrderDetailInput> fields;
}

View file

@ -1,25 +0,0 @@
import "package:flutter/material.dart";
/// Error Builder for form fields.
class FormFieldErrorBuilder extends StatelessWidget {
/// Constructor for the form field error builder.
const FormFieldErrorBuilder({
required this.errorMessage,
super.key,
});
/// Error message to display.
final String errorMessage;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Text(
errorMessage,
textAlign: TextAlign.left,
style: TextStyle(
color: theme.colorScheme.error,
),
);
}
}

View file

@ -1,160 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Order input for addresses with with predefined text fields and validation.
class OrderAddressInput extends OrderDetailInput<String> {
/// Constructor of the order address input.
OrderAddressInput({
required super.title,
required super.outputKey,
required this.textController,
super.titleStyle,
super.titleAlignment,
super.titlePadding,
super.subtitle,
super.errorIsRequired,
super.hint = "0000XX",
super.isRequired,
super.isReadOnly,
super.initialValue,
this.streetNameTitle = "Street name",
this.postalCodeTitle = "Postal code",
this.cityTitle = "City",
this.streetNameValidators,
this.postalCodeValidators,
this.cityValidators,
this.inputFormatters,
super.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4),
});
/// Title for the street name.
final String streetNameTitle;
/// Title for the postal code.
final String postalCodeTitle;
/// Title for the city.
final String cityTitle;
/// Text Control parent that contains the value of all the other three
/// controllers.
final TextEditingController textController;
/// Text Controller for street names.
final TextEditingController streetNameController = TextEditingController();
/// Text Controller for postal codes.
final TextEditingController postalCodeController = TextEditingController();
/// Text Controller for the city name.
final TextEditingController cityController = TextEditingController();
/// Validators for the street name.
final List<String? Function(String?)>? streetNameValidators;
/// Validators for the postal code.
final List<String? Function(String?)>? postalCodeValidators;
/// Validators for the city.
final List<String? Function(String?)>? cityValidators;
/// Input formatters for the postal code.
final List<TextInputFormatter>? inputFormatters;
@override
Widget build(
BuildContext context,
String? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
void setUpControllers(String address) {
var addressParts = address.split(", ");
if (addressParts.isNotEmpty) {
streetNameController.text = addressParts[0];
}
if (addressParts.length > 1) {
postalCodeController.text = addressParts[1];
}
if (addressParts.length > 2) {
cityController.text = addressParts[2];
}
}
void inputChanged(String _) {
var address = "${streetNameController.text}, "
"${postalCodeController.text}, "
"${cityController.text}";
textController.text = address;
currentValue = address;
onValueChanged?.call(address);
}
textController.text = initialValue ?? buildInitialValue ?? "";
currentValue = textController.text;
setUpControllers(currentValue ?? "");
return buildOutline(
context,
[
OrderTextInput(
title: streetNameTitle,
outputKey: "internal_street_name",
textController: streetNameController,
titleStyle: OrderDetailTitleStyle.none,
onValueChanged: inputChanged,
hint: "De Dam 1",
initialValue: streetNameController.text,
validators: streetNameValidators ?? [],
),
OrderTextInput(
title: postalCodeTitle,
outputKey: "internal_postal_code",
textController: postalCodeController,
titleStyle: OrderDetailTitleStyle.none,
onValueChanged: inputChanged,
validators: postalCodeValidators ??
[
(value) {
if (value?.length != 6) {
return "Postal code must be 6 characters";
}
return null;
},
(value) {
if (value != null &&
!RegExp(r"^\d{4}\s?[a-zA-Z]{2}$").hasMatch(value)) {
return "Postal code must be in the format 0000XX";
}
return null;
}
],
inputFormatters: inputFormatters ??
[
FilteringTextInputFormatter.allow(RegExp(r"^\d{0,4}[A-Z]*")),
LengthLimitingTextInputFormatter(6),
],
hint: hint,
initialValue: postalCodeController.text,
),
OrderTextInput(
title: cityTitle,
outputKey: "internal_city",
textController: cityController,
titleStyle: OrderDetailTitleStyle.none,
onValueChanged: inputChanged,
hint: "Amsterdam",
initialValue: cityController.text,
validators: cityValidators ?? [],
),
],
onBlurBackground,
);
}
}

View file

@ -1,175 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_order_details/src/models/formfield_error_builder.dart";
/// Order input for choice with predefined text fields and validation.
class OrderChoiceInput extends OrderDetailInput<String> {
/// Constructor of the order choice input.
OrderChoiceInput({
required super.title,
required super.outputKey,
required this.items,
super.titleStyle,
super.titleAlignment,
super.titlePadding,
super.subtitle,
super.errorIsRequired,
super.isRequired,
super.isReadOnly,
super.initialValue,
this.fieldHeight = 140,
this.fieldPadding = const EdgeInsets.symmetric(
horizontal: 4,
vertical: 64,
),
this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 12),
});
/// Items to show within the dropdown menu.
final List<String> items;
/// Padding for the field.
final EdgeInsets fieldPadding;
/// Padding between fields.
@override
// ignore: overridden_fields
final EdgeInsets paddingBetweenFields;
/// The height of the input field.
final double fieldHeight;
final _ChoiceNotifier _notifier = _ChoiceNotifier();
@override
Widget build(
BuildContext context,
String? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
void onItemChanged(String value) {
if (value == currentValue) {
currentValue = null;
onValueChanged?.call("");
_notifier.setValue("");
} else {
currentValue = value;
onValueChanged?.call(value);
_notifier.setValue(value);
}
}
return buildOutline(
context,
ListenableBuilder(
listenable: _notifier,
builder: (context, child) => _ChoiceInputField(
currentValue: currentValue ?? initialValue ?? buildInitialValue ?? "",
items: items,
onTap: onItemChanged,
validate: validate,
fieldPadding: fieldPadding,
paddingBetweenFields: paddingBetweenFields,
),
),
onBlurBackground,
);
}
}
class _ChoiceNotifier extends ChangeNotifier {
String? _value;
String? get value => _value;
void setValue(String value) {
_value = value;
notifyListeners();
}
}
class _ChoiceInputField<T> extends FormField<T> {
_ChoiceInputField({
required T currentValue,
required List<T> items,
required Function(T) onTap,
required String? Function(T?) validate,
required EdgeInsets fieldPadding,
required EdgeInsets paddingBetweenFields,
super.key,
}) : super(
validator: (value) => validate(currentValue),
builder: (FormFieldState<T> field) => Padding(
padding: fieldPadding,
child: Column(
children: [
for (var item in items) ...[
Padding(
padding: paddingBetweenFields,
child: _InputContent<T>(
i: item,
currentValue: currentValue,
onTap: onTap,
),
),
],
if (field.hasError) ...[
FormFieldErrorBuilder(errorMessage: field.errorText!),
],
],
),
),
);
}
class _InputContent<T> extends StatelessWidget {
const _InputContent({
required this.i,
required this.currentValue,
required this.onTap,
});
final T i;
final T currentValue;
final Function(T) onTap;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var boxDecoration = BoxDecoration(
color: currentValue == i.toString()
? theme.colorScheme.primary
: theme.colorScheme.secondary,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.primary,
width: 1,
),
);
var decoratedBox = Container(
decoration: boxDecoration,
width: double.infinity,
height: 150,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
i.toString(),
style: theme.textTheme.labelLarge?.copyWith(
color: currentValue == i.toString()
? theme.colorScheme.onPrimary
: theme.colorScheme.primary,
),
),
],
),
);
return GestureDetector(
onTap: () => onTap(i),
child: decoratedBox,
);
}
}

View file

@ -1,153 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Order Detail input for a dropdown input.
class OrderDropdownInput<T> extends OrderDetailInput<T> {
/// Constructor for the order dropdown input.
OrderDropdownInput({
required super.title,
required super.outputKey,
required this.items,
super.titleStyle,
super.titleAlignment,
super.titlePadding,
super.subtitle,
super.errorIsRequired,
super.isRequired = true,
super.isReadOnly,
super.initialValue,
this.blurOnInteraction = true,
});
/// Items to show within the dropdown menu.
final List<T> items;
/// Whether or not the screen should blur when interacting.
final bool blurOnInteraction;
@override
Widget build(
BuildContext context,
T? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
var theme = Theme.of(context);
void onItemChanged(T? value) {
currentValue = value;
onValueChanged?.call(value as T);
onBlurBackground(needsBlur: false);
}
void onPopupOpen() {
if (blurOnInteraction)
onBlurBackground(
needsBlur: true,
);
}
var inputDecoration = InputDecoration(
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
hintText: hint,
filled: true,
fillColor: theme.inputDecorationTheme.fillColor,
border: InputBorder.none,
);
currentValue =
currentValue ?? initialValue ?? buildInitialValue ?? items[0];
return buildOutline(
context,
DropdownButtonFormField<T>(
value: currentValue ?? initialValue ?? buildInitialValue ?? items[0],
selectedItemBuilder: (context) => items
.map(
(item) => Text(
item.toString(),
style: theme.textTheme.labelMedium,
),
)
.toList(),
items: items
.map(
(item) => DropdownMenuItem<T>(
value: item,
child: _DropdownButtonBuilder<T>(
item: item,
currentValue: currentValue,
),
),
)
.toList(),
onChanged: onItemChanged,
onTap: onPopupOpen,
style: theme.textTheme.labelMedium,
decoration: inputDecoration,
borderRadius: BorderRadius.circular(10),
icon: const Icon(Icons.keyboard_arrow_down_sharp),
validator: super.validate,
),
onBlurBackground,
);
}
}
class _DropdownButtonBuilder<T> extends StatelessWidget {
const _DropdownButtonBuilder({
required this.item,
this.currentValue,
super.key,
});
final T item;
final T? currentValue;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var textBuilder = Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
item.toString(),
style: theme.textTheme.labelMedium?.copyWith(
color: item == currentValue ? theme.colorScheme.onPrimary : null,
fontWeight: FontWeight.w500,
),
),
),
);
var selectedIcon = Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.check,
color: theme.colorScheme.onPrimary,
),
),
);
return DecoratedBox(
decoration: BoxDecoration(
color: item == currentValue ? theme.colorScheme.primary : null,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: theme.colorScheme.primary,
),
),
child: Stack(
children: [
textBuilder,
if (currentValue == item) ...[
selectedIcon,
],
],
),
);
}
}

View file

@ -1,75 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Order Email input with predefined validators.
class OrderEmailInput extends OrderDetailInput<String> {
/// Constructor of the order email input.
OrderEmailInput({
required super.title,
required super.outputKey,
required this.textController,
super.titleStyle,
super.titleAlignment,
super.titlePadding,
super.subtitle,
super.hint,
super.errorIsRequired,
super.isRequired,
super.isReadOnly,
super.initialValue,
this.errorInvalidEmail = "Invalid email ( your_name@example.com )",
}) : super(
validators: [
(value) {
if (value != null && !RegExp(r"^\w+@\w+\.\w+$").hasMatch(value)) {
return errorInvalidEmail;
}
return null;
},
],
);
/// Text Controller for email input.
final TextEditingController textController;
/// Error message for invalid email.
final String errorInvalidEmail;
@override
Widget build(
BuildContext context,
String? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
var theme = Theme.of(context);
textController.text = initialValue ?? buildInitialValue ?? "";
currentValue = textController.text;
return buildOutline(
context,
TextFormField(
style: theme.textTheme.labelMedium,
controller: textController,
onChanged: (String value) {
currentValue = value;
super.onValueChanged?.call(value);
},
decoration: InputDecoration(
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
hintText: hint,
filled: true,
fillColor: theme.inputDecorationTheme.fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
validator: (value) => super.validate(value),
keyboardType: TextInputType.emailAddress,
readOnly: isReadOnly,
),
onBlurBackground,
);
}
}

View file

@ -1,101 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Order input for phone numbers with with predefined
/// text fields and validation.
class OrderPhoneInput extends OrderDetailInput<String> {
/// Constructor for the phone input.
OrderPhoneInput({
required super.title,
required super.outputKey,
required this.textController,
this.errorMustBe11Digits = "Number must be 11 digits (+31 6 XXXX XXXX)",
this.errorMustStartWith316 = "Number must start with +316",
this.errorMustBeNumeric = "Number must be numeric",
super.errorIsRequired,
super.subtitle,
super.titleAlignment,
super.titlePadding,
super.titleStyle,
super.isRequired,
super.isReadOnly,
super.initialValue,
}) : super(
validators: [
(value) {
if (value != null && value.length != 11) {
return errorMustBe11Digits;
}
return null;
},
(value) {
if (value != null && !value.startsWith("316")) {
return errorMustStartWith316;
}
return null;
},
(value) {
if (value != null && !RegExp(r"^\d+$").hasMatch(value)) {
return errorMustBeNumeric;
}
return null;
},
],
);
/// Text Controller for phone input.
final TextEditingController textController;
/// Error message that notifies the number must be 11 digits long.
final String errorMustBe11Digits;
/// Error message that notifies the number must start with +316
final String errorMustStartWith316;
/// Error message that notifies the number must be numeric.
final String errorMustBeNumeric;
@override
Widget build(
BuildContext context,
String? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
var theme = Theme.of(context);
textController.text = initialValue ?? buildInitialValue ?? "31";
currentValue = textController.text;
return buildOutline(
context,
TextFormField(
style: theme.textTheme.labelMedium,
controller: textController,
onChanged: (String value) {
currentValue = value;
super.onValueChanged?.call(value);
},
decoration: InputDecoration(
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
prefixText: "+",
prefixStyle: theme.textTheme.labelMedium,
hintText: hint,
filled: true,
fillColor: theme.inputDecorationTheme.fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
validator: (value) => super.validate(value),
readOnly: isReadOnly,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(11), // international phone number
],
),
onBlurBackground,
);
}
}

View file

@ -1,71 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Default text input for order details.
class OrderTextInput extends OrderDetailInput<String> {
/// Default text input for order details.
OrderTextInput({
required super.title,
required super.outputKey,
required this.textController,
super.titleStyle,
super.titleAlignment,
super.titlePadding,
super.subtitle,
super.isRequired,
super.isReadOnly,
super.initialValue,
super.validators,
super.onValueChanged,
super.errorIsRequired,
super.hint,
this.inputFormatters = const [],
});
/// Text Controller for the input field.
final TextEditingController textController;
/// List of input formatters for the text field.
final List<TextInputFormatter> inputFormatters;
@override
Widget build(
BuildContext context,
String? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
var theme = Theme.of(context);
textController.text = initialValue ?? buildInitialValue ?? "";
currentValue = textController.text;
return buildOutline(
context,
TextFormField(
style: theme.textTheme.labelMedium,
controller: textController,
onChanged: (String value) {
currentValue = value;
super.onValueChanged?.call(value);
},
decoration: InputDecoration(
labelText: titleStyle == OrderDetailTitleStyle.label ? title : null,
hintText: hint,
filled: true,
fillColor: theme.inputDecorationTheme.fillColor,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
),
validator: super.validate,
readOnly: isReadOnly,
inputFormatters: [
...inputFormatters,
],
),
onBlurBackground,
);
}
}

View file

@ -1,353 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_order_details/src/models/formfield_error_builder.dart";
/// Order time picker input with predefined text fields and validation.
class OrderTimePicker extends OrderDetailInput<String> {
/// Constructor for the time picker.
OrderTimePicker({
required super.title,
required super.outputKey,
super.titleStyle,
super.titleAlignment,
super.titlePadding,
super.subtitle,
super.isRequired,
super.initialValue,
super.validators,
super.onValueChanged,
super.errorIsRequired,
super.hint,
this.beginTime = 9,
this.endTime = 17,
this.interval = 0.25,
this.morningLabel = "Morning",
this.afternoonLabel = "Afternoon",
this.eveningLabel = "Evening",
this.padding = const EdgeInsets.only(top: 12, bottom: 20.0),
}) : assert(
beginTime < endTime,
"Begin time cannot be greater than end time",
);
/// Minimum time of times to show. For example 9 (for 9AM).
final double beginTime;
/// Final time to show. For example 17 (for 5PM).
final double endTime;
/// For each interval a button gets generated within the begin time and
/// the end time. For example 0.25 (for ever 15 minutes).
final double interval;
/// Translation for morning texts.
final String morningLabel;
/// Translation for afternoon texts.
final String afternoonLabel;
/// Translation for evening texts.
final String eveningLabel;
/// Padding around the time picker.
final EdgeInsets padding;
final _selectedTimeOfDay = _SelectedTimeOfDay();
@override
Widget build(
BuildContext context,
String? buildInitialValue,
Function({bool needsBlur}) onBlurBackground,
) {
void updateSelectedTimeOfDay(_TimeOfDay timeOfDay) {
if (_selectedTimeOfDay.selectedTimeOfDay == timeOfDay) return;
_selectedTimeOfDay.selectedTimeOfDay = timeOfDay;
currentValue = null;
}
void updateSelectedTimeAsString(String? time) {
currentValue = time;
onValueChanged?.call(time ?? "");
_selectedTimeOfDay.selectedTime = time;
}
void updateSelectedTime(double time) {
if (currentValue == time.toString()) {
updateSelectedTimeAsString(null);
} else {
updateSelectedTimeAsString(time.toString());
}
}
if (currentValue != null) {
var currentValueAsDouble = double.parse(currentValue!);
for (var timeOfDay in _TimeOfDay.values) {
if (_isTimeWithinTimeOfDay(
currentValueAsDouble,
currentValueAsDouble,
timeOfDay,
)) {
_selectedTimeOfDay.selectedTimeOfDay = timeOfDay;
}
}
updateSelectedTimeAsString(currentValue);
} else {
for (var timeOfDay in _TimeOfDay.values) {
if (_isTimeWithinTimeOfDay(beginTime, endTime, timeOfDay)) {
_selectedTimeOfDay.selectedTimeOfDay = timeOfDay;
break;
}
}
}
return buildOutline(
context,
ListenableBuilder(
listenable: _selectedTimeOfDay,
builder: (context, _) {
var startTime = _selectedTimeOfDay.selection != null
? _selectedTimeOfDay.selection!.minTime.clamp(beginTime, endTime)
: beginTime;
var finalTime = _selectedTimeOfDay.selection != null
? _selectedTimeOfDay.selection!.maxTime.clamp(beginTime, endTime)
: endTime;
return Column(
children: [
_TimeOfDaySelector(
selectedTimeOfDay: _selectedTimeOfDay,
updateSelectedTimeOfDay: updateSelectedTimeOfDay,
startTime: beginTime,
endTime: endTime,
morningLabel: morningLabel,
afternoonLabel: afternoonLabel,
eveningLabel: eveningLabel,
padding: padding,
),
_TimeWrap<String>(
currentValue: currentValue ?? "",
startTime: startTime,
finalTime: finalTime,
interval: interval,
onTap: updateSelectedTime,
validate: super.validate,
),
],
);
},
),
onBlurBackground,
);
}
}
bool _isTimeWithinTimeOfDay(
double openingTime,
double closingTime,
_TimeOfDay timeOfDay,
) =>
(timeOfDay.minTime >= openingTime && timeOfDay.minTime <= closingTime) ||
(timeOfDay.maxTime > openingTime && timeOfDay.maxTime <= closingTime) ||
(timeOfDay.minTime <= openingTime && timeOfDay.maxTime >= closingTime);
class _TimeOfDaySelector extends StatelessWidget {
const _TimeOfDaySelector({
required this.selectedTimeOfDay,
required this.updateSelectedTimeOfDay,
required this.startTime,
required this.endTime,
required this.morningLabel,
required this.afternoonLabel,
required this.eveningLabel,
required this.padding,
});
final _SelectedTimeOfDay selectedTimeOfDay;
final Function(_TimeOfDay) updateSelectedTimeOfDay;
final double startTime;
final double endTime;
final String morningLabel;
final String afternoonLabel;
final String eveningLabel;
final EdgeInsets padding;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
String getLabelName(_TimeOfDay timeOfDay) => switch (timeOfDay) {
_TimeOfDay.morning => morningLabel,
_TimeOfDay.afternoon => afternoonLabel,
_TimeOfDay.evening => eveningLabel,
};
return Padding(
padding: padding,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(90),
border: Border.all(
color: theme.colorScheme.primary,
strokeAlign: BorderSide.strokeAlignOutside,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var timeOfDay in _TimeOfDay.values) ...[
if (_isTimeWithinTimeOfDay(startTime, endTime, timeOfDay)) ...[
GestureDetector(
onTap: () => updateSelectedTimeOfDay(timeOfDay),
child: DecoratedBox(
decoration: BoxDecoration(
color: selectedTimeOfDay.selectedTimeOfDay == timeOfDay
? theme.colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(90),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
child: Text(
getLabelName(timeOfDay),
style: theme.textTheme.labelMedium?.copyWith(
color:
selectedTimeOfDay.selectedTimeOfDay == timeOfDay
? Colors.white
: theme.colorScheme.primary,
),
),
),
),
),
],
],
],
),
),
);
}
}
class _TimeWrap<T> extends FormField<T> {
_TimeWrap({
required this.currentValue,
required this.startTime,
required this.finalTime,
required this.interval,
required this.onTap,
required String? Function(T?) validate,
}) : super(
validator: (value) => validate(currentValue),
builder: (FormFieldState<T> field) => Column(
children: [
Wrap(
children: [
for (var i = startTime; i < finalTime; i += interval) ...[
_TimeWrapContent(
i: i,
currentValue: currentValue,
onTap: onTap,
),
],
],
),
if (field.hasError) ...[
FormFieldErrorBuilder(errorMessage: field.errorText!),
],
],
),
);
final T currentValue;
final double startTime;
final double finalTime;
final double interval;
final Function(double) onTap;
}
class _TimeWrapContent<T> extends StatelessWidget {
const _TimeWrapContent({
required this.i,
required this.currentValue,
required this.onTap,
});
final double i;
final T currentValue;
final Function(double) onTap;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var boxDecoration = BoxDecoration(
color: currentValue == i.toString()
? theme.colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(16),
);
var decoratedBox = Container(
decoration: boxDecoration,
width: MediaQuery.of(context).size.width * .25,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 28,
vertical: 12,
),
child: Text(
'${i.floor().toString().padLeft(2, '0')}:'
'${((i - i.floor()) * 60).toInt().toString().padLeft(2, '0')}',
style: theme.textTheme.labelMedium?.copyWith(
color: currentValue == i.toString()
? Colors.white
: theme.colorScheme.primary,
),
),
),
);
return GestureDetector(
onTap: () => onTap(i),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: decoratedBox,
),
);
}
}
class _SelectedTimeOfDay extends ChangeNotifier {
_TimeOfDay? selection;
String? time = "";
_TimeOfDay? get selectedTimeOfDay => selection;
String? get selectedTime => time;
set selectedTimeOfDay(_TimeOfDay? value) {
selection = value;
notifyListeners();
}
set selectedTime(String? value) {
time = value;
notifyListeners();
}
}
enum _TimeOfDay {
morning(0, 12),
afternoon(12, 18),
evening(18, 24);
const _TimeOfDay(this.minTime, this.maxTime);
final double minTime;
final double maxTime;
}

View file

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_order_details/flutter_order_details.dart"; import "package:flutter_order_details/flutter_order_details.dart";
/// Order Detail Screen. /// Order Detail Screen.
@ -17,257 +18,29 @@ class OrderDetailScreen extends StatefulWidget {
} }
class _OrderDetailScreenState extends State<OrderDetailScreen> { class _OrderDetailScreenState extends State<OrderDetailScreen> {
final _CurrentStep _currentStep = _CurrentStep();
final OrderResult _orderResult = OrderResult(order: {});
bool _blurBackground = false;
void _toggleBlurBackground({bool? needsBlur}) {
setState(() {
_blurBackground = needsBlur!;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var controller = FlutterFormController();
var pageBody = SafeArea(
left: false,
right: false,
bottom: true,
child: _OrderDetailBody(
configuration: widget.configuration,
orderResult: _orderResult,
currentStep: _currentStep,
onBlurBackground: _toggleBlurBackground,
),
);
var pageBlur = GestureDetector(
onTap: () => _toggleBlurBackground(needsBlur: false),
child: Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: theme.colorScheme.surface.withOpacity(0.5),
),
),
);
return Scaffold( return Scaffold(
appBar: widget.configuration.appBar, appBar: widget.configuration.appBar
body: Stack( .call(context, widget.configuration.localization),
children: [ body: FlutterForm(
pageBody, formController: controller,
if (_blurBackground) pageBlur, options: FlutterFormOptions(
], nextButton: (a, b) => widget.configuration.nextbuttonBuilder(
a,
b,
context,
widget.configuration,
controller,
),
pages: widget.configuration.pages.call(context),
onFinished: (data) {
widget.configuration.onCompleted.call(data);
},
onNext: (step, data) {},
),
), ),
); );
} }
} }
class _CurrentStep extends ChangeNotifier {
int _step = 0;
int get step => _step;
void increment() {
_step++;
notifyListeners();
}
void decrement() {
_step--;
notifyListeners();
}
}
class _OrderDetailBody extends StatelessWidget {
const _OrderDetailBody({
required this.configuration,
required this.orderResult,
required this.currentStep,
required this.onBlurBackground,
});
final OrderDetailConfiguration configuration;
final OrderResult orderResult;
final _CurrentStep currentStep;
final Function({bool needsBlur}) onBlurBackground;
@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: currentStep,
builder: (context, _) => Builder(
builder: (context) => _FormBuilder(
currentStep: currentStep,
orderResult: orderResult,
configuration: configuration,
onBlurBackground: onBlurBackground,
),
),
);
}
class _FormBuilder extends StatelessWidget {
const _FormBuilder({
required this.currentStep,
required this.configuration,
required this.orderResult,
required this.onBlurBackground,
});
final _CurrentStep currentStep;
final OrderDetailConfiguration configuration;
final OrderResult orderResult;
final Function({bool needsBlur}) onBlurBackground;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var progressIndicator = LinearProgressIndicator(
value: currentStep.step / configuration.steps.length,
backgroundColor: theme.colorScheme.surface,
);
var stepForm = Form(
key: configuration.steps[currentStep.step].formKey,
child: _StepBuilder(
configuration: configuration,
currentStep: configuration.steps[currentStep.step],
orderResult: orderResult,
theme: theme,
onBlurBackground: onBlurBackground,
),
);
void onPressedNext() {
var formInfo = configuration.steps[currentStep.step];
var formkey = formInfo.formKey;
for (var input in formInfo.fields) {
orderResult.order[input.outputKey] = input.currentValue;
}
if (formkey.currentState!.validate()) {
currentStep.increment();
}
}
void onPressedPrevious() {
var formInfo = configuration.steps[currentStep.step];
for (var input in formInfo.fields) {
orderResult.order[input.outputKey] = input.currentValue;
}
currentStep.decrement();
}
void onPressedComplete() {
var formInfo = configuration.steps[currentStep.step];
var formkey = formInfo.formKey;
for (var input in formInfo.fields) {
orderResult.order[input.outputKey] = input.currentValue;
}
if (formkey.currentState!.validate()) {
configuration.onCompleted(orderResult);
}
}
var navigationControl = Row(
children: [
if (currentStep.step > 0) ...[
TextButton(
onPressed: onPressedPrevious,
child: Text(
configuration.localization.backButton,
),
),
],
const Spacer(),
if (currentStep.step < configuration.steps.length - 1) ...[
TextButton(
onPressed: onPressedNext,
child: Text(
configuration.localization.nextButton,
),
),
] else ...[
TextButton(
onPressed: onPressedComplete,
child: Text(
configuration.localization.completeButton,
),
),
],
],
);
return Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
if (configuration.progressIndicator) ...[
progressIndicator,
],
stepForm,
],
),
),
Align(
alignment: Alignment.bottomCenter,
child: navigationControl,
),
],
);
}
}
class _StepBuilder extends StatelessWidget {
const _StepBuilder({
required this.configuration,
required this.currentStep,
required this.orderResult,
required this.theme,
required this.onBlurBackground,
});
final OrderDetailConfiguration configuration;
final OrderDetailStep currentStep;
final OrderResult orderResult;
final ThemeData theme;
final Function({bool needsBlur}) onBlurBackground;
@override
Widget build(BuildContext context) {
var title = currentStep.stepName != null
? Padding(
padding: configuration.titlePadding,
child: Text(
currentStep.stepName!,
style: theme.textTheme.titleMedium,
),
)
: const SizedBox.shrink();
return Column(
children: [
title,
for (var input in currentStep.fields)
Padding(
padding: configuration.inputFieldPadding,
child: input.build(
context,
orderResult.order[input.outputKey],
onBlurBackground,
),
),
],
);
}
}

View file

@ -0,0 +1,38 @@
/// The product page shop class contains all the required information
class Product {
/// Constructor for the product.
Product({
required this.id,
required this.name,
required this.imageUrl,
required this.category,
required this.price,
required this.hasDiscount,
this.discountPrice,
this.quantity = 1,
});
/// The unique identifier for the product.
final String id;
/// The name of the product.
final String name;
/// The image URL of the product.
final String imageUrl;
/// The category of the product.
final String category;
/// The price of the product.
final double price;
/// Whether the product has a discount or not.
final bool hasDiscount;
/// The discounted price of the product. Only used if [hasDiscount] is true.
final double? discountPrice;
/// Quantity for the product.
final int quantity;
}

View file

@ -34,7 +34,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
), ),
// (REQUIRED): Function to navigate to the shopping cart // (REQUIRED): Function to navigate to the shopping cart
onNavigateToShoppingCart: () => onCompleteProductPage(context), onNavigateToShoppingCart: () async => onCompleteProductPage(context),
// (RECOMMENDED): Function to get the number of products in the // (RECOMMENDED): Function to get the number of products in the
// shopping cart. This is used to display the number of products // shopping cart. This is used to display the number of products
@ -116,7 +116,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
// (OPTIONAL/REQUIRED) on confirm order callback: // (OPTIONAL/REQUIRED) on confirm order callback:
// Either use this callback or the placeOrderButtonBuilder. // Either use this callback or the placeOrderButtonBuilder.
onConfirmOrder: (products) => onCompleteShoppingCart(context), onConfirmOrder: (products) async => onCompleteShoppingCart(context),
// (RECOMMENDED) localizations: // (RECOMMENDED) localizations:
localizations: const ShoppingCartLocalizations(), localizations: const ShoppingCartLocalizations(),

View file

@ -1,68 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
/// Default order detail configuration for the app.
/// This configuration is used to create the order detail page.
OrderDetailConfiguration getDefaultOrderDetailConfiguration(
BuildContext context,
FlutterShoppingConfiguration configuration,
) =>
OrderDetailConfiguration(
steps: [
OrderDetailStep(
formKey: GlobalKey<FormState>(),
stepName: "Basic Information",
fields: [
OrderTextInput(
title: "First name",
outputKey: "first_name",
textController: TextEditingController(),
),
OrderTextInput(
title: "Last name",
outputKey: "last_name",
textController: TextEditingController(),
),
OrderEmailInput(
title: "Your email address",
outputKey: "email",
textController: TextEditingController(),
subtitle: "* We will send your order confirmation here",
hint: "your_email@mail.com",
),
],
),
OrderDetailStep(
formKey: GlobalKey<FormState>(),
stepName: "Address Information",
fields: [
OrderAddressInput(
title: "Your address",
outputKey: "address",
textController: TextEditingController(),
),
],
),
OrderDetailStep(
formKey: GlobalKey<FormState>(),
stepName: "Payment Information",
fields: [
OrderChoiceInput(
title: "Payment option",
outputKey: "payment_option",
items: ["Pay now", "Pay later"],
),
],
),
],
onCompleted: (OrderResult result) async =>
onCompleteOrderDetails(context, configuration, result),
appBar: AppBar(
title: const Text("Order Details"),
leading: IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => context.go(FlutterShoppingPathRoutes.shoppingCart),
),
),
);

View file

@ -0,0 +1,10 @@
/// Contains the localized strings for the order details screen.
class OrderDetailsLocalizations {
/// Creates order details localizations
OrderDetailsLocalizations({
this.orderDetailsTitle = "Information",
});
/// Title for the order details screen.
final String orderDetailsTitle;
}

View file

@ -1,5 +1,4 @@
import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping/src/config/default_order_detail_configuration.dart";
import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart"; import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart";
import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart"; import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
@ -29,8 +28,11 @@ List<GoRoute> getShoppingStoryRoutes({
builder: (context, state) => configuration.orderDetailsBuilder != null builder: (context, state) => configuration.orderDetailsBuilder != null
? configuration.orderDetailsBuilder!(context) ? configuration.orderDetailsBuilder!(context)
: OrderDetailScreen( : OrderDetailScreen(
configuration: configuration: OrderDetailConfiguration(
getDefaultOrderDetailConfiguration(context, configuration), onCompleted: (result) {
context.go(FlutterShoppingPathRoutes.orderSuccess);
},
),
), ),
), ),
GoRoute( GoRoute(

View file

@ -35,10 +35,10 @@ Future<void> onCompleteOrderDetails(
/// ///
/// You can create your own implementation if you decide to use a different /// You can create your own implementation if you decide to use a different
/// approach. /// approach.
void onCompleteShoppingCart( Future<void> onCompleteShoppingCart(
BuildContext context, BuildContext context,
) { ) async {
context.go(FlutterShoppingPathRoutes.orderDetails); await context.push(FlutterShoppingPathRoutes.orderDetails);
} }
/// Default on complete product page function. /// Default on complete product page function.

View file

@ -16,51 +16,169 @@ class DefaultOrderSucces extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
var finishOrderButton = FilledButton(
onPressed: () => configuration.onCompleteUserStory(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32.0,
vertical: 8.0,
),
child: Text("Finish Order".toUpperCase()),
),
);
var content = Column(
children: [
const Spacer(),
Text("#123456", style: theme.textTheme.titleLarge),
const SizedBox(height: 16),
Text(
"Order Succesfully Placed!",
style: theme.textTheme.titleLarge,
),
Text(
"Thank you for your order!",
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
"Your order will be delivered soon.",
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
"Do you want to order again?",
style: theme.textTheme.bodyMedium,
),
const Spacer(),
finishOrderButton,
],
);
return Scaffold( return Scaffold(
appBar: AppBar(
title: Text(
"Confirmation",
style: theme.textTheme.headlineLarge,
),
),
body: SafeArea( body: SafeArea(
child: Center( child: Column(
child: content, children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
top: 32,
right: 32,
),
child: Column(
children: [
Text(
"Success!",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Text(
"Thank you Peter for your order!",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 16,
),
Text(
"The order was placed at Bakkerij de Goudkorst."
" You can pick this"
" up on Monday, February 7 at 1:00 PM.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 16,
),
Text(
"If you want, you can place another order in this street.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 32,
),
Text(
"Weekly offers",
style: theme.textTheme.headlineSmall
?.copyWith(color: Colors.black),
),
const SizedBox(
height: 4,
),
],
),
),
SizedBox(
height: 272,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 32),
_discount(context),
const SizedBox(width: 8),
_discount(context),
const SizedBox(width: 32),
],
),
),
const Spacer(),
FilledButton(
onPressed: () {
configuration.onCompleteUserStory.call(context);
},
child: Text(
"Place another order",
style: theme.textTheme.displayLarge,
),
),
],
), ),
), ),
); );
} }
} }
Widget _discount(BuildContext context) {
var theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
),
borderRadius: BorderRadius.circular(10),
),
width: MediaQuery.of(context).size.width - 64,
height: 200,
child: Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
child: Image.network(
"https://picsum.photos/150",
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Container(
alignment: Alignment.centerLeft,
height: 38,
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Butcher Puurvlees",
style: theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
alignment: Alignment.centerLeft,
width: MediaQuery.of(context).size.width,
height: 68,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Chicken legs, now for 4,99",
style: theme.textTheme.bodyMedium,
),
),
),
),
],
),
);
}

View file

@ -136,7 +136,10 @@ Widget _defaultProductItemBuilder(
await showModalBottomSheet( await showModalBottomSheet(
context: context, context: context,
backgroundColor: theme.colorScheme.surface, backgroundColor: theme.colorScheme.surface,
builder: (context) => ProductItemPopup(product: product, configuration: configuration) builder: (context) => ProductItemPopup(
product: product,
configuration: configuration,
),
); );
}, },
icon: Icon( icon: Icon(