From 25d708ae0712f30abd07ea5c71cdf88ea1e13f15 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Sun, 25 Jul 2021 15:20:07 +0300 Subject: [PATCH 01/20] move fake merchaints collection inside a region document --- .../functions/src/system/fakeDataPopulator.ts | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/backend/functions/src/system/fakeDataPopulator.ts b/src/backend/functions/src/system/fakeDataPopulator.ts index 2c3f478..6670394 100644 --- a/src/backend/functions/src/system/fakeDataPopulator.ts +++ b/src/backend/functions/src/system/fakeDataPopulator.ts @@ -53,35 +53,36 @@ export class FakeDataPopulator { 'numberOfRatings': faker.datatype.number(200), }; - let merchantId = await this.createMerchantDocument(merchant); - await this.generateMerchantsProducts(merchantId); + // let merchantId = + await this.createMerchantDocumentForSpecificRegion(merchant,'cape-town'); + // await this.generateMerchantsProducts(merchantId); } } - private async generateMerchantsProducts(merchatId: string) { - log(`generateMerchantsProducts merchatId:${merchatId}`); + // private async generateMerchantsProducts(merchatId: string) { + // log(`generateMerchantsProducts merchatId:${merchatId}`); - for (let index = 0; index < 30; index++) { - let product = { - 'name': faker.commerce.productName(), - 'description': faker.lorem.paragraph(2), - 'image': faker.image.imageUrl(640, 640, 'food'), - 'category': faker.commerce.department(), - 'price': faker.datatype.number(8999), - }; + // for (let index = 0; index < 30; index++) { + // let product = { + // 'name': faker.commerce.productName(), + // 'description': faker.lorem.paragraph(2), + // 'image': faker.image.imageUrl(640, 640, 'food'), + // 'category': faker.commerce.department(), + // 'price': faker.datatype.number(8999), + // }; - await this.createMerchantProduct(merchatId, product); - } - } + // await this.createMerchantProduct(merchatId, product); + // } + // } - private async createMerchantProduct(merchantId: string, product: any) { - await this.firestoreDatabase.collection('merchants').doc(merchantId).collection('products').add(product); - } + // private async createMerchantProduct(merchantId: string, product: any) { + // await this.firestoreDatabase.collection('merchants').doc(merchantId).collection('products').add(product); + // } - private async createMerchantDocument(merchant: any): Promise { - let documentReference = await this.firestoreDatabase.collection('merchants').add(merchant); - return documentReference.id; - } + // private async createMerchantDocument(merchant: any): Promise { + // let documentReference = await this.firestoreDatabase.collection('merchants').add(merchant); + // return documentReference.id; + // } private async createGenerateDocument(): Promise { log('createGenerateDocument'); @@ -91,4 +92,15 @@ export class FakeDataPopulator { private getGenerateDocument(): firestore.DocumentReference { return this.firestoreDatabase.collection('data').doc('generate'); } + + + + + // private async createMerchantProductForSpecificRegion(merchantId: string, product: any) { + // await this.firestoreDatabase.collection('merchants').doc(merchantId).collection('products').add(product); + // } + private async createMerchantDocumentForSpecificRegion(merchant: any, regionId: string): Promise { + let documentReference = await this.firestoreDatabase.collection('regions').doc(regionId).collection('merchants').add(merchant); + return documentReference.id; + } } \ No newline at end of file From 546acb478c375c8036e3dba685d0926a8ee14991 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Sun, 25 Jul 2021 15:20:14 +0300 Subject: [PATCH 02/20] move fake products collection under merchants new path --- .../functions/src/system/fakeDataPopulator.ts | 65 ++++++++----------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/src/backend/functions/src/system/fakeDataPopulator.ts b/src/backend/functions/src/system/fakeDataPopulator.ts index 6670394..2c68e41 100644 --- a/src/backend/functions/src/system/fakeDataPopulator.ts +++ b/src/backend/functions/src/system/fakeDataPopulator.ts @@ -4,7 +4,9 @@ import * as faker from 'faker'; // enable short hand for console.log() function log(message: string) { console.log(`FakeDataPopulator | ${message}`); } - +const FAKE_REGION_NAME = 'cape-town' +const NUMBER_OF_FAKE_MERCHANTS = 10 +const NUMBER_OF_FAKE_PRODUCTS_PER_MERCHANTS = 30 /** * A class that helps with populating a local firestore database */ @@ -35,13 +37,13 @@ export class FakeDataPopulator { private async generateRegions() { log('generateRegions'); - await this.firestoreDatabase.collection('regions').doc('cape-town').set({}); + await this.firestoreDatabase.collection('regions').doc(FAKE_REGION_NAME).set({}); } private async generateMerchants() { log('generateMerchants'); - for (let index = 0; index < 30; index++) { + for (let index = 0; index < NUMBER_OF_FAKE_MERCHANTS; index++) { let merchant = { 'name': faker.commerce.productName(), 'image': faker.image.imageUrl(640, 640, 'food'), @@ -53,36 +55,35 @@ export class FakeDataPopulator { 'numberOfRatings': faker.datatype.number(200), }; - // let merchantId = - await this.createMerchantDocumentForSpecificRegion(merchant,'cape-town'); - // await this.generateMerchantsProducts(merchantId); + let merchantId = + await this.createMerchantDocumentForSpecificRegion(merchant, FAKE_REGION_NAME); + await this.generateMerchantsProducts(merchantId); } } - // private async generateMerchantsProducts(merchatId: string) { - // log(`generateMerchantsProducts merchatId:${merchatId}`); - - // for (let index = 0; index < 30; index++) { - // let product = { - // 'name': faker.commerce.productName(), - // 'description': faker.lorem.paragraph(2), - // 'image': faker.image.imageUrl(640, 640, 'food'), - // 'category': faker.commerce.department(), - // 'price': faker.datatype.number(8999), - // }; + private async generateMerchantsProducts(merchantId: string) { + log(`generateMerchantsProducts merchatId:${merchantId}`); - // await this.createMerchantProduct(merchatId, product); - // } - // } + for (let index = 0; index < NUMBER_OF_FAKE_PRODUCTS_PER_MERCHANTS; index++) { + let product = { + 'name': faker.commerce.productName(), + 'description': faker.lorem.paragraph(2), + 'image': faker.image.imageUrl(640, 640, 'food'), + 'category': faker.commerce.department(), + 'price': faker.datatype.number(8999), + }; - // private async createMerchantProduct(merchantId: string, product: any) { - // await this.firestoreDatabase.collection('merchants').doc(merchantId).collection('products').add(product); - // } + await this.createMerchantProductForSpecificRegion(merchantId, product); + } + } + private async createMerchantDocumentForSpecificRegion(merchant: any, regionId: string): Promise { + let documentReference = await this.firestoreDatabase.collection('regions').doc(regionId).collection('merchants').add(merchant); + return documentReference.id; + } - // private async createMerchantDocument(merchant: any): Promise { - // let documentReference = await this.firestoreDatabase.collection('merchants').add(merchant); - // return documentReference.id; - // } + private async createMerchantProductForSpecificRegion(merchantId: string, product: any) { + await this.firestoreDatabase.collection('regions').doc(FAKE_REGION_NAME).collection('merchants').doc(merchantId).collection('products').add(product) + } private async createGenerateDocument(): Promise { log('createGenerateDocument'); @@ -93,14 +94,4 @@ export class FakeDataPopulator { return this.firestoreDatabase.collection('data').doc('generate'); } - - - - // private async createMerchantProductForSpecificRegion(merchantId: string, product: any) { - // await this.firestoreDatabase.collection('merchants').doc(merchantId).collection('products').add(product); - // } - private async createMerchantDocumentForSpecificRegion(merchant: any, regionId: string): Promise { - let documentReference = await this.firestoreDatabase.collection('regions').doc(regionId).collection('merchants').add(merchant); - return documentReference.id; - } } \ No newline at end of file From 1db7b59380be27e5e8cb48097358a9d071a3c008 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Tue, 27 Jul 2021 00:08:37 +0300 Subject: [PATCH 03/20] Build the LargeMerchantItem in the box_ui project and add it to the example --- .../box_ui/example/lib/example_view.dart | 35 +++++++ src/clients/box_ui/lib/box_ui.dart | 1 + .../box_ui/lib/src/shared/app_colors.dart | 2 + .../lib/src/widgets/large_merchants_item.dart | 95 +++++++++++++++++++ .../large_merchants_item_images_carousel.dart | 58 +++++++++++ src/clients/box_ui/pubspec.yaml | 2 +- 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 src/clients/box_ui/lib/src/widgets/large_merchants_item.dart create mode 100644 src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart diff --git a/src/clients/box_ui/example/lib/example_view.dart b/src/clients/box_ui/example/lib/example_view.dart index b9e9fc8..d8b86e9 100644 --- a/src/clients/box_ui/example/lib/example_view.dart +++ b/src/clients/box_ui/example/lib/example_view.dart @@ -17,11 +17,46 @@ class ExampleView extends StatelessWidget { ...buttonWidgets, ...textWidgets, ...inputFields, + ...largeRestaurantItems, ], ), ); } + List get largeRestaurantItems => [ + verticalSpaceLarge, + BoxText.headline('LargeMerchantItem'), + verticalSpaceMedium, + LargeMerchantItem( + restaurantName: 'McDonald', + imagesUrl: [ + 'https://baconmockup.com/640/360', + 'https://baconmockup.com/641/360', + 'https://baconmockup.com/639/360', + 'https://baconmockup.com/638/360' + ], + cuisines: ['Arabic', 'Turkish', 'Chinese'], + deliveryCost: 4.2, + deliveryInMinutes: 26, + rating: 3.4, + ratingCount: 41, + ), + verticalSpaceMedium, + LargeMerchantItem( + restaurantName: 'McDonald', + imagesUrl: [ + 'https://baconmockup.com/640/360', + 'https://baconmockup.com/641/360', + 'https://baconmockup.com/639/360', + 'https://baconmockup.com/638/360' + ], + cuisines: ['Arabic', 'Turkish', 'Chinese'], + deliveryCost: 0, + deliveryInMinutes: 26, + rating: 3.4, + ratingCount: 41, + ) + ]; List get textWidgets => [ BoxText.headline('Text Styles'), verticalSpaceMedium, diff --git a/src/clients/box_ui/lib/box_ui.dart b/src/clients/box_ui/lib/box_ui.dart index daab696..b9f56ae 100644 --- a/src/clients/box_ui/lib/box_ui.dart +++ b/src/clients/box_ui/lib/box_ui.dart @@ -5,6 +5,7 @@ export 'src/widgets/box_text.dart'; export 'src/widgets/box_button.dart'; export 'src/widgets/box_input_field.dart'; export 'src/widgets/autocomplete_listItem.dart'; +export 'src/widgets/large_merchants_item.dart'; // Colors Export export 'src/shared/app_colors.dart'; diff --git a/src/clients/box_ui/lib/src/shared/app_colors.dart b/src/clients/box_ui/lib/src/shared/app_colors.dart index 88ebf43..fb2ce1a 100644 --- a/src/clients/box_ui/lib/src/shared/app_colors.dart +++ b/src/clients/box_ui/lib/src/shared/app_colors.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; const Color kcPrimaryColor = Color(0xff22A45D); +const Color kcDeepGreyColor = Color(0xff010F07); const Color kcMediumGreyColor = Color(0xff868686); +const Color kcSemiLightColor = Color(0xffD8D8D8); const Color kcLightGreyColor = Color(0xffe5e5e5); const Color kcVeryLightGreyColor = Color(0xfff2f2f2); diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart new file mode 100644 index 0000000..33c826d --- /dev/null +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart @@ -0,0 +1,95 @@ +import 'package:box_ui/box_ui.dart'; +import 'package:box_ui/src/shared/styles.dart'; +import 'package:flutter/material.dart'; + +import 'large_merchants_item_images_carousel.dart'; + +class LargeMerchantItem extends StatelessWidget { + final List imagesUrl; + final String restaurantName; + final List cuisines; + final double? rating; + final int? ratingCount; + final int deliveryInMinutes; + final double deliveryCost; + final bool isClosed; + + const LargeMerchantItem( + {Key? key, + required this.imagesUrl, + required this.restaurantName, + required this.cuisines, + required this.deliveryInMinutes, + required this.deliveryCost, + this.rating, + this.ratingCount, + this.isClosed = false}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LargeMerchantsItemImagesCarsouel( + imagesUrl: imagesUrl, + ), + verticalSpaceSmall, + Text(restaurantName, + style: subheadingStyle.copyWith(color: kcDeepGreyColor)), + Text(cuisines.join(' • '), + style: bodyStyle.copyWith(color: kcMediumGreyColor)), + verticalSpaceSmall, + Row( + children: [ + Text( + rating.toString(), + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + horizontalSpaceTiny, + Icon( + Icons.star_rounded, + color: kcPrimaryColor, + size: 15, + ), + horizontalSpaceTiny, + Text( + ratingCount.toString() + ' Ratings', + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + horizontalSpaceTiny, + Icon( + Icons.watch_later_rounded, + color: Colors.black.withOpacity(0.6), + size: 15, + ), + horizontalSpaceTiny, + Text( + deliveryInMinutes.toString() + ' Min', + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + horizontalSpaceTiny, + Text('•', + style: bodyStyle.copyWith( + color: kcMediumGreyColor.withOpacity(0.5))), + horizontalSpaceTiny, + Icon( + Icons.attach_money_rounded, + color: kcMediumGreyColor, + size: 15, + ), + horizontalSpaceTiny, + Text( + deliveryCost == 0.0 ? 'Free' : deliveryCost.toString(), + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + ], + ) + ], + ); + } +} diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart new file mode 100644 index 0000000..0565297 --- /dev/null +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart @@ -0,0 +1,58 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; + +class LargeMerchantsItemImagesCarsouel extends StatefulWidget { + final List imagesUrl; + const LargeMerchantsItemImagesCarsouel({Key? key, required this.imagesUrl}) + : super(key: key); + + @override + _LargeMerchantsItemImagesCarsouelState createState() => + _LargeMerchantsItemImagesCarsouelState(); +} + +class _LargeMerchantsItemImagesCarsouelState + extends State { + int _currentIndex = 0; + @override + Widget build(BuildContext context) { + return Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CarouselSlider( + items: widget.imagesUrl + .map((imageUrl) => Image.network( + imageUrl, + )) + .toList(), + options: CarouselOptions( + aspectRatio: 1.8, + viewportFraction: 1, + onPageChanged: (currentPageIndex, _) { + setState(() { + _currentIndex = currentPageIndex; + }); + })), + ), + Positioned( + bottom: 20, + right: 20, + child: Row( + children: [ + ...widget.imagesUrl.asMap().entries.map((map) => Container( + margin: const EdgeInsets.only(right: 8), + width: 8, + height: 5, + decoration: BoxDecoration( + color: Colors.white + .withOpacity(_currentIndex == map.key ? 1 : 0.3), + borderRadius: BorderRadius.circular(32)), + )) + ], + ), + ), + ], + ); + } +} diff --git a/src/clients/box_ui/pubspec.yaml b/src/clients/box_ui/pubspec.yaml index 1524fcd..c6ed7e8 100644 --- a/src/clients/box_ui/pubspec.yaml +++ b/src/clients/box_ui/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - + carousel_slider: ^4.0.0 dev_dependencies: flutter_test: sdk: flutter From 27d5d4dac9eb378e3ffbf8e9d7a70fdf99734a4c Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Tue, 27 Jul 2021 09:41:21 +0300 Subject: [PATCH 04/20] Add the function in the FirestoreApi to get the merchants for a region when landing on home --- .../customer/lib/api/firestore_api.dart | 27 ++ .../lib/models/application_models.dart | 16 +- .../models/application_models.freezed.dart | 270 ++++++++++++++++++ .../lib/models/application_models.g.dart | 23 ++ .../customer/lib/ui/home/home_view.dart | 10 +- .../customer/lib/ui/home/home_viewmodel.dart | 11 +- .../lib/ui/startup/startup_viewmodel.dart | 2 + .../test/helpers/test_helpers.mocks.dart | 4 + 8 files changed, 360 insertions(+), 3 deletions(-) diff --git a/src/clients/customer/lib/api/firestore_api.dart b/src/clients/customer/lib/api/firestore_api.dart index 3bd7359..0da364f 100644 --- a/src/clients/customer/lib/api/firestore_api.dart +++ b/src/clients/customer/lib/api/firestore_api.dart @@ -99,4 +99,31 @@ class FirestoreApi { CollectionReference getAddressCollectionForUser(String userId) { return usersCollection.doc(userId).collection(AddressesFirestoreKey); } + + Future> getMerchantsCollectionForRegion( + {required String regionId}) async { + log.i('regionId:$regionId'); + try { + final regionCollections = + await regionsCollection.doc(regionId).collection('merchants').get(); + if (regionCollections.docs.isEmpty) { + log.v('We have no merchants in this region'); + return []; + } + + final regionCollectionsDocuments = regionCollections.docs; + log.v( + 'for regionId: $regionId, Merchants fetched: $regionCollectionsDocuments'); + List merchants = regionCollectionsDocuments.map((merchant) { + var data = merchant.data(); + data.putIfAbsent('id', () => merchant.id); + return Merchant.fromJson(data); + }).toList(); + return merchants; + } catch (error) { + throw FirestoreApiException( + message: + 'An error ocurred while calling getMerchantsCollectionForRegion(): $error'); + } + } } diff --git a/src/clients/customer/lib/models/application_models.dart b/src/clients/customer/lib/models/application_models.dart index 1a0625e..723940c 100644 --- a/src/clients/customer/lib/models/application_models.dart +++ b/src/clients/customer/lib/models/application_models.dart @@ -19,7 +19,7 @@ class User with _$User { } @freezed -abstract class Address with _$Address { +class Address with _$Address { factory Address({ String? id, required String placeId, @@ -34,3 +34,17 @@ abstract class Address with _$Address { factory Address.fromJson(Map json) => _$AddressFromJson(json); } + +@freezed +class Merchant with _$Merchant { + factory Merchant( + {required String id, + List? categories, + String? image, + String? name, + int? numberOfRatings, + double? rating}) = _Merchant; + + factory Merchant.fromJson(Map json) => + _$MerchantFromJson(json); +} diff --git a/src/clients/customer/lib/models/application_models.freezed.dart b/src/clients/customer/lib/models/application_models.freezed.dart index 6a88349..2728847 100644 --- a/src/clients/customer/lib/models/application_models.freezed.dart +++ b/src/clients/customer/lib/models/application_models.freezed.dart @@ -512,3 +512,273 @@ abstract class _Address implements Address { _$AddressCopyWith<_Address> get copyWith => throw _privateConstructorUsedError; } + +Merchant _$MerchantFromJson(Map json) { + return _Merchant.fromJson(json); +} + +/// @nodoc +class _$MerchantTearOff { + const _$MerchantTearOff(); + + _Merchant call( + {required String id, + List? categories, + String? image, + String? name, + int? numberOfRatings, + double? rating}) { + return _Merchant( + id: id, + categories: categories, + image: image, + name: name, + numberOfRatings: numberOfRatings, + rating: rating, + ); + } + + Merchant fromJson(Map json) { + return Merchant.fromJson(json); + } +} + +/// @nodoc +const $Merchant = _$MerchantTearOff(); + +/// @nodoc +mixin _$Merchant { + String get id => throw _privateConstructorUsedError; + List? get categories => throw _privateConstructorUsedError; + String? get image => throw _privateConstructorUsedError; + String? get name => throw _privateConstructorUsedError; + int? get numberOfRatings => throw _privateConstructorUsedError; + double? get rating => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MerchantCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MerchantCopyWith<$Res> { + factory $MerchantCopyWith(Merchant value, $Res Function(Merchant) then) = + _$MerchantCopyWithImpl<$Res>; + $Res call( + {String id, + List? categories, + String? image, + String? name, + int? numberOfRatings, + double? rating}); +} + +/// @nodoc +class _$MerchantCopyWithImpl<$Res> implements $MerchantCopyWith<$Res> { + _$MerchantCopyWithImpl(this._value, this._then); + + final Merchant _value; + // ignore: unused_field + final $Res Function(Merchant) _then; + + @override + $Res call({ + Object? id = freezed, + Object? categories = freezed, + Object? image = freezed, + Object? name = freezed, + Object? numberOfRatings = freezed, + Object? rating = freezed, + }) { + return _then(_value.copyWith( + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + categories: categories == freezed + ? _value.categories + : categories // ignore: cast_nullable_to_non_nullable + as List?, + image: image == freezed + ? _value.image + : image // ignore: cast_nullable_to_non_nullable + as String?, + name: name == freezed + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + numberOfRatings: numberOfRatings == freezed + ? _value.numberOfRatings + : numberOfRatings // ignore: cast_nullable_to_non_nullable + as int?, + rating: rating == freezed + ? _value.rating + : rating // ignore: cast_nullable_to_non_nullable + as double?, + )); + } +} + +/// @nodoc +abstract class _$MerchantCopyWith<$Res> implements $MerchantCopyWith<$Res> { + factory _$MerchantCopyWith(_Merchant value, $Res Function(_Merchant) then) = + __$MerchantCopyWithImpl<$Res>; + @override + $Res call( + {String id, + List? categories, + String? image, + String? name, + int? numberOfRatings, + double? rating}); +} + +/// @nodoc +class __$MerchantCopyWithImpl<$Res> extends _$MerchantCopyWithImpl<$Res> + implements _$MerchantCopyWith<$Res> { + __$MerchantCopyWithImpl(_Merchant _value, $Res Function(_Merchant) _then) + : super(_value, (v) => _then(v as _Merchant)); + + @override + _Merchant get _value => super._value as _Merchant; + + @override + $Res call({ + Object? id = freezed, + Object? categories = freezed, + Object? image = freezed, + Object? name = freezed, + Object? numberOfRatings = freezed, + Object? rating = freezed, + }) { + return _then(_Merchant( + id: id == freezed + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + categories: categories == freezed + ? _value.categories + : categories // ignore: cast_nullable_to_non_nullable + as List?, + image: image == freezed + ? _value.image + : image // ignore: cast_nullable_to_non_nullable + as String?, + name: name == freezed + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + numberOfRatings: numberOfRatings == freezed + ? _value.numberOfRatings + : numberOfRatings // ignore: cast_nullable_to_non_nullable + as int?, + rating: rating == freezed + ? _value.rating + : rating // ignore: cast_nullable_to_non_nullable + as double?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_Merchant implements _Merchant { + _$_Merchant( + {required this.id, + this.categories, + this.image, + this.name, + this.numberOfRatings, + this.rating}); + + factory _$_Merchant.fromJson(Map json) => + _$_$_MerchantFromJson(json); + + @override + final String id; + @override + final List? categories; + @override + final String? image; + @override + final String? name; + @override + final int? numberOfRatings; + @override + final double? rating; + + @override + String toString() { + return 'Merchant(id: $id, categories: $categories, image: $image, name: $name, numberOfRatings: $numberOfRatings, rating: $rating)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other is _Merchant && + (identical(other.id, id) || + const DeepCollectionEquality().equals(other.id, id)) && + (identical(other.categories, categories) || + const DeepCollectionEquality() + .equals(other.categories, categories)) && + (identical(other.image, image) || + const DeepCollectionEquality().equals(other.image, image)) && + (identical(other.name, name) || + const DeepCollectionEquality().equals(other.name, name)) && + (identical(other.numberOfRatings, numberOfRatings) || + const DeepCollectionEquality() + .equals(other.numberOfRatings, numberOfRatings)) && + (identical(other.rating, rating) || + const DeepCollectionEquality().equals(other.rating, rating))); + } + + @override + int get hashCode => + runtimeType.hashCode ^ + const DeepCollectionEquality().hash(id) ^ + const DeepCollectionEquality().hash(categories) ^ + const DeepCollectionEquality().hash(image) ^ + const DeepCollectionEquality().hash(name) ^ + const DeepCollectionEquality().hash(numberOfRatings) ^ + const DeepCollectionEquality().hash(rating); + + @JsonKey(ignore: true) + @override + _$MerchantCopyWith<_Merchant> get copyWith => + __$MerchantCopyWithImpl<_Merchant>(this, _$identity); + + @override + Map toJson() { + return _$_$_MerchantToJson(this); + } +} + +abstract class _Merchant implements Merchant { + factory _Merchant( + {required String id, + List? categories, + String? image, + String? name, + int? numberOfRatings, + double? rating}) = _$_Merchant; + + factory _Merchant.fromJson(Map json) = _$_Merchant.fromJson; + + @override + String get id => throw _privateConstructorUsedError; + @override + List? get categories => throw _privateConstructorUsedError; + @override + String? get image => throw _privateConstructorUsedError; + @override + String? get name => throw _privateConstructorUsedError; + @override + int? get numberOfRatings => throw _privateConstructorUsedError; + @override + double? get rating => throw _privateConstructorUsedError; + @override + @JsonKey(ignore: true) + _$MerchantCopyWith<_Merchant> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/src/clients/customer/lib/models/application_models.g.dart b/src/clients/customer/lib/models/application_models.g.dart index f42969c..c45982f 100644 --- a/src/clients/customer/lib/models/application_models.g.dart +++ b/src/clients/customer/lib/models/application_models.g.dart @@ -44,3 +44,26 @@ Map _$_$_AddressToJson(_$_Address instance) => 'state': instance.state, 'postalCode': instance.postalCode, }; + +_$_Merchant _$_$_MerchantFromJson(Map json) { + return _$_Merchant( + id: json['id'] as String, + categories: (json['categories'] as List?) + ?.map((e) => e as String) + .toList(), + image: json['image'] as String?, + name: json['name'] as String?, + numberOfRatings: json['numberOfRatings'] as int?, + rating: (json['rating'] as num?)?.toDouble(), + ); +} + +Map _$_$_MerchantToJson(_$_Merchant instance) => + { + 'id': instance.id, + 'categories': instance.categories, + 'image': instance.image, + 'name': instance.name, + 'numberOfRatings': instance.numberOfRatings, + 'rating': instance.rating, + }; diff --git a/src/clients/customer/lib/ui/home/home_view.dart b/src/clients/customer/lib/ui/home/home_view.dart index af95df6..197e999 100644 --- a/src/clients/customer/lib/ui/home/home_view.dart +++ b/src/clients/customer/lib/ui/home/home_view.dart @@ -1,3 +1,4 @@ +import 'package:box_ui/box_ui.dart'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; @@ -9,7 +10,14 @@ class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { return ViewModelBuilder.reactive( - builder: (context, model, child) => Scaffold(), + builder: (context, model, child) => Scaffold( + body: Center( + child: BoxButton( + title: 'title', + onTap: model.onTap, + ), + ), + ), viewModelBuilder: () => HomeViewModel(), ); } diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index cc90252..ebabded 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -1,3 +1,12 @@ +import 'package:customer/api/firestore_api.dart'; +import 'package:customer/app/app.locator.dart'; import 'package:stacked/stacked.dart'; -class HomeViewModel extends BaseViewModel {} \ No newline at end of file +class HomeViewModel extends BaseViewModel { + final _fireStoreApi = locator(); + void onTap() async { + final merchantes = await _fireStoreApi.getMerchantsCollectionForRegion( + regionId: 'cape-town'); + print(merchantes); + } +} diff --git a/src/clients/customer/lib/ui/startup/startup_viewmodel.dart b/src/clients/customer/lib/ui/startup/startup_viewmodel.dart index 65db851..3d38820 100644 --- a/src/clients/customer/lib/ui/startup/startup_viewmodel.dart +++ b/src/clients/customer/lib/ui/startup/startup_viewmodel.dart @@ -1,3 +1,4 @@ +import 'package:customer/api/firestore_api.dart'; import 'package:customer/app/app.locator.dart'; import 'package:customer/app/app.logger.dart'; import 'package:customer/app/app.router.dart'; @@ -13,6 +14,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; class StartUpViewModel extends BaseViewModel { final log = getLogger('StartUpViewModel'); final _userService = locator(); + final _fireStoreApi = locator(); final _navigationService = locator(); final _placesService = locator(); final _environmentService = locator(); diff --git a/src/clients/customer/test/helpers/test_helpers.mocks.dart b/src/clients/customer/test/helpers/test_helpers.mocks.dart index 75e2a17..06f29a9 100644 --- a/src/clients/customer/test/helpers/test_helpers.mocks.dart +++ b/src/clients/customer/test/helpers/test_helpers.mocks.dart @@ -385,6 +385,10 @@ class MockFirestoreApi extends _i1.Mock implements _i16.FirestoreApi { (super.noSuchMethod(Invocation.getter(#usersCollection), returnValue: _FakeCollectionReference()) as _i5.CollectionReference); @override + _i5.CollectionReference get regionsCollection => + (super.noSuchMethod(Invocation.getter(#regionsCollection), + returnValue: _FakeCollectionReference()) as _i5.CollectionReference); + @override _i7.Future createUser({_i2.User? user}) => (super.noSuchMethod(Invocation.method(#createUser, [], {#user: user}), returnValue: Future.value(null), From 6d7b48baff81010701a83d676a9eaebdd386d25c Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Tue, 27 Jul 2021 12:44:14 +0300 Subject: [PATCH 05/20] change faker api field image to images to match the card design --- src/backend/functions/src/system/fakeDataPopulator.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/backend/functions/src/system/fakeDataPopulator.ts b/src/backend/functions/src/system/fakeDataPopulator.ts index 2c68e41..cd4d56c 100644 --- a/src/backend/functions/src/system/fakeDataPopulator.ts +++ b/src/backend/functions/src/system/fakeDataPopulator.ts @@ -46,7 +46,11 @@ export class FakeDataPopulator { for (let index = 0; index < NUMBER_OF_FAKE_MERCHANTS; index++) { let merchant = { 'name': faker.commerce.productName(), - 'image': faker.image.imageUrl(640, 640, 'food'), + 'images': [ + faker.image.imageUrl(1024, 640, 'food',true), + faker.image.imageUrl(1024, 640, 'food',true), + faker.image.imageUrl(1024, 640, 'food',true), + ], 'categories': [ faker.commerce.department(), faker.commerce.department() From a7bcf3c25ca81e7e000e5dd2dde14bef623493e1 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Tue, 27 Jul 2021 12:49:15 +0300 Subject: [PATCH 06/20] update LargeMerchantItem field names --- .../box_ui/example/lib/example_view.dart | 16 +-- .../box_ui/lib/src/shared/ui_helpers.dart | 3 +- .../lib/src/widgets/large_merchants_item.dart | 128 +++++++++--------- .../large_merchants_item_images_carousel.dart | 18 ++- 4 files changed, 89 insertions(+), 76 deletions(-) diff --git a/src/clients/box_ui/example/lib/example_view.dart b/src/clients/box_ui/example/lib/example_view.dart index d8b86e9..00f9e94 100644 --- a/src/clients/box_ui/example/lib/example_view.dart +++ b/src/clients/box_ui/example/lib/example_view.dart @@ -28,33 +28,33 @@ class ExampleView extends StatelessWidget { BoxText.headline('LargeMerchantItem'), verticalSpaceMedium, LargeMerchantItem( - restaurantName: 'McDonald', - imagesUrl: [ + name: 'McDonald', + images: [ 'https://baconmockup.com/640/360', 'https://baconmockup.com/641/360', 'https://baconmockup.com/639/360', 'https://baconmockup.com/638/360' ], - cuisines: ['Arabic', 'Turkish', 'Chinese'], + categories: ['Arabic', 'Turkish', 'Chinese'], deliveryCost: 4.2, deliveryInMinutes: 26, rating: 3.4, - ratingCount: 41, + numberOfRatings: 41, ), verticalSpaceMedium, LargeMerchantItem( - restaurantName: 'McDonald', - imagesUrl: [ + name: 'McDonald', + images: [ 'https://baconmockup.com/640/360', 'https://baconmockup.com/641/360', 'https://baconmockup.com/639/360', 'https://baconmockup.com/638/360' ], - cuisines: ['Arabic', 'Turkish', 'Chinese'], + categories: ['Arabic', 'Turkish', 'Chinese'], deliveryCost: 0, deliveryInMinutes: 26, rating: 3.4, - ratingCount: 41, + numberOfRatings: 41, ) ]; List get textWidgets => [ diff --git a/src/clients/box_ui/lib/src/shared/ui_helpers.dart b/src/clients/box_ui/lib/src/shared/ui_helpers.dart index 1dcbe36..ef95be2 100644 --- a/src/clients/box_ui/lib/src/shared/ui_helpers.dart +++ b/src/clients/box_ui/lib/src/shared/ui_helpers.dart @@ -1,4 +1,3 @@ -// Horizontal Spacing import 'package:flutter/material.dart'; const Widget horizontalSpaceTiny = SizedBox(width: 5.0); @@ -23,3 +22,5 @@ double screenHeightPercentage(BuildContext context, {double percentage = 1}) => double screenWidthPercentage(BuildContext context, {double percentage = 1}) => screenWidth(context) * percentage; + +const double screenHorizontalPadding = 16; diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart index 33c826d..9842b1a 100644 --- a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart @@ -5,24 +5,24 @@ import 'package:flutter/material.dart'; import 'large_merchants_item_images_carousel.dart'; class LargeMerchantItem extends StatelessWidget { - final List imagesUrl; - final String restaurantName; - final List cuisines; + final List images; + final String name; + final List categories; final double? rating; - final int? ratingCount; - final int deliveryInMinutes; - final double deliveryCost; + final int? numberOfRatings; + final int? deliveryInMinutes; + final double? deliveryCost; final bool isClosed; const LargeMerchantItem( {Key? key, - required this.imagesUrl, - required this.restaurantName, - required this.cuisines, - required this.deliveryInMinutes, - required this.deliveryCost, + required this.images, + required this.name, + required this.categories, + this.deliveryInMinutes, + this.deliveryCost, this.rating, - this.ratingCount, + this.numberOfRatings, this.isClosed = false}) : super(key: key); @@ -32,61 +32,67 @@ class LargeMerchantItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ LargeMerchantsItemImagesCarsouel( - imagesUrl: imagesUrl, + images: images, ), verticalSpaceSmall, - Text(restaurantName, - style: subheadingStyle.copyWith(color: kcDeepGreyColor)), - Text(cuisines.join(' • '), + Text(name, style: subheadingStyle.copyWith(color: kcDeepGreyColor)), + verticalSpaceTiny, + Text(categories.join(' • '), style: bodyStyle.copyWith(color: kcMediumGreyColor)), verticalSpaceSmall, Row( children: [ - Text( - rating.toString(), - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), - ), - horizontalSpaceTiny, - Icon( - Icons.star_rounded, - color: kcPrimaryColor, - size: 15, - ), - horizontalSpaceTiny, - Text( - ratingCount.toString() + ' Ratings', - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), - ), - horizontalSpaceTiny, - Icon( - Icons.watch_later_rounded, - color: Colors.black.withOpacity(0.6), - size: 15, - ), - horizontalSpaceTiny, - Text( - deliveryInMinutes.toString() + ' Min', - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), - ), - horizontalSpaceTiny, - Text('•', - style: bodyStyle.copyWith( - color: kcMediumGreyColor.withOpacity(0.5))), - horizontalSpaceTiny, - Icon( - Icons.attach_money_rounded, - color: kcMediumGreyColor, - size: 15, - ), - horizontalSpaceTiny, - Text( - deliveryCost == 0.0 ? 'Free' : deliveryCost.toString(), - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), - ), + if (rating != null) ...[ + Text( + rating.toString(), + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + horizontalSpaceTiny, + Icon( + Icons.star_rounded, + color: kcPrimaryColor, + size: 15, + ), + horizontalSpaceTiny, + Text( + numberOfRatings.toString() + ' Ratings', + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + horizontalSpaceTiny, + ], + if (deliveryInMinutes != null) ...[ + Icon( + Icons.watch_later_rounded, + color: Colors.black.withOpacity(0.6), + size: 15, + ), + horizontalSpaceTiny, + Text( + deliveryInMinutes.toString() + ' Min', + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + horizontalSpaceTiny, + ], + if (deliveryCost != null) ...[ + Text('•', + style: bodyStyle.copyWith( + color: kcMediumGreyColor.withOpacity(0.5))), + horizontalSpaceTiny, + Icon( + Icons.attach_money_rounded, + color: kcMediumGreyColor, + size: 15, + ), + horizontalSpaceTiny, + Text( + deliveryCost == 0.0 ? 'Free' : deliveryCost.toString(), + style: captionStyle.copyWith( + color: kcDeepGreyColor.withOpacity(0.74)), + ), + ] ], ) ], diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart index 0565297..3d7178f 100644 --- a/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart @@ -1,9 +1,10 @@ +import 'package:box_ui/src/shared/app_colors.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; class LargeMerchantsItemImagesCarsouel extends StatefulWidget { - final List imagesUrl; - const LargeMerchantsItemImagesCarsouel({Key? key, required this.imagesUrl}) + final List images; + const LargeMerchantsItemImagesCarsouel({Key? key, required this.images}) : super(key: key); @override @@ -21,9 +22,14 @@ class _LargeMerchantsItemImagesCarsouelState ClipRRect( borderRadius: BorderRadius.circular(20), child: CarouselSlider( - items: widget.imagesUrl - .map((imageUrl) => Image.network( - imageUrl, + items: widget.images + .map((imageUrl) => Container( + width: double.infinity, + color: kcLightGreyColor, + child: Image.network( + imageUrl, + fit: BoxFit.cover, + ), )) .toList(), options: CarouselOptions( @@ -40,7 +46,7 @@ class _LargeMerchantsItemImagesCarsouelState right: 20, child: Row( children: [ - ...widget.imagesUrl.asMap().entries.map((map) => Container( + ...widget.images.asMap().entries.map((map) => Container( margin: const EdgeInsets.only(right: 8), width: 8, height: 5, From 33b09b8302ff8434cc0d6c7567cc68f1686e538d Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Tue, 27 Jul 2021 12:59:47 +0300 Subject: [PATCH 07/20] Display merchants list through the ViewModel using a FutureViewModel --- .../lib/models/application_models.dart | 2 +- .../customer/lib/ui/home/home_view.dart | 35 +++++++++++++++---- .../customer/lib/ui/home/home_viewmodel.dart | 28 ++++++++++++--- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/clients/customer/lib/models/application_models.dart b/src/clients/customer/lib/models/application_models.dart index 723940c..e72d7ef 100644 --- a/src/clients/customer/lib/models/application_models.dart +++ b/src/clients/customer/lib/models/application_models.dart @@ -40,7 +40,7 @@ class Merchant with _$Merchant { factory Merchant( {required String id, List? categories, - String? image, + List? images, String? name, int? numberOfRatings, double? rating}) = _Merchant; diff --git a/src/clients/customer/lib/ui/home/home_view.dart b/src/clients/customer/lib/ui/home/home_view.dart index 197e999..b8ed640 100644 --- a/src/clients/customer/lib/ui/home/home_view.dart +++ b/src/clients/customer/lib/ui/home/home_view.dart @@ -11,13 +11,34 @@ class HomeView extends StatelessWidget { Widget build(BuildContext context) { return ViewModelBuilder.reactive( builder: (context, model, child) => Scaffold( - body: Center( - child: BoxButton( - title: 'title', - onTap: model.onTap, - ), - ), - ), + body: model.isBusy + ? Center( + child: CircularProgressIndicator(), + ) + : model.hasError + ? BoxText.headingThree( + 'An error has occered while running the future', + ) + : model.data.isEmpty + ? Text('There is currently no merchants for this region') + : ListView.builder( + padding: EdgeInsets.symmetric( + vertical: screenHeightPercentage(context, + percentage: 0.1), + horizontal: screenHorizontalPadding), + itemCount: model.data.length, + itemBuilder: (context, index) => Padding( + padding: const EdgeInsets.only(bottom: 24), + child: LargeMerchantItem( + images: model.data[index].images ?? [], + categories: + model.data[index].categories ?? [], + name: model.data[index].name ?? '', + rating: model.data[index].rating, + numberOfRatings: + model.data[index].numberOfRatings, + ), + ))), viewModelBuilder: () => HomeViewModel(), ); } diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index ebabded..0eaf788 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -1,12 +1,30 @@ import 'package:customer/api/firestore_api.dart'; import 'package:customer/app/app.locator.dart'; +import 'package:customer/exceptions/firestore_api_exception.dart'; +import 'package:customer/models/application_models.dart'; import 'package:stacked/stacked.dart'; +import 'package:customer/app/app.logger.dart'; + +class HomeViewModel extends FutureViewModel { + final log = getLogger('HomeViewModel'); -class HomeViewModel extends BaseViewModel { final _fireStoreApi = locator(); - void onTap() async { - final merchantes = await _fireStoreApi.getMerchantsCollectionForRegion( - regionId: 'cape-town'); - print(merchantes); + + Future> getMerchantsForRegion() async { + try { + log.i("fetch merchints from firestore"); + + final merchants = await _fireStoreApi.getMerchantsCollectionForRegion( + regionId: 'cape-town'); + + log.v('List of merchants: ${merchants.toString()}'); + return merchants; + } on FirestoreApiException catch (e) { + log.e(e.toString()); + throw Exception('An error happened while fetching merchints'); + } } + + @override + Future futureToRun() => getMerchantsForRegion(); } From 68d557bef87d69fa8e4cca94cb477710d1423a47 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Wed, 28 Jul 2021 00:40:11 +0300 Subject: [PATCH 08/20] use BoxText widget --- .../lib/src/widgets/large_merchants_item.dart | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart index 9842b1a..61923b9 100644 --- a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart @@ -35,18 +35,18 @@ class LargeMerchantItem extends StatelessWidget { images: images, ), verticalSpaceSmall, - Text(name, style: subheadingStyle.copyWith(color: kcDeepGreyColor)), + BoxText.subheading(name), verticalSpaceTiny, - Text(categories.join(' • '), - style: bodyStyle.copyWith(color: kcMediumGreyColor)), + BoxText.body( + categories.join(' • '), + color: kcMediumGreyColor, + ), verticalSpaceSmall, Row( children: [ if (rating != null) ...[ - Text( + BoxText.caption( rating.toString(), - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), ), horizontalSpaceTiny, Icon( @@ -55,10 +55,8 @@ class LargeMerchantItem extends StatelessWidget { size: 15, ), horizontalSpaceTiny, - Text( + BoxText.caption( numberOfRatings.toString() + ' Ratings', - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), ), horizontalSpaceTiny, ], @@ -69,17 +67,16 @@ class LargeMerchantItem extends StatelessWidget { size: 15, ), horizontalSpaceTiny, - Text( + BoxText.caption( deliveryInMinutes.toString() + ' Min', - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), ), horizontalSpaceTiny, ], if (deliveryCost != null) ...[ - Text('•', - style: bodyStyle.copyWith( - color: kcMediumGreyColor.withOpacity(0.5))), + BoxText.body( + '•', + color: kcMediumGreyColor.withOpacity(0.5), + ), horizontalSpaceTiny, Icon( Icons.attach_money_rounded, @@ -87,10 +84,8 @@ class LargeMerchantItem extends StatelessWidget { size: 15, ), horizontalSpaceTiny, - Text( + BoxText.caption( deliveryCost == 0.0 ? 'Free' : deliveryCost.toString(), - style: captionStyle.copyWith( - color: kcDeepGreyColor.withOpacity(0.74)), ), ] ], From 3008951c8a160cac76ef78329828af6fd07a9219 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Wed, 28 Jul 2021 00:50:26 +0300 Subject: [PATCH 09/20] constants instead of dublicate strings --- src/backend/functions/src/system/fakeDataPopulator.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/backend/functions/src/system/fakeDataPopulator.ts b/src/backend/functions/src/system/fakeDataPopulator.ts index cd4d56c..f2b999c 100644 --- a/src/backend/functions/src/system/fakeDataPopulator.ts +++ b/src/backend/functions/src/system/fakeDataPopulator.ts @@ -7,6 +7,9 @@ function log(message: string) { console.log(`FakeDataPopulator | ${message}`); } const FAKE_REGION_NAME = 'cape-town' const NUMBER_OF_FAKE_MERCHANTS = 10 const NUMBER_OF_FAKE_PRODUCTS_PER_MERCHANTS = 30 +const MERCHANTS_COLLECTION = 'merchants' +const REGIONS_COLLECTION = 'regions' +const PRODUCTS_COLLECTION = 'products' /** * A class that helps with populating a local firestore database */ @@ -37,7 +40,7 @@ export class FakeDataPopulator { private async generateRegions() { log('generateRegions'); - await this.firestoreDatabase.collection('regions').doc(FAKE_REGION_NAME).set({}); + await this.firestoreDatabase.collection(REGIONS_COLLECTION).doc(FAKE_REGION_NAME).set({}); } private async generateMerchants() { @@ -81,12 +84,12 @@ export class FakeDataPopulator { } } private async createMerchantDocumentForSpecificRegion(merchant: any, regionId: string): Promise { - let documentReference = await this.firestoreDatabase.collection('regions').doc(regionId).collection('merchants').add(merchant); + let documentReference = await this.firestoreDatabase.collection(REGIONS_COLLECTION).doc(regionId).collection(MERCHANTS_COLLECTION).add(merchant); return documentReference.id; } private async createMerchantProductForSpecificRegion(merchantId: string, product: any) { - await this.firestoreDatabase.collection('regions').doc(FAKE_REGION_NAME).collection('merchants').doc(merchantId).collection('products').add(product) + await this.firestoreDatabase.collection(REGIONS_COLLECTION).doc(FAKE_REGION_NAME).collection(MERCHANTS_COLLECTION).doc(merchantId).collection(PRODUCTS_COLLECTION).add(product) } private async createGenerateDocument(): Promise { From 35b833112263095c468147dbeab18fffe90479d6 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Wed, 28 Jul 2021 01:01:59 +0300 Subject: [PATCH 10/20] rerun build runner --- .../models/application_models.freezed.dart | 46 +++++++++---------- .../lib/models/application_models.g.dart | 5 +- src/clients/customer/pubspec.lock | 7 +++ .../test/helpers/test_helpers.mocks.dart | 8 ++++ 4 files changed, 41 insertions(+), 25 deletions(-) diff --git a/src/clients/customer/lib/models/application_models.freezed.dart b/src/clients/customer/lib/models/application_models.freezed.dart index 2728847..ca0c3d2 100644 --- a/src/clients/customer/lib/models/application_models.freezed.dart +++ b/src/clients/customer/lib/models/application_models.freezed.dart @@ -524,14 +524,14 @@ class _$MerchantTearOff { _Merchant call( {required String id, List? categories, - String? image, + List? images, String? name, int? numberOfRatings, double? rating}) { return _Merchant( id: id, categories: categories, - image: image, + images: images, name: name, numberOfRatings: numberOfRatings, rating: rating, @@ -550,7 +550,7 @@ const $Merchant = _$MerchantTearOff(); mixin _$Merchant { String get id => throw _privateConstructorUsedError; List? get categories => throw _privateConstructorUsedError; - String? get image => throw _privateConstructorUsedError; + List? get images => throw _privateConstructorUsedError; String? get name => throw _privateConstructorUsedError; int? get numberOfRatings => throw _privateConstructorUsedError; double? get rating => throw _privateConstructorUsedError; @@ -568,7 +568,7 @@ abstract class $MerchantCopyWith<$Res> { $Res call( {String id, List? categories, - String? image, + List? images, String? name, int? numberOfRatings, double? rating}); @@ -586,7 +586,7 @@ class _$MerchantCopyWithImpl<$Res> implements $MerchantCopyWith<$Res> { $Res call({ Object? id = freezed, Object? categories = freezed, - Object? image = freezed, + Object? images = freezed, Object? name = freezed, Object? numberOfRatings = freezed, Object? rating = freezed, @@ -600,10 +600,10 @@ class _$MerchantCopyWithImpl<$Res> implements $MerchantCopyWith<$Res> { ? _value.categories : categories // ignore: cast_nullable_to_non_nullable as List?, - image: image == freezed - ? _value.image - : image // ignore: cast_nullable_to_non_nullable - as String?, + images: images == freezed + ? _value.images + : images // ignore: cast_nullable_to_non_nullable + as List?, name: name == freezed ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -628,7 +628,7 @@ abstract class _$MerchantCopyWith<$Res> implements $MerchantCopyWith<$Res> { $Res call( {String id, List? categories, - String? image, + List? images, String? name, int? numberOfRatings, double? rating}); @@ -647,7 +647,7 @@ class __$MerchantCopyWithImpl<$Res> extends _$MerchantCopyWithImpl<$Res> $Res call({ Object? id = freezed, Object? categories = freezed, - Object? image = freezed, + Object? images = freezed, Object? name = freezed, Object? numberOfRatings = freezed, Object? rating = freezed, @@ -661,10 +661,10 @@ class __$MerchantCopyWithImpl<$Res> extends _$MerchantCopyWithImpl<$Res> ? _value.categories : categories // ignore: cast_nullable_to_non_nullable as List?, - image: image == freezed - ? _value.image - : image // ignore: cast_nullable_to_non_nullable - as String?, + images: images == freezed + ? _value.images + : images // ignore: cast_nullable_to_non_nullable + as List?, name: name == freezed ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -687,7 +687,7 @@ class _$_Merchant implements _Merchant { _$_Merchant( {required this.id, this.categories, - this.image, + this.images, this.name, this.numberOfRatings, this.rating}); @@ -700,7 +700,7 @@ class _$_Merchant implements _Merchant { @override final List? categories; @override - final String? image; + final List? images; @override final String? name; @override @@ -710,7 +710,7 @@ class _$_Merchant implements _Merchant { @override String toString() { - return 'Merchant(id: $id, categories: $categories, image: $image, name: $name, numberOfRatings: $numberOfRatings, rating: $rating)'; + return 'Merchant(id: $id, categories: $categories, images: $images, name: $name, numberOfRatings: $numberOfRatings, rating: $rating)'; } @override @@ -722,8 +722,8 @@ class _$_Merchant implements _Merchant { (identical(other.categories, categories) || const DeepCollectionEquality() .equals(other.categories, categories)) && - (identical(other.image, image) || - const DeepCollectionEquality().equals(other.image, image)) && + (identical(other.images, images) || + const DeepCollectionEquality().equals(other.images, images)) && (identical(other.name, name) || const DeepCollectionEquality().equals(other.name, name)) && (identical(other.numberOfRatings, numberOfRatings) || @@ -738,7 +738,7 @@ class _$_Merchant implements _Merchant { runtimeType.hashCode ^ const DeepCollectionEquality().hash(id) ^ const DeepCollectionEquality().hash(categories) ^ - const DeepCollectionEquality().hash(image) ^ + const DeepCollectionEquality().hash(images) ^ const DeepCollectionEquality().hash(name) ^ const DeepCollectionEquality().hash(numberOfRatings) ^ const DeepCollectionEquality().hash(rating); @@ -758,7 +758,7 @@ abstract class _Merchant implements Merchant { factory _Merchant( {required String id, List? categories, - String? image, + List? images, String? name, int? numberOfRatings, double? rating}) = _$_Merchant; @@ -770,7 +770,7 @@ abstract class _Merchant implements Merchant { @override List? get categories => throw _privateConstructorUsedError; @override - String? get image => throw _privateConstructorUsedError; + List? get images => throw _privateConstructorUsedError; @override String? get name => throw _privateConstructorUsedError; @override diff --git a/src/clients/customer/lib/models/application_models.g.dart b/src/clients/customer/lib/models/application_models.g.dart index c45982f..0fb2fea 100644 --- a/src/clients/customer/lib/models/application_models.g.dart +++ b/src/clients/customer/lib/models/application_models.g.dart @@ -51,7 +51,8 @@ _$_Merchant _$_$_MerchantFromJson(Map json) { categories: (json['categories'] as List?) ?.map((e) => e as String) .toList(), - image: json['image'] as String?, + images: + (json['images'] as List?)?.map((e) => e as String).toList(), name: json['name'] as String?, numberOfRatings: json['numberOfRatings'] as int?, rating: (json['rating'] as num?)?.toDouble(), @@ -62,7 +63,7 @@ Map _$_$_MerchantToJson(_$_Merchant instance) => { 'id': instance.id, 'categories': instance.categories, - 'image': instance.image, + 'images': instance.images, 'name': instance.name, 'numberOfRatings': instance.numberOfRatings, 'rating': instance.rating, diff --git a/src/clients/customer/pubspec.lock b/src/clients/customer/pubspec.lock index 1b547bc..16104d7 100644 --- a/src/clients/customer/pubspec.lock +++ b/src/clients/customer/pubspec.lock @@ -106,6 +106,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.0.5" + carousel_slider: + dependency: transitive + description: + name: carousel_slider + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" characters: dependency: transitive description: diff --git a/src/clients/customer/test/helpers/test_helpers.mocks.dart b/src/clients/customer/test/helpers/test_helpers.mocks.dart index 06f29a9..724b164 100644 --- a/src/clients/customer/test/helpers/test_helpers.mocks.dart +++ b/src/clients/customer/test/helpers/test_helpers.mocks.dart @@ -411,4 +411,12 @@ class MockFirestoreApi extends _i1.Mock implements _i16.FirestoreApi { _i5.CollectionReference getAddressCollectionForUser(String? userId) => (super .noSuchMethod(Invocation.method(#getAddressCollectionForUser, [userId]), returnValue: _FakeCollectionReference()) as _i5.CollectionReference); + @override + _i7.Future> getMerchantsCollectionForRegion( + {String? regionId}) => + (super.noSuchMethod( + Invocation.method( + #getMerchantsCollectionForRegion, [], {#regionId: regionId}), + returnValue: Future>.value(<_i2.Merchant>[])) + as _i7.Future>); } From 50d8a40bcbb3a8385ea6938d9c3e1aa01306a5eb Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Wed, 28 Jul 2021 13:14:10 +0300 Subject: [PATCH 11/20] change request by Dane --- .../customer/lib/api/firestore_api.dart | 8 +++-- .../customer/lib/constants/app_keys.dart | 1 + .../customer/lib/ui/home/home_view.dart | 32 ++++++++++--------- .../customer/lib/ui/home/home_viewmodel.dart | 4 +-- .../lib/ui/startup/startup_viewmodel.dart | 1 - 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/clients/customer/lib/api/firestore_api.dart b/src/clients/customer/lib/api/firestore_api.dart index 0da364f..a6996f4 100644 --- a/src/clients/customer/lib/api/firestore_api.dart +++ b/src/clients/customer/lib/api/firestore_api.dart @@ -104,10 +104,12 @@ class FirestoreApi { {required String regionId}) async { log.i('regionId:$regionId'); try { - final regionCollections = - await regionsCollection.doc(regionId).collection('merchants').get(); + final regionCollections = await regionsCollection + .doc(regionId) + .collection(MerchantsFirestoreKey) + .get(); if (regionCollections.docs.isEmpty) { - log.v('We have no merchants in this region'); + log.w('We have no merchants in this region'); return []; } diff --git a/src/clients/customer/lib/constants/app_keys.dart b/src/clients/customer/lib/constants/app_keys.dart index fc4c125..d9d04be 100644 --- a/src/clients/customer/lib/constants/app_keys.dart +++ b/src/clients/customer/lib/constants/app_keys.dart @@ -5,3 +5,4 @@ const String NoKey = 'NO_KEY'; const String UsersFirestoreKey = 'users'; const String AddressesFirestoreKey = 'addresses'; const String RegionsFirestoreKey = 'regions'; +const String MerchantsFirestoreKey = 'merchants'; diff --git a/src/clients/customer/lib/ui/home/home_view.dart b/src/clients/customer/lib/ui/home/home_view.dart index b8ed640..6cb823c 100644 --- a/src/clients/customer/lib/ui/home/home_view.dart +++ b/src/clients/customer/lib/ui/home/home_view.dart @@ -19,26 +19,28 @@ class HomeView extends StatelessWidget { ? BoxText.headingThree( 'An error has occered while running the future', ) - : model.data.isEmpty - ? Text('There is currently no merchants for this region') + : model.data!.isEmpty + ? BoxText.headingThree( + 'There is currently no merchants for this region') : ListView.builder( padding: EdgeInsets.symmetric( vertical: screenHeightPercentage(context, percentage: 0.1), horizontal: screenHorizontalPadding), - itemCount: model.data.length, - itemBuilder: (context, index) => Padding( - padding: const EdgeInsets.only(bottom: 24), - child: LargeMerchantItem( - images: model.data[index].images ?? [], - categories: - model.data[index].categories ?? [], - name: model.data[index].name ?? '', - rating: model.data[index].rating, - numberOfRatings: - model.data[index].numberOfRatings, - ), - ))), + itemCount: model.data!.length, + itemBuilder: (context, index) { + final merchantItem = model.data![index]; + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: LargeMerchantItem( + images: merchantItem.images ?? [], + categories: merchantItem.categories ?? [], + name: merchantItem.name ?? '', + rating: merchantItem.rating, + numberOfRatings: merchantItem.numberOfRatings, + ), + ); + })), viewModelBuilder: () => HomeViewModel(), ); } diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index 0eaf788..2385e25 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -5,7 +5,7 @@ import 'package:customer/models/application_models.dart'; import 'package:stacked/stacked.dart'; import 'package:customer/app/app.logger.dart'; -class HomeViewModel extends FutureViewModel { +class HomeViewModel extends FutureViewModel> { final log = getLogger('HomeViewModel'); final _fireStoreApi = locator(); @@ -26,5 +26,5 @@ class HomeViewModel extends FutureViewModel { } @override - Future futureToRun() => getMerchantsForRegion(); + Future> futureToRun() => getMerchantsForRegion(); } diff --git a/src/clients/customer/lib/ui/startup/startup_viewmodel.dart b/src/clients/customer/lib/ui/startup/startup_viewmodel.dart index 3d38820..686e3d7 100644 --- a/src/clients/customer/lib/ui/startup/startup_viewmodel.dart +++ b/src/clients/customer/lib/ui/startup/startup_viewmodel.dart @@ -14,7 +14,6 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; class StartUpViewModel extends BaseViewModel { final log = getLogger('StartUpViewModel'); final _userService = locator(); - final _fireStoreApi = locator(); final _navigationService = locator(); final _placesService = locator(); final _environmentService = locator(); From 450ec922759320f69891ab533443c0eeaf462159 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Wed, 28 Jul 2021 15:42:53 +0300 Subject: [PATCH 12/20] change fucntion name createMerchantProduct --- src/backend/functions/src/system/fakeDataPopulator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/functions/src/system/fakeDataPopulator.ts b/src/backend/functions/src/system/fakeDataPopulator.ts index f2b999c..9668c3d 100644 --- a/src/backend/functions/src/system/fakeDataPopulator.ts +++ b/src/backend/functions/src/system/fakeDataPopulator.ts @@ -80,7 +80,7 @@ export class FakeDataPopulator { 'price': faker.datatype.number(8999), }; - await this.createMerchantProductForSpecificRegion(merchantId, product); + await this.createMerchantProduct(merchantId, product); } } private async createMerchantDocumentForSpecificRegion(merchant: any, regionId: string): Promise { @@ -88,7 +88,7 @@ export class FakeDataPopulator { return documentReference.id; } - private async createMerchantProductForSpecificRegion(merchantId: string, product: any) { + private async createMerchantProduct(merchantId: string, product: any) { await this.firestoreDatabase.collection(REGIONS_COLLECTION).doc(FAKE_REGION_NAME).collection(MERCHANTS_COLLECTION).doc(merchantId).collection(PRODUCTS_COLLECTION).add(product) } From d558827af42207f9e9a44c45927cff33b34b329e Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Wed, 28 Jul 2021 17:21:57 +0300 Subject: [PATCH 13/20] word spelling and a refactor --- .../lib/src/widgets/large_merchants_item.dart | 2 +- .../large_merchants_item_images_carousel.dart | 66 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart index 61923b9..88439ae 100644 --- a/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item.dart @@ -31,7 +31,7 @@ class LargeMerchantItem extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LargeMerchantsItemImagesCarsouel( + LargeMerchantsItemImagesCarousel( images: images, ), verticalSpaceSmall, diff --git a/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart b/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart index 3d7178f..6f66a93 100644 --- a/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart +++ b/src/clients/box_ui/lib/src/widgets/large_merchants_item_images_carousel.dart @@ -2,18 +2,18 @@ import 'package:box_ui/src/shared/app_colors.dart'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; -class LargeMerchantsItemImagesCarsouel extends StatefulWidget { +class LargeMerchantsItemImagesCarousel extends StatefulWidget { final List images; - const LargeMerchantsItemImagesCarsouel({Key? key, required this.images}) + const LargeMerchantsItemImagesCarousel({Key? key, required this.images}) : super(key: key); @override - _LargeMerchantsItemImagesCarsouelState createState() => - _LargeMerchantsItemImagesCarsouelState(); + _LargeMerchantsItemImagesCarouselState createState() => + _LargeMerchantsItemImagesCarouselState(); } -class _LargeMerchantsItemImagesCarsouelState - extends State { +class _LargeMerchantsItemImagesCarouselState + extends State { int _currentIndex = 0; @override Widget build(BuildContext context) { @@ -23,11 +23,11 @@ class _LargeMerchantsItemImagesCarsouelState borderRadius: BorderRadius.circular(20), child: CarouselSlider( items: widget.images - .map((imageUrl) => Container( + .map((image) => Container( width: double.infinity, color: kcLightGreyColor, child: Image.network( - imageUrl, + image, fit: BoxFit.cover, ), )) @@ -41,24 +41,40 @@ class _LargeMerchantsItemImagesCarsouelState }); })), ), - Positioned( - bottom: 20, - right: 20, - child: Row( - children: [ - ...widget.images.asMap().entries.map((map) => Container( - margin: const EdgeInsets.only(right: 8), - width: 8, - height: 5, - decoration: BoxDecoration( - color: Colors.white - .withOpacity(_currentIndex == map.key ? 1 : 0.3), - borderRadius: BorderRadius.circular(32)), - )) - ], - ), - ), + _CarouselCustomIndexWidget( + currentIndex: _currentIndex, + images: widget.images, + ) ], ); } } + +class _CarouselCustomIndexWidget extends StatelessWidget { + final List images; + final int currentIndex; + const _CarouselCustomIndexWidget( + {Key? key, required this.currentIndex, required this.images}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + bottom: 20, + right: 20, + child: Row( + children: [ + ...images.asMap().entries.map((map) => Container( + margin: const EdgeInsets.only(right: 8), + width: 8, + height: 5, + decoration: BoxDecoration( + color: Colors.white + .withOpacity(currentIndex == map.key ? 1 : 0.3), + borderRadius: BorderRadius.circular(32)), + )) + ], + ), + ); + } +} From d4c97d2b87b2dd04bf58b339e0c4c990eb51ab73 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Fri, 30 Jul 2021 11:51:15 +0300 Subject: [PATCH 14/20] get the user region Id instead of hardcoded one --- .../customer/lib/api/firestore_api.dart | 34 +++++++++++++++++-- .../customer/lib/ui/home/home_view.dart | 15 +++++--- .../customer/lib/ui/home/home_viewmodel.dart | 12 +++++-- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/clients/customer/lib/api/firestore_api.dart b/src/clients/customer/lib/api/firestore_api.dart index a6996f4..7a3bca6 100644 --- a/src/clients/customer/lib/api/firestore_api.dart +++ b/src/clients/customer/lib/api/firestore_api.dart @@ -4,6 +4,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:customer/constants/app_keys.dart'; import 'package:customer/exceptions/firestore_api_exception.dart'; import 'package:customer/models/application_models.dart'; +import 'package:customer/extensions/string_extensions.dart'; class FirestoreApi { final log = getLogger('FirestoreApi'); @@ -61,7 +62,7 @@ class FirestoreApi { log.i('address:$address'); try { - final addressDoc = getAddressCollectionForUser(user.id).doc(); + final addressDoc = _getAddressCollectionForUser(user.id).doc(); final newAddressId = addressDoc.id; log.v('Address will be stored with id: $newAddressId'); @@ -96,10 +97,39 @@ class FirestoreApi { return cityDocument.exists; } - CollectionReference getAddressCollectionForUser(String userId) { + CollectionReference _getAddressCollectionForUser(String userId) { return usersCollection.doc(userId).collection(AddressesFirestoreKey); } + Future> getAddressListForUser(String userId) async { + log.i('userId:$userId'); + final addressCollection = await _getAddressCollectionForUser(userId).get(); + + List
addresses = addressCollection.docs.map((address) { + return Address.fromJson(address.data()); + }).toList(); + return addresses; + } + + String getRegionIdForUser( + {required List
addresses, + required String userDefaultAddressId}) { + log.i('addresses:$addresses, userDefaultAddressId:$userDefaultAddressId'); + try { + return addresses + .firstWhere( + (address) => address.id == userDefaultAddressId, + ) + .city! + .toCityDocument; + } on StateError catch (e) { + throw FirestoreApiException( + message: + "we couldn't found the default address of the user in our address collection, ${e.message}", + ); + } + } + Future> getMerchantsCollectionForRegion( {required String regionId}) async { log.i('regionId:$regionId'); diff --git a/src/clients/customer/lib/ui/home/home_view.dart b/src/clients/customer/lib/ui/home/home_view.dart index 6cb823c..b4f8fd4 100644 --- a/src/clients/customer/lib/ui/home/home_view.dart +++ b/src/clients/customer/lib/ui/home/home_view.dart @@ -16,12 +16,19 @@ class HomeView extends StatelessWidget { child: CircularProgressIndicator(), ) : model.hasError - ? BoxText.headingThree( - 'An error has occered while running the future', + ? Center( + child: BoxText.headingThree( + 'An error has occered while running the future', + align: TextAlign.center, + ), ) : model.data!.isEmpty - ? BoxText.headingThree( - 'There is currently no merchants for this region') + ? Center( + child: BoxText.headingThree( + 'There is currently no merchants for this region', + align: TextAlign.center, + ), + ) : ListView.builder( padding: EdgeInsets.symmetric( vertical: screenHeightPercentage(context, diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index 2385e25..3195133 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -2,6 +2,7 @@ import 'package:customer/api/firestore_api.dart'; import 'package:customer/app/app.locator.dart'; import 'package:customer/exceptions/firestore_api_exception.dart'; import 'package:customer/models/application_models.dart'; +import 'package:customer/services/user_service.dart'; import 'package:stacked/stacked.dart'; import 'package:customer/app/app.logger.dart'; @@ -9,13 +10,20 @@ class HomeViewModel extends FutureViewModel> { final log = getLogger('HomeViewModel'); final _fireStoreApi = locator(); - + final _userService = locator(); Future> getMerchantsForRegion() async { try { log.i("fetch merchints from firestore"); + final userAddresses = await _fireStoreApi + .getAddressListForUser(_userService.currentUser.id); + + final addressRegionId = _fireStoreApi.getRegionIdForUser( + addresses: userAddresses, + userDefaultAddressId: _userService.currentUser.defaultAddress!); + final merchants = await _fireStoreApi.getMerchantsCollectionForRegion( - regionId: 'cape-town'); + regionId: addressRegionId); log.v('List of merchants: ${merchants.toString()}'); return merchants; From adfcaa65fda0f3b318bfaa8e802d3d37706be147 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Fri, 30 Jul 2021 12:07:37 +0300 Subject: [PATCH 15/20] add try-catch to getAddressListForUser --- .../customer/lib/api/firestore_api.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/clients/customer/lib/api/firestore_api.dart b/src/clients/customer/lib/api/firestore_api.dart index 7a3bca6..54297dd 100644 --- a/src/clients/customer/lib/api/firestore_api.dart +++ b/src/clients/customer/lib/api/firestore_api.dart @@ -103,12 +103,20 @@ class FirestoreApi { Future> getAddressListForUser(String userId) async { log.i('userId:$userId'); - final addressCollection = await _getAddressCollectionForUser(userId).get(); + try { + final addressCollection = + await _getAddressCollectionForUser(userId).get(); + log.v('addressCollection: ${addressCollection.toString()}'); - List
addresses = addressCollection.docs.map((address) { - return Address.fromJson(address.data()); - }).toList(); - return addresses; + List
addresses = addressCollection.docs.map((address) { + return Address.fromJson(address.data()); + }).toList(); + return addresses; + } catch (e) { + throw FirestoreApiException( + message: "getAddressListForUser() failed, $e", + ); + } } String getRegionIdForUser( From 480adc1fee7263a4c5c8e33b05bbae59d61f3ad7 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Fri, 30 Jul 2021 12:20:26 +0300 Subject: [PATCH 16/20] clean error handling --- src/clients/customer/lib/api/firestore_api.dart | 17 ++++++++++------- .../customer/lib/ui/home/home_viewmodel.dart | 5 +++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/clients/customer/lib/api/firestore_api.dart b/src/clients/customer/lib/api/firestore_api.dart index 54297dd..7ab8248 100644 --- a/src/clients/customer/lib/api/firestore_api.dart +++ b/src/clients/customer/lib/api/firestore_api.dart @@ -85,8 +85,8 @@ class FirestoreApi { } return true; - } on Exception catch (e) { - log.e('we could not save the users address. $e'); + } on Exception catch (error) { + log.e('we could not save the users address. $error'); return false; } } @@ -112,9 +112,10 @@ class FirestoreApi { return Address.fromJson(address.data()); }).toList(); return addresses; - } catch (e) { + } catch (error) { throw FirestoreApiException( - message: "getAddressListForUser() failed, $e", + devDetails: error.toString(), + message: "getAddressListForUser() failed,", ); } } @@ -130,10 +131,11 @@ class FirestoreApi { ) .city! .toCityDocument; - } on StateError catch (e) { + } on StateError catch (error) { throw FirestoreApiException( + devDetails: error.toString(), message: - "we couldn't found the default address of the user in our address collection, ${e.message}", + "we couldn't found the default address of the user in our address collection", ); } } @@ -162,8 +164,9 @@ class FirestoreApi { return merchants; } catch (error) { throw FirestoreApiException( + devDetails: error.toString(), message: - 'An error ocurred while calling getMerchantsCollectionForRegion(): $error'); + 'An error ocurred while calling getMerchantsCollectionForRegion()'); } } } diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index 3195133..be7190e 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -11,6 +11,7 @@ class HomeViewModel extends FutureViewModel> { final _fireStoreApi = locator(); final _userService = locator(); + Future> getMerchantsForRegion() async { try { log.i("fetch merchints from firestore"); @@ -27,8 +28,8 @@ class HomeViewModel extends FutureViewModel> { log.v('List of merchants: ${merchants.toString()}'); return merchants; - } on FirestoreApiException catch (e) { - log.e(e.toString()); + } on FirestoreApiException catch (error) { + log.e(error.toString()); throw Exception('An error happened while fetching merchints'); } } From f0e63a1e8d1e23205820d99d1074111fe9c18466 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Fri, 30 Jul 2021 15:54:49 +0300 Subject: [PATCH 17/20] limit fake merchants rating to be between 0-5 --- .../functions/src/system/fakeDataPopulator.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/backend/functions/src/system/fakeDataPopulator.ts b/src/backend/functions/src/system/fakeDataPopulator.ts index 9668c3d..2c6ab7d 100644 --- a/src/backend/functions/src/system/fakeDataPopulator.ts +++ b/src/backend/functions/src/system/fakeDataPopulator.ts @@ -50,15 +50,19 @@ export class FakeDataPopulator { let merchant = { 'name': faker.commerce.productName(), 'images': [ - faker.image.imageUrl(1024, 640, 'food',true), - faker.image.imageUrl(1024, 640, 'food',true), - faker.image.imageUrl(1024, 640, 'food',true), + faker.image.imageUrl(1024, 640, 'food', true), + faker.image.imageUrl(1024, 640, 'food', true), + faker.image.imageUrl(1024, 640, 'food', true), ], 'categories': [ faker.commerce.department(), faker.commerce.department() ], - 'rating': faker.datatype.float(2), + 'rating': faker.datatype.float({ + min: 0, + max: 5, + precision: 2 + }), 'numberOfRatings': faker.datatype.number(200), }; From 864ff079faec2d9ff846f2e9584e6a5fa379e380 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Fri, 30 Jul 2021 15:56:01 +0300 Subject: [PATCH 18/20] add 2 unit tests to homeviewmodel --- .../customer/lib/api/firestore_api.dart | 2 +- .../customer/lib/ui/home/home_viewmodel.dart | 6 +-- .../customer/test/helpers/test_helpers.dart | 17 ++++-- .../test/helpers/test_helpers.mocks.dart | 16 ++++-- .../viewmodel_tests/home_viewmodel_test.dart | 53 +++++++++++++++++++ 5 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart diff --git a/src/clients/customer/lib/api/firestore_api.dart b/src/clients/customer/lib/api/firestore_api.dart index 7ab8248..a7d19f4 100644 --- a/src/clients/customer/lib/api/firestore_api.dart +++ b/src/clients/customer/lib/api/firestore_api.dart @@ -120,7 +120,7 @@ class FirestoreApi { } } - String getRegionIdForUser( + String extractRegionIdFromUserAddresses( {required List
addresses, required String userDefaultAddressId}) { log.i('addresses:$addresses, userDefaultAddressId:$userDefaultAddressId'); diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index be7190e..0c9e7d5 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -14,12 +14,12 @@ class HomeViewModel extends FutureViewModel> { Future> getMerchantsForRegion() async { try { - log.i("fetch merchints from firestore"); + log.i( + "fetch merchints from firestore where user: ${_userService.currentUser}"); final userAddresses = await _fireStoreApi .getAddressListForUser(_userService.currentUser.id); - - final addressRegionId = _fireStoreApi.getRegionIdForUser( + final addressRegionId = _fireStoreApi.extractRegionIdFromUserAddresses( addresses: userAddresses, userDefaultAddressId: _userService.currentUser.defaultAddress!); diff --git a/src/clients/customer/test/helpers/test_helpers.dart b/src/clients/customer/test/helpers/test_helpers.dart index 5434eb4..f7559bc 100644 --- a/src/clients/customer/test/helpers/test_helpers.dart +++ b/src/clients/customer/test/helpers/test_helpers.dart @@ -91,16 +91,25 @@ MockDialogService getAndRegisterDialogService() { return service; } -MockFirestoreApi getAndRegisterFirestoreApi({ - bool saveAddressSuccess = true, - bool isCityServiced = true, -}) { +MockFirestoreApi getAndRegisterFirestoreApi( + {bool saveAddressSuccess = true, + bool isCityServiced = true, + List
? userAdresses}) { _removeRegistrationIfExists(); final service = MockFirestoreApi(); when(service.isCityServiced(city: anyNamed('city'))) .thenAnswer((realInvocation) => Future.value(isCityServiced)); + when(service.getAddressListForUser(any)) + .thenAnswer((realInvocation) => Future.value(userAdresses ?? [])); + when(service.getMerchantsCollectionForRegion(regionId: anyNamed('regionId'))) + .thenAnswer((realInvocation) => Future.value([])); + when(service.extractRegionIdFromUserAddresses( + addresses: userAdresses ?? anyNamed('addresses'), + userDefaultAddressId: anyNamed('userDefaultAddressId'))) + .thenReturn('RegionId'); + when(service.saveAddress( address: anyNamed('address'), user: anyNamed('user'), diff --git a/src/clients/customer/test/helpers/test_helpers.mocks.dart b/src/clients/customer/test/helpers/test_helpers.mocks.dart index 724b164..09be73c 100644 --- a/src/clients/customer/test/helpers/test_helpers.mocks.dart +++ b/src/clients/customer/test/helpers/test_helpers.mocks.dart @@ -408,9 +408,19 @@ class MockFirestoreApi extends _i1.Mock implements _i16.FirestoreApi { (super.noSuchMethod(Invocation.method(#isCityServiced, [], {#city: city}), returnValue: Future.value(false)) as _i7.Future); @override - _i5.CollectionReference getAddressCollectionForUser(String? userId) => (super - .noSuchMethod(Invocation.method(#getAddressCollectionForUser, [userId]), - returnValue: _FakeCollectionReference()) as _i5.CollectionReference); + _i7.Future> getAddressListForUser(String? userId) => + (super.noSuchMethod(Invocation.method(#getAddressListForUser, [userId]), + returnValue: Future>.value(<_i2.Address>[])) + as _i7.Future>); + @override + String extractRegionIdFromUserAddresses( + {List<_i2.Address>? addresses, String? userDefaultAddressId}) => + (super.noSuchMethod( + Invocation.method(#extractRegionIdFromUserAddresses, [], { + #addresses: addresses, + #userDefaultAddressId: userDefaultAddressId + }), + returnValue: '') as String); @override _i7.Future> getMerchantsCollectionForRegion( {String? regionId}) => diff --git a/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart b/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart new file mode 100644 index 0000000..c1529d9 --- /dev/null +++ b/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart @@ -0,0 +1,53 @@ +import 'package:customer/models/application_models.dart'; +import 'package:customer/ui/home/home_viewmodel.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../helpers/test_helpers.dart'; + +HomeViewModel _getModel() => HomeViewModel(); + +void main() { + group('HomeViewModelTest -', () { + setUp(() => registerServices()); + tearDown(() => unregisterServices()); + group('getMerchantsForRegion -', () { + test('When called, should call getAddressListForUser from firestoreApi', + () async { + final userService = getAndRegisterUserService( + currentUser: User(id: 'id', defaultAddress: 'i-am-here')); + final firestoreApiService = getAndRegisterFirestoreApi(); + + final model = _getModel(); + await model.getMerchantsForRegion(); + final userId = userService.currentUser.id; + verify(firestoreApiService.getAddressListForUser(userId)); + }); + test( + 'When called, should call extractRegionIdFromUserAddresses using addresses from getAddressListForUser', + () async { + final userAdresses = [ + Address( + id: 'i-am-here', + placeId: 'placeId', + city: 'cape-town', + lattitude: 1, + longitute: 2) + ]; + final userService = getAndRegisterUserService( + hasLoggedInUser: true, + currentUser: User(id: 'id', defaultAddress: 'i-am-here')); + final firestoreApi = + getAndRegisterFirestoreApi(userAdresses: userAdresses); + + final model = _getModel(); + await model.getMerchantsForRegion(); + final userDefaultAddress = userService.currentUser.defaultAddress; + + verify(firestoreApi.extractRegionIdFromUserAddresses( + addresses: userAdresses, + userDefaultAddressId: userDefaultAddress!)); + }); + }); + }); +} From 5e3b5bec5785f5118535ac1547b2e28eee6a5396 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Fri, 30 Jul 2021 16:55:36 +0300 Subject: [PATCH 19/20] add one more test to homeviewmodel --- .../customer/test/helpers/test_helpers.dart | 7 +++---- .../viewmodel_tests/home_viewmodel_test.dart | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/clients/customer/test/helpers/test_helpers.dart b/src/clients/customer/test/helpers/test_helpers.dart index f7559bc..76d43c4 100644 --- a/src/clients/customer/test/helpers/test_helpers.dart +++ b/src/clients/customer/test/helpers/test_helpers.dart @@ -94,13 +94,13 @@ MockDialogService getAndRegisterDialogService() { MockFirestoreApi getAndRegisterFirestoreApi( {bool saveAddressSuccess = true, bool isCityServiced = true, - List
? userAdresses}) { + List
? userAdresses, + String? regionId}) { _removeRegistrationIfExists(); final service = MockFirestoreApi(); when(service.isCityServiced(city: anyNamed('city'))) .thenAnswer((realInvocation) => Future.value(isCityServiced)); - when(service.getAddressListForUser(any)) .thenAnswer((realInvocation) => Future.value(userAdresses ?? [])); when(service.getMerchantsCollectionForRegion(regionId: anyNamed('regionId'))) @@ -108,8 +108,7 @@ MockFirestoreApi getAndRegisterFirestoreApi( when(service.extractRegionIdFromUserAddresses( addresses: userAdresses ?? anyNamed('addresses'), userDefaultAddressId: anyNamed('userDefaultAddressId'))) - .thenReturn('RegionId'); - + .thenReturn(regionId ?? 'RegionId'); when(service.saveAddress( address: anyNamed('address'), user: anyNamed('user'), diff --git a/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart b/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart index c1529d9..fc8a5d6 100644 --- a/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart +++ b/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart @@ -16,12 +16,12 @@ void main() { () async { final userService = getAndRegisterUserService( currentUser: User(id: 'id', defaultAddress: 'i-am-here')); - final firestoreApiService = getAndRegisterFirestoreApi(); + final firestoreApi = getAndRegisterFirestoreApi(); final model = _getModel(); await model.getMerchantsForRegion(); final userId = userService.currentUser.id; - verify(firestoreApiService.getAddressListForUser(userId)); + verify(firestoreApi.getAddressListForUser(userId)); }); test( 'When called, should call extractRegionIdFromUserAddresses using addresses from getAddressListForUser', @@ -31,8 +31,8 @@ void main() { id: 'i-am-here', placeId: 'placeId', city: 'cape-town', - lattitude: 1, - longitute: 2) + lattitude: 0, + longitute: 0) ]; final userService = getAndRegisterUserService( hasLoggedInUser: true, @@ -48,6 +48,19 @@ void main() { addresses: userAdresses, userDefaultAddressId: userDefaultAddress!)); }); + test( + 'When called, should call getMerchantsCollectionForRegion using addressRegionId from extractRegionIdFromUserAddresses', + () async { + const RegionId = 'id'; + getAndRegisterUserService( + currentUser: User(id: 'id', defaultAddress: 'i-am-here')); + final firestoreApi = getAndRegisterFirestoreApi(regionId: RegionId); + + final model = _getModel(); + await model.getMerchantsForRegion(); + verify( + firestoreApi.getMerchantsCollectionForRegion(regionId: RegionId)); + }); }); }); } From e16e5028ae369b4fdb5f6c814f2385c5a5bd5a95 Mon Sep 17 00:00:00 2001 From: Ebrahim Soliman Date: Mon, 2 Aug 2021 11:03:16 +0300 Subject: [PATCH 20/20] minor changes --- src/clients/customer/lib/ui/home/home_viewmodel.dart | 4 ++-- .../customer/test/viewmodel_tests/home_viewmodel_test.dart | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/clients/customer/lib/ui/home/home_viewmodel.dart b/src/clients/customer/lib/ui/home/home_viewmodel.dart index 0c9e7d5..40732eb 100644 --- a/src/clients/customer/lib/ui/home/home_viewmodel.dart +++ b/src/clients/customer/lib/ui/home/home_viewmodel.dart @@ -19,12 +19,12 @@ class HomeViewModel extends FutureViewModel> { final userAddresses = await _fireStoreApi .getAddressListForUser(_userService.currentUser.id); - final addressRegionId = _fireStoreApi.extractRegionIdFromUserAddresses( + final regionId = _fireStoreApi.extractRegionIdFromUserAddresses( addresses: userAddresses, userDefaultAddressId: _userService.currentUser.defaultAddress!); final merchants = await _fireStoreApi.getMerchantsCollectionForRegion( - regionId: addressRegionId); + regionId: regionId); log.v('List of merchants: ${merchants.toString()}'); return merchants; diff --git a/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart b/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart index fc8a5d6..5dedd03 100644 --- a/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart +++ b/src/clients/customer/test/viewmodel_tests/home_viewmodel_test.dart @@ -35,7 +35,6 @@ void main() { longitute: 0) ]; final userService = getAndRegisterUserService( - hasLoggedInUser: true, currentUser: User(id: 'id', defaultAddress: 'i-am-here')); final firestoreApi = getAndRegisterFirestoreApi(userAdresses: userAdresses);