diff --git a/app/lib/common/components/animation/animated_check.dart b/app/lib/common/components/animation/animated_check.dart new file mode 100644 index 000000000..96e4535e3 --- /dev/null +++ b/app/lib/common/components/animation/animated_check.dart @@ -0,0 +1,87 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'package:encointer_wallet/theme/theme.dart'; + +class AnimatedCheck extends StatelessWidget { + const AnimatedCheck({ + required this.progress, + required this.size, + this.color, + super.key, + }); + + final Animation progress; + final double size; + final Color? color; + + @override + Widget build(BuildContext context) { + return CustomPaint( + foregroundPainter: AnimatedPathPainter(progress, color ?? context.colorScheme.primary), + child: SizedBox(width: size, height: size), + ); + } +} + +class AnimatedPathPainter extends CustomPainter { + const AnimatedPathPainter(this.animation, this.color) : super(repaint: animation); + + final Animation animation; + final Color color; + + Path _createAnyPath(Size size) { + return Path() + ..moveTo(0.27083 * size.width, 0.54167 * size.height) + ..lineTo(0.41667 * size.width, 0.68750 * size.height) + ..lineTo(0.75000 * size.width, 0.35417 * size.height); + } + + Path createAnimatedPath(Path originalPath, double animationPercent) { + final totalLength = + // ignore: prefer_int_literals + originalPath.computeMetrics().fold(0.0, (double prev, PathMetric metric) => prev + metric.length); + final currentLength = totalLength * animationPercent; + + return extractPathUntilLength(originalPath, currentLength); + } + + Path extractPathUntilLength(Path originalPath, double length) { + var currentLength = 0.0; + final path = Path(); + final metricsIterator = originalPath.computeMetrics().iterator; + + while (metricsIterator.moveNext()) { + final metric = metricsIterator.current; + final nextLength = currentLength + metric.length; + final isLastSegment = nextLength > length; + + if (isLastSegment) { + final remainingLength = length - currentLength; + final pathSegment = metric.extractPath(0, remainingLength); + path.addPath(pathSegment, Offset.zero); + break; + } else { + final pathSegment = metric.extractPath(0, metric.length); + path.addPath(pathSegment, Offset.zero); + } + currentLength = nextLength; + } + return path; + } + + @override + void paint(Canvas canvas, Size size) { + final animationPercent = animation.value; + final path = createAnimatedPath(_createAnyPath(size), animationPercent); + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = size.width * 0.06; + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) => true; +} diff --git a/app/lib/common/components/logo/community_icon.dart b/app/lib/common/components/logo/community_icon.dart index 032609ec0..377d6652c 100644 --- a/app/lib/common/components/logo/community_icon.dart +++ b/app/lib/common/components/logo/community_icon.dart @@ -28,6 +28,7 @@ class CommunityIconObserver extends StatelessWidget { return SvgPicture.asset(fallBackCommunityIcon); } } else { + ///TODO(Azamat): Add some image saying community not selected return const CupertinoActivityIndicator(); } }, diff --git a/app/lib/common/components/map/encointer_map.dart b/app/lib/common/components/map/encointer_map.dart index 685df359f..8a97d1bd9 100644 --- a/app/lib/common/components/map/encointer_map.dart +++ b/app/lib/common/components/map/encointer_map.dart @@ -11,7 +11,7 @@ class EncointerMap extends StatelessWidget { required this.popupBuilder, required this.locations, this.initialZoom = 13, - this.maxZoom = 18, + this.maxZoom = 20, this.center, this.mapController, this.onPointerDown, diff --git a/app/lib/config/consts.dart b/app/lib/config/consts.dart index 8f1eff688..a134e9897 100644 --- a/app/lib/config/consts.dart +++ b/app/lib/config/consts.dart @@ -116,6 +116,7 @@ String ceremonyInfoLink(String locale, String? cid) { const assignmentFAQLinkEN = 'https://leu.zuerich/en/#why-have-i-not-been-assigned-to-a-cycle'; const assignmentFAQLinkDE = 'https://leu.zuerich/#warum-wurde-ich-keinem-cycle-zugewiesen'; +const infuraIpfsUrl = 'https://encointer.infura-ipfs.io/ipfs'; String leuZurichCycleAssignmentFAQLink(String locale) { return switch (locale) { diff --git a/app/lib/l10n/arb/app_de.arb b/app/lib/l10n/arb/app_de.arb index e616b42f1..709e8bb54 100644 --- a/app/lib/l10n/arb/app_de.arb +++ b/app/lib/l10n/arb/app_de.arb @@ -416,5 +416,14 @@ "successfullySentNAttestations": "Du hast erfolgreich Bezeugungen für {participantsCount} andere Leute eingereicht.", "tokenSend": "{symbol} senden", "communityWithName": "{name} Gemeinschaft", - "verifyAuthTitle": "{useBioAuth, select, true{deine Identität} false{deine PIN} other{}} überprüfen, um die Transaktion abzuschließen." + "verifyAuthTitle": "{useBioAuth, select, true{deine Identität} false{deine PIN} other{}} überprüfen, um die Transaktion abzuschließen.", + "offersForCommunity": "Angebote für {value}", + "@offersForCommunity": { + "placeholders": { + "value": { + "type": "String", + "example": "LEU" + } + } + } } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_en.arb b/app/lib/l10n/arb/app_en.arb index 120d3bc41..03d2c8a6b 100644 --- a/app/lib/l10n/arb/app_en.arb +++ b/app/lib/l10n/arb/app_en.arb @@ -568,5 +568,14 @@ "example": "true" } } + }, + "offersForCommunity": "Offers for {value}", + "@offersForCommunity": { + "placeholders": { + "value": { + "type": "String", + "example": "LEU" + } + } } } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_fr.arb b/app/lib/l10n/arb/app_fr.arb index 2a6c92060..ed5ce8933 100644 --- a/app/lib/l10n/arb/app_fr.arb +++ b/app/lib/l10n/arb/app_fr.arb @@ -416,5 +416,14 @@ "successfullySentNAttestations": "Tu as soumis avec succès des attestations pour {participantsCount} autres personnes.", "tokenSend": "Envoyer {symbol}", "communityWithName": "Communauté {name}", - "verifyAuthTitle": "Veuillez vérifier {useBioAuth, select, true{ta identité} false{le NIP} other{}} pour finaliser la transaction." + "verifyAuthTitle": "Veuillez vérifier {useBioAuth, select, true{ta identité} false{le NIP} other{}} pour finaliser la transaction.", + "offersForCommunity": "Des offres pour un {value}", + "@offersForCommunity": { + "placeholders": { + "value": { + "type": "String", + "example": "LEU" + } + } + } } \ No newline at end of file diff --git a/app/lib/l10n/arb/app_ru.arb b/app/lib/l10n/arb/app_ru.arb index 75a81042f..745e95392 100644 --- a/app/lib/l10n/arb/app_ru.arb +++ b/app/lib/l10n/arb/app_ru.arb @@ -267,7 +267,7 @@ "pinSecure": "Защитите свой аккаунт с помощью PIN-кода", "pleaseCommunityChoose": "Пожалуйста выберите общину", "pleaseConfirmYourNewPin": "Подтвердите PIN-код", - "preview": "Предварительный просмотр", + "preview": "Предпросмотр", "price": "Цена", "print": "Распечатать", "productNewness": "Новые продукты", @@ -320,7 +320,7 @@ "settingPrefix": "Префикс адреса", "settingPrefixList": "Доступные префиксы", "share": "Поделиться", - "shareInvoice": "Поделиться инвойсом", + "shareInvoice": "Отправить инвойс", "shareLinkHint": "Или Вы можете поделиться ссылкой", "showAll": "+ Показать все", "showRouteMeetupLocation": "Показать маршрут", @@ -416,5 +416,14 @@ "successfullySentNAttestations": "Вы успешно отправили аттестации {participantsCount} других людей.", "tokenSend": "Отправить {symbol}", "communityWithName": "Сообщество {name}", - "verifyAuthTitle": "Для завершения транзакции {useBioAuth, select, true{подтвердите вашу личность} false{введите PIN-код} other{}}." + "verifyAuthTitle": "Для завершения транзакции {useBioAuth, select, true{подтвердите вашу личность} false{введите PIN-код} other{}}.", + "offersForCommunity": "Предложения за {value}", + "@offersForCommunity": { + "placeholders": { + "value": { + "type": "String", + "example": "LEU" + } + } + } } \ No newline at end of file diff --git a/app/lib/mocks/mock_bazaar_data.dart b/app/lib/mocks/mock_bazaar_data.dart index 77c3727ee..ee1ba1ae9 100644 --- a/app/lib/mocks/mock_bazaar_data.dart +++ b/app/lib/mocks/mock_bazaar_data.dart @@ -1,6 +1,7 @@ import 'package:encointer_wallet/models/bazaar/account_business_tuple.dart'; import 'package:encointer_wallet/models/bazaar/business_data.dart'; import 'package:encointer_wallet/models/bazaar/business_identifier.dart'; +import 'package:encointer_wallet/models/bazaar/businesses.dart'; import 'package:encointer_wallet/models/bazaar/ipfs_business.dart'; import 'package:encointer_wallet/models/bazaar/ipfs_offering.dart'; import 'package:encointer_wallet/models/bazaar/offering_data.dart'; @@ -17,6 +18,9 @@ const String businessIpfsCid3 = '0x3ebf164a5bb618ec6caad31488161b237e24d75efa304 final CommunityIdentifier cid1 = CommunityIdentifier.fromFmtString('gbsuv7YXq9G'); final CommunityIdentifier cid2 = CommunityIdentifier.fromFmtString('fbsuv7YXq9G'); +/// EdisonPaula +final CommunityIdentifier cidEdisonPaula = CommunityIdentifier.fromFmtString('u0qj94fxxJ6'); + final BusinessIdentifier bid1 = BusinessIdentifier(cid1, controller1); final BusinessIdentifier bid2 = BusinessIdentifier(cid1, controller2); final BusinessIdentifier bid3 = BusinessIdentifier(cid1, controller3); @@ -32,10 +36,10 @@ const String offeringIpfsCid2 = '0x77ebf164a5bb618ec6caad31488161b237e24d75efa30 const String offeringIpfsCid3 = '0x87ebf164a5bb618ec6caad31488161b237e24d75efa3040286767b620d9183989'; const String offeringIpfsCid4 = '0x97ebf164a5bb618ec6caad31488161b237e24d75efa3040286767b620d9183989'; -final OfferingData offeringData1 = OfferingData(offeringIpfsCid1); -final OfferingData offeringData2 = OfferingData(offeringIpfsCid2); -final OfferingData offeringData3 = OfferingData(offeringIpfsCid3); -final OfferingData offeringData4 = OfferingData(offeringIpfsCid4); +final OfferingData offeringData1 = OfferingData(url: offeringIpfsCid1); +final OfferingData offeringData2 = OfferingData(url: offeringIpfsCid2); +final OfferingData offeringData3 = OfferingData(url: offeringIpfsCid3); +final OfferingData offeringData4 = OfferingData(url: offeringIpfsCid4); final Map> offeringsForBusiness = { bid1: business1MockOfferings, @@ -96,3 +100,22 @@ final ipfsOffering3 = IpfsOffering('Harry Potter Heptalogy', 1, 'I am interestin 'assets/images/assets/assets_nav_0.png'); final ipfsOffering4 = IpfsOffering( 'Picasso Fake as NFT by C.L.', 1, 'I am beautiful', 'Miami Beach', 'assets/images/assets/assets_nav_0.png'); + +final mockBusinessData = { + 'name': 'HIGHLIGHTED', + 'description': 'wir offerieren kühles Bier', + 'category': 'food', + 'photo': null, + 'address': 'Technoparkstrasse 1, 8005 Zürich', + 'telephone': null, + 'email': null, + 'longitude': '8.515377938747404', + 'latitude': '47.389401263868514', + 'openingHours': 'Mon-Fri 8h-18h', + 'photos': 'QmaQfq6Zr2yCMkSMe8VjSxoYd89hyzcJjeE8jTUG3uXpBG', + 'logo': 'QmcULG6AN5wwMfuwtpsMcjQmFwwUnSHsvSEUFLrCoWMpWh', + 'status': 'highlight', + 'controller': controller1, +}; + +final Businesses businessesMockForSingleBusiness = Businesses.fromJson(mockBusinessData); diff --git a/app/lib/models/bazaar/businesses.dart b/app/lib/models/bazaar/businesses.dart index fc5357b79..bf412bd69 100644 --- a/app/lib/models/bazaar/businesses.dart +++ b/app/lib/models/bazaar/businesses.dart @@ -1,17 +1,28 @@ -import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart'; import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:encointer_wallet/config/consts.dart'; +import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart'; + part 'businesses.g.dart'; @JsonSerializable() class Businesses { - const Businesses({ + Businesses({ required this.name, required this.description, required this.category, - required this.photo, + required this.address, + required this.longitude, + required this.latitude, + required this.openingHours, + this.logo, + this.photos, + this.photo, + this.telephone, + this.email, this.status, + this.controller, }); factory Businesses.fromJson(Map json) => _$BusinessesFromJson(json); Map toJson() => _$BusinessesToJson(this); @@ -19,13 +30,23 @@ class Businesses { final String name; final String description; final Category category; - final String photo; + final String? photo; + final String address; + final String? telephone; + final String? email; + final String longitude; + final String latitude; + final String openingHours; + String? photos; + String? controller; + @ImageHashToLinkOrNullConverter() + String? logo; final Status? status; Color get statusColor { switch (status) { case Status.highlight: return const Color(0xFFE8FBFF); - case Status.neuBeiLeu: + case Status.recently: return Colors.lightGreen.shade100; // ignore: no_default_cases default: @@ -34,11 +55,23 @@ class Businesses { } } +class ImageHashToLinkOrNullConverter implements JsonConverter { + const ImageHashToLinkOrNullConverter(); + + @override + String? fromJson(String? value) { + return '$infuraIpfsUrl/$value'; + } + + @override + String? toJson(String? val) => val; +} + enum Status { @JsonValue('highlight') highlight('Highlight', Color(0xFF00A3FF)), - @JsonValue('neu_bei_leu') - neuBeiLeu('Neu bei Leu', Color(0xFF00BA77)); + @JsonValue('new') + recently('New', Color(0xFF00BA77)); const Status( this.name, @@ -55,39 +88,49 @@ enum Status { const businessesMockData = { 'businesses': [ { - 'name': 'Yoga-Kurse mit Hatha Lisa', - 'description': 'Nutze deine Leu, um deinem Körper und Geist etwas Gutes zu tun. ...', - 'category': 'body_soul', - 'photo': 'https://github.com/SourbaevaJanaraJ/lock_screen/blob/master/assets/hatha-lisa.png?raw=true', - 'status': 'highlight' - }, - { - 'name': 'Hardi – Kafi am Tag, Kultur am Abend und zwischen ...', - 'description': 'Herzhaft unkompliziert empfängt das Hardi seine Gäste im ...', - 'category': 'food_beverage_store', - 'photo': 'https://github.com/SourbaevaJanaraJ/lock_screen/blob/master/assets/hatha-lisa-02.png?raw=true', - 'status': 'highlight' + 'name': 'HIGHLIGHTED', + 'description': 'wir offerieren kühles Bier', + 'category': 'food', + 'photo': null, + 'address': 'Technoparkstrasse 1, 8005 Zürich', + 'telephone': null, + 'email': null, + 'longitude': '8.515377938747404', + 'latitude': '47.389401263868514', + 'openingHours': 'Mon-Fri 8h-18h', + 'photos': 'QmaQfq6Zr2yCMkSMe8VjSxoYd89hyzcJjeE8jTUG3uXpBG', + 'logo': 'QmcULG6AN5wwMfuwtpsMcjQmFwwUnSHsvSEUFLrCoWMpWh', + 'status': 'highlight', }, { - 'name': 'KAOZ', - 'description': 'Wir sind KAOZ. Das heisst: kreativ, authentisch, optimistisch und ...', + 'name': 'NEW', + 'description': 'wir offerieren kühles Bier', 'category': 'fashion_clothing', - 'photo': 'https://github.com/SourbaevaJanaraJ/lock_screen/blob/master/assets/hatha-lisa-02%20(1).png?raw=true' - }, - { - 'name': 'GRRRR', - 'description': 'Papierware, Zines, Bücher, Zeichnungen aus Züri und anders...', - 'category': 'art_music', - 'photo': - 'https://github.com/SourbaevaJanaraJ/lock_screen/blob/master/assets/hatha-lisa-02%20(1)%203.png?raw=true', - 'status': 'neu_bei_leu' + 'photo': null, + 'address': 'Technoparkstrasse 1, 8005 Zürich', + 'telephone': null, + 'email': null, + 'longitude': '8.515377938747404', + 'latitude': '47.389401263868514', + 'openingHours': 'Mon-Fri 8h-18h', + 'photos': 'QmaQfq6Zr2yCMkSMe8VjSxoYd89hyzcJjeE8jTUG3uXpBG', + 'logo': 'QmcULG6AN5wwMfuwtpsMcjQmFwwUnSHsvSEUFLrCoWMpWh', + 'status': 'new', }, { - 'name': 'Sørenbrød', - 'description': 'Der Künstler Søren Berner (geb. 1977 in Dänemark) begann 1999, als er ...', + 'name': 'NORMAL', + 'description': 'wir offerieren kühles Bier', 'category': 'food_beverage_store', - 'photo': 'https://github.com/SourbaevaJanaraJ/lock_screen/blob/master/assets/sorenbrod.png?raw=true', - 'status': 'neu_bei_leu' + 'photo': null, + 'address': 'Technoparkstrasse 1, 8005 Zürich', + 'telephone': null, + 'email': null, + 'longitude': '8.515377938747404', + 'latitude': '47.389401263868514', + 'openingHours': 'Mon-Fri 8h-18h', + 'photos': 'QmaQfq6Zr2yCMkSMe8VjSxoYd89hyzcJjeE8jTUG3uXpBG', + 'logo': 'QmcULG6AN5wwMfuwtpsMcjQmFwwUnSHsvSEUFLrCoWMpWh', + 'status': null, } ] }; diff --git a/app/lib/models/bazaar/businesses.g.dart b/app/lib/models/bazaar/businesses.g.dart index 576411cd4..89002f5f2 100644 --- a/app/lib/models/bazaar/businesses.g.dart +++ b/app/lib/models/bazaar/businesses.g.dart @@ -10,8 +10,17 @@ Businesses _$BusinessesFromJson(Map json) => Businesses( name: json['name'] as String, description: json['description'] as String, category: $enumDecode(_$CategoryEnumMap, json['category']), - photo: json['photo'] as String, + address: json['address'] as String, + longitude: json['longitude'] as String, + latitude: json['latitude'] as String, + openingHours: json['openingHours'] as String, + logo: const ImageHashToLinkOrNullConverter().fromJson(json['logo'] as String?), + photos: json['photos'] as String?, + photo: json['photo'] as String?, + telephone: json['telephone'] as String?, + email: json['email'] as String?, status: $enumDecodeNullable(_$StatusEnumMap, json['status']), + controller: json['controller'] as String?, ); Map _$BusinessesToJson(Businesses instance) => { @@ -19,6 +28,15 @@ Map _$BusinessesToJson(Businesses instance) => json) => _$IpfsProductFromJson(json); + Map toJson() => _$IpfsProductToJson(this); + + /// name of the business + final String name; + + /// brief description of the business + final String description; + + /// contact info of the business + final String category; + + /// ipfs-cid where the images live + @ImageHashToLinkOrNullConverter() + final String? image; + + final String? itemCondition; + + /// comes from [ItemOffered] + String? price; + + @override + String toString() { + return jsonEncode(this); + } +} diff --git a/app/lib/models/bazaar/ipfs_product.g.dart b/app/lib/models/bazaar/ipfs_product.g.dart new file mode 100644 index 000000000..574bd3540 --- /dev/null +++ b/app/lib/models/bazaar/ipfs_product.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ipfs_product.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +IpfsProduct _$IpfsProductFromJson(Map json) => IpfsProduct( + json['name'] as String, + json['description'] as String, + json['category'] as String, + const ImageHashToLinkOrNullConverter().fromJson(json['image'] as String?), + json['itemCondition'] as String?, + json['price'] as String?, + ); + +Map _$IpfsProductToJson(IpfsProduct instance) => { + 'name': instance.name, + 'description': instance.description, + 'category': instance.category, + 'image': const ImageHashToLinkOrNullConverter().toJson(instance.image), + 'itemCondition': instance.itemCondition, + 'price': instance.price, + }; diff --git a/app/lib/models/bazaar/item_offered.dart b/app/lib/models/bazaar/item_offered.dart new file mode 100644 index 000000000..35e32b0b6 --- /dev/null +++ b/app/lib/models/bazaar/item_offered.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import 'package:json_annotation/json_annotation.dart'; + +part 'item_offered.g.dart'; + +/// Product metadata living in ipfs +@JsonSerializable() +class ItemOffered { + ItemOffered( + this.itemOffered, + this.price, + ); + + factory ItemOffered.fromJson(Map json) => _$ItemOfferedFromJson(json); + Map toJson() => _$ItemOfferedToJson(this); + + final String itemOffered; + + final String price; + + @override + String toString() { + return jsonEncode(this); + } +} diff --git a/app/lib/models/bazaar/item_offered.g.dart b/app/lib/models/bazaar/item_offered.g.dart new file mode 100644 index 000000000..de6c5995c --- /dev/null +++ b/app/lib/models/bazaar/item_offered.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'item_offered.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ItemOffered _$ItemOfferedFromJson(Map json) => ItemOffered( + json['itemOffered'] as String, + json['price'] as String, + ); + +Map _$ItemOfferedToJson(ItemOffered instance) => { + 'itemOffered': instance.itemOffered, + 'price': instance.price, + }; diff --git a/app/lib/models/bazaar/offering_data.dart b/app/lib/models/bazaar/offering_data.dart index 8ac21fd4a..b9408f10d 100644 --- a/app/lib/models/bazaar/offering_data.dart +++ b/app/lib/models/bazaar/offering_data.dart @@ -7,7 +7,9 @@ part 'offering_data.g.dart'; /// Offering data living onchain @JsonSerializable() class OfferingData { - OfferingData(this.url); + OfferingData({ + required this.url, + }); factory OfferingData.fromJson(Map json) => _$OfferingDataFromJson(json); Map toJson() => _$OfferingDataToJson(this); diff --git a/app/lib/models/bazaar/offering_data.g.dart b/app/lib/models/bazaar/offering_data.g.dart index 71d4a1b7f..a91942af4 100644 --- a/app/lib/models/bazaar/offering_data.g.dart +++ b/app/lib/models/bazaar/offering_data.g.dart @@ -7,7 +7,7 @@ part of 'offering_data.dart'; // ************************************************************************** OfferingData _$OfferingDataFromJson(Map json) => OfferingData( - json['url'] as String?, + url: json['url'] as String?, ); Map _$OfferingDataToJson(OfferingData instance) => { diff --git a/app/lib/models/bazaar/single_business.dart b/app/lib/models/bazaar/single_business.dart index fc4498f0c..8f9866006 100644 --- a/app/lib/models/bazaar/single_business.dart +++ b/app/lib/models/bazaar/single_business.dart @@ -4,7 +4,7 @@ part 'single_business.g.dart'; @JsonSerializable() class SingleBusiness { - const SingleBusiness({ + SingleBusiness({ required this.name, required this.description, required this.category, @@ -15,8 +15,7 @@ class SingleBusiness { required this.email, required this.longitude, required this.latitude, - required this.openingHours1, - required this.openingHours2, + required this.openingHours, required this.logo, required this.photo, required this.offer, @@ -31,28 +30,27 @@ class SingleBusiness { factory SingleBusiness.fromJson(Map json) => _$SingleBusinessFromJson(json); Map toJson() => _$SingleBusinessToJson(this); - final String name; - final String description; - final String category; - final String address; - final String zipcode; - final String addressDescription; - final String telephone; - final String email; - final double longitude; - final double latitude; - final String openingHours1; - final String openingHours2; - final String logo; - final String photo; - final String offer; - final String offerName1; - final String offerName2; - final String moreInfo; - final String? status; - final bool isLiked; - final bool isLikedPersonally; - final int countLikes; + String name; + String description; + String category; + String address; + String zipcode; + String addressDescription; + String telephone; + String email; + double longitude; + double latitude; + String openingHours; + String logo; + String photo; + String offer; + String offerName1; + String offerName2; + String moreInfo; + String? status; + bool isLiked; + bool isLikedPersonally; + int countLikes; } const singleBusinessMockData = { diff --git a/app/lib/models/bazaar/single_business.g.dart b/app/lib/models/bazaar/single_business.g.dart index f6ce8e078..1608d7817 100644 --- a/app/lib/models/bazaar/single_business.g.dart +++ b/app/lib/models/bazaar/single_business.g.dart @@ -17,8 +17,7 @@ SingleBusiness _$SingleBusinessFromJson(Map json) => SingleBusi email: json['email'] as String, longitude: (json['longitude'] as num).toDouble(), latitude: (json['latitude'] as num).toDouble(), - openingHours1: json['openingHours1'] as String, - openingHours2: json['openingHours2'] as String, + openingHours: json['openingHours'] as String, logo: json['logo'] as String, photo: json['photo'] as String, offer: json['offer'] as String, @@ -42,8 +41,7 @@ Map _$SingleBusinessToJson(SingleBusiness instance) => BiometricAuthState.enabled, + false => BiometricAuthState.disabled, + }; + + // migrate the old storage to the new one + setBiometricAuthState(biometricAuthStateOld); + // clear the old storage + preferences.remove(oldBiometricAuthStateKey); + + return biometricAuthStateOld; } Future setBiometricAuthState(BiometricAuthState biometricAuthState) async { diff --git a/app/lib/modules/settings/service/app_service.dart b/app/lib/modules/settings/service/app_service.dart index d034a8662..c5bd19031 100644 --- a/app/lib/modules/settings/service/app_service.dart +++ b/app/lib/modules/settings/service/app_service.dart @@ -9,7 +9,6 @@ class AppService { final SharedPreferences storage; static const localStorageLocaleKey = 'locale'; - static const biometricAuthStateKey = 'biometric-auth-state'; Locale get getLocale { final code = storage.getString(localStorageLocaleKey); @@ -24,6 +23,4 @@ class AppService { await storage.setString(localStorageLocaleKey, languageCode); return Locale(languageCode); } - - String? get getBiometricAuthState => storage.getString(biometricAuthStateKey); } diff --git a/app/lib/modules/transfer/view/transfer_history_view.dart b/app/lib/modules/transfer/view/transfer_history_view.dart index 8e9bfbd85..47ecff09b 100644 --- a/app/lib/modules/transfer/view/transfer_history_view.dart +++ b/app/lib/modules/transfer/view/transfer_history_view.dart @@ -36,6 +36,7 @@ class TransferHistoryView extends StatelessWidget { context.read().getTransfers(appStore); }, ), + FetchStatus.noData => const SizedBox.shrink(), }; }), ), diff --git a/app/lib/page-encointer/bazaar/0_main/bazaar_main.dart b/app/lib/page-encointer/bazaar/0_main/bazaar_main.dart index 70602eeea..b358f278a 100644 --- a/app/lib/page-encointer/bazaar/0_main/bazaar_main.dart +++ b/app/lib/page-encointer/bazaar/0_main/bazaar_main.dart @@ -1,15 +1,51 @@ -import 'package:encointer_wallet/theme/custom/typography/typography_theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:encointer_wallet/models/communities/community_identifier.dart'; +import 'package:encointer_wallet/store/app.dart'; +import 'package:encointer_wallet/theme/custom/typography/typography_theme.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/logic/businesses_store.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/view/businesses_view.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart'; import 'package:encointer_wallet/l10n/l10.dart'; +class BazaarMainArgs { + BazaarMainArgs({ + required this.cid, + required this.appStore, + }); + final CommunityIdentifier cid; + final AppStore appStore; +} + +class BazaarMain extends StatelessWidget { + const BazaarMain({ + required this.args, + super.key, + }); + final BazaarMainArgs args; + + @override + Widget build(BuildContext context) { + return Provider( + create: (context) => BusinessesStore(args.cid)..getBusinesses(), + child: BazaarPage( + cid: args.cid, + appStore: args.appStore, + ), + ); + } +} + class BazaarPage extends StatelessWidget { - const BazaarPage({super.key}); + const BazaarPage({ + super.key, + required this.cid, + required this.appStore, + }); + final CommunityIdentifier cid; static const String route = '/bazaar'; + final AppStore appStore; @override Widget build(BuildContext context) { @@ -42,19 +78,7 @@ class BazaarPage extends StatelessWidget { ), ), ), - body: const BusinessesView(), - ); - } -} - -class BazaarMain extends StatelessWidget { - const BazaarMain({super.key}); - - @override - Widget build(BuildContext context) { - return Provider( - create: (context) => BusinessesStore()..getBusinesses(), - child: const BazaarPage(), + body: BusinessesView(appStore: appStore), ); } } diff --git a/app/lib/page-encointer/common/community_chooser_on_map.dart b/app/lib/page-encointer/common/community_chooser_on_map.dart index 37cdae9dd..27be93028 100644 --- a/app/lib/page-encointer/common/community_chooser_on_map.dart +++ b/app/lib/page-encointer/common/community_chooser_on_map.dart @@ -104,6 +104,13 @@ List getLocations(AppStore store) { } LatLng coordinatesOf(CidName community) { + /// EdisonPaula has similar map data as to Leu + /// thus it is too close to Zurich Leu, + /// and very hard to choose it from map + /// thus moved little bit to the left on map + if (community.name == 'EdisonPaula') { + return LatLng(47.3962467, 8.4815019); + } final coordinates = GeoHash(utf8.decode(community.cid.geohash)); return LatLng(coordinates.latitude(), coordinates.longitude()); } diff --git a/app/lib/page-encointer/new_bazaar/businesses/logic/business_utils.dart b/app/lib/page-encointer/new_bazaar/businesses/logic/business_utils.dart new file mode 100644 index 000000000..cf993b791 --- /dev/null +++ b/app/lib/page-encointer/new_bazaar/businesses/logic/business_utils.dart @@ -0,0 +1,9 @@ +import 'dart:convert'; + +class BusinessUtils { + /// To correctly display German Umlauts coming from IPFS. + static String utf8convert(String text) { + final bytes = text.codeUnits; + return utf8.decode(bytes); + } +} diff --git a/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.dart b/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.dart index cf6836954..c69e51556 100644 --- a/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.dart +++ b/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.dart @@ -1,36 +1,142 @@ import 'package:mobx/mobx.dart'; +import 'package:ew_http/ew_http.dart'; +import 'package:encointer_wallet/utils/extensions/string/string_extensions.dart'; +import 'package:encointer_wallet/models/bazaar/account_business_tuple.dart'; +import 'package:encointer_wallet/models/communities/community_identifier.dart'; +import 'package:encointer_wallet/service/log/log_service.dart'; +import 'package:encointer_wallet/service/substrate_api/api.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart'; import 'package:encointer_wallet/utils/fetch_status.dart'; import 'package:encointer_wallet/models/bazaar/businesses.dart'; part 'businesses_store.g.dart'; +const _targetLogger = 'BusinessesStore'; + // ignore: library_private_types_in_public_api class BusinessesStore = _BusinessesStoreBase with _$BusinessesStore; abstract class _BusinessesStoreBase with Store { + _BusinessesStoreBase(this.cid); + @observable - List? businesses; + late CommunityIdentifier cid; + + @observable + List businesses = []; + + @observable + List sortedBusinesses = []; @observable FetchStatus fetchStatus = FetchStatus.loading; + @observable + String? error; + + Future> _bazaarGetBusinesses() { + Log.d('_bazaarGetBusinesses: cid = $cid', _targetLogger); + return webApi.encointer.bazaarGetBusinesses(cid); + } + + Future> _getBusinesses(String ipfsUrlHash) { + Log.d('_getBusinesses: ipfsUrlHash = $ipfsUrlHash', _targetLogger); + return webApi.encointer.getBusinesseses(ipfsUrlHash); + } + @action - Future getBusinesses({Category category = Category.all}) async { + Future getBusinesses() async { fetchStatus = FetchStatus.loading; - await Future.delayed(const Duration(seconds: 1)); - final data = businessesMockData['businesses']; - final items = data!.map(Businesses.fromJson).toList(); - if (category != Category.all) items.removeWhere((element) => element.category != category); - items.sort((a, b) { - if (a.status == Status.highlight && b.status == Status.neuBeiLeu) return -1; - if (a.status == Status.neuBeiLeu && b.status == Status.highlight) return 1; + Log.d('getBusinesses: before update businesses = $businesses', _targetLogger); + + final accountBusinessTuples = await _bazaarGetBusinesses(); + + await _getBusinessesLogosAndUpdate(accountBusinessTuples); + + Log.d('getBusinesses: after update businesses = $businesses', _targetLogger); + + _update(); + } + + void _update() { + if (sortedBusinesses.isEmpty) { + fetchStatus = FetchStatus.noData; + } else { + fetchStatus = FetchStatus.success; + } + } + + void _sortByStatus() { + sortedBusinesses.sort((a, b) { + if (a.status == Status.highlight && b.status == Status.recently) return -1; + if (a.status == Status.recently && b.status == Status.highlight) return 1; if (a.status == null && b.status == Status.highlight) return 1; if (a.status == Status.highlight && b.status == null) return -1; return 0; }); - businesses = items; - fetchStatus = FetchStatus.success; + } + + Future _getBusinessesLogosAndUpdate(List accountBusinessTuples) async { + Log.d('_getBusinessesLogosAndUpdate: accountBusinessTuples = $accountBusinessTuples', _targetLogger); + + if (accountBusinessTuples.isNotEmpty) { + await Future.forEach(accountBusinessTuples, (element) async { + if (element.businessData != null && element.businessData!.url.isNotNullOrEmpty) { + Log.d( + '_getBusinessesLogosAndUpdate: accountBusinessTuple.businessData!.url! = ${element.businessData!.url!}', + _targetLogger, + ); + final response = await _getBusinesses(element.businessData!.url!); + + Log.d('_getBusinesses: response = $response', _targetLogger); + + response.fold( + (l) => error = l.failureType.name, + (r) { + r.controller = element.controller; + Log.d('_getBusinesses: right = ${r.toJson()}', _targetLogger); + businesses.add(r); + }, + ); + } + }); + } + + sortedBusinesses.addAll(businesses); + } + + @action + void filterBusinessesByCategory({required Category category}) { + if (category == Category.all) { + sortedBusinesses = []; + sortedBusinesses.addAll(businesses); + } else { + sortedBusinesses = []; + sortedBusinesses + ..addAll(businesses) + ..removeWhere((element) => element.category != category); + } + + _sortByStatus(); + _update(); + } + + ///TOOD(Azamat): Need to fix the method + // ignore: unused_element + Future _getBusinessesPhotos() async { + await Future.forEach(businesses, (element) async { + if (element.photos.isNotNullOrEmpty) { + Log.d('_getBusinessesPhotos: element.photos = ${element.photos}', _targetLogger); + final photosReponse = await webApi.encointer.getBusinessesPhotos(element.photos!); + + photosReponse.fold( + (l) => error = l.failureType.name, + (r) { + Log.d('_getBusinessesPhotos: right = $r', _targetLogger); + }, + ); + } + }); } } diff --git a/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.g.dart b/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.g.dart index c786cd68d..9b279cf8e 100644 --- a/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.g.dart +++ b/app/lib/page-encointer/new_bazaar/businesses/logic/businesses_store.g.dart @@ -9,21 +9,51 @@ part of 'businesses_store.dart'; // ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers mixin _$BusinessesStore on _BusinessesStoreBase, Store { + late final _$cidAtom = Atom(name: '_BusinessesStoreBase.cid', context: context); + + @override + CommunityIdentifier get cid { + _$cidAtom.reportRead(); + return super.cid; + } + + @override + set cid(CommunityIdentifier value) { + _$cidAtom.reportWrite(value, super.cid, () { + super.cid = value; + }); + } + late final _$businessesAtom = Atom(name: '_BusinessesStoreBase.businesses', context: context); @override - List? get businesses { + List get businesses { _$businessesAtom.reportRead(); return super.businesses; } @override - set businesses(List? value) { + set businesses(List value) { _$businessesAtom.reportWrite(value, super.businesses, () { super.businesses = value; }); } + late final _$sortedBusinessesAtom = Atom(name: '_BusinessesStoreBase.sortedBusinesses', context: context); + + @override + List get sortedBusinesses { + _$sortedBusinessesAtom.reportRead(); + return super.sortedBusinesses; + } + + @override + set sortedBusinesses(List value) { + _$sortedBusinessesAtom.reportWrite(value, super.sortedBusinesses, () { + super.sortedBusinesses = value; + }); + } + late final _$fetchStatusAtom = Atom(name: '_BusinessesStoreBase.fetchStatus', context: context); @override @@ -39,18 +69,49 @@ mixin _$BusinessesStore on _BusinessesStoreBase, Store { }); } + late final _$errorAtom = Atom(name: '_BusinessesStoreBase.error', context: context); + + @override + String? get error { + _$errorAtom.reportRead(); + return super.error; + } + + @override + set error(String? value) { + _$errorAtom.reportWrite(value, super.error, () { + super.error = value; + }); + } + late final _$getBusinessesAsyncAction = AsyncAction('_BusinessesStoreBase.getBusinesses', context: context); @override - Future getBusinesses({Category category = Category.all}) { - return _$getBusinessesAsyncAction.run(() => super.getBusinesses(category: category)); + Future getBusinesses() { + return _$getBusinessesAsyncAction.run(() => super.getBusinesses()); + } + + late final _$_BusinessesStoreBaseActionController = ActionController(name: '_BusinessesStoreBase', context: context); + + @override + void filterBusinessesByCategory({required Category category}) { + final _$actionInfo = + _$_BusinessesStoreBaseActionController.startAction(name: '_BusinessesStoreBase.filterBusinessesByCategory'); + try { + return super.filterBusinessesByCategory(category: category); + } finally { + _$_BusinessesStoreBaseActionController.endAction(_$actionInfo); + } } @override String toString() { return ''' +cid: ${cid}, businesses: ${businesses}, -fetchStatus: ${fetchStatus} +sortedBusinesses: ${sortedBusinesses}, +fetchStatus: ${fetchStatus}, +error: ${error} '''; } } diff --git a/app/lib/page-encointer/new_bazaar/businesses/view/businesses_view.dart b/app/lib/page-encointer/new_bazaar/businesses/view/businesses_view.dart index 7208b3c00..853b7460c 100644 --- a/app/lib/page-encointer/new_bazaar/businesses/view/businesses_view.dart +++ b/app/lib/page-encointer/new_bazaar/businesses/view/businesses_view.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:provider/provider.dart'; +import 'package:encointer_wallet/models/communities/community_identifier.dart'; +import 'package:encointer_wallet/store/app.dart'; import 'package:encointer_wallet/common/components/error/error_view.dart'; import 'package:encointer_wallet/common/components/loading/centered_activity_indicator.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/empty_businesses.dart'; @@ -11,7 +14,12 @@ import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/bu import 'package:encointer_wallet/utils/fetch_status.dart'; class BusinessesView extends StatelessWidget { - const BusinessesView({super.key}); + const BusinessesView({ + required this.appStore, + super.key, + }); + + final AppStore appStore; @override Widget build(BuildContext context) { @@ -19,17 +27,25 @@ class BusinessesView extends StatelessWidget { return Observer(builder: (_) { return switch (store.fetchStatus) { FetchStatus.loading => const CenteredActivityIndicator(), - FetchStatus.success => BusinessesList(businesses: store.businesses!), + FetchStatus.success => BusinessesList(businesses: store.sortedBusinesses, cid: store.cid, appStore: appStore), FetchStatus.error => const ErrorView(), + FetchStatus.noData => const EmptyBusiness(), }; }); } } class BusinessesList extends StatelessWidget { - const BusinessesList({super.key, required this.businesses}); + const BusinessesList({ + super.key, + required this.businesses, + required this.cid, + required this.appStore, + }); final List businesses; + final CommunityIdentifier cid; + final AppStore appStore; @override Widget build(BuildContext context) { @@ -39,7 +55,7 @@ class BusinessesList extends StatelessWidget { itemCount: businesses.length, itemBuilder: (BuildContext context, int index) { final business = businesses[index]; - return BusinessesCard(businesses: business); + return BusinessesCard(businesses: business, cid: cid, appStore: appStore); }, ); } diff --git a/app/lib/page-encointer/new_bazaar/businesses/widgets/businesses_card.dart b/app/lib/page-encointer/new_bazaar/businesses/widgets/businesses_card.dart index 7b513343d..33680b5a3 100644 --- a/app/lib/page-encointer/new_bazaar/businesses/widgets/businesses_card.dart +++ b/app/lib/page-encointer/new_bazaar/businesses/widgets/businesses_card.dart @@ -1,15 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/logic/business_utils.dart'; +import 'package:encointer_wallet/gen/assets.gen.dart'; +import 'package:encointer_wallet/utils/extensions/string/string_extensions.dart'; +import 'package:encointer_wallet/models/communities/community_identifier.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/logic/single_business_store.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/views/single_business_view.dart'; -import 'package:flutter/material.dart'; +import 'package:encointer_wallet/store/app.dart'; import 'package:encointer_wallet/models/bazaar/businesses.dart'; import 'package:encointer_wallet/theme/theme.dart'; -import 'package:provider/provider.dart'; class BusinessesCard extends StatelessWidget { - const BusinessesCard({super.key, required this.businesses}); + const BusinessesCard({ + super.key, + required this.businesses, + required this.cid, + required this.appStore, + }); final Businesses businesses; + final CommunityIdentifier cid; + final AppStore appStore; @override Widget build(BuildContext context) { @@ -19,8 +32,8 @@ class BusinessesCard extends StatelessWidget { context, MaterialPageRoute( builder: (context) => Provider( - create: (context) => SingleBusinessStore(businesses)..getSingleBusiness(), - child: const SingleBusinessView(), + create: (context) => SingleBusinessStore(businesses, cid)..getSingleBusiness(), + child: SingleBusinessView(appStore: appStore), ), ), ); @@ -33,19 +46,34 @@ class BusinessesCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - DecoratedBox( - decoration: BoxDecoration( - image: DecorationImage( - fit: BoxFit.cover, - image: NetworkImage(businesses.photo), + if (businesses.logo.isNotNullOrEmpty) + DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage(businesses.logo!), + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), ), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - bottomLeft: Radius.circular(20), + child: const SizedBox(height: double.infinity, width: 130), + ) + else + DecoratedBox( + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.cover, + image: AssetImage(Assets.images.assets.mosaicBackground.path), + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + bottomLeft: Radius.circular(20), + ), ), + child: const SizedBox(height: double.infinity, width: 130), ), - child: const SizedBox(height: double.infinity, width: 130), - ), Expanded( child: ListTile( contentPadding: const EdgeInsets.all(10), @@ -64,14 +92,14 @@ class BusinessesCard extends StatelessWidget { children: [ const SizedBox(height: 28), Text( - businesses.name, + BusinessUtils.utf8convert(businesses.name), style: context.labelLarge, overflow: TextOverflow.ellipsis, maxLines: 1, ), const SizedBox(height: 8), Text( - businesses.description, + BusinessUtils.utf8convert(businesses.description), style: context.bodyMedium, overflow: TextOverflow.ellipsis, maxLines: 2, diff --git a/app/lib/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart b/app/lib/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart index 4b575f85c..5e1222193 100644 --- a/app/lib/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart +++ b/app/lib/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart @@ -41,11 +41,11 @@ class _DropdownWidgetState extends State { ), ) .toList(), - onSelected: (category) async { + onSelected: (category) { if (category != null && selectedCategory != category) { selectedCategory = category; - await context.read().getBusinesses(category: selectedCategory); + context.read().filterBusinessesByCategory(category: selectedCategory); } }, inputDecorationTheme: InputDecorationTheme( @@ -55,17 +55,19 @@ class _DropdownWidgetState extends State { borderRadius: BorderRadius.circular(20), borderSide: BorderSide.none, ), - contentPadding: const EdgeInsets.only(left: 15), + contentPadding: const EdgeInsets.only(left: 18), constraints: const BoxConstraints(maxHeight: 40), ), textStyle: context.textTheme.bodyMedium, trailingIcon: const Icon( Icons.keyboard_arrow_down_outlined, color: AppColors.encointerGrey, + size: 18, ), selectedTrailingIcon: const Icon( Icons.keyboard_arrow_up_outlined, color: AppColors.encointerGrey, + size: 18, ), ); } @@ -83,7 +85,9 @@ enum Category { @JsonValue('food_beverage_store') foodAndBeverageStore('Food & Beverage Store'), @JsonValue('restaurants_bars') - restaurantsAndBars('Restaurants & Bars'); + restaurantsAndBars('Restaurants & Bars'), + @JsonValue('food') + food('Food'); const Category(this.name); final String name; diff --git a/app/lib/page-encointer/new_bazaar/businesses/widgets/empty_businesses.dart b/app/lib/page-encointer/new_bazaar/businesses/widgets/empty_businesses.dart index 03188f4b2..6f13358ad 100644 --- a/app/lib/page-encointer/new_bazaar/businesses/widgets/empty_businesses.dart +++ b/app/lib/page-encointer/new_bazaar/businesses/widgets/empty_businesses.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:encointer_wallet/l10n/l10.dart'; +import 'package:encointer_wallet/l10n/l10.dart'; import 'package:encointer_wallet/theme/theme.dart'; class EmptyBusiness extends StatelessWidget { diff --git a/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.dart b/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.dart index 47f2f1fe9..662537241 100644 --- a/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.dart +++ b/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.dart @@ -1,4 +1,14 @@ +import 'dart:developer'; + import 'package:encointer_wallet/models/bazaar/businesses.dart'; +import 'package:encointer_wallet/models/bazaar/ipfs_product.dart'; +import 'package:encointer_wallet/models/bazaar/item_offered.dart'; +import 'package:encointer_wallet/models/bazaar/offering_data.dart'; +import 'package:encointer_wallet/models/communities/community_identifier.dart'; +import 'package:encointer_wallet/service/log/log_service.dart'; +import 'package:encointer_wallet/service/substrate_api/api.dart'; +import 'package:encointer_wallet/utils/extensions/string/string_extensions.dart'; +import 'package:ew_http/ew_http.dart'; import 'package:mobx/mobx.dart'; import 'package:encointer_wallet/models/bazaar/single_business.dart'; @@ -6,16 +16,25 @@ import 'package:encointer_wallet/utils/fetch_status.dart'; part 'single_business_store.g.dart'; +const _targetLogger = 'SingleBusinessStore'; + // ignore: library_private_types_in_public_api class SingleBusinessStore = _SingleBusinessStoreBase with _$SingleBusinessStore; abstract class _SingleBusinessStoreBase with Store { - _SingleBusinessStoreBase(this.businesses, - {bool isLiked1 = false, bool isLikedPersonally1 = false, int countLikes1 = 0}) - : isLiked = isLiked1, + _SingleBusinessStoreBase( + this._businesses, + this._cid, { + bool isLiked1 = false, + bool isLikedPersonally1 = false, + int countLikes1 = 0, + }) : isLiked = isLiked1, isLikedPersonally = isLikedPersonally1, countLikes = countLikes1; - final Businesses businesses; + + late final Businesses _businesses; + + late final CommunityIdentifier _cid; @observable late bool isLiked; @@ -32,15 +51,86 @@ abstract class _SingleBusinessStoreBase with Store { @observable FetchStatus fetchStatus = FetchStatus.loading; + @observable + List ipfsProducts = []; + + @observable + String? error; + @action Future getSingleBusiness() async { fetchStatus = FetchStatus.loading; - await Future.delayed(const Duration(seconds: 1)); - final items = SingleBusiness.fromJson(singleBusinessMockData); - singleBusiness = items; + + final singleB = SingleBusiness( + name: _businesses.name, + description: _businesses.description, + category: _businesses.category.name, + address: _businesses.address, + zipcode: 'zipcode', + addressDescription: 'addressDescription', + status: _businesses.status?.name ?? '', + telephone: _businesses.telephone ?? '', + email: _businesses.email ?? '', + longitude: double.tryParse(_businesses.longitude) ?? 0, + latitude: double.tryParse(_businesses.latitude) ?? 0, + openingHours: _businesses.openingHours, + logo: _businesses.logo ?? '', + photo: _businesses.photo ?? '', + offer: 'offer', + offerName1: 'offerName1', + offerName2: 'offerName2', + moreInfo: 'moreInfo', + ); + + singleBusiness = singleB; + + final offerings = await _bazaarGetOfferingsForBusines(); + await _getIpfsProducts(offerings); + fetchStatus = FetchStatus.success; } + Future _getIpfsProducts(List offerings) async { + if (offerings.isNotEmpty) { + for (final offering in offerings) { + if (offering.url.isNotNullOrEmpty) { + final itemOffered = await _getItemOffered(offering.url!); + + await itemOffered.fold((l) => null, (item) async { + final ipfsProduct = await _getSingleBusinessProduct(item.itemOffered); + + ipfsProduct.fold( + (l) { + error = l.failureType.name; + }, + (r) { + r.price = item.price; + log('SingleBusinessStore _getIpfsProducts r: ${r.toJson()}'); + ipfsProducts.add(r); + fetchStatus = FetchStatus.success; + }, + ); + }); + } + } + } + } + + Future> _bazaarGetOfferingsForBusines() { + Log.d('_bazaarGetOfferingsForBusines: _cid = $_cid', _targetLogger); + return webApi.encointer.bazaarGetOfferingsForBusines(_cid, _businesses.controller); + } + + Future> _getItemOffered(String ipfsUrlHash) { + Log.d('_getItemOffered: ipfsUrlHash = $ipfsUrlHash', _targetLogger); + return webApi.encointer.getItemOffered(ipfsUrlHash); + } + + Future> _getSingleBusinessProduct(String ipfsUrlHash) { + Log.d('_getSingleBusinessProduct: ipfsUrlHash = $ipfsUrlHash', _targetLogger); + return webApi.encointer.getSingleBusinessProduct(ipfsUrlHash); + } + @action void toggleLikes() { isLiked = !isLiked; diff --git a/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.g.dart b/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.g.dart index 3e0ad0d7b..4dca331ca 100644 --- a/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.g.dart +++ b/app/lib/page-encointer/new_bazaar/single_business/logic/single_business_store.g.dart @@ -84,6 +84,36 @@ mixin _$SingleBusinessStore on _SingleBusinessStoreBase, Store { }); } + late final _$ipfsProductsAtom = Atom(name: '_SingleBusinessStoreBase.ipfsProducts', context: context); + + @override + List get ipfsProducts { + _$ipfsProductsAtom.reportRead(); + return super.ipfsProducts; + } + + @override + set ipfsProducts(List value) { + _$ipfsProductsAtom.reportWrite(value, super.ipfsProducts, () { + super.ipfsProducts = value; + }); + } + + late final _$errorAtom = Atom(name: '_SingleBusinessStoreBase.error', context: context); + + @override + String? get error { + _$errorAtom.reportRead(); + return super.error; + } + + @override + set error(String? value) { + _$errorAtom.reportWrite(value, super.error, () { + super.error = value; + }); + } + late final _$getSingleBusinessAsyncAction = AsyncAction('_SingleBusinessStoreBase.getSingleBusiness', context: context); @@ -124,7 +154,9 @@ isLiked: ${isLiked}, isLikedPersonally: ${isLikedPersonally}, countLikes: ${countLikes}, singleBusiness: ${singleBusiness}, -fetchStatus: ${fetchStatus} +fetchStatus: ${fetchStatus}, +ipfsProducts: ${ipfsProducts}, +error: ${error} '''; } } diff --git a/app/lib/page-encointer/new_bazaar/single_business/views/single_business_view.dart b/app/lib/page-encointer/new_bazaar/single_business/views/single_business_view.dart index 5db3d64b0..62709214f 100644 --- a/app/lib/page-encointer/new_bazaar/single_business/views/single_business_view.dart +++ b/app/lib/page-encointer/new_bazaar/single_business/views/single_business_view.dart @@ -1,3 +1,5 @@ +import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/logic/business_utils.dart'; +import 'package:encointer_wallet/store/app.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; @@ -10,7 +12,11 @@ import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/logic import 'package:encointer_wallet/utils/fetch_status.dart'; class SingleBusinessView extends StatelessWidget { - const SingleBusinessView({super.key}); + const SingleBusinessView({ + required this.appStore, + super.key, + }); + final AppStore appStore; @override Widget build(BuildContext context) { @@ -19,7 +25,7 @@ class SingleBusinessView extends StatelessWidget { appBar: AppBar( title: Observer(builder: (_) { return switch (store.fetchStatus) { - FetchStatus.success => Text(store.singleBusiness!.name.toUpperCase()), + FetchStatus.success => Text(BusinessUtils.utf8convert(store.singleBusiness!.name.toUpperCase())), _ => const SizedBox(), }; }), @@ -29,9 +35,11 @@ class SingleBusinessView extends StatelessWidget { case FetchStatus.loading: return const CenteredActivityIndicator(); case FetchStatus.success: - return SingleBusinessDetail(singleBusiness: store.singleBusiness!); + return SingleBusinessDetail(singleBusiness: store.singleBusiness!, appStore: appStore); case FetchStatus.error: return const ErrorView(); + case FetchStatus.noData: + return const SizedBox.shrink(); } }), ); diff --git a/app/lib/page-encointer/new_bazaar/single_business/widgets/business_detail_text_widget.dart b/app/lib/page-encointer/new_bazaar/single_business/widgets/business_detail_text_widget.dart index 52b4afd7b..6f1a12dc2 100644 --- a/app/lib/page-encointer/new_bazaar/single_business/widgets/business_detail_text_widget.dart +++ b/app/lib/page-encointer/new_bazaar/single_business/widgets/business_detail_text_widget.dart @@ -2,39 +2,59 @@ import 'package:flutter/material.dart'; import 'package:encointer_wallet/theme/custom/extension/theme_extension.dart'; -class BusinessDetailTextWidget extends StatelessWidget { - const BusinessDetailTextWidget({ - required this.text, - required this.text1, - required this.text2, +class BusinessOfferDetails extends StatelessWidget { + const BusinessOfferDetails({ + required this.title, + required this.description, + required this.price, + required this.openingHours, + required this.businessName, super.key, }); - final String text; - final String text1; - final String text2; + final String title; + final String description; + final String price; + final String openingHours; + final String businessName; @override Widget build(BuildContext context) { - return Align( - alignment: Alignment.topLeft, - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: '$text\n', - style: context.textTheme.titleLarge!.copyWith(color: context.colorScheme.primary, fontSize: 18), - ), - TextSpan( - text: '$text1\n', - style: context.textTheme.bodyMedium!.copyWith(height: 1.4), - ), - TextSpan( - text: '$text2\n', - style: context.textTheme.bodyMedium!.copyWith(height: 1.4), - ), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: '$title\n', + style: context.textTheme.bodyLarge!.copyWith( + height: 1.4, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: '$price\n', + style: context.textTheme.bodyMedium!.copyWith(height: 1.4), + ), + ], + ), ), - ), + Text( + description, + style: context.textTheme.bodyLarge!.copyWith( + height: 1.4, + fontWeight: FontWeight.w500, + ), + ), + Text( + openingHours, + style: context.textTheme.bodyMedium!.copyWith(height: 1.4), + ), + Text( + '@ $businessName', + style: context.textTheme.bodyMedium!.copyWith(height: 1.4), + ), + ], ); } } diff --git a/app/lib/page-encointer/new_bazaar/single_business/widgets/single_business_detail.dart b/app/lib/page-encointer/new_bazaar/single_business/widgets/single_business_detail.dart index 6dd7fc15c..7dcb29fb5 100644 --- a/app/lib/page-encointer/new_bazaar/single_business/widgets/single_business_detail.dart +++ b/app/lib/page-encointer/new_bazaar/single_business/widgets/single_business_detail.dart @@ -1,8 +1,11 @@ -import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/logic/single_business_store.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:provider/provider.dart'; +import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/logic/single_business_store.dart'; +import 'package:encointer_wallet/store/app.dart'; +import 'package:encointer_wallet/utils/extensions/string/string_extensions.dart'; +import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/logic/business_utils.dart'; import 'package:encointer_wallet/l10n/l10.dart'; import 'package:encointer_wallet/gen/assets.gen.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/widgets/business_detail_text_widget.dart'; @@ -16,10 +19,12 @@ import 'package:encointer_wallet/service/launch/app_launch.dart'; class SingleBusinessDetail extends StatelessWidget { const SingleBusinessDetail({ required this.singleBusiness, + required this.appStore, super.key, }); final SingleBusiness singleBusiness; + final AppStore appStore; @override Widget build(BuildContext context) { @@ -29,10 +34,11 @@ class SingleBusinessDetail extends StatelessWidget { child: Card( child: Column( children: [ - Image.network(singleBusiness.photo, width: double.infinity, fit: BoxFit.cover), + Image.network(singleBusiness.logo, width: double.infinity, fit: BoxFit.cover), Padding( padding: const EdgeInsets.fromLTRB(30, 20, 30, 60), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -41,10 +47,11 @@ class SingleBusinessDetail extends StatelessWidget { singleBusiness.category, style: context.bodySmall, ), - Text( - singleBusiness.status!, - style: context.bodySmall.copyWith(color: const Color(0xFF35B731)), - ) + if (singleBusiness.status.isNotNullOrEmpty) + Text( + singleBusiness.status!, + style: context.bodySmall.copyWith(color: const Color(0xFF35B731)), + ) ], ), const SizedBox(height: 8), @@ -88,30 +95,46 @@ class SingleBusinessDetail extends StatelessWidget { }), const SizedBox(height: 20), Text( - singleBusiness.description, + BusinessUtils.utf8convert(singleBusiness.description), style: context.bodyMedium.copyWith(height: 1.5), ), const SizedBox(height: 40), - BusinessDetailTextWidget( - text: singleBusiness.offer, - text1: singleBusiness.offerName1, - text2: singleBusiness.offerName2, - ), - BusinessDetailTextWidget( - text: l10n.openningHours, - text1: singleBusiness.openingHours1, - text2: singleBusiness.openingHours2, - ), + if (store.ipfsProducts.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.offersForCommunity(appStore.encointer.community?.symbol ?? 'LEU'), + style: + context.titleLarge.copyWith(color: context.colorScheme.primary, fontSize: 18), + ), + const SizedBox(height: 8), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: store.ipfsProducts.length, + itemBuilder: (context, index) { + final ipfsProduct = store.ipfsProducts[index]; + return BusinessOfferDetails( + title: BusinessUtils.utf8convert(ipfsProduct.name), + description: BusinessUtils.utf8convert(ipfsProduct.description), + price: '${appStore.encointer.community?.symbol} ${ipfsProduct.price ?? 0}', + openingHours: store.singleBusiness!.openingHours, + businessName: store.singleBusiness!.name, + ); + }, + ), + ], + ), const SizedBox(height: 20), BusinessDetailAddressWidget( text: l10n.address, - description: singleBusiness.addressDescription, - address: singleBusiness.address, + description: BusinessUtils.utf8convert(singleBusiness.addressDescription), + address: BusinessUtils.utf8convert(singleBusiness.address), zipCode: singleBusiness.zipcode, email: singleBusiness.email, phoneNum: singleBusiness.telephone, ), - const SizedBox(height: 20), MapButton( onPressed: () { final location = Location( @@ -122,11 +145,6 @@ class SingleBusinessDetail extends StatelessWidget { }, ), const SizedBox(height: 40), - BusinessDetailTextWidget( - text: l10n.moreInfo, - text1: singleBusiness.moreInfo, - text2: '', - ), ], ), ), diff --git a/app/lib/page-encointer/new_bazaar/widgets/businesses_card.dart b/app/lib/page-encointer/new_bazaar/widgets/businesses_card.dart index 411c580a2..2ab016b87 100644 --- a/app/lib/page-encointer/new_bazaar/widgets/businesses_card.dart +++ b/app/lib/page-encointer/new_bazaar/widgets/businesses_card.dart @@ -22,7 +22,7 @@ class BusinessesCard extends StatelessWidget { decoration: BoxDecoration( image: DecorationImage( fit: BoxFit.cover, - image: NetworkImage(businesses.photo), + image: NetworkImage(businesses.photo!), ), borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), diff --git a/app/lib/page/assets/account_or_community/switch_account_or_community.dart b/app/lib/page/assets/account_or_community/switch_account_or_community.dart index c81579316..ac554c139 100644 --- a/app/lib/page/assets/account_or_community/switch_account_or_community.dart +++ b/app/lib/page/assets/account_or_community/switch_account_or_community.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; - import 'package:encointer_wallet/page/assets/account_or_community/account_or_community_data.dart'; import 'package:encointer_wallet/page/assets/account_or_community/account_or_community_item_horizontal.dart'; import 'package:encointer_wallet/theme/theme.dart'; @@ -8,14 +7,14 @@ class SwitchAccountOrCommunity extends StatefulWidget { const SwitchAccountOrCommunity({ super.key, this.rowTitle, - this.data, + this.accountOrCommunityData, this.onTap, required this.onAddIconPressed, required this.addIconButtonKey, }); final String? rowTitle; - final List? data; + final List? accountOrCommunityData; final void Function(int index)? onTap; final VoidCallback onAddIconPressed; final Key addIconButtonKey; @@ -66,9 +65,9 @@ class _SwitchAccountOrCommunityState extends State { scrollDirection: Axis.horizontal, shrinkWrap: true, itemExtent: itemExtent, - itemCount: widget.data != null ? widget.data!.length : 0, + itemCount: widget.accountOrCommunityData != null ? widget.accountOrCommunityData!.length : 0, itemBuilder: (context, index) => AccountOrCommunityItemHorizontal( - itemData: widget.data![index], + itemData: widget.accountOrCommunityData![index], index: index, onTap: widget.onTap, ), diff --git a/app/lib/page/assets/announcement/logic/announcement_store.dart b/app/lib/page/assets/announcement/logic/announcement_store.dart index a4ccf81e3..9a5853b3a 100644 --- a/app/lib/page/assets/announcement/logic/announcement_store.dart +++ b/app/lib/page/assets/announcement/logic/announcement_store.dart @@ -55,9 +55,12 @@ abstract class _AnnouncementStoreBase with Store { fromJson: Announcement.fromJson, ); - communityAnnouncementsResponse.fold((l) { + await communityAnnouncementsResponse.fold((l) { error = l.error.toString(); Log.e('announcement_view', '${l.error}'); + + // fallback to English if the app language's one is not available + if (l.statusCode == 404) return getCommunityAnnouncements(cid, devMode: devMode, langCode: 'en'); fetchStatus = FetchStatus.error; }, (r) { announcementsCommunnity = r; @@ -75,9 +78,11 @@ abstract class _AnnouncementStoreBase with Store { fromJson: Announcement.fromJson, ); - globalAnnouncementsResponse.fold((l) { + await globalAnnouncementsResponse.fold((l) { error = l.error.toString(); Log.e('announcement_view', '${l.error}'); + // fallback to English if the app language's one is not available + if (l.statusCode == 404) return getGlobalAnnouncements(devMode: devMode, langCode: 'en'); fetchStatus = FetchStatus.error; }, (r) { announcementsGlobal = r; diff --git a/app/lib/page/assets/announcement/view/announcement_view.dart b/app/lib/page/assets/announcement/view/announcement_view.dart index fb452d89b..86c3bed11 100644 --- a/app/lib/page/assets/announcement/view/announcement_view.dart +++ b/app/lib/page/assets/announcement/view/announcement_view.dart @@ -49,6 +49,7 @@ class _AnnouncementViewState extends State { FetchStatus.loading => const Center(child: CupertinoActivityIndicator()), FetchStatus.success => AnnouncementList(announcements: _announcementStore.announcements), FetchStatus.error => const SizedBox.shrink(), + FetchStatus.noData => const SizedBox.shrink(), }; }); } diff --git a/app/lib/page/assets/announcement/widgets/announcement_card.dart b/app/lib/page/assets/announcement/widgets/announcement_card.dart index 5fee38a75..918190ca3 100644 --- a/app/lib/page/assets/announcement/widgets/announcement_card.dart +++ b/app/lib/page/assets/announcement/widgets/announcement_card.dart @@ -7,6 +7,7 @@ import 'package:share_plus/share_plus.dart'; import 'package:encointer_wallet/page/assets/announcement/widgets/publisher_and_community_icon.dart'; import 'package:encointer_wallet/page/assets/announcement/logic/announcement_card_store.dart'; import 'package:encointer_wallet/models/announcement/announcement.dart'; +import 'package:encointer_wallet/config/consts.dart'; import 'package:encointer_wallet/common/components/logo/community_icon.dart'; import 'package:encointer_wallet/gen/assets.gen.dart'; import 'package:encointer_wallet/theme/theme.dart'; @@ -74,7 +75,7 @@ class AnnouncementCard extends StatelessWidget { }), IconButton( icon: const Icon(Icons.share, size: 20, color: AppColors.encointerGrey), - onPressed: () => Share.share(announcement.content), + onPressed: () => Share.share('${announcement.title}\n${announcement.content}\n${encointerLink}home'), ) ], ) diff --git a/app/lib/page/assets/index.dart b/app/lib/page/assets/index.dart index 6383b2077..e239700fa 100644 --- a/app/lib/page/assets/index.dart +++ b/app/lib/page/assets/index.dart @@ -58,94 +58,63 @@ class _AssetsViewState extends State { static const double panelHeight = 396; static const double fractionOfScreenHeight = .7; static const double avatarSize = 70; - - PanelController? panelController; - - PausableTimer? balanceWatchdog; - + late PanelController _panelController; + late PausableTimer _balanceWatchdog; + late AppSettings _appSettingsStore; late double _panelHeightOpen; final double _panelHeightClosed = 0; late AppLocalizations l10n; @override void initState() { - super.initState(); - - // if network connected failed, reconnect - if (!widget.store.settings.loading && widget.store.settings.networkName == null) { - widget.store.settings.setNetworkLoading(true); - webApi.connectNodeAll(); - } + _connectNodeAll(); + _panelController = PanelController(); + _postFrameCallbacks(); - panelController ??= PanelController(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (context.read().encointer.community?.communityIcon == null) { - context.read().encointer.community?.getCommunityIcon(); - } - }); + super.initState(); } @override void didChangeDependencies() { + _appSettingsStore = context.read(); + _startBalanceWatchdog(); l10n = context.l10n; + // Should typically not be higher than panelHeight, but on really small devices + // it should not exceed fractionOfScreenHeight x the screen height. + _panelHeightOpen = min( + MediaQuery.of(context).size.height * fractionOfScreenHeight, + panelHeight, + ); super.didChangeDependencies(); } @override void dispose() { - balanceWatchdog!.cancel(); + _balanceWatchdog.cancel(); super.dispose(); } - Future _refreshEncointerState() async { - // getCurrentPhase is the root of all state updates. - await webApi.encointer.getCurrentPhase(); - } - @override Widget build(BuildContext context) { - final appSettingsStore = context.watch(); - - // Should typically not be higher than panelHeight, but on really small devices - // it should not exceed fractionOfScreenHeight x the screen height. - _panelHeightOpen = min( - MediaQuery.of(context).size.height * fractionOfScreenHeight, - panelHeight, - ); - - final allAccounts = []; - - balanceWatchdog = PausableTimer( - const Duration(seconds: 12), - () { - Log.d('[balanceWatchdog] triggered', 'Assets'); - - _refreshBalanceAndNotify(); - balanceWatchdog! - ..reset() - ..start(); - }, - )..start(); - return FocusDetector( onFocusLost: () { Log.d('[home:FocusDetector] Focus Lost.'); - balanceWatchdog!.pause(); + _balanceWatchdog.pause(); }, onFocusGained: () { Log.d('[home:FocusDetector] Focus Gained.'); if (!widget.store.settings.loading) { _refreshBalanceAndNotify(); } - balanceWatchdog!.reset(); - balanceWatchdog!.start(); + _balanceWatchdog + ..reset() + ..start(); }, child: Scaffold( appBar: _appBar(), body: RepositoryProvider.of(context).isIntegrationTest - ? _slidingUpPanel(_appBar(), appSettingsStore, allAccounts) - : _upgradeAlert(_appBar(), appSettingsStore, allAccounts), + ? _slidingUpPanel(_appBar()) + : _upgradeAlert(_appBar()), ), ); } @@ -159,8 +128,6 @@ class _AssetsViewState extends State { UpgradeAlert _upgradeAlert( AppBar appBar, - AppSettings appSettingsStore, - List allAccounts, ) { return UpgradeAlert( upgrader: Upgrader( @@ -169,14 +136,12 @@ class _AssetsViewState extends State { shouldPopScope: () => true, canDismissDialog: true, ), - child: _slidingUpPanel(appBar, appSettingsStore, allAccounts), + child: _slidingUpPanel(appBar), ); } SlidingUpPanel _slidingUpPanel( AppBar appBar, - AppSettings appSettingsStore, - List allAccounts, ) { return SlidingUpPanel( maxHeight: _panelHeightOpen, @@ -184,7 +149,7 @@ class _AssetsViewState extends State { parallaxEnabled: true, parallaxOffset: .5, backdropEnabled: true, - controller: panelController, + controller: _panelController, // The padding is a hack for #559, which needs https://github.com/akshathjain/sliding_up_panel/pull/303 body: Padding( padding: @@ -204,8 +169,8 @@ class _AssetsViewState extends State { key: const Key('panel-controller'), child: CombinedCommunityAndAccountAvatar(widget.store), onTap: () { - if (panelController != null && panelController!.isAttached) { - panelController!.open(); + if (_panelController.isAttached) { + _panelController.open(); } }, ), @@ -239,7 +204,7 @@ class _AssetsViewState extends State { ); }, ), - if (appSettingsStore.developerMode) + if (_appSettingsStore.developerMode) ElevatedButton( onPressed: widget.store.dataUpdate.setInvalidated, child: const Text('Invalidate data to trigger state update'), @@ -304,7 +269,7 @@ class _AssetsViewState extends State { ), ); } else { - return appSettingsStore.developerMode + return _appSettingsStore.developerMode ? ElevatedButton( onPressed: null, child: Text(l10n.issuanceClaimed), @@ -341,7 +306,7 @@ class _AssetsViewState extends State { Observer(builder: (_) { return SwitchAccountOrCommunity( rowTitle: l10n.switchCommunity, - data: _allCommunities(), + accountOrCommunityData: _allCommunities(), onTap: (int index) async { final store = context.read(); final communityStores = store.encointer.communityStores?.values.toList() ?? []; @@ -360,7 +325,7 @@ class _AssetsViewState extends State { Observer(builder: (BuildContext context) { return SwitchAccountOrCommunity( rowTitle: l10n.switchAccount, - data: initAllAccounts(), + accountOrCommunityData: initAllAccounts(), onTap: (int index) { setState(() { switchAccount(widget.store.account.accountListAll[index]); @@ -448,6 +413,22 @@ class _AssetsViewState extends State { } } + void _connectNodeAll() { + // if network connected failed, reconnect + if (!widget.store.settings.loading && widget.store.settings.networkName == null) { + widget.store.settings.setNetworkLoading(true); + webApi.connectNodeAll(); + } + } + + void _postFrameCallbacks() { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (context.read().encointer.community?.communityIcon == null) { + context.read().encointer.community?.getCommunityIcon(); + } + }); + } + void _refreshBalanceAndNotify() { webApi.encointer.getAllBalances(widget.store.account.currentAddress).then((balances) { Log.d('[home:refreshBalanceAndNotify] get all balances', 'Assets'); @@ -500,6 +481,25 @@ class _AssetsViewState extends State { Log.e('[home:refreshBalanceAndNotify] WARNING: could not update balance: $e', 'Assets', s); }); } + + void _startBalanceWatchdog() { + _balanceWatchdog = PausableTimer( + const Duration(seconds: 12), + () { + Log.d('[balanceWatchdog] triggered', 'Assets'); + + _refreshBalanceAndNotify(); + _balanceWatchdog + ..reset() + ..start(); + }, + )..start(); + } + + Future _refreshEncointerState() async { + // getCurrentPhase is the root of all state updates. + await webApi.encointer.getCurrentPhase(); + } } class ActionButton extends StatelessWidget { diff --git a/app/lib/page/assets/transfer/payment_confirmation_page/index.dart b/app/lib/page/assets/transfer/payment_confirmation_page/index.dart index afbda67ea..608b63156 100644 --- a/app/lib/page/assets/transfer/payment_confirmation_page/index.dart +++ b/app/lib/page/assets/transfer/payment_confirmation_page/index.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:animated_check/animated_check.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:iconsax/iconsax.dart'; @@ -11,6 +10,7 @@ import 'package:encointer_wallet/common/components/gradient_elements.dart'; import 'package:encointer_wallet/theme/theme.dart'; import 'package:encointer_wallet/config.dart'; import 'package:encointer_wallet/utils/repository_provider.dart'; +import 'package:encointer_wallet/common/components/animation/animated_check.dart'; import 'package:encointer_wallet/models/communities/community_identifier.dart'; import 'package:encointer_wallet/page/assets/transfer/payment_confirmation_page/components/payment_overview.dart'; import 'package:encointer_wallet/page/assets/transfer/payment_confirmation_page/components/transfer_state.dart'; @@ -192,11 +192,7 @@ class _PaymentConfirmationPageState extends State with return DecoratedBox( decoration: const BoxDecoration(shape: BoxShape.circle, color: Colors.green), - child: AnimatedCheck( - progress: _animation!, - size: 100, - color: Colors.white, - ), + child: AnimatedCheck(progress: _animation!, size: 100, color: Colors.white), ); } case TransferState.failed: diff --git a/app/lib/page/network_select_page.dart b/app/lib/page/network_select_page.dart index 7361c3717..dc5cb55f3 100644 --- a/app/lib/page/network_select_page.dart +++ b/app/lib/page/network_select_page.dart @@ -64,11 +64,11 @@ class _NetworkSelectPageState extends State { } } - Future _onSelect(AccountData i, String? address) async { + Future _onSelect(AccountData accountData, String? address) async { final isCurrentNetwork = _selectedNetwork.info == context.read().settings.endpoint.info; if (address != context.read().account.currentAddress || !isCurrentNetwork) { /// set current account - await context.read().setCurrentAccount(i.pubKey); + await context.read().setCurrentAccount(accountData.pubKey); if (isCurrentNetwork) { await context.read().loadAccountCache(); @@ -99,8 +99,8 @@ class _NetworkSelectPageState extends State { /// first item is current account final accounts = [appStore.account.currentAccount, ...appStore.account.optionalAccounts]; - res.addAll(accounts.map((i) { - final address = Fmt.ss58Encode(i.pubKey, prefix: appStore.settings.endpoint.ss58 ?? 42); + res.addAll(accounts.map((accountData) { + final address = Fmt.ss58Encode(accountData.pubKey, prefix: appStore.settings.endpoint.ss58 ?? 42); return Card( shape: RoundedRectangleBorder( @@ -113,10 +113,10 @@ class _NetworkSelectPageState extends State { ), margin: const EdgeInsets.only(bottom: 16), child: ListTile( - leading: AddressIcon(address, i.pubKey, size: 55), - title: Text(Fmt.accountName(context, i)), + leading: AddressIcon(address, accountData.pubKey, size: 55), + title: Text(Fmt.accountName(context, accountData)), subtitle: Text(Fmt.address(address)!, maxLines: 2), - onTap: _networkChanging ? null : () => _onSelect(i, address), + onTap: _networkChanging ? null : () => _onSelect(accountData, address), ), ); }).toList()); diff --git a/app/lib/presentation/home/views/home_page.dart b/app/lib/presentation/home/views/home_page.dart index df075811d..f81bbe33e 100644 --- a/app/lib/presentation/home/views/home_page.dart +++ b/app/lib/presentation/home/views/home_page.dart @@ -56,7 +56,10 @@ class _EncointerHomePageState extends State { controller: _pageController, children: [ AssetsView(_store.appStore), - if (context.select((store) => _store.appStore.settings.enableBazaar)) const BazaarMain(), + if (context.select((store) => _store.appStore.settings.enableBazaar)) + BazaarMain( + args: BazaarMainArgs(cid: _store.appStore.encointer.community!.cid, appStore: _store.appStore), + ), /// empty widget here because when qr code is clicked, we navigate to [ScanPage] const SizedBox(), diff --git a/app/lib/router/app_router.dart b/app/lib/router/app_router.dart index a3cbbc37c..fce789b9a 100644 --- a/app/lib/router/app_router.dart +++ b/app/lib/router/app_router.dart @@ -177,7 +177,7 @@ class AppRoute { ); case BazaarPage.route: return CupertinoPageRoute( - builder: (_) => const BazaarMain(), + builder: (_) => BazaarMain(args: arguments! as BazaarMainArgs), settings: settings, ); case LangPage.route: diff --git a/app/lib/service/substrate_api/encointer/encointer_api.dart b/app/lib/service/substrate_api/encointer/encointer_api.dart index d64a518b1..75465bfa5 100644 --- a/app/lib/service/substrate_api/encointer/encointer_api.dart +++ b/app/lib/service/substrate_api/encointer/encointer_api.dart @@ -1,5 +1,8 @@ import 'dart:convert'; +import 'package:encointer_wallet/models/bazaar/businesses.dart'; +import 'package:encointer_wallet/models/bazaar/ipfs_product.dart'; +import 'package:encointer_wallet/models/bazaar/item_offered.dart'; import 'package:ew_http/ew_http.dart'; import 'package:encointer_wallet/config/consts.dart'; @@ -240,7 +243,7 @@ class EncointerApi { (list) => list.map((cn) => CidName.fromJson(cn as Map)).toList(), ); - Log.d('api: CidNames: $cn', 'EncointerApi'); + Log.d('api: CidNames: ${cn.length} and $cn ', 'EncointerApi'); store.encointer.setCommunities(cn); } @@ -521,6 +524,23 @@ class EncointerApi { return allMockBusinesses; } + Future> bazaarGetBusinesses(CommunityIdentifier cid) async { + return _dartApi.bazaarGetBusinesses(cid); + } + + Future> getBusinesseses(String ipfsUrlHash) async { + final url = '$infuraIpfsUrl/$ipfsUrlHash'; + return ewHttp.getType(url, fromJson: Businesses.fromJson); + } + + ///TODO(Azamat): method not working, fix it + Future, EwHttpException>> getBusinessesPhotos(String ipfsUrlHash) async { + final url = '$infuraIpfsUrl/$ipfsUrlHash'; + final response = ewHttp.get>(url); + + return response; + } + /// Get all the registered offerings for the current `chosenCid` Future> getOfferings() async { // Todo: @armin you'd probably extend the encointer store and also set the store here. @@ -532,4 +552,18 @@ class EncointerApi { // Todo: @armin you'd probably extend the encointer store and also set the store here. return business1MockOfferings; } + + Future> bazaarGetOfferingsForBusines(CommunityIdentifier cid, String? controller) async { + return _dartApi.bazaarGetOfferingsForBusines(cid, controller); + } + + Future> getItemOffered(String ipfsUrlHash) async { + final url = '$infuraIpfsUrl/$ipfsUrlHash'; + return ewHttp.getType(url, fromJson: ItemOffered.fromJson); + } + + Future> getSingleBusinessProduct(String ipfsUrlHash) async { + final url = '$infuraIpfsUrl/$ipfsUrlHash'; + return ewHttp.getType(url, fromJson: IpfsProduct.fromJson); + } } diff --git a/app/lib/service/substrate_api/encointer/encointer_dart_api.dart b/app/lib/service/substrate_api/encointer/encointer_dart_api.dart index d47d0a916..7d1152c5f 100644 --- a/app/lib/service/substrate_api/encointer/encointer_dart_api.dart +++ b/app/lib/service/substrate_api/encointer/encointer_dart_api.dart @@ -1,9 +1,15 @@ +import 'dart:developer'; + +import 'package:encointer_wallet/models/bazaar/account_business_tuple.dart'; +import 'package:encointer_wallet/models/bazaar/offering_data.dart'; import 'package:encointer_wallet/models/communities/community_identifier.dart'; import 'package:encointer_wallet/models/encointer_balance_data/balance_entry.dart'; import 'package:encointer_wallet/models/index.dart'; import 'package:encointer_wallet/service/log/log_service.dart'; import 'package:encointer_wallet/service/substrate_api/core/dart_api.dart'; +const _targetLogger = 'EncointerDartApi'; + class EncointerDartApi { const EncointerDartApi(this._dartApi); @@ -22,11 +28,20 @@ class EncointerDartApi { ); } - /// Future> pendingExtrinsics() { return _dartApi.rpc>('author_pendingExtrinsics', []).then(List.from); } + Future> bazaarGetBusinesses(CommunityIdentifier cid) async { + final response = await _dartApi.rpc>('encointer_bazaarGetBusinesses', [cid.toJson()]); + + if (response.isEmpty) { + return []; + } + + return response.map((e) => AccountBusinessTuple.fromJson(e as Map)).toList(); + } + Future> getAllBalances(String account) { return _dartApi.rpc>('encointer_getAllBalances', [account]).then((data) { return { @@ -36,4 +51,22 @@ class EncointerDartApi { }; }); } + + Future> bazaarGetOfferingsForBusines(CommunityIdentifier cid, String? controller) async { + Log.d('bazaarGetOfferingsForBusines: cid = $cid, controller = $controller', _targetLogger); + final response = await _dartApi.rpc>('encointer_bazaarGetOfferingsForBusiness', [ + { + 'communityIdentifier': cid.toJson(), + 'controller': controller, + } + ]); + + log('$_targetLogger.bazaarGetOfferingsForBusines ${response.runtimeType} and $response'); + + if (response.isEmpty) { + return []; + } + + return response.map((e) => OfferingData.fromJson(e as Map)).toList(); + } } diff --git a/app/lib/store/encointer/sub_stores/bazaar_store/bazaar_store.dart b/app/lib/store/encointer/sub_stores/bazaar_store/bazaar_store.dart index c3fdf3ef1..efe7e70cf 100644 --- a/app/lib/store/encointer/sub_stores/bazaar_store/bazaar_store.dart +++ b/app/lib/store/encointer/sub_stores/bazaar_store/bazaar_store.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:encointer_wallet/models/bazaar/account_business_tuple.dart'; import 'package:encointer_wallet/models/communities/community_identifier.dart'; +import 'package:encointer_wallet/service/substrate_api/api.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:mobx/mobx.dart'; @@ -49,6 +51,12 @@ abstract class _BazaarStore with Store { _cacheFn = cacheFn as Future Function()?; } + Future bazaarGetBusinesses(CommunityIdentifier cid) async { + final bazaars = await webApi.encointer.bazaarGetBusinesses(cid); + log('bazaarGetBusinesses bazaars: $bazaars'); + setBusinessRegistry(bazaars); + } + Future writeToCache() { if (_cacheFn != null) { return _cacheFn!(); diff --git a/app/lib/utils/extensions/extensions.dart b/app/lib/utils/extensions/extensions.dart index d9e188bde..c7762e2de 100644 --- a/app/lib/utils/extensions/extensions.dart +++ b/app/lib/utils/extensions/extensions.dart @@ -1 +1,2 @@ export 'layout/layout_extensions.dart'; +export 'iterables/iterables_extensions.dart'; diff --git a/app/lib/utils/extensions/iterables/iterables_extensions.dart b/app/lib/utils/extensions/iterables/iterables_extensions.dart new file mode 100644 index 000000000..bd92ad1f5 --- /dev/null +++ b/app/lib/utils/extensions/iterables/iterables_extensions.dart @@ -0,0 +1,3 @@ +extension IterableModifier on Iterable { + E? firstWhereOrNull(bool Function(E) test) => cast().firstWhere((v) => v != null && test(v), orElse: () => null); +} diff --git a/app/lib/utils/fetch_status.dart b/app/lib/utils/fetch_status.dart index 3d0b405d5..ca597a28b 100644 --- a/app/lib/utils/fetch_status.dart +++ b/app/lib/utils/fetch_status.dart @@ -1 +1 @@ -enum FetchStatus { loading, success, error } +enum FetchStatus { loading, success, error, noData } diff --git a/app/pubspec.lock b/app/pubspec.lock index 03d25c330..0415d00ee 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -33,14 +33,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" - animated_check: - dependency: "direct main" - description: - name: animated_check - sha256: "1da103a44b6aeacbecec799343b7170e627f4af465ca642b430c1d94922868aa" - url: "https://pub.dev" - source: hosted - version: "1.0.5" animated_stack_widget: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 85fdf4694..fa9c81877 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -42,7 +42,6 @@ dependencies: package_info_plus: ^3.1.2 quiver: ^3.2.1 image_picker: ^0.8.8 - animated_check: ^1.0.5 # Todo: #1269: replace with alternative package share_plus: ^6.3.4 flutter_timezone: ^1.0.6 diff --git a/app/test/mock/api/mock_encointer_api.dart b/app/test/mock/api/mock_encointer_api.dart index 8db265254..e24144e3e 100644 --- a/app/test/mock/api/mock_encointer_api.dart +++ b/app/test/mock/api/mock_encointer_api.dart @@ -1,10 +1,16 @@ +import 'package:encointer_wallet/mocks/mock_bazaar_data.dart'; import 'package:encointer_wallet/models/bazaar/account_business_tuple.dart'; +import 'package:encointer_wallet/models/bazaar/businesses.dart'; +import 'package:encointer_wallet/models/bazaar/ipfs_product.dart'; +import 'package:encointer_wallet/models/bazaar/item_offered.dart'; +import 'package:encointer_wallet/models/bazaar/offering_data.dart'; import 'package:encointer_wallet/models/ceremonies/ceremonies.dart'; import 'package:encointer_wallet/models/communities/cid_name.dart'; import 'package:encointer_wallet/models/communities/community_identifier.dart'; import 'package:encointer_wallet/models/encointer_balance_data/balance_entry.dart'; import 'package:encointer_wallet/service/log/log_service.dart'; import 'package:encointer_wallet/service/substrate_api/encointer/encointer_api.dart'; +import 'package:ew_http/ew_http.dart'; import '../data/mock_encointer_data.dart'; import 'mock_js_api.dart'; @@ -134,4 +140,32 @@ class MockEncointerApi extends EncointerApi { Future getNumberOfNewbieTicketsForReputable() { return Future.value(0); } + + @override + Future> bazaarGetBusinesses(CommunityIdentifier cid) async { + return Future.value(allMockBusinesses); + } + + @override + Future> getBusinesseses(String ipfsUrlHash) async { + Either getRight() => Right(Businesses.fromJson(mockBusinessData)); + return Future.value(getRight()); + } + + @override + Future> bazaarGetOfferingsForBusines(CommunityIdentifier cid, String? controller) async { + return Future.value(offeringDataMockList); + } + + @override + Future> getItemOffered(String ipfsUrlHash) async { + Either getRight() => Right(ItemOffered.fromJson(itemOfferedMock)); + return Future.value(getRight()); + } + + @override + Future> getSingleBusinessProduct(String ipfsUrlHash) async { + Either getRight() => Right(IpfsProduct.fromJson(ipfsProductMock)); + return Future.value(getRight()); + } } diff --git a/app/test/mock/data/mock_encointer_data.dart b/app/test/mock/data/mock_encointer_data.dart index 3f3064f9b..3fc61da09 100644 --- a/app/test/mock/data/mock_encointer_data.dart +++ b/app/test/mock/data/mock_encointer_data.dart @@ -1,3 +1,4 @@ +import 'package:encointer_wallet/models/bazaar/offering_data.dart'; import 'package:encointer_wallet/models/ceremonies/ceremonies.dart'; import 'package:encointer_wallet/models/communities/cid_name.dart'; import 'package:encointer_wallet/models/communities/community_identifier.dart'; @@ -68,3 +69,32 @@ EndpointData unitTestEndpoint = EndpointData.fromJson({ 'overrideConfig': Map.of({}), 'ipfsGateway': 'Unit-Test network must no connect to ipfs' }); + +final List offeringDataMockList = [OfferingData(url: 'url')]; + +const itemOfferedMock = { + 'itemOffered': 'QmZ1f6v39DZXdmhLgaGD2i2XY8sucNaMGKJuoSHduqHp15', + 'price': '0', +}; + +const ipfsProductMock = { + 'name': 'Bier', + 'description': 'Kühles Bier', + 'category': 'food', + 'image': 'Qmeh8yNeDn7WjoMLuRZYAtiE5D5tafTqBaB1RPwyU1pyKK', + 'itemCondition': 'new' +}; + +final businessesMock = { + 'name': 'Kueche Edison', + 'description': 'bei uns gibt es köstlichen Kaffe', + 'category': 'food', + 'address': 'Technoparkstrasse 1, 8005 Zürich', + 'telephone': null, + 'email': null, + 'longitude': '8.515962660312653', + 'latitude': '47.390349148891545', + 'openingHours': 'Mon-Fri 8h-18h', + 'logo': 'QmUH7W2eAWTfHRYYV1YitZaz54sTjEwv6udjZjh7Tg47Xv', + 'photos': '' +}; diff --git a/app/test/mock/data/mock_single_business_data.dart b/app/test/mock/data/mock_single_business_data.dart new file mode 100644 index 000000000..b518b9a25 --- /dev/null +++ b/app/test/mock/data/mock_single_business_data.dart @@ -0,0 +1,24 @@ +const mockSingleBusiness = { + 'name': 'Hatha Lisa', + 'description': + 'Nutze deine Leu, um deinem Körper und Geist etwas Gutes zu tun. Besuche eine Yogastunde im Kreis 4 oder Kreis 5 mit Lisa Stähli, einer Hatha-Yoga-Lehrerin mit über 4 Jahren Unterrichtserfahrung. Die Klassen sind für alle Niveaus geeignet, werden auf Englisch unterrichtet und bieten sowohl eine Herausforderung als auch die Möglichkeit, sein Gleichgewicht zu finden.\n\nErfahre mehr oder kontaktiere uns:\nhttps://hathalisa.com/', + 'category': 'Body & Soul', + 'addressDescription': 'Yoga Studio Zürich', + 'address': 'Zwinglistrasse, 8', + 'zipcode': '8004, Zürich', + 'telephone': '+41 123 456 789', + 'email': 'info@hathalisa.com', + 'longitude': 8.515962660312653, + 'latitude': 47.390349148891545, + 'openingHours': 'Tuesdays 07:30-08:30', + 'logo': 'QmUH7W2eAWTfHRYYV1YitZaz54sTjEwv6udjZjh7Tg47Xv', + 'photo': 'https://github.com/SourbaevaJanaraJ/lock_screen/blob/master/assets/hatha_lisa_single_b.png?raw=true', + 'offer': 'Offer for Leu', + 'offerName1': 'Single course LEU 25', + 'offerName2': '10-course subscription LEU 200', + 'moreInfo': 'With Leu since 01 January 2023', + 'status': 'Neu bei Leu', + 'isLiked': false, + 'isLikedPersonally': false, + 'countLikes': 0 +}; diff --git a/app/test/mock/mock.dart b/app/test/mock/mock.dart index b6b91a0ca..220bc07a9 100644 --- a/app/test/mock/mock.dart +++ b/app/test/mock/mock.dart @@ -7,6 +7,7 @@ export 'api/mock_encointer_api.dart'; export 'api/mock_substrate_dart_api.dart'; export 'data/mock_account_data.dart'; export 'data/mock_encointer_data.dart'; +export 'data/mock_single_business_data.dart'; export 'fixtures/fixture_reader.dart'; export 'storage/mock_local_storage.dart'; export 'storage/flutter_secure_storage.dart'; diff --git a/app/test/models/single_business/single_business_test.dart b/app/test/models/single_business/single_business_test.dart new file mode 100644 index 000000000..b06bdfddf --- /dev/null +++ b/app/test/models/single_business/single_business_test.dart @@ -0,0 +1,18 @@ +import 'package:encointer_wallet/models/bazaar/single_business.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../mock/data/mock_single_business_data.dart'; + +void main() { + group('SingleBusiness Model', () { + test('fromJson() should return a SingleBusiness object', () { + final singleBusiness = SingleBusiness.fromJson(mockSingleBusiness); + expect(singleBusiness, isA()); + }); + + test('toJson() should return a JSON map', () { + final singleBusiness = SingleBusiness.fromJson(mockSingleBusiness); + expect(singleBusiness.toJson(), mockSingleBusiness); + }); + }); +} diff --git a/app/test/store/bazaar/businesses_store_test.dart b/app/test/store/bazaar/businesses_store_test.dart index 2eced7ebc..d052a0567 100644 --- a/app/test/store/bazaar/businesses_store_test.dart +++ b/app/test/store/bazaar/businesses_store_test.dart @@ -1,31 +1,41 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:encointer_wallet/service/substrate_api/api.dart'; +import 'package:encointer_wallet/store/app.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/widgets/dropdown_widget.dart'; import 'package:encointer_wallet/page-encointer/new_bazaar/businesses/logic/businesses_store.dart'; import 'package:encointer_wallet/utils/fetch_status.dart'; +import '../../mock/mock.dart'; + void main() { late BusinessesStore businessesStore; - setUp(() => businessesStore = BusinessesStore()); + setUp(() async { + webApi = getMockApi(AppStore(MockLocalStorage()), withUI: false); + await webApi.init(); + + businessesStore = BusinessesStore(cid); + }); group('BusinessesStore Test', () { test('`getBusinesses()` should update fetchStatus to success and populate businesses list', () async { expect(businessesStore.fetchStatus, FetchStatus.loading); - expect(businessesStore.businesses, isNull); + expect(businessesStore.sortedBusinesses, isEmpty); await businessesStore.getBusinesses(); expect(businessesStore.fetchStatus, FetchStatus.success); - expect(businessesStore.businesses, isNotNull); - expect(businessesStore.businesses!.length, greaterThan(0)); + expect(businessesStore.sortedBusinesses, isNotEmpty); + expect(businessesStore.sortedBusinesses.length, greaterThan(0)); + expect(businessesStore.businesses.length, greaterThan(0)); }); test('`getBusinesses()` should filter businesses by category', () async { - await businessesStore.getBusinesses(category: Category.artAndMusic); + await businessesStore.getBusinesses(); expect(businessesStore.businesses, isNotNull); - expect(businessesStore.businesses!.every((business) => business.category == Category.artAndMusic), isTrue); + expect(businessesStore.businesses.every((business) => business.category == Category.food), isTrue); }); }); } diff --git a/app/test/store/bazaar/single_business_view_store_test.dart b/app/test/store/bazaar/single_business_view_store_test.dart new file mode 100644 index 000000000..1a0de0730 --- /dev/null +++ b/app/test/store/bazaar/single_business_view_store_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:encointer_wallet/mocks/mock_bazaar_data.dart'; +import 'package:encointer_wallet/page-encointer/new_bazaar/single_business/logic/single_business_store.dart'; +import 'package:encointer_wallet/service/substrate_api/api.dart'; +import 'package:encointer_wallet/store/app.dart'; +import 'package:encointer_wallet/utils/fetch_status.dart'; + +import '../../mock/api/mock_api.dart'; +import '../../mock/storage/mock_local_storage.dart'; + +void main() { + late SingleBusinessStore businessesStore; + + setUp(() async { + webApi = getMockApi(AppStore(MockLocalStorage()), withUI: false); + await webApi.init(); + + businessesStore = SingleBusinessStore(businessesMockForSingleBusiness, cidEdisonPaula); + }); + + group('SingleBusinessStore Test', () { + test('`getSingleBusiness()` should update fetchStatus to success and populate ipfsProducts list and singleBusiness', + () async { + expect(businessesStore.fetchStatus, FetchStatus.loading); + expect(businessesStore.singleBusiness, isNull); + + await businessesStore.getSingleBusiness(); + + expect(businessesStore.fetchStatus, FetchStatus.success); + expect(businessesStore.singleBusiness, isNotNull); + expect(businessesStore.singleBusiness!.name, businessesMockForSingleBusiness.name); + + expect(businessesStore.ipfsProducts, isNotNull); + expect(businessesStore.ipfsProducts.length, greaterThan(0)); + }); + + test('`toggleLikes()` test likes', () async { + expect(businessesStore.isLiked, false); + + businessesStore.toggleLikes(); + + expect(businessesStore.isLiked, isNotNull); + expect(businessesStore.isLiked, isNot(!businessesStore.isLiked)); + }); + + test('`toggleOwnLikes()` test own likes', () async { + expect(businessesStore.isLikedPersonally, false); + + businessesStore.toggleOwnLikes(); + + expect(businessesStore.isLikedPersonally, isNotNull); + expect(businessesStore.isLikedPersonally, isNot(!businessesStore.isLikedPersonally)); + }); + }); +} diff --git a/app/test_driver/helpers/command/app_functions.dart b/app/test_driver/helpers/command/app_functions.dart index ff0d408cc..1ca591c01 100644 --- a/app/test_driver/helpers/command/app_functions.dart +++ b/app/test_driver/helpers/command/app_functions.dart @@ -13,5 +13,5 @@ String toggleDeveloperMode(AppSettings appSettings, bool devMode) { } String getBiometricAuthState(AppService appService) { - return appService.getBiometricAuthState ?? ''; + return Platform.isAndroid ? 'Device not supported' : ''; }