From 33184a3b018a2957e814dc17d17e8a438573c4c2 Mon Sep 17 00:00:00 2001 From: Mark <94751350+MarkKiepe@users.noreply.github.com> Date: Mon, 27 May 2024 14:30:17 +0200 Subject: [PATCH] Feat/v1.0.0 (#1) * feat: intial user story code * fix: remove unused import * fix: remove unused asset * feat: readme * feat: dart documentation * fix: component versions * fix: remove unused environment config * fix: feedback --- README.md | 157 ++++++++++++++++- example/analysis_options.yaml | 2 - example/lib/main.dart | 21 +++ .../lib/src/configuration/configuration.dart | 165 ++++++++++++++++++ example/lib/src/models/my_product.dart | 26 +++ example/lib/src/models/my_shop.dart | 8 + example/lib/src/routes.dart | 31 ++++ example/lib/src/services/order_service.dart | 7 + example/lib/src/services/shop_service.dart | 47 +++++ example/lib/src/ui/homepage.dart | 20 +++ example/lib/src/utils/go_router.dart | 26 +++ example/lib/src/utils/theme.dart | 32 ++++ example/pubspec.yaml | 27 ++- lib/flutter_shopping.dart | 7 + .../default_order_detail_configuration.dart | 69 ++++++++ .../flutter_shopping_configuration.dart | 39 +++++ lib/src/go_router.dart | 28 +++ lib/src/routes.dart | 17 ++ .../flutter_shopping_userstory_go_router.dart | 97 ++++++++++ ...flutter_shopping_userstory_navigation.dart | 53 ++++++ .../widgets/default_order_failed_widget.dart | 65 +++++++ .../widgets/default_order_succes_widget.dart | 66 +++++++ pubspec.yaml | 7 +- 23 files changed, 1003 insertions(+), 14 deletions(-) create mode 100644 example/lib/src/configuration/configuration.dart create mode 100644 example/lib/src/models/my_product.dart create mode 100644 example/lib/src/models/my_shop.dart create mode 100644 example/lib/src/routes.dart create mode 100644 example/lib/src/services/order_service.dart create mode 100644 example/lib/src/services/shop_service.dart create mode 100644 example/lib/src/ui/homepage.dart create mode 100644 example/lib/src/utils/go_router.dart create mode 100644 example/lib/src/utils/theme.dart create mode 100644 lib/flutter_shopping.dart create mode 100644 lib/src/config/default_order_detail_configuration.dart create mode 100644 lib/src/config/flutter_shopping_configuration.dart create mode 100644 lib/src/go_router.dart create mode 100644 lib/src/routes.dart create mode 100644 lib/src/user_stores/flutter_shopping_userstory_go_router.dart create mode 100644 lib/src/user_stores/flutter_shopping_userstory_navigation.dart create mode 100644 lib/src/widgets/default_order_failed_widget.dart create mode 100644 lib/src/widgets/default_order_succes_widget.dart diff --git a/README.md b/README.md index 2d6d07f..c4ce84d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,161 @@ # flutter_shopping -This component contains TODO... +The flutter_shopping user-story allows you to create an user shopping flow within minutes of work. This user-story contains the ability to show products, shopping cart, gathering user information and order succes and failed screens. -## Features +## Setup -* TODO... +(1) Set up your `MyShop` model by extending from the `ProductPageShop` class. The most basic version looks like this: + +```dart +class MyShop extends ProductPageShop { + const MyShop({ + required super.id, + required super.name, + }); +} +``` + +(2) Set up your `MyProduct` model by extending from `ShoppingCartProduct` and extending from the mixin `ProductPageProduct`, like this: + +```dart +class MyProduct extends ShoppingCartProduct with ProductPageProduct { + MyProduct({ + required super.id, + required super.name, + required super.price, + required this.category, + required this.imageUrl, + this.discountPrice, + this.hasDiscount = false, + }); + + @override + final String category; + + @override + final String imageUrl; + + @override + final double? discountPrice; + + @override + final bool hasDiscount; +} +``` + +(3) Finally in your `routes.dart` import all the routes from the user-story: + +```dart +...getShoppingStoryRoutes( + configuration: ... +), +``` + +(4) Create a new instantiation of the ProductService class: + +```dart +final ProductService productService = ProductService([]); +``` + +(5) Set up the `FlutterShoppingConfiguration`: + +```dart +FlutterShoppingConfiguration( + // (REQUIRED): Shop builder configuration + shopBuilder: (BuildContext context) => ProductPageScreen( + configuration: ProductPageConfiguration( + // (REQUIRED): List of shops that should be displayed + // If there is only one, make a list with just one shop. + shops: getShops(), + + // (REQUIRED): Function to add a product to the cart + onAddToCart: (ProductPageProduct product) => + productService.addProduct(product as MyProduct), + + // (REQUIRED): Function to get the products for a shop + getProducts: (String shopId) => Future.value( + getShopContent(shopId), + ), + + // (REQUIRED): Function to navigate to the shopping cart + onNavigateToShoppingCart: () => onCompleteProductPage(context), + + // (RECOMMENDED) Function that returns the description for a + // product that is on sale. + getDiscountDescription: (ProductPageProduct product) => + """${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""", + + // (RECOMMENDED) Function that is fired when the shop selection + // changes. You could use this to clear your shopping cart or to + // change the products so they belong to the correct shop again. + onShopSelectionChange: (shopId) => productService.clear(), + + // (RECOMMENDED) The shop that is initially selected. + // Must be one of the shops in the [shops] list. + initialShop: getShops().first, + + // (RECOMMENDED) Localizations for the product page. + localizations: const ProductPageLocalization(), + + // (OPTIONAL) Appbar + appBar: ... + ), + ), + + // (REQUIRED): Shopping cart builder configuration + shoppingCartBuilder: (BuildContext context) => ShoppingCartScreen( + configuration: ShoppingCartConfig( + // (REQUIRED) product service instance: + productService: productService, + + // (REQUIRED) product item builder: + productItemBuilder: (context, locale, product) => ... + + // (OPTIONAL/REQUIRED) on confirm order callback: + // Either use this callback or the placeOrderButtonBuilder. + onConfirmOrder: (products) => onCompleteShoppingCart(context), + + // (RECOMMENDED) localizations: + localizations: const ShoppingCartLocalizations(), + + // (OPTIONAL) custom appbar: + appBar: ... + ), + ), + + // (REQUIRED): Configuration on what to do when the user story is + // completed. + onCompleteUserStory: (BuildContext context) { + context.go(homePage); + }, + + // (RECOMMENDED) Handle processing of the order details. This function + // should return true if the order was processed successfully, otherwise + // false. + // + // If this function is not provided, it is assumed that the order is + // always processed successfully. + // + // Example use cases that could be implemented here: + // - Sending and storing the order on a server, + // - Processing payment (if the user decides to pay upfront). + // - And many more... + onCompleteOrderDetails: + (BuildContext context, OrderResult orderDetails) async { + ... + }, +) +``` + +For more information about the component specific items please take a look at their repositories: + +- [flutter_product_page](https://github.com/Iconica-Development/flutter_product_page/) +- [flutter_shopping_cart](https://github.com/Iconica-Development/flutter_shopping_cart) +- [flutter_order_details](https://github.com/Iconica-Development/flutter_order_details) ## Usage -First, TODO... - -For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping/tree/main/example). +For a detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping/tree/main/example). Or, you could run the example yourself: ``` diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 31b4b51..2a97d5c 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,7 +1,5 @@ include: package:flutter_iconica_analysis/analysis_options.yaml -# Possible to overwrite the rules from the package - analyzer: exclude: diff --git a/example/lib/main.dart b/example/lib/main.dart index 8b13789..1157c00 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1 +1,22 @@ +import "package:example/src/routes.dart"; +import "package:example/src/utils/theme.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +void main() { + runApp(const ProviderScope(child: MyApp())); +} + +class MyApp extends HookConsumerWidget { + const MyApp({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router( + debugShowCheckedModeBanner: false, + restorationScopeId: "app", + theme: getTheme(), + routerConfig: ref.read(routerProvider), + ); +} diff --git a/example/lib/src/configuration/configuration.dart b/example/lib/src/configuration/configuration.dart new file mode 100644 index 0000000..85281f3 --- /dev/null +++ b/example/lib/src/configuration/configuration.dart @@ -0,0 +1,165 @@ +import "package:example/src/models/my_product.dart"; +import "package:example/src/routes.dart"; +import "package:example/src/services/order_service.dart"; +import "package:example/src/services/shop_service.dart"; +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; +import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; +import "package:go_router/go_router.dart"; + +// (REQUIRED): Create your own instance of the ProductService. +final ProductService productService = ProductService([]); + +FlutterShoppingConfiguration getFlutterShoppingConfiguration() => + FlutterShoppingConfiguration( + // (REQUIRED): Shop builder configuration + shopBuilder: (BuildContext context) => ProductPageScreen( + configuration: ProductPageConfiguration( + // (REQUIRED): List of shops that should be displayed + // If there is only one, make a list with just one shop. + shops: getShops(), + + // (REQUIRED): Function to add a product to the cart + onAddToCart: (ProductPageProduct product) => + productService.addProduct(product as MyProduct), + + // (REQUIRED): Function to get the products for a shop + getProducts: (String shopId) => Future.value( + getShopContent(shopId), + ), + + // (REQUIRED): Function to navigate to the shopping cart + onNavigateToShoppingCart: () => onCompleteProductPage(context), + + // (RECOMMENDED): Function to get the number of products in the + // shopping cart. This is used to display the number of products + // in the shopping cart on the product page. + getProductsInShoppingCart: productService.countProducts, + + // (RECOMMENDED) Function that returns the description for a + // product that is on sale. + getDiscountDescription: (ProductPageProduct product) => + """${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""", + + // (RECOMMENDED) Function that is fired when the shop selection + // changes. You could use this to clear your shopping cart or to + // change the products so they belong to the correct shop again. + onShopSelectionChange: (shopId) => productService.clear(), + + // (RECOMMENDED) The shop that is initially selected. + // Must be one of the shops in the [shops] list. + initialShop: getShops().first, + + // (RECOMMENDED) Localizations for the product page. + localizations: const ProductPageLocalization(), + + // (OPTIONAL) Appbar + appBar: AppBar( + title: const Text("Shop"), + leading: IconButton( + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + onPressed: () { + context.go(homePage); + }, + ), + ), + ), + ), + + // (REQUIRED): Shopping cart builder configuration + shoppingCartBuilder: (BuildContext context) => ShoppingCartScreen( + configuration: ShoppingCartConfig( + // (REQUIRED) product service instance: + productService: productService, + + // (REQUIRED) product item builder: + productItemBuilder: (context, locale, product) => ListTile( + title: Text(product.name), + subtitle: Text(product.price.toStringAsFixed(2)), + leading: Image.network( + product.imageUrl, + errorBuilder: (context, error, stackTrace) => const Tooltip( + message: "Error loading image", + child: Icon( + Icons.error, + color: Colors.red, + ), + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: () => productService.removeOneProduct(product), + ), + Text("${product.quantity}"), + IconButton( + icon: const Icon(Icons.add), + onPressed: () => productService.addProduct(product), + ), + ], + ), + ), + + // (OPTIONAL/REQUIRED) on confirm order callback: + // Either use this callback or the placeOrderButtonBuilder. + onConfirmOrder: (products) => onCompleteShoppingCart(context), + + // (RECOMMENDED) localizations: + localizations: const ShoppingCartLocalizations(), + + // (OPTIONAL) title above product list: + title: "Products", + + // (OPTIONAL) custom appbar: + appBar: AppBar( + title: const Text("Shopping Cart"), + leading: IconButton( + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + ), + onPressed: () { + context.go(FlutterShoppingRoutes.shop); + }, + ), + ), + ), + ), + + // (REQUIRED): Configuration on what to do when the user story is + // completed. + onCompleteUserStory: (BuildContext context) { + context.go(homePage); + }, + + // (RECOMMENDED) Handle processing of the order details. This function + // should return true if the order was processed successfully, otherwise + // false. + // + // If this function is not provided, it is assumed that the order is + // always processed successfully. + // + // Example use cases that could be implemented here: + // - Sending and storing the order on a server, + // - Processing payment (if the user decides to pay upfront). + // - And many more... + onCompleteOrderDetails: + (BuildContext context, OrderResult orderDetails) async { + if (orderDetails.order["payment_option"] == "Pay now") { + // Make the user pay upfront. + } + + // If all went well, we can store the order in the database. + // Make sure to register whether or not the order was paid. + storeOrderInDatabase(productService.products, orderDetails); + + return true; + }, + ); diff --git a/example/lib/src/models/my_product.dart b/example/lib/src/models/my_product.dart new file mode 100644 index 0000000..d066d31 --- /dev/null +++ b/example/lib/src/models/my_product.dart @@ -0,0 +1,26 @@ +import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; + +class MyProduct extends ShoppingCartProduct with ProductPageProduct { + MyProduct({ + required super.id, + required super.name, + required super.price, + required this.category, + required this.imageUrl, + this.discountPrice, + this.hasDiscount = false, + }); + + @override + final String category; + + @override + final String imageUrl; + + @override + final double? discountPrice; + + @override + final bool hasDiscount; +} diff --git a/example/lib/src/models/my_shop.dart b/example/lib/src/models/my_shop.dart new file mode 100644 index 0000000..ddf05b9 --- /dev/null +++ b/example/lib/src/models/my_shop.dart @@ -0,0 +1,8 @@ +import "package:flutter_product_page/flutter_product_page.dart"; + +class MyShop extends ProductPageShop { + const MyShop({ + required super.id, + required super.name, + }); +} diff --git a/example/lib/src/routes.dart b/example/lib/src/routes.dart new file mode 100644 index 0000000..4899345 --- /dev/null +++ b/example/lib/src/routes.dart @@ -0,0 +1,31 @@ +import "package:example/src/configuration/configuration.dart"; +import "package:example/src/ui/homepage.dart"; +import "package:example/src/utils/go_router.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; +import "package:go_router/go_router.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; + +const String homePage = "/"; + +final routerProvider = Provider( + (ref) => GoRouter( + initialLocation: homePage, + routes: [ + // Flutter Shopping Story Routes + ...getShoppingStoryRoutes( + configuration: getFlutterShoppingConfiguration(), + ), + + // Home Route + GoRoute( + name: "home", + path: homePage, + pageBuilder: (context, state) => buildScreenWithFadeTransition( + context: context, + state: state, + child: const Homepage(), + ), + ), + ], + ), +); diff --git a/example/lib/src/services/order_service.dart b/example/lib/src/services/order_service.dart new file mode 100644 index 0000000..2dbaa45 --- /dev/null +++ b/example/lib/src/services/order_service.dart @@ -0,0 +1,7 @@ +import "package:example/src/models/my_product.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Example implementation of storing an order in a database. +void storeOrderInDatabase(List products, OrderResult result) { + return; +} diff --git a/example/lib/src/services/shop_service.dart b/example/lib/src/services/shop_service.dart new file mode 100644 index 0000000..414076d --- /dev/null +++ b/example/lib/src/services/shop_service.dart @@ -0,0 +1,47 @@ +import "package:example/src/models/my_product.dart"; +import "package:example/src/models/my_shop.dart"; +import "package:flutter_product_page/flutter_product_page.dart"; + +/// This function should have your own implementation. Generally this would +/// contain some API call to fetch the list of shops. +List getShops() => [ + const MyShop(id: "1", name: "Shop 1"), + const MyShop(id: "2", name: "Shop 2"), + const MyShop(id: "3", name: "Shop 3"), + ]; + +ProductPageContent getShopContent(String shopId) { + var products = getProducts(shopId); + return ProductPageContent( + discountedProduct: products.first, + products: products, + ); +} + +/// This function should have your own implementation. Generally this would +/// contain some API call to fetch the list of products for a shop. +List getProducts(String shopId) => [ + MyProduct( + id: "1", + name: "White bread", + price: 2.99, + category: "Loaves", + imageUrl: "https://via.placeholder.com/150", + hasDiscount: true, + discountPrice: 1.99, + ), + MyProduct( + id: "2", + name: "Brown bread", + price: 2.99, + category: "Loaves", + imageUrl: "https://via.placeholder.com/150", + ), + MyProduct( + id: "3", + name: "Cheese sandwich", + price: 1.99, + category: "Sandwiches", + imageUrl: "https://via.placeholder.com/150", + ), + ]; diff --git a/example/lib/src/ui/homepage.dart b/example/lib/src/ui/homepage.dart new file mode 100644 index 0000000..800e20b --- /dev/null +++ b/example/lib/src/ui/homepage.dart @@ -0,0 +1,20 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; +import "package:go_router/go_router.dart"; + +class Homepage extends StatelessWidget { + const Homepage({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + body: Center( + child: Badge( + label: const Text("1"), + child: IconButton( + icon: const Icon(Icons.shopping_cart_outlined, size: 50), + onPressed: () => context.go(FlutterShoppingRoutes.shop), + ), + ), + ), + ); +} diff --git a/example/lib/src/utils/go_router.dart b/example/lib/src/utils/go_router.dart new file mode 100644 index 0000000..d7b239a --- /dev/null +++ b/example/lib/src/utils/go_router.dart @@ -0,0 +1,26 @@ +import "package:flutter/material.dart"; +import "package:go_router/go_router.dart"; + +CustomTransitionPage buildScreenWithFadeTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) => + CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ); + +CustomTransitionPage buildScreenWithoutTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) => + CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) => + child, + ); diff --git a/example/lib/src/utils/theme.dart b/example/lib/src/utils/theme.dart new file mode 100644 index 0000000..a0bf1d8 --- /dev/null +++ b/example/lib/src/utils/theme.dart @@ -0,0 +1,32 @@ +import "package:flutter/material.dart"; + +ThemeData getTheme() => ThemeData( + scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1), + textTheme: const TextTheme( + labelMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.black, + ), + titleMedium: TextStyle( + fontSize: 16, + color: Color.fromRGBO(60, 60, 59, 1), + fontWeight: FontWeight.w700, + ), + ), + inputDecorationTheme: const InputDecorationTheme( + fillColor: Colors.white, + ), + colorScheme: const ColorScheme.light( + primary: Color.fromRGBO(64, 87, 122, 1), + secondary: Colors.white, + surface: Color.fromRGBO(250, 249, 246, 1), + ), + appBarTheme: const AppBarTheme( + backgroundColor: Color.fromRGBO(64, 87, 122, 1), + titleTextStyle: TextStyle( + fontSize: 28, + color: Colors.white, + ), + ), + ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ea5796e..99f72f1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example -description: "Demonstrates how to use the flutter_shopping user story." -publish_to: 'none' +description: Demonstrates how to use the flutter_shopping package." +publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: @@ -9,9 +9,30 @@ environment: dependencies: flutter: sdk: flutter + flutter_hooks: ^0.20.0 + hooks_riverpod: ^2.1.1 + go_router: 12.1.3 + + # Iconica packages + + ## Userstories flutter_shopping: path: ../ + ## Normal Packages + flutter_product_page: + git: + url: https://github.com/Iconica-Development/flutter_product_page + ref: 1.1.0 + flutter_shopping_cart: + git: + url: https://github.com/Iconica-Development/flutter_shopping_cart + ref: 1.1.0 + flutter_order_details: + git: + url: https://github.com/Iconica-Development/flutter_order_details + ref: 1.0.0 + dev_dependencies: flutter_test: sdk: flutter @@ -31,4 +52,4 @@ flutter: # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf - # style: italic \ No newline at end of file + # style: italic diff --git a/lib/flutter_shopping.dart b/lib/flutter_shopping.dart new file mode 100644 index 0000000..3cdd31e --- /dev/null +++ b/lib/flutter_shopping.dart @@ -0,0 +1,7 @@ +/// Flutter Shopping +library flutter_shopping; + +export "package:flutter_shopping/src/config/flutter_shopping_configuration.dart"; +export "package:flutter_shopping/src/routes.dart"; +export "package:flutter_shopping/src/user_stores/flutter_shopping_userstory_go_router.dart"; +export "package:flutter_shopping/src/user_stores/flutter_shopping_userstory_navigation.dart"; diff --git a/lib/src/config/default_order_detail_configuration.dart b/lib/src/config/default_order_detail_configuration.dart new file mode 100644 index 0000000..952f716 --- /dev/null +++ b/lib/src/config/default_order_detail_configuration.dart @@ -0,0 +1,69 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.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(), + 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(), + stepName: "Address Information", + fields: [ + OrderAddressInput( + title: "Your address", + outputKey: "address", + textController: TextEditingController(), + ), + ], + ), + OrderDetailStep( + formKey: GlobalKey(), + 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(FlutterShoppingRoutes.shoppingCart), + ), + ), + ); diff --git a/lib/src/config/flutter_shopping_configuration.dart b/lib/src/config/flutter_shopping_configuration.dart new file mode 100644 index 0000000..f5be6db --- /dev/null +++ b/lib/src/config/flutter_shopping_configuration.dart @@ -0,0 +1,39 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; + +/// Configuration class for the flutter_shopping user-story. +class FlutterShoppingConfiguration { + /// Constructor for the FlutterShoppingConfiguration. + const FlutterShoppingConfiguration({ + required this.shopBuilder, + required this.shoppingCartBuilder, + required this.onCompleteUserStory, + this.orderDetailsBuilder, + this.onCompleteOrderDetails, + this.orderSuccessBuilder, + this.orderFailedBuilder, + }); + + /// Builder for the shop/product page. + final Widget Function(BuildContext context) shopBuilder; + + /// Builder for the shopping cart page. + final Widget Function(BuildContext context) shoppingCartBuilder; + + /// Function that is called when the user-story is completed. + final Function(BuildContext context) onCompleteUserStory; + + /// Builder for the order details page. This does not have to be set if you + /// are using the default order details page. + final Widget Function(BuildContext context)? orderDetailsBuilder; + + /// Allows you to execute actions before + final Future Function(BuildContext context, OrderResult result)? + onCompleteOrderDetails; + + /// Builder for when the order is successful. + final Widget Function(BuildContext context)? orderSuccessBuilder; + + /// Builder for when the order failed. + final Widget Function(BuildContext context)? orderFailedBuilder; +} diff --git a/lib/src/go_router.dart b/lib/src/go_router.dart new file mode 100644 index 0000000..cce7de5 --- /dev/null +++ b/lib/src/go_router.dart @@ -0,0 +1,28 @@ +import "package:flutter/material.dart"; +import "package:go_router/go_router.dart"; + +/// Builder with a fade transition for when navigating to a new screen. +CustomTransitionPage buildScreenWithFadeTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) => + CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ); + +/// Builder without a transition for when navigating to a new screen. +CustomTransitionPage buildScreenWithoutTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) => + CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) => + child, + ); diff --git a/lib/src/routes.dart b/lib/src/routes.dart new file mode 100644 index 0000000..753285a --- /dev/null +++ b/lib/src/routes.dart @@ -0,0 +1,17 @@ +/// All the routes used in the user-story. +mixin FlutterShoppingRoutes { + /// The shop page route. + static const String shop = "/shop"; + + /// The shopping cart page route. + static const String shoppingCart = "/shopping-cart"; + + /// The order details page route. + static const String orderDetails = "/order-details"; + + /// The order success page route. + static const String orderSuccess = "/order-success"; + + /// The order failed page route. + static const String orderFailed = "/order-failed"; +} diff --git a/lib/src/user_stores/flutter_shopping_userstory_go_router.dart b/lib/src/user_stores/flutter_shopping_userstory_go_router.dart new file mode 100644 index 0000000..5bac572 --- /dev/null +++ b/lib/src/user_stores/flutter_shopping_userstory_go_router.dart @@ -0,0 +1,97 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; +import "package:flutter_shopping/src/config/default_order_detail_configuration.dart"; +import "package:flutter_shopping/src/go_router.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:go_router/go_router.dart"; + +/// All the routes for the shopping story. +List getShoppingStoryRoutes({ + required FlutterShoppingConfiguration configuration, +}) => + [ + GoRoute( + name: "shop", + path: FlutterShoppingRoutes.shop, + pageBuilder: (BuildContext context, GoRouterState state) => + buildScreenWithFadeTransition( + context: context, + state: state, + child: configuration.shopBuilder(context), + ), + ), + GoRoute( + name: "shoppingCart", + path: FlutterShoppingRoutes.shoppingCart, + pageBuilder: (BuildContext context, GoRouterState state) => + buildScreenWithFadeTransition( + context: context, + state: state, + child: configuration.shoppingCartBuilder(context), + ), + ), + GoRoute( + name: "orderDetails", + path: FlutterShoppingRoutes.orderDetails, + pageBuilder: (BuildContext context, GoRouterState state) { + if (configuration.orderDetailsBuilder != null) { + return buildScreenWithFadeTransition( + context: context, + state: state, + child: configuration.orderDetailsBuilder!(context), + ); + } + + return buildScreenWithFadeTransition( + context: context, + state: state, + child: OrderDetailScreen( + configuration: + getDefaultOrderDetailConfiguration(context, configuration), + ), + ); + }, + ), + GoRoute( + name: "orderSuccess", + path: FlutterShoppingRoutes.orderSuccess, + pageBuilder: (BuildContext context, GoRouterState state) { + if (configuration.orderSuccessBuilder != null) { + return buildScreenWithFadeTransition( + context: context, + state: state, + child: configuration.orderSuccessBuilder!(context), + ); + } + + return buildScreenWithFadeTransition( + context: context, + state: state, + child: DefaultOrderSucces(configuration: configuration), + ); + }, + ), + GoRoute( + name: "orderFailed", + path: FlutterShoppingRoutes.orderFailed, + pageBuilder: (BuildContext context, GoRouterState state) { + if (configuration.orderFailedBuilder != null) { + return buildScreenWithFadeTransition( + context: context, + state: state, + child: configuration.orderFailedBuilder!(context), + ); + } + + return buildScreenWithFadeTransition( + context: context, + state: state, + child: DefaultOrderFailed( + configuration: configuration, + ), + ); + }, + ), + ]; diff --git a/lib/src/user_stores/flutter_shopping_userstory_navigation.dart b/lib/src/user_stores/flutter_shopping_userstory_navigation.dart new file mode 100644 index 0000000..5ced74d --- /dev/null +++ b/lib/src/user_stores/flutter_shopping_userstory_navigation.dart @@ -0,0 +1,53 @@ +import "package:flutter/material.dart"; +import "package:flutter_order_details/flutter_order_details.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; +import "package:go_router/go_router.dart"; + +/// Default on complete order details function. +/// This function will navigate to the order success or order failed page. +/// +/// You can create your own implementation if you decide to use a different +/// approach. +Future onCompleteOrderDetails( + BuildContext context, + FlutterShoppingConfiguration configuration, + OrderResult result, +) async { + var go = context.go; + var succesful = true; + + if (configuration.onCompleteOrderDetails != null) { + var executionResult = + await configuration.onCompleteOrderDetails?.call(context, result); + + if (executionResult == null || !executionResult) { + succesful = false; + } + } + + if (succesful) { + go(FlutterShoppingRoutes.orderSuccess); + } else { + go(FlutterShoppingRoutes.orderFailed); + } +} + +/// Default on complete shopping cart function. +/// +/// You can create your own implementation if you decide to use a different +/// approach. +void onCompleteShoppingCart( + BuildContext context, +) { + context.go(FlutterShoppingRoutes.orderDetails); +} + +/// Default on complete product page function. +/// +/// You can create your own implementation if you decide to use a different +/// approach. +void onCompleteProductPage( + BuildContext context, +) { + context.go(FlutterShoppingRoutes.shoppingCart); +} diff --git a/lib/src/widgets/default_order_failed_widget.dart b/lib/src/widgets/default_order_failed_widget.dart new file mode 100644 index 0000000..24debae --- /dev/null +++ b/lib/src/widgets/default_order_failed_widget.dart @@ -0,0 +1,65 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// Default order failed widget. +class DefaultOrderFailed extends StatelessWidget { + /// Constructor for the DefaultOrderFailed. + const DefaultOrderFailed({ + required this.configuration, + super.key, + }); + + /// Configuration for the user-story. + final FlutterShoppingConfiguration configuration; + + @override + Widget build(BuildContext 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("Go back".toUpperCase()), + ), + ); + + var content = Column( + children: [ + const Spacer(), + const Icon( + Icons.error, + size: 100, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + "Uh oh.", + style: theme.textTheme.titleLarge, + ), + const SizedBox(height: 32), + Text( + "It seems that something went wrong.", + style: theme.textTheme.bodyMedium, + ), + Text( + "Please try again later.", + style: theme.textTheme.bodyMedium, + ), + const Spacer(), + finishOrderButton, + ], + ); + + return Scaffold( + body: SafeArea( + child: Center( + child: content, + ), + ), + ); + } +} diff --git a/lib/src/widgets/default_order_succes_widget.dart b/lib/src/widgets/default_order_succes_widget.dart new file mode 100644 index 0000000..645959a --- /dev/null +++ b/lib/src/widgets/default_order_succes_widget.dart @@ -0,0 +1,66 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// Default order success widget. +class DefaultOrderSucces extends StatelessWidget { + /// Constructor for the DefaultOrderSucces. + const DefaultOrderSucces({ + required this.configuration, + super.key, + }); + + /// Configuration for the user-story. + final FlutterShoppingConfiguration configuration; + + @override + Widget build(BuildContext 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( + body: SafeArea( + child: Center( + child: content, + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 7ec6b07..14ff230 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,18 +9,19 @@ environment: dependencies: flutter: sdk: flutter + go_router: any flutter_product_page: git: url: https://github.com/Iconica-Development/flutter_product_page - ref: 1.0.0 + ref: 1.1.0 flutter_shopping_cart: git: url: https://github.com/Iconica-Development/flutter_shopping_cart - ref: 1.0.0 + ref: 1.1.0 flutter_order_details: git: url: https://github.com/Iconica-Development/flutter_order_details - ref: feat/v1.0.0 + ref: 1.0.0 dev_dependencies: flutter_test: