Skip to content

Commit

Permalink
Starter project section 9
Browse files Browse the repository at this point in the history
# Conflicts:
#	ecommerce_app/lib/src/features/orders/presentation/orders_list/orders_list_screen.dart
  • Loading branch information
bizz84 committed Nov 16, 2023
1 parent c152ca9 commit f789597
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ abstract class RemoteCartRepository {

final remoteCartRepositoryProvider = Provider<RemoteCartRepository>((ref) {
// TODO: replace with "real" remote cart repository
return FakeRemoteCartRepository();
return FakeRemoteCartRepository(addDelay: false);
});
Original file line number Diff line number Diff line change
@@ -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<void> 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<FakeCheckoutService>((ref) {
return FakeCheckoutService(ref);
});
Original file line number Diff line number Diff line change
Expand Up @@ -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});

Expand All @@ -27,6 +22,7 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
final _controller = PageController();

var _subRoute = CheckoutSubRoute.register;
// TODO: Load the correct initial page when this screen is presented

@override
void dispose() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<List<Order>>((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();
}
});
Original file line number Diff line number Diff line change
@@ -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<Map<String, List<Order>>>({});

// A stream that returns all the orders for a given user, ordered by date
Stream<List<Order>> 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<void> 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<FakeOrdersRepository>((ref) {
return FakeOrdersRepository();
});
Original file line number Diff line number Diff line change
@@ -1,58 +1,52 @@
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 {
const OrdersListScreen({super.key});

@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: <Widget>[
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<List<Order>>(
value: userOrdersValue,
data: (orders) => orders.isEmpty
? Center(
child: Text(
'No previous orders'.hardcoded,
style: Theme.of(context).textTheme.displaySmall,
textAlign: TextAlign.center,
),
)
: CustomScrollView(
slivers: <Widget>[
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,
),
],
),
],
),
);
}),
);
}
}

0 comments on commit f789597

Please sign in to comment.