From f789597fa81eef6eed2a9524cf7beaa575789c1f Mon Sep 17 00:00:00 2001 From: Andrea Bizzotto Date: Wed, 6 Jul 2022 11:16:41 +0100 Subject: [PATCH] Starter project section 9 # Conflicts: # ecommerce_app/lib/src/features/orders/presentation/orders_list/orders_list_screen.dart --- .../data/remote/remote_cart_repository.dart | 2 +- .../application/fake_checkout_service.dart | 69 +++++++++++++++++++ .../checkout_screen/checkout_screen.dart | 8 +-- .../application/user_orders_provider.dart | 17 +++++ .../orders/data/fake_orders_repository.dart | 39 +++++++++++ .../orders_list/orders_list_screen.dart | 64 ++++++++--------- 6 files changed, 157 insertions(+), 42 deletions(-) create mode 100644 ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart create mode 100644 ecommerce_app/lib/src/features/orders/application/user_orders_provider.dart create mode 100644 ecommerce_app/lib/src/features/orders/data/fake_orders_repository.dart diff --git a/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart b/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart index 119057c6..00db6b83 100644 --- a/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart +++ b/ecommerce_app/lib/src/features/cart/data/remote/remote_cart_repository.dart @@ -13,5 +13,5 @@ abstract class RemoteCartRepository { final remoteCartRepositoryProvider = Provider((ref) { // TODO: replace with "real" remote cart repository - return FakeRemoteCartRepository(); + return FakeRemoteCartRepository(addDelay: false); }); diff --git a/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart b/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart new file mode 100644 index 00000000..c092e4ae --- /dev/null +++ b/ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart @@ -0,0 +1,69 @@ +import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart'; +import 'package:ecommerce_app/src/features/cart/data/remote/remote_cart_repository.dart'; +import 'package:ecommerce_app/src/features/cart/domain/cart.dart'; +import 'package:ecommerce_app/src/features/orders/data/fake_orders_repository.dart'; +import 'package:ecommerce_app/src/features/orders/domain/order.dart'; +import 'package:ecommerce_app/src/features/products/data/fake_products_repository.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// A fake checkout service that doesn't process real payments. +class FakeCheckoutService { + FakeCheckoutService(this.ref); + final Ref ref; + + /// Temporary client-side logic for placing an order. + /// Part of this logic should run on the server, so that we can: + /// - setup a payment intent + /// - show the payment UI + /// - process the payment and fullfill the order + /// The server-side logic will be covered in course #2 + Future placeOrder() async { + final authRepository = ref.read(authRepositoryProvider); + final remoteCartRepository = ref.read(remoteCartRepositoryProvider); + final ordersRepository = ref.read(ordersRepositoryProvider); + // * Assertion operator is ok here since this method is only called from + // * a place where the user is signed in + final uid = authRepository.currentUser!.uid; + // 1. Get the cart object + final cart = await remoteCartRepository.fetchCart(uid); + final total = _totalPrice(cart); + // * If we want to make this code more testable, a DateTime builder + // * should be injected as a dependency + final orderDate = DateTime.now(); + // * The orderId is a unique string that could be generated with the UUID + // * package. Since this is a fake service, we just derive it from the date. + final orderId = orderDate.toIso8601String(); + // 2. Create an order + final order = Order( + id: orderId, + userId: uid, + items: cart.items, + orderStatus: OrderStatus.confirmed, + orderDate: orderDate, + total: total, + ); + // 3. Save it using the repository + await ordersRepository.addOrder(uid, order); + // 4. Empty the cart + await remoteCartRepository.setCart(uid, const Cart()); + } + + // Helper method to calculate the total price + double _totalPrice(Cart cart) { + if (cart.items.isEmpty) { + return 0.0; + } + final producsRepository = ref.read(productsRepositoryProvider); + return cart.items.entries + // first extract quantity * price for each item + .map((entry) => + entry.value * // quantity + producsRepository.getProduct(entry.key)!.price) // price + // then add them up + .reduce((value, element) => value + element); + } +} + +final checkoutServiceProvider = Provider((ref) { + return FakeCheckoutService(ref); +}); diff --git a/ecommerce_app/lib/src/features/checkout/presentation/checkout_screen/checkout_screen.dart b/ecommerce_app/lib/src/features/checkout/presentation/checkout_screen/checkout_screen.dart index 550c906f..891a4574 100644 --- a/ecommerce_app/lib/src/features/checkout/presentation/checkout_screen/checkout_screen.dart +++ b/ecommerce_app/lib/src/features/checkout/presentation/checkout_screen/checkout_screen.dart @@ -5,17 +5,12 @@ import 'package:ecommerce_app/src/localization/string_hardcoded.dart'; import 'package:flutter/material.dart'; /// The two sub-routes that are presented as part of the checkout flow. -/// TODO: add the address page as well (see [AddressScreen]). enum CheckoutSubRoute { register, payment } /// This is the root widget of the checkout flow, which is composed of 2 pages: /// 1. Register page /// 2. Payment page -/// The correct page is displayed (and updated) based on whether the user is -/// signed in. -/// The logic for the entire flow is implemented in the -/// [CheckoutScreenController], while UI updates are handled by a -/// [PageController]. +/// TODO: Show the correct page based on whether the user is signed in. class CheckoutScreen extends StatefulWidget { const CheckoutScreen({super.key}); @@ -27,6 +22,7 @@ class _CheckoutScreenState extends State { final _controller = PageController(); var _subRoute = CheckoutSubRoute.register; + // TODO: Load the correct initial page when this screen is presented @override void dispose() { diff --git a/ecommerce_app/lib/src/features/orders/application/user_orders_provider.dart b/ecommerce_app/lib/src/features/orders/application/user_orders_provider.dart new file mode 100644 index 00000000..877bb99c --- /dev/null +++ b/ecommerce_app/lib/src/features/orders/application/user_orders_provider.dart @@ -0,0 +1,17 @@ +import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart'; +import 'package:ecommerce_app/src/features/orders/data/fake_orders_repository.dart'; +import 'package:ecommerce_app/src/features/orders/domain/order.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Watch the list of user orders +/// NOTE: Only watch this provider if the user is signed in. +final userOrdersProvider = StreamProvider.autoDispose>((ref) { + final user = ref.watch(authStateChangesProvider).value; + if (user != null) { + final ordersRepository = ref.watch(ordersRepositoryProvider); + return ordersRepository.watchUserOrders(user.uid); + } else { + // If the user is null, just return an empty screen. + return const Stream.empty(); + } +}); diff --git a/ecommerce_app/lib/src/features/orders/data/fake_orders_repository.dart b/ecommerce_app/lib/src/features/orders/data/fake_orders_repository.dart new file mode 100644 index 00000000..1d63a509 --- /dev/null +++ b/ecommerce_app/lib/src/features/orders/data/fake_orders_repository.dart @@ -0,0 +1,39 @@ +import 'package:ecommerce_app/src/features/orders/domain/order.dart'; +import 'package:ecommerce_app/src/utils/delay.dart'; +import 'package:ecommerce_app/src/utils/in_memory_store.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class FakeOrdersRepository { + FakeOrdersRepository({this.addDelay = true}); + final bool addDelay; + + /// A map of all the orders placed by each user, where: + /// - key: user ID + /// - value: list of orders for that user + final _orders = InMemoryStore>>({}); + + // A stream that returns all the orders for a given user, ordered by date + Stream> watchUserOrders(String uid) { + return _orders.stream.map((ordersData) { + final ordersList = ordersData[uid] ?? []; + ordersList.sort( + (lhs, rhs) => rhs.orderDate.compareTo(lhs.orderDate), + ); + return ordersList; + }); + } + + // A method to add a new order to the list for a given user + Future addOrder(String uid, Order order) async { + await delay(addDelay); + final value = _orders.value; + final userOrders = value[uid] ?? []; + userOrders.add(order); + value[uid] = userOrders; + _orders.value = value; + } +} + +final ordersRepositoryProvider = Provider((ref) { + return FakeOrdersRepository(); +}); diff --git a/ecommerce_app/lib/src/features/orders/presentation/orders_list/orders_list_screen.dart b/ecommerce_app/lib/src/features/orders/presentation/orders_list/orders_list_screen.dart index d3b309d8..3d56fae1 100644 --- a/ecommerce_app/lib/src/features/orders/presentation/orders_list/orders_list_screen.dart +++ b/ecommerce_app/lib/src/features/orders/presentation/orders_list/orders_list_screen.dart @@ -1,9 +1,12 @@ +import 'package:ecommerce_app/src/common_widgets/async_value_widget.dart'; +import 'package:ecommerce_app/src/features/orders/application/user_orders_provider.dart'; import 'package:ecommerce_app/src/features/orders/presentation/orders_list/order_card.dart'; import 'package:ecommerce_app/src/localization/string_hardcoded.dart'; import 'package:flutter/material.dart'; import 'package:ecommerce_app/src/common_widgets/responsive_center.dart'; import 'package:ecommerce_app/src/constants/app_sizes.dart'; import 'package:ecommerce_app/src/features/orders/domain/order.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; /// Shows the list of orders placed by the signed-in user. class OrdersListScreen extends StatelessWidget { @@ -11,48 +14,39 @@ class OrdersListScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: Read from data source - final orders = [ - Order( - id: 'abc', - userId: '123', - items: { - '1': 1, - '2': 2, - '3': 3, - }, - orderStatus: OrderStatus.confirmed, - orderDate: DateTime.now(), - total: 104, - ), - ]; return Scaffold( appBar: AppBar( title: Text('Your Orders'.hardcoded), ), - body: orders.isEmpty - ? Center( - child: Text( - 'No previous orders'.hardcoded, - style: Theme.of(context).textTheme.displaySmall, - textAlign: TextAlign.center, - ), - ) - : CustomScrollView( - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) => ResponsiveCenter( - padding: const EdgeInsets.all(Sizes.p8), - child: OrderCard( - order: orders[index], + body: Consumer(builder: (context, ref, _) { + final userOrdersValue = ref.watch(userOrdersProvider); + return AsyncValueWidget>( + value: userOrdersValue, + data: (orders) => orders.isEmpty + ? Center( + child: Text( + 'No previous orders'.hardcoded, + style: Theme.of(context).textTheme.displaySmall, + textAlign: TextAlign.center, + ), + ) + : CustomScrollView( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) => ResponsiveCenter( + padding: const EdgeInsets.all(Sizes.p8), + child: OrderCard( + order: orders[index], + ), + ), + childCount: orders.length, ), ), - childCount: orders.length, - ), + ], ), - ], - ), + ); + }), ); } }