From 9660cb76e2ca6d95d7cb952fa8bb0cd4e0152c08 Mon Sep 17 00:00:00 2001 From: raviramnani Date: Mon, 2 Sep 2024 14:46:18 +0530 Subject: [PATCH] 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);