Skip to content

Commit 3764dd6

Browse files
committed
Starter project for section 11
# Conflicts: # ecommerce_app/lib/src/app.dart # ecommerce_app/pubspec.lock # ecommerce_app/pubspec.yaml # Conflicts: # ecommerce_app/lib/src/features/products/presentation/product_screen/leave_review_action.dart
1 parent 0dce242 commit 3764dd6

File tree

25 files changed

+480
-91
lines changed

25 files changed

+480
-91
lines changed

ecommerce_app/integration_test/purchase_flow_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ void main() {
2727
await r.cart.openCart();
2828
r.cart.expectFindZeroCartItems();
2929
await r.closePage();
30+
// reviews flow
31+
// await r.products.selectProduct();
32+
// r.reviews.expectFindLeaveReview();
33+
// await r.reviews.tapLeaveReviewButton();
34+
// await r.reviews.createAndSubmitReview();
35+
// r.reviews.expectFindOneReview();
3036
// sign out
3137
await r.openPopupMenu();
3238
await r.auth.openAccountScreen();

ecommerce_app/lib/src/common_widgets/async_value_widget.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,23 @@ class AsyncValueWidget<T> extends StatelessWidget {
1616
);
1717
}
1818
}
19+
20+
/// Sliver equivalent of [AsyncValueWidget]
21+
class AsyncValueSliverWidget<T> extends StatelessWidget {
22+
const AsyncValueSliverWidget(
23+
{super.key, required this.value, required this.data});
24+
final AsyncValue<T> value;
25+
final Widget Function(T) data;
26+
27+
@override
28+
Widget build(BuildContext context) {
29+
return value.when(
30+
data: data,
31+
loading: () => const SliverToBoxAdapter(
32+
child: Center(child: CircularProgressIndicator())),
33+
error: (e, st) => SliverToBoxAdapter(
34+
child: Center(child: ErrorMessageWidget(e.toString())),
35+
),
36+
);
37+
}
38+
}

ecommerce_app/lib/src/constants/test_products.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const kTestProducts = [
99
description: 'Lorem ipsum',
1010
price: 15,
1111
availableQuantity: 5,
12-
avgRating: 4.5,
13-
numRatings: 2,
1412
),
1513
Product(
1614
id: '2',
@@ -19,8 +17,6 @@ const kTestProducts = [
1917
description: 'Lorem ipsum',
2018
price: 13,
2119
availableQuantity: 5,
22-
avgRating: 4,
23-
numRatings: 2,
2420
),
2521
Product(
2622
id: '3',
@@ -29,8 +25,6 @@ const kTestProducts = [
2925
description: 'Lorem ipsum',
3026
price: 17,
3127
availableQuantity: 5,
32-
avgRating: 5,
33-
numRatings: 2,
3428
),
3529
Product(
3630
id: '4',

ecommerce_app/lib/src/features/checkout/application/fake_checkout_service.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:ecommerce_app/src/features/orders/data/fake_orders_repository.da
55
import 'package:ecommerce_app/src/features/orders/domain/order.dart';
66
import 'package:ecommerce_app/src/features/products/data/fake_products_repository.dart';
77
import 'package:ecommerce_app/src/localization/string_hardcoded.dart';
8+
import 'package:ecommerce_app/src/utils/current_date_provider.dart';
89
import 'package:flutter_riverpod/flutter_riverpod.dart';
910

1011
/// A fake checkout service that doesn't process real payments.
@@ -22,16 +23,15 @@ class FakeCheckoutService {
2223
final authRepository = ref.read(authRepositoryProvider);
2324
final remoteCartRepository = ref.read(remoteCartRepositoryProvider);
2425
final ordersRepository = ref.read(ordersRepositoryProvider);
26+
final currentDateBuilder = ref.read(currentDateBuilderProvider);
2527
// * Assertion operator is ok here since this method is only called from
2628
// * a place where the user is signed in
2729
final uid = authRepository.currentUser!.uid;
2830
// 1. Get the cart object
2931
final cart = await remoteCartRepository.fetchCart(uid);
3032
if (cart.items.isNotEmpty) {
3133
final total = _totalPrice(cart);
32-
// * If we want to make this code more testable, a DateTime builder
33-
// * should be injected as a dependency
34-
final orderDate = DateTime.now();
34+
final orderDate = currentDateBuilder();
3535
// * The orderId is a unique string that could be generated with the UUID
3636
// * package. Since this is a fake service, we just derive it from the date.
3737
final orderId = orderDate.toIso8601String();
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import 'package:ecommerce_app/src/features/authentication/data/fake_auth_repository.dart';
22
import 'package:ecommerce_app/src/features/orders/data/fake_orders_repository.dart';
33
import 'package:ecommerce_app/src/features/orders/domain/order.dart';
4+
import 'package:ecommerce_app/src/features/products/domain/product.dart';
45
import 'package:flutter_riverpod/flutter_riverpod.dart';
56

67
/// Watch the list of user orders
78
/// NOTE: Only watch this provider if the user is signed in.
89
final userOrdersProvider = StreamProvider.autoDispose<List<Order>>((ref) {
910
final user = ref.watch(authStateChangesProvider).value;
1011
if (user != null) {
11-
final ordersRepository = ref.watch(ordersRepositoryProvider);
12-
return ordersRepository.watchUserOrders(user.uid);
12+
return ref.watch(ordersRepositoryProvider).watchUserOrders(user.uid);
1313
} else {
14-
// If the user is null, just return an empty screen.
15-
return const Stream.empty();
14+
// If the user is null, return an empty list (no orders)
15+
return Stream.value([]);
16+
}
17+
});
18+
19+
/// Check if a product was previously purchased by the user
20+
final matchingUserOrdersProvider =
21+
StreamProvider.autoDispose.family<List<Order>, ProductID>((ref, productId) {
22+
final user = ref.watch(authStateChangesProvider).value;
23+
if (user != null) {
24+
return ref
25+
.watch(ordersRepositoryProvider)
26+
.watchUserOrders(user.uid, productId: productId);
27+
} else {
28+
// If the user is null, return an empty list (no orders)
29+
return Stream.value([]);
1630
}
1731
});

ecommerce_app/lib/src/features/orders/data/fake_orders_repository.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:ecommerce_app/src/features/orders/domain/order.dart';
2+
import 'package:ecommerce_app/src/features/products/domain/product.dart';
23
import 'package:ecommerce_app/src/utils/delay.dart';
34
import 'package:ecommerce_app/src/utils/in_memory_store.dart';
45
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -12,14 +13,22 @@ class FakeOrdersRepository {
1213
/// - value: list of orders for that user
1314
final _orders = InMemoryStore<Map<String, List<Order>>>({});
1415

15-
// A stream that returns all the orders for a given user, ordered by date
16-
Stream<List<Order>> watchUserOrders(String uid) {
16+
/// A stream that returns all the orders for a given user, ordered by date
17+
/// Only user orders that match the given productId will be returned.
18+
/// If a productId is not passed, all user orders will be returned.
19+
Stream<List<Order>> watchUserOrders(String uid, {ProductID? productId}) {
1720
return _orders.stream.map((ordersData) {
1821
final ordersList = ordersData[uid] ?? [];
1922
ordersList.sort(
2023
(lhs, rhs) => rhs.orderDate.compareTo(lhs.orderDate),
2124
);
22-
return ordersList;
25+
if (productId != null) {
26+
return ordersList
27+
.where((order) => order.items.keys.contains(productId))
28+
.toList();
29+
} else {
30+
return ordersList;
31+
}
2332
});
2433
}
2534

ecommerce_app/lib/src/features/orders/domain/order.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:ecommerce_app/src/exceptions/app_exception.dart';
2+
import 'package:ecommerce_app/src/features/products/domain/product.dart';
23

34
/// Order status
45
enum OrderStatus { confirmed, shipped, delivered }
@@ -34,7 +35,7 @@ class Order {
3435
final String userId;
3536

3637
/// List of items in that order
37-
final Map<String, int> items;
38+
final Map<ProductID, int> items;
3839
final OrderStatus orderStatus;
3940
final DateTime orderDate;
4041
final double total;

ecommerce_app/lib/src/features/orders/domain/purchase.dart

Lines changed: 0 additions & 12 deletions
This file was deleted.

ecommerce_app/lib/src/features/products/data/fake_products_repository.dart

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,54 @@
1+
import 'dart:async';
2+
13
import 'package:ecommerce_app/src/constants/test_products.dart';
24
import 'package:ecommerce_app/src/features/products/domain/product.dart';
35
import 'package:ecommerce_app/src/utils/delay.dart';
6+
import 'package:ecommerce_app/src/utils/in_memory_store.dart';
47
import 'package:flutter_riverpod/flutter_riverpod.dart';
58

69
class FakeProductsRepository {
710
FakeProductsRepository({this.addDelay = true});
811
final bool addDelay;
9-
final List<Product> _products = kTestProducts;
12+
13+
/// Preload with the default list of products when the app starts
14+
final _products = InMemoryStore<List<Product>>(List.from(kTestProducts));
1015

1116
List<Product> getProductsList() {
12-
return _products;
17+
return _products.value;
1318
}
1419

1520
Product? getProduct(String id) {
16-
return _getProduct(_products, id);
21+
return _getProduct(_products.value, id);
1722
}
1823

1924
Future<List<Product>> fetchProductsList() async {
2025
await delay(addDelay);
21-
return Future.value(_products);
26+
return Future.value(_products.value);
2227
}
2328

24-
Stream<List<Product>> watchProductsList() async* {
25-
await delay(addDelay);
26-
yield _products;
29+
Stream<List<Product>> watchProductsList() {
30+
return _products.stream;
2731
}
2832

2933
Stream<Product?> watchProduct(String id) {
3034
return watchProductsList().map((products) => _getProduct(products, id));
3135
}
3236

37+
/// Update product or add a new one
38+
Future<void> setProduct(Product product) async {
39+
await delay(addDelay);
40+
final products = _products.value;
41+
final index = products.indexWhere((p) => p.id == product.id);
42+
if (index == -1) {
43+
// if not found, add as a new product
44+
products.add(product);
45+
} else {
46+
// else, overwrite previous product
47+
products[index] = product;
48+
}
49+
_products.value = products;
50+
}
51+
3352
static Product? _getProduct(List<Product> products, String id) {
3453
try {
3554
return products.firstWhere((product) => product.id == id);

ecommerce_app/lib/src/features/products/domain/product.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,26 @@ class Product {
5555
avgRating.hashCode ^
5656
numRatings.hashCode;
5757
}
58+
59+
Product copyWith({
60+
ProductID? id,
61+
String? imageUrl,
62+
String? title,
63+
String? description,
64+
double? price,
65+
int? availableQuantity,
66+
double? avgRating,
67+
int? numRatings,
68+
}) {
69+
return Product(
70+
id: id ?? this.id,
71+
imageUrl: imageUrl ?? this.imageUrl,
72+
title: title ?? this.title,
73+
description: description ?? this.description,
74+
price: price ?? this.price,
75+
availableQuantity: availableQuantity ?? this.availableQuantity,
76+
avgRating: avgRating ?? this.avgRating,
77+
numRatings: numRatings ?? this.numRatings,
78+
);
79+
}
5880
}

0 commit comments

Comments
 (0)