From 3843417777b3a06834b87c91f6d2a5fd1537f7be Mon Sep 17 00:00:00 2001 From: raviramnani Date: Thu, 22 Aug 2024 21:14:16 +0530 Subject: [PATCH 1/6] added ticket stamp feature --- .../data_stores/remote_data_store.dart | 19 +++++++++++++++++++ .../lib/services/repository/repository.dart | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/wallet/lib/services/data_stores/remote_data_store.dart b/wallet/lib/services/data_stores/remote_data_store.dart index f66686523a..27b933810a 100644 --- a/wallet/lib/services/data_stores/remote_data_store.dart +++ b/wallet/lib/services/data_stores/remote_data_store.dart @@ -1305,6 +1305,25 @@ class RemoteDataStoreImp implements RemoteDataStore { return _signAndBroadcast(msgUpdateRecipe); } + Future stampTicket({ + required CookbookId cookBookId, + required RecipeId recipeId, + required Address creatorAddress, + }) async { + final recipe = await getRecipe(cookBookId: cookBookId, recipeId: recipeId); + + final recipeProto3Json = recipe.toProto3Json()! as Map; + recipeProto3Json.remove(kCreatedAtCamelCase); + recipeProto3Json.remove(kUpdatedAtCamelCase); + recipeProto3Json.putIfAbsent("isStamped", true) + + final msgUpdateRecipe = pylons.MsgUpdateRecipe.create()..mergeFromProto3Json(recipeProto3Json); + msgUpdateRecipe.version = msgUpdateRecipe.version.incrementRecipeVersion(); + msgUpdateRecipe.creator = creatorAddress.toString(); + + return _signAndBroadcast(msgUpdateRecipe); + } + @override Future createDynamicLinkForItemNftShare({ required String address, diff --git a/wallet/lib/services/repository/repository.dart b/wallet/lib/services/repository/repository.dart index 3deae3e142..6a53e86768 100644 --- a/wallet/lib/services/repository/repository.dart +++ b/wallet/lib/services/repository/repository.dart @@ -577,6 +577,12 @@ abstract class Repository { required Address creatorAddress, }); + Future> stampTicket({ + required CookbookId cookBookId, + required RecipeId recipeId, + required Address creatorAddress, + }); + Future> createTrade({required pylons.MsgCreateTrade msgCreateTrade}); Future> cancelTrade({required TradeId tradeId, required Address address}); From 9a81feea49da317cc4c45f9fe2f47a149f632c14 Mon Sep 17 00:00:00 2001 From: raviramnani Date: Thu, 22 Aug 2024 21:16:46 +0530 Subject: [PATCH 2/6] added ticket stamp feature --- wallet/lib/services/data_stores/remote_data_store.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/lib/services/data_stores/remote_data_store.dart b/wallet/lib/services/data_stores/remote_data_store.dart index 27b933810a..a2d610c3a7 100644 --- a/wallet/lib/services/data_stores/remote_data_store.dart +++ b/wallet/lib/services/data_stores/remote_data_store.dart @@ -1315,7 +1315,7 @@ class RemoteDataStoreImp implements RemoteDataStore { final recipeProto3Json = recipe.toProto3Json()! as Map; recipeProto3Json.remove(kCreatedAtCamelCase); recipeProto3Json.remove(kUpdatedAtCamelCase); - recipeProto3Json.putIfAbsent("isStamped", true) + recipeProto3Json.putIfAbsent("isStamped", true as Function()); final msgUpdateRecipe = pylons.MsgUpdateRecipe.create()..mergeFromProto3Json(recipeProto3Json); msgUpdateRecipe.version = msgUpdateRecipe.version.incrementRecipeVersion(); From 19753a64e5c311094bf22d41f317cee0bfdc1015 Mon Sep 17 00:00:00 2001 From: raviramnani Date: Thu, 22 Aug 2024 22:12:07 +0530 Subject: [PATCH 3/6] ui and scanner --- wallet/lib/model/event.dart | 3 +- .../owner_view_view_model.dart | 16 +++ .../pages/events/event_qr_code_screen.dart | 136 +++++++++++------- wallet/lib/pages/events/mobile_scanner.dart | 32 ++++- 4 files changed, 131 insertions(+), 56 deletions(-) diff --git a/wallet/lib/model/event.dart b/wallet/lib/model/event.dart index 86dbdf7a45..f2d1874c96 100644 --- a/wallet/lib/model/event.dart +++ b/wallet/lib/model/event.dart @@ -5,7 +5,6 @@ import 'package:pylons_wallet/pages/home/currency_screen/model/ibc_coins.dart'; import 'package:pylons_wallet/stores/wallet_store.dart'; import '../modules/Pylonstech.pylons.pylons/module/client/cosmos/base/v1beta1/coin.pb.dart'; import '../modules/Pylonstech.pylons.pylons/module/client/pylons/recipe.pb.dart'; - enum FreeDrop { yes, no, unselected } class Events extends Equatable { @@ -29,6 +28,7 @@ class Events extends Equatable { final IBCCoins denom; String ownerAddress = ""; String owner = ""; + bool isStamped = false; Events({ this.id, @@ -65,6 +65,7 @@ class Events extends Equatable { ///* this.ownerAddress = "", this.owner = '', + this.isStamped = false, }); Map toJson() { diff --git a/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart b/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart index 743ac368bb..ee03cbcfa2 100644 --- a/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart +++ b/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart @@ -506,6 +506,7 @@ class OwnerViewViewModel extends ChangeNotifier { notifyListeners(); } + Future cancelTrade({required String tradeId, required String address}) async { final tradeResponse = await repository.cancelTrade( tradeId: TradeId(Int64(int.parse(tradeId))), @@ -517,6 +518,21 @@ class OwnerViewViewModel extends ChangeNotifier { } } + Future stampTicket({required bool enabled}) async { + final response = await repository.stampTicket( + cookBookId: CookbookId(events.cookbookID), + recipeId: RecipeId(events.recipeID), + creatorAddress: Address(events.ownerAddress), + ); + + if (response.isLeft()) { + throw response.getLeft(); + } + + events.isStamped = true; + notifyListeners(); + } + void logEvent() { repository.logUserJourney(screenName: AnalyticsScreenEvents.ownerView); } diff --git a/wallet/lib/pages/events/event_qr_code_screen.dart b/wallet/lib/pages/events/event_qr_code_screen.dart index b75d18e23a..062bd2f615 100644 --- a/wallet/lib/pages/events/event_qr_code_screen.dart +++ b/wallet/lib/pages/events/event_qr_code_screen.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; import 'package:pylons_wallet/components/buttons/custom_paint_button.dart'; import 'package:pylons_wallet/gen/assets.gen.dart'; import 'package:pylons_wallet/generated/locale_keys.g.dart'; @@ -11,6 +12,9 @@ import 'package:pylons_wallet/model/event.dart'; import 'package:pylons_wallet/pages/detailed_asset_view/widgets/nft_image_asset.dart'; import 'package:pylons_wallet/utils/constants.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import 'package:pylons_wallet/utils/extension.dart'; + +import '../../providers/account_provider.dart'; class EventQrCodeScreen extends StatefulWidget { const EventQrCodeScreen({ @@ -25,67 +29,93 @@ class EventQrCodeScreen extends StatefulWidget { } class _EventQrCodeScreenState extends State { + String link = ""; GlobalKey renderObjectKey = GlobalKey(); + @override - Widget build(BuildContext context) { - return Material( - color: AppColors.kBlack, - child: Stack( - children: [ - NftImageWidget( - url: widget.events.thumbnail, - opacity: 0.5, - ), - Padding( - padding: EdgeInsets.only(left: 23.w, top: MediaQuery.of(context).viewPadding.top + 13.h), - child: GestureDetector( - onTap: () async { - Navigator.pop(context); - }, - child: SvgPicture.asset( - Assets.images.icons.back, - height: 25.h, - ), + void initState() { + super.initState(); + + createLink(); + } + + void createLink() { + final wallet = context + .read() + .accountPublicInfo; + + if (wallet == null) { + return; + } + link = widget.events.recipeID; + } + + @override + Widget build(BuildContext context) { + return Material( + color: AppColors.kBlack, + child: Stack( + children: [ + NftImageWidget( + url: widget.events.thumbnail, + opacity: 0.5, ), - ), - ColoredBox( - color: AppColors.kBlack.withOpacity(0.5), - child: Align( - child: RepaintBoundary( - key: renderObjectKey, - child: QrImageView( - padding: EdgeInsets.zero, - data: jsonEncode(widget.events.toJson()), - size: 200, - dataModuleStyle: const QrDataModuleStyle(color: AppColors.kWhite), - eyeStyle: const QrEyeStyle(eyeShape: QrEyeShape.square, color: AppColors.kWhite), + Padding( + padding: EdgeInsets.only(left: 23.w, top: MediaQuery + .of(context) + .viewPadding + .top + 13.h), + child: GestureDetector( + onTap: () async { + Navigator.pop(context); + }, + child: SvgPicture.asset( + Assets.images.icons.back, + height: 25.h, ), ), ), - ), - Align( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: SvgPicture.asset(Assets.images.svg.qrSideBorder), + ColoredBox( + color: AppColors.kBlack.withOpacity(0.5), + child: Align( + child: RepaintBoundary( + key: renderObjectKey, + child: QrImageView( + padding: EdgeInsets.zero, + data: link, + size: 200, + dataModuleStyle: const QrDataModuleStyle( + color: AppColors.kWhite), + eyeStyle: const QrEyeStyle( + eyeShape: QrEyeShape.square, color: AppColors.kWhite), + ), + ), + ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: EdgeInsets.only(bottom: 30.h), - child: CustomPaintButton( - title: LocaleKeys.done.tr(), - bgColor: AppColors.kWhite.withOpacity(0.3), - width: 280.w, - onPressed: () { - Navigator.pop(context); - }, + Align( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.w), + child: SvgPicture.asset(Assets.images.svg.qrSideBorder), ), ), - ) - ], - ), - ); + Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: EdgeInsets.only(bottom: 30.h), + child: CustomPaintButton( + title: LocaleKeys.done.tr(), + bgColor: AppColors.kWhite.withOpacity(0.3), + width: 280.w, + onPressed: () { + Navigator.pop(context); + }, + ), + ), + ) + ], + ), + ); + } } -} + diff --git a/wallet/lib/pages/events/mobile_scanner.dart b/wallet/lib/pages/events/mobile_scanner.dart index dcf11ccaf8..0333e89047 100644 --- a/wallet/lib/pages/events/mobile_scanner.dart +++ b/wallet/lib/pages/events/mobile_scanner.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import '../detailed_asset_view/owner_view_view_model.dart'; + class MobileQrScanner extends StatefulWidget { const MobileQrScanner({super.key}); @@ -9,16 +12,19 @@ class MobileQrScanner extends StatefulWidget { } class _MobileQrScannerState extends State { + OwnerViewViewModel ownerViewViewModel = GetIt.I.get(); Barcode? _barcode; + Widget _buildBarcode(Barcode? value) { if (value == null) { return const Text( - 'Scan something!', + 'Scan Ticket!', overflow: TextOverflow.fade, style: TextStyle(color: Colors.white), ); } + print('QR Code Data: ${value.displayValue}'); return Text( value.displayValue ?? 'No display value.', @@ -31,14 +37,36 @@ class _MobileQrScannerState extends State { if (mounted) { setState(() { _barcode = barcodes.barcodes.firstOrNull; + + if (_barcode != null) { + + final String eventId = _barcode!.displayValue ?? ''; + + // Use the QR code data to stamp the ticket + _stampTicket(eventId); + } }); } } + Future stampTicket(String eventId) async { + try { + + await ownerViewViewModel.stampTicket(enabled: true); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ticket $eventId stamped successfully!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to stamp ticket: $e')), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Simple scanner')), + appBar: AppBar(title: const Text('Ticket scanner')), backgroundColor: Colors.black, body: Stack( children: [ From 955f96144593e4dff44745ae8c1585d37b2712be Mon Sep 17 00:00:00 2001 From: raviramnani Date: Thu, 22 Aug 2024 22:19:04 +0530 Subject: [PATCH 4/6] scanner route and home screen button for scanner --- wallet/lib/pages/home/home.dart | 10 ++++++++++ wallet/lib/utils/route_util.dart | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/wallet/lib/pages/home/home.dart b/wallet/lib/pages/home/home.dart index a8cd050ca1..e92acd3a12 100644 --- a/wallet/lib/pages/home/home.dart +++ b/wallet/lib/pages/home/home.dart @@ -309,6 +309,16 @@ class HomeScreenState extends State with SingleTickerProviderStateMi ), ), ), + Positioned( + top: 0.06.sh, + right: 0.25.sw, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pushNamed(Routes.mobileQrScanner.name); + }, + child: const Text('Open Scanner'), + ), + ), Positioned( top: 0.06.sh, left: 0.09.sw, diff --git a/wallet/lib/utils/route_util.dart b/wallet/lib/utils/route_util.dart index 313e12ca85..2c6c039c88 100644 --- a/wallet/lib/utils/route_util.dart +++ b/wallet/lib/utils/route_util.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:pylons_wallet/pages/events/event_purchase_view.dart'; import 'package:pylons_wallet/pages/events/events_owner_view.dart'; +import 'package:pylons_wallet/pages/events/mobile_scanner.dart'; import 'package:pylons_wallet/pages/settings/screens/general_screen/general_screen.dart'; import 'package:pylons_wallet/pages/settings/screens/recovery_screen/recovery_screen.dart'; - import '../model/event.dart'; import '../model/nft.dart'; import '../model/transaction_failure_model.dart'; @@ -32,12 +32,16 @@ import '../pages/settings/settings_screen.dart'; import '../pages/transaction_failure_manager/local_transaction_detail_screen.dart'; import '../pages/transaction_failure_manager/local_transactions_screen.dart'; import 'dependency_injection/dependency_injection.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; class RouteUtil { RouteUtil(); static Route? onGenerateRoute(RouteSettings settings) { final route = Routes.getAppRouteFromString(settings.name ?? ""); + switch (route) { + case Routes.mobileQrScanner: + return createRoute(const MobileQrScanner()); case Routes.initial: return createRoute(const SplashScreen()); case Routes.home: @@ -170,10 +174,13 @@ enum Routes { acceptPolicy, fallback, eventOwnerView, - eventPurchaseView; + eventPurchaseView, + mobileQrScanner; static Routes getAppRouteFromString(String routeName) { switch (routeName) { + case'mobileQrScanner': + return mobileQrScanner; case '/': return initial; case 'home': From ff2566b48e30e0c18d62021d98b48fa7482be1c0 Mon Sep 17 00:00:00 2001 From: raviramnani Date: Wed, 28 Aug 2024 15:12:49 +0530 Subject: [PATCH 5/6] added random string generator to create challenge to stamp ticket and its implementation in repository.dart remote_data_store.dart owner_view_view_model.dart mobile_scanner.dart --- .../owner_view_view_model.dart | 13 ++++++-- .../pages/events/event_qr_code_screen.dart | 10 +++--- wallet/lib/pages/events/mobile_scanner.dart | 31 +++++++++++++++---- .../data_stores/remote_data_store.dart | 17 ++++++++-- .../lib/services/repository/repository.dart | 23 ++++++++++++++ wallet/lib/utils/string_utils.dart | 15 +++++++++ 6 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 wallet/lib/utils/string_utils.dart diff --git a/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart b/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart index ee03cbcfa2..09068e32fd 100644 --- a/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart +++ b/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart @@ -518,11 +518,17 @@ class OwnerViewViewModel extends ChangeNotifier { } } - Future stampTicket({required bool enabled}) async { + Future stampTicket({ + required bool enabled, + required String cookbookId, + required String recipeId, + required String challenge, + }) async { final response = await repository.stampTicket( - cookBookId: CookbookId(events.cookbookID), - recipeId: RecipeId(events.recipeID), + cookBookId: CookbookId(cookbookId), + recipeId: RecipeId(recipeId), creatorAddress: Address(events.ownerAddress), + challenge: challenge, ); if (response.isLeft()) { @@ -533,6 +539,7 @@ class OwnerViewViewModel extends ChangeNotifier { notifyListeners(); } + void logEvent() { repository.logUserJourney(screenName: AnalyticsScreenEvents.ownerView); } diff --git a/wallet/lib/pages/events/event_qr_code_screen.dart b/wallet/lib/pages/events/event_qr_code_screen.dart index 062bd2f615..fdb8c41199 100644 --- a/wallet/lib/pages/events/event_qr_code_screen.dart +++ b/wallet/lib/pages/events/event_qr_code_screen.dart @@ -12,7 +12,6 @@ import 'package:pylons_wallet/model/event.dart'; import 'package:pylons_wallet/pages/detailed_asset_view/widgets/nft_image_asset.dart'; import 'package:pylons_wallet/utils/constants.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:pylons_wallet/utils/extension.dart'; import '../../providers/account_provider.dart'; @@ -29,7 +28,7 @@ class EventQrCodeScreen extends StatefulWidget { } class _EventQrCodeScreenState extends State { - String link = ""; + String qrdata = ""; GlobalKey renderObjectKey = GlobalKey(); @@ -48,7 +47,10 @@ class _EventQrCodeScreenState extends State { if (wallet == null) { return; } - link = widget.events.recipeID; + qrdata = jsonEncode({ + 'cookbookId': widget.events.cookbookID, + 'recipeId': widget.events.recipeID, + }); } @override @@ -83,7 +85,7 @@ class _EventQrCodeScreenState extends State { key: renderObjectKey, child: QrImageView( padding: EdgeInsets.zero, - data: link, + data: qrdata, size: 200, dataModuleStyle: const QrDataModuleStyle( color: AppColors.kWhite), diff --git a/wallet/lib/pages/events/mobile_scanner.dart b/wallet/lib/pages/events/mobile_scanner.dart index 0333e89047..319517b3b1 100644 --- a/wallet/lib/pages/events/mobile_scanner.dart +++ b/wallet/lib/pages/events/mobile_scanner.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import '../../utils/string_utils.dart'; import '../detailed_asset_view/owner_view_view_model.dart'; class MobileQrScanner extends StatefulWidget { @@ -39,22 +40,39 @@ class _MobileQrScannerState extends State { _barcode = barcodes.barcodes.firstOrNull; if (_barcode != null) { + final qrData = _barcode!.displayValue ?? ''; + final dataParts = qrData.split(','); - final String eventId = _barcode!.displayValue ?? ''; + if (dataParts.length >= 2) { + final cookbookId = dataParts[0]; + final recipeId = dataParts[1]; - // Use the QR code data to stamp the ticket - _stampTicket(eventId); + + final challenge = StringUtils.generateRandomString(16); + + + stampTicket(cookbookId, recipeId, challenge); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Invalid QR code data.')), + ); + } } }); } } - Future stampTicket(String eventId) async { + Future stampTicket(String cookbookId, String recipeId, String challenge) async { try { + await ownerViewViewModel.stampTicket( + enabled: true, + cookbookId: cookbookId, + recipeId: recipeId, + challenge: challenge, + ); - await ownerViewViewModel.stampTicket(enabled: true); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ticket $eventId stamped successfully!')), + SnackBar(content: Text('Ticket with challenge $challenge stamped successfully!')), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( @@ -63,6 +81,7 @@ class _MobileQrScannerState extends State { } } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/wallet/lib/services/data_stores/remote_data_store.dart b/wallet/lib/services/data_stores/remote_data_store.dart index a2d610c3a7..7e0a97f259 100644 --- a/wallet/lib/services/data_stores/remote_data_store.dart +++ b/wallet/lib/services/data_stores/remote_data_store.dart @@ -361,6 +361,13 @@ abstract class RemoteDataStore { /// Input : [address] the address & [Event] against which the invite link to be generated /// Output: [String] return the generated dynamic link else will throw error Future createDynamicLinkForRecipeEventShare({required String address, required Events events}); + + Future stampTicket({ + required CookbookId cookBookId, + required RecipeId recipeId, + required Address creatorAddress, + required String challenge, + }); } class RemoteDataStoreImp implements RemoteDataStore { @@ -1305,25 +1312,31 @@ class RemoteDataStoreImp implements RemoteDataStore { return _signAndBroadcast(msgUpdateRecipe); } + @override Future stampTicket({ required CookbookId cookBookId, required RecipeId recipeId, required Address creatorAddress, + required String challenge, }) async { final recipe = await getRecipe(cookBookId: cookBookId, recipeId: recipeId); final recipeProto3Json = recipe.toProto3Json()! as Map; recipeProto3Json.remove(kCreatedAtCamelCase); recipeProto3Json.remove(kUpdatedAtCamelCase); - recipeProto3Json.putIfAbsent("isStamped", true as Function()); + recipeProto3Json.putIfAbsent("isStamped", () => true); - final msgUpdateRecipe = pylons.MsgUpdateRecipe.create()..mergeFromProto3Json(recipeProto3Json); + final msgUpdateRecipe = pylons.MsgUpdateRecipe.create() + ..mergeFromProto3Json(recipeProto3Json); + + msgUpdateRecipe.extraInfo = challenge; msgUpdateRecipe.version = msgUpdateRecipe.version.incrementRecipeVersion(); msgUpdateRecipe.creator = creatorAddress.toString(); return _signAndBroadcast(msgUpdateRecipe); } + @override Future createDynamicLinkForItemNftShare({ required String address, diff --git a/wallet/lib/services/repository/repository.dart b/wallet/lib/services/repository/repository.dart index 6a53e86768..3bda2ea60b 100644 --- a/wallet/lib/services/repository/repository.dart +++ b/wallet/lib/services/repository/repository.dart @@ -581,6 +581,7 @@ abstract class Repository { required CookbookId cookBookId, required RecipeId recipeId, required Address creatorAddress, + required String challenge, }); Future> createTrade({required pylons.MsgCreateTrade msgCreateTrade}); @@ -2428,6 +2429,28 @@ class RepositoryImp implements Repository { } } + @override + Future> stampTicket({ + required CookbookId cookBookId, + required RecipeId recipeId, + required Address creatorAddress, + required String challenge, + }) async { + try { + await remoteDataStore.stampTicket( + cookBookId: cookBookId, + recipeId: recipeId, + creatorAddress: creatorAddress, + challenge: challenge, + ); + return const Right(null); + } on Exception catch (e) { + recordErrorInCrashlytics(e); + return Left(ServerFailure(e.toString())); + } + } + + @override Future> createTrade({required pylons.MsgCreateTrade msgCreateTrade}) async { try { diff --git a/wallet/lib/utils/string_utils.dart b/wallet/lib/utils/string_utils.dart new file mode 100644 index 0000000000..9dfef46148 --- /dev/null +++ b/wallet/lib/utils/string_utils.dart @@ -0,0 +1,15 @@ +import 'dart:math'; + +class StringUtils { + static String generateRandomString(int length) { + const String _chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + Random _rnd = Random(); + + return String.fromCharCodes( + Iterable.generate( + length, + (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)), + ), + ); + } +} From 9660cb76e2ca6d95d7cb952fa8bb0cd4e0152c08 Mon Sep 17 00:00:00 2001 From: raviramnani Date: Mon, 2 Sep 2024 14:46:18 +0530 Subject: [PATCH 6/6] updates for stamping implementation --- wallet/lib/model/event.dart | 13 +- wallet/lib/model/update_recipe_model.dart | 4 +- .../owner_view_view_model.dart | 6 +- .../pages/events/event_qr_code_screen.dart | 1 + wallet/lib/pages/events/mobile_scanner.dart | 133 +++++++------- wallet/lib/pages/events/stamping_screen.dart | 166 ++++++++++++++++++ .../data_stores/remote_data_store.dart | 1 - 7 files changed, 247 insertions(+), 77 deletions(-) create mode 100644 wallet/lib/pages/events/stamping_screen.dart diff --git a/wallet/lib/model/event.dart b/wallet/lib/model/event.dart index f2d1874c96..031aefffa3 100644 --- a/wallet/lib/model/event.dart +++ b/wallet/lib/model/event.dart @@ -21,6 +21,7 @@ class Events extends Equatable { final String description; final String numberOfTickets; final String price; + final String? challenge; final List? listOfPerks; final String isFreeDrops; final String cookbookID; @@ -28,7 +29,6 @@ class Events extends Equatable { final IBCCoins denom; String ownerAddress = ""; String owner = ""; - bool isStamped = false; Events({ this.id, @@ -58,6 +58,7 @@ class Events extends Equatable { ///* other this.cookbookID = '', this.recipeID = '', + this.challenge, ///* for tracking where its save as draft this.step = '', @@ -65,7 +66,6 @@ class Events extends Equatable { ///* this.ownerAddress = "", this.owner = '', - this.isStamped = false, }); Map toJson() { @@ -90,6 +90,7 @@ class Events extends Equatable { map['price'] = price; map['isFreeDrops'] = isFreeDrops; map['cookbookID'] = cookbookID; + map['challenge'] = challenge ?? ''; map['step'] = step; map['denom'] = denom.toString(); map['listOfPerks'] = perks; @@ -117,6 +118,7 @@ class Events extends Equatable { price: json['price'] as String, isFreeDrops: json['isFreeDrops'] as String, cookbookID: json['cookbookID'] as String, + challenge: json['challenge'] as String?, step: json['step'] as String, listOfPerks: listOfPerks, denom: json['denom'].toString().toIBCCoinsEnumforEvent(), @@ -161,6 +163,7 @@ class Events extends Equatable { listOfPerks: listOfPerks, cookbookID: map[kCookBookId]!, recipeID: map[kRecipeId]!, + challenge: map[kChallenge], denom: denom.isEmpty ? IBCCoins.upylon : denom.toIBCCoinsEnum(), ); } @@ -183,6 +186,7 @@ class Events extends Equatable { case kPerks: case kFreeDrop: case kCookBookId: + case kChallenge: case kRecipeId: attributeValues[attribute.key] = attribute.value; break; @@ -203,11 +207,11 @@ class Events extends Equatable { @override List get props => - [eventName, hostName, thumbnail, startDate, endDate, startTime, endTime, location, description, numberOfTickets, price, listOfPerks, isFreeDrops, cookbookID, recipeID, step]; + [eventName, hostName, thumbnail, startDate, endDate, startTime, endTime, location, description, numberOfTickets, price, listOfPerks, isFreeDrops, cookbookID, recipeID, step, challenge]; @override String toString() { - return 'Event{eventName: $eventName, hostName: $hostName, thumbnail: $thumbnail, startDate: $startDate, endDate: $endDate, startTime: $startTime, endTime: $endTime, location: $location, description: $description, numberOfTickets: $numberOfTickets, price: $price, listOfPerks: $listOfPerks, isFreeDrop: $isFreeDrops, cookbookID: $cookbookID, recipeID: $recipeID, step: $step}'; + return 'Event{eventName: $eventName, hostName: $hostName, thumbnail: $thumbnail, startDate: $startDate, endDate: $endDate, startTime: $startTime, endTime: $endTime, location: $location, description: $description, numberOfTickets: $numberOfTickets, price: $price, listOfPerks: $listOfPerks, isFreeDrop: $isFreeDrops, cookbookID: $cookbookID, recipeID: $recipeID, step: $step, challenge: $challenge}'; } static Future eventFromRecipeId(String cookbookId, String recipeId) async { @@ -238,6 +242,7 @@ const kPrice = "kPrice"; const kFreeDrop = "kFreeDrop"; const kRecipeId = "kRecipeId"; const kCookBookId = "kCookBookId"; +const kChallenge = "kChallenge"; const kVersion = "v0.2.0"; const kUpylon = "upylon"; const kPylonSymbol = 'upylon'; diff --git a/wallet/lib/model/update_recipe_model.dart b/wallet/lib/model/update_recipe_model.dart index a8d6c64e19..05437cc84d 100644 --- a/wallet/lib/model/update_recipe_model.dart +++ b/wallet/lib/model/update_recipe_model.dart @@ -7,6 +7,7 @@ class UpdateRecipeModel { String nftPrice = ""; String denom = ""; int quantity = 0; + String challenge = ""; UpdateRecipeModel({ required this.recipe, @@ -15,10 +16,11 @@ class UpdateRecipeModel { required this.nftPrice, required this.denom, required this.quantity, + required this.challenge, }); @override String toString() { - return 'UpdateRecipeModel{recipe: $recipe, publicAddress: $publicAddress, enabledStatus: $enabledStatus, nftPrice: $nftPrice, denom: $denom, quantity: $quantity}'; + return 'UpdateRecipeModel{recipe: $recipe, publicAddress: $publicAddress, enabledStatus: $enabledStatus, nftPrice: $nftPrice, denom: $denom, quantity: $quantity, challenge: $challenge}'; } } diff --git a/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart b/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart index 09068e32fd..d5e2a47545 100644 --- a/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart +++ b/wallet/lib/pages/detailed_asset_view/owner_view_view_model.dart @@ -519,15 +519,15 @@ class OwnerViewViewModel extends ChangeNotifier { } Future stampTicket({ - required bool enabled, required String cookbookId, required String recipeId, + required Address creatorAddress, required String challenge, }) async { final response = await repository.stampTicket( cookBookId: CookbookId(cookbookId), recipeId: RecipeId(recipeId), - creatorAddress: Address(events.ownerAddress), + creatorAddress: creatorAddress, challenge: challenge, ); @@ -535,11 +535,11 @@ class OwnerViewViewModel extends ChangeNotifier { throw response.getLeft(); } - events.isStamped = true; notifyListeners(); } + void logEvent() { repository.logUserJourney(screenName: AnalyticsScreenEvents.ownerView); } diff --git a/wallet/lib/pages/events/event_qr_code_screen.dart b/wallet/lib/pages/events/event_qr_code_screen.dart index fdb8c41199..c2413735a6 100644 --- a/wallet/lib/pages/events/event_qr_code_screen.dart +++ b/wallet/lib/pages/events/event_qr_code_screen.dart @@ -50,6 +50,7 @@ class _EventQrCodeScreenState extends State { qrdata = jsonEncode({ 'cookbookId': widget.events.cookbookID, 'recipeId': widget.events.recipeID, + 'challenge': widget.events.challenge, }); } diff --git a/wallet/lib/pages/events/mobile_scanner.dart b/wallet/lib/pages/events/mobile_scanner.dart index 319517b3b1..049c1c44bd 100644 --- a/wallet/lib/pages/events/mobile_scanner.dart +++ b/wallet/lib/pages/events/mobile_scanner.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; - -import '../../utils/string_utils.dart'; +import 'package:pylons_wallet/pages/events/stamping_screen.dart'; +import '../../model/event.dart'; import '../detailed_asset_view/owner_view_view_model.dart'; class MobileQrScanner extends StatefulWidget { @@ -13,96 +13,93 @@ class MobileQrScanner extends StatefulWidget { } class _MobileQrScannerState extends State { - OwnerViewViewModel ownerViewViewModel = GetIt.I.get(); + final OwnerViewViewModel ownerViewViewModel = GetIt.I.get(); Barcode? _barcode; + bool isScanning = true; - - Widget _buildBarcode(Barcode? value) { - if (value == null) { + Widget _buildBarcodeDisplay(Barcode? barcode) { + if (barcode == null) { return const Text( 'Scan Ticket!', - overflow: TextOverflow.fade, - style: TextStyle(color: Colors.white), + overflow: TextOverflow.ellipsis, + style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), ); } - print('QR Code Data: ${value.displayValue}'); - return Text( - value.displayValue ?? 'No display value.', - overflow: TextOverflow.fade, - style: const TextStyle(color: Colors.white), + barcode.displayValue ?? 'No display value.', + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), ); } - void _handleBarcode(BarcodeCapture barcodes) { - if (mounted) { - setState(() { - _barcode = barcodes.barcodes.firstOrNull; - - if (_barcode != null) { - final qrData = _barcode!.displayValue ?? ''; - final dataParts = qrData.split(','); - - if (dataParts.length >= 2) { - final cookbookId = dataParts[0]; - final recipeId = dataParts[1]; - - - final challenge = StringUtils.generateRandomString(16); - - - stampTicket(cookbookId, recipeId, challenge); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Invalid QR code data.')), - ); - } + void _handleBarcode(BarcodeCapture barcodeCapture) { + if (!isScanning) return; + + setState(() { + if (barcodeCapture.barcodes.isNotEmpty) { + _barcode = barcodeCapture.barcodes[0]; + final qrData = _barcode?.displayValue ?? ''; + final dataParts = qrData.split(','); + + if (dataParts.length >= 2) { + final cookbookId = dataParts[0]; + final recipeId = dataParts[1]; + final challenge = dataParts.length > 2 ? dataParts[2] : ''; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StampingScreen( + cookbookId: cookbookId, + recipeId: recipeId, + challenge: challenge, + ownerViewViewModel: ownerViewViewModel, + event: Events(eventName: 'Event Name', thumbnail: 'Thumbnail URL', description: 'Description'), + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Invalid QR code data.')), + ); } - }); - } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No barcodes detected.')), + ); + } + }); } - Future stampTicket(String cookbookId, String recipeId, String challenge) async { - try { - await ownerViewViewModel.stampTicket( - enabled: true, - cookbookId: cookbookId, - recipeId: recipeId, - challenge: challenge, - ); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ticket with challenge $challenge stamped successfully!')), - ); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to stamp ticket: $e')), - ); - } - } - - @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Ticket scanner')), + appBar: AppBar( + title: const Text('Ticket Scanner'), + backgroundColor: Colors.blueGrey[900], + ), backgroundColor: Colors.black, body: Stack( children: [ MobileScanner( onDetect: _handleBarcode, + scanWindow: Rect.fromLTWH( + 0, + 0, + MediaQuery.of(context).size.width, + MediaQuery.of(context).size.height, + ), ), - Align( - alignment: Alignment.bottomCenter, + Positioned( + bottom: 0, + left: 0, + right: 0, child: Container( - alignment: Alignment.bottomCenter, + alignment: Alignment.center, height: 100, - color: Colors.black.withOpacity(0.4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded(child: Center(child: _buildBarcode(_barcode))), - ], + color: Colors.black.withOpacity(0.5), + child: Center( + child: _buildBarcodeDisplay(_barcode), ), ), ), diff --git a/wallet/lib/pages/events/stamping_screen.dart b/wallet/lib/pages/events/stamping_screen.dart new file mode 100644 index 0000000000..02a6d0a234 --- /dev/null +++ b/wallet/lib/pages/events/stamping_screen.dart @@ -0,0 +1,166 @@ +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get_it/get_it.dart'; +import 'package:pylons_wallet/pages/detailed_asset_view/owner_view_view_model.dart'; +import '../../model/common.dart'; +import '../../model/event.dart'; +import '../../stores/wallet_store.dart'; + +class StampingScreen extends StatefulWidget { + final String cookbookId; + final String recipeId; + final String challenge; + final OwnerViewViewModel ownerViewViewModel; + final Events event; + + const StampingScreen({ + super.key, + required this.cookbookId, + required this.recipeId, + required this.challenge, + required this.ownerViewViewModel, + required this.event, + }); + + @override + _StampingScreenState createState() => _StampingScreenState(); +} + +class _StampingScreenState extends State { + Events? event; + late OwnerViewViewModel ownerViewViewModel; + bool isLoading = true; + bool isAlreadyStamped = false; + + @override + void initState() { + super.initState(); + ownerViewViewModel = GetIt.I.get(); + fetchEvent(); + } + + Future fetchEvent() async { + try { + final fetchedEvent = await eventFromRecipeId(widget.cookbookId, widget.recipeId); + + if (mounted) { + setState(() { + event = fetchedEvent; + isLoading = false; + isAlreadyStamped = widget.challenge.isNotEmpty; + }); + } + } catch (e) { + if (mounted) { + setState(() { + isLoading = false; + }); + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to fetch event: $e')), + ); + } + } + + Future _stampTicket(BuildContext context) async { + try { + if (isAlreadyStamped) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('This ticket is already stamped.')), + ); + return; + } + + await ownerViewViewModel.stampTicket( + cookbookId: widget.cookbookId, + recipeId: widget.recipeId, + creatorAddress: Address(widget.ownerViewViewModel.owner), + challenge: widget.challenge, + ); + + final updatedEvent = await eventFromRecipeId(widget.cookbookId, widget.recipeId); + + if (mounted) { + setState(() { + event = updatedEvent; + isAlreadyStamped = true; + }); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ticket with challenge ${widget.challenge} stamped successfully!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to stamp ticket: $e')), + ); + } + } + + Future eventFromRecipeId(String cookbookId, String recipeId) async { + final walletsStore = GetIt.I.get(); + final recipeEither = await walletsStore.getRecipe(cookbookId, recipeId); + + if (recipeEither.isLeft()) { + return null; + } + + return Events.fromRecipe(recipeEither.toOption().toNullable()!); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Stamp Ticket')), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : event == null + ? const Center(child: Text('No event found for the provided IDs.')) + : SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CachedNetworkImage( + imageUrl: event!.thumbnail, + height: 200.h, + width: double.infinity, + fit: BoxFit.cover, + placeholder: (context, url) => const CircularProgressIndicator(), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + const SizedBox(height: 16), + Text( + event!.eventName, + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + event!.description, + style: TextStyle( + fontSize: 16.sp, + color: Colors.grey[700], + ), + ), + const SizedBox(height: 16), + Text( + 'Cookbook ID: ${widget.cookbookId}\nRecipe ID: ${widget.recipeId}\nChallenge: ${widget.challenge}', + style: TextStyle(fontSize: 18.sp), + ), + const SizedBox(height: 20), + Center( + child: ElevatedButton( + onPressed: () => _stampTicket(context), + child: const Text('Stamp Ticket'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/wallet/lib/services/data_stores/remote_data_store.dart b/wallet/lib/services/data_stores/remote_data_store.dart index 7e0a97f259..c6d2b71c42 100644 --- a/wallet/lib/services/data_stores/remote_data_store.dart +++ b/wallet/lib/services/data_stores/remote_data_store.dart @@ -1324,7 +1324,6 @@ class RemoteDataStoreImp implements RemoteDataStore { final recipeProto3Json = recipe.toProto3Json()! as Map; recipeProto3Json.remove(kCreatedAtCamelCase); recipeProto3Json.remove(kUpdatedAtCamelCase); - recipeProto3Json.putIfAbsent("isStamped", () => true); final msgUpdateRecipe = pylons.MsgUpdateRecipe.create() ..mergeFromProto3Json(recipeProto3Json);