From 1cd1febf991e17ec1db98772e9dd79691c485a14 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sun, 24 Aug 2025 01:58:28 +0530 Subject: [PATCH 1/9] add: github workflow for maintaining structure and build checks --- .env.stencil | 3 +- .github/workflows/flutter.yaml | 35 +++ lib/components/universal_navbar.dart | 25 +- lib/components/wallet_connect_dialog.dart | 5 + lib/models/wallet_chain_option.dart | 10 +- lib/pages/home_page.dart | 2 - lib/pages/mint_nft/mint_nft_coordinates.dart | 3 +- lib/pages/mint_nft/mint_nft_details.dart | 24 +- lib/pages/mint_nft/mint_nft_images.dart | 7 +- lib/pages/register_user_page.dart | 12 +- lib/providers/mint_nft_provider.dart | 1 - lib/providers/wallet_provider.dart | 1 + lib/utils/constants/contractDetails.dart | 6 - .../contract_abis/tree_nft_contract_abi.dart | 8 +- .../services/contract_read_services.dart | 14 +- .../services/contract_write_functions.dart | 10 +- lib/utils/services/ipfs_services.dart | 8 +- lib/utils/services/switch_chain_utils.dart | 2 +- .../map_widgets/flutter_map_widget.dart | 49 ++-- .../tree_nft_view_details_with_map.dart | 4 +- ..._widget.dart => tree_nft_view_widget.dart} | 1 + .../nft_display_utils/user_nfts_widget.dart | 69 +++--- .../profile_section_widget.dart | 215 +++++++++--------- lib/widgets/wallet_not_connected_widget.dart | 1 + 24 files changed, 270 insertions(+), 245 deletions(-) create mode 100644 .github/workflows/flutter.yaml delete mode 100644 lib/utils/constants/contractDetails.dart rename lib/widgets/nft_display_utils/{tree_NFT_view_widget.dart => tree_nft_view_widget.dart} (98%) diff --git a/.env.stencil b/.env.stencil index f7081d9..a81de8d 100644 --- a/.env.stencil +++ b/.env.stencil @@ -2,4 +2,5 @@ WALLETCONNECT_PROJECT_ID= API_KEY= API_SECRET= ALCHEMY_API_KEY= -CONTRACT_ADDRESS= \ No newline at end of file +CONTRACT_ADDRESS= +APPLICATION_ID= \ No newline at end of file diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml new file mode 100644 index 0000000..36ee323 --- /dev/null +++ b/.github/workflows/flutter.yaml @@ -0,0 +1,35 @@ +name: Flutter CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup environment file from template + run: cp .env.stencil .env + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + - name: Dart analyze + run: flutter analyze + + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Flutter build (apk) + run: flutter build apk --release --no-tree-shake-icons 2>/dev/null | grep -v "deprecated" diff --git a/lib/components/universal_navbar.dart b/lib/components/universal_navbar.dart index b44169d..b73d52f 100644 --- a/lib/components/universal_navbar.dart +++ b/lib/components/universal_navbar.dart @@ -33,7 +33,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { bottom: 0, left: 0, right: 0, - child: Container( + child: SizedBox( height: 40, child: _buildPlantIllustrations(), ), @@ -54,7 +54,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.white.withOpacity(0.3), + color: Colors.white, width: 1, ), ), @@ -151,14 +151,13 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { Widget _buildPlantIllustrations() { return Container( decoration: BoxDecoration( - color: const Color.fromARGB(255, 251, 251, 99) - .withOpacity(0.9), // Beige background + color: const Color.fromARGB(255, 251, 251, 99), borderRadius: const BorderRadius.only( topLeft: Radius.circular(40), topRight: Radius.circular(40), ), border: Border.all( - color: Colors.black.withOpacity(0.2), + color: Colors.black, width: 1, ), ), @@ -183,7 +182,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: treeImages.map((imagePath) { - return Container( + return SizedBox( width: plantWidth, height: plantWidth, child: Image.asset( @@ -241,15 +240,15 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { constraints: const BoxConstraints(maxWidth: 100, minHeight: 20), // Limit max width decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), + color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( - color: Colors.green.withOpacity(0.3), + color: Colors.green, width: 1, ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black, blurRadius: 4, offset: const Offset(0, 2), ), @@ -267,7 +266,7 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { color: Colors.green[700], ), const SizedBox(width: 4), - Container( + SizedBox( width: 10, child: Flexible( child: Text( @@ -366,15 +365,15 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { return Container( constraints: const BoxConstraints(maxWidth: 80), // Limit max width decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), + color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( - color: Colors.green.withOpacity(0.3), + color: Colors.green, width: 1, ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black, blurRadius: 4, offset: const Offset(0, 2), ), diff --git a/lib/components/wallet_connect_dialog.dart b/lib/components/wallet_connect_dialog.dart index 7bedf61..4dc8082 100644 --- a/lib/components/wallet_connect_dialog.dart +++ b/lib/components/wallet_connect_dialog.dart @@ -40,9 +40,12 @@ class WalletConnectDialog extends StatelessWidget { onPressed: () async { try { await walletProvider.openWallet(wallet, uri); + // ignore: use_build_context_synchronously Navigator.of(context).pop(); } catch (e) { + // ignore: use_build_context_synchronously Navigator.of(context).pop(); + // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), @@ -75,7 +78,9 @@ class WalletConnectDialog extends StatelessWidget { child: OutlinedButton.icon( onPressed: () async { await Clipboard.setData(ClipboardData(text: uri)); + // ignore: use_build_context_synchronously Navigator.of(context).pop(); + // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('URI copied to clipboard!'), diff --git a/lib/models/wallet_chain_option.dart b/lib/models/wallet_chain_option.dart index bc419b3..fa1a6bd 100644 --- a/lib/models/wallet_chain_option.dart +++ b/lib/models/wallet_chain_option.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -final String ALCHEMY_API_KEY = dotenv.env['ALCHEMY_API_KEY'] ?? ''; +final String alchemyApiKey = dotenv.env['ALCHEMY_API_KEY'] ?? ''; class WalletOption { final String name; @@ -30,14 +30,14 @@ final List walletOptionsList = [ ]; final Map rpcUrls = { - '11155111': 'https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY', - '1': 'https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_API_KEY', + '11155111': 'https://eth-sepolia.g.alchemy.com/v2/$alchemyApiKey', + '1': 'https://eth-mainnet.g.alchemy.com/v2/$alchemyApiKey', }; final Map> chainInfoList = { '1': { 'name': 'Ethereum Mainnet', - 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/$ALCHEMY_API_KEY', + 'rpcUrl': 'https://eth-mainnet.g.alchemy.com/v2/$alchemyApiKey', 'nativeCurrency': { 'name': 'Ether', 'symbol': 'ETH', @@ -47,7 +47,7 @@ final Map> chainInfoList = { }, '11155111': { 'name': 'Sepolia Testnet', - 'rpcUrl': 'https://eth-sepolia.g.alchemy.com/v2/$ALCHEMY_API_KEY', + 'rpcUrl': 'https://eth-sepolia.g.alchemy.com/v2/$alchemyApiKey', 'nativeCurrency': { 'name': 'Sepolia Ether', 'symbol': 'SEP', diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index a01f3e9..55e6c8a 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/navbar_constants.dart'; -import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/profile_widgets/profile_section_widget.dart'; diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index edb4532..d7fdcd6 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -6,7 +6,7 @@ import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/map_widgets/flutter_map_widget.dart'; -import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_NFT_view_widget.dart'; +import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_view_widget.dart'; import 'package:tree_planting_protocol/utils/services/get_current_location.dart'; import 'package:dart_geohash/dart_geohash.dart'; @@ -618,6 +618,7 @@ class _MintNftCoordinatesPageState extends State { ); } + // ignore: unused_element Widget _buildPreviewSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 886ec74..17e4778 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -4,8 +4,6 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; -import 'package:tree_planting_protocol/widgets/map_widgets/flutter_map_widget.dart'; -import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_NFT_view_widget.dart'; import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_view_details_with_map.dart'; class MintNftDetailsPage extends StatefulWidget { @@ -103,14 +101,14 @@ class _MintNftCoordinatesPageState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - const Color(0xFF1CD381).withOpacity(0.05), - const Color(0xFFFAEB96).withOpacity(0.1), + const Color(0xFF1CD381), + const Color(0xFFFAEB96), ], ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: const Color(0xFF1CD381).withOpacity(0.15), + color: const Color(0xFF1CD381), blurRadius: 20, offset: const Offset(0, 8), ), @@ -126,7 +124,7 @@ class _MintNftCoordinatesPageState extends State { gradient: LinearGradient( colors: [ const Color(0xFF1CD381), - const Color(0xFF1CD381).withOpacity(0.8), + const Color(0xFF1CD381), ], ), borderRadius: const BorderRadius.only( @@ -139,7 +137,7 @@ class _MintNftCoordinatesPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white, borderRadius: BorderRadius.circular(14), ), child: const Icon( @@ -208,7 +206,7 @@ class _MintNftCoordinatesPageState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - shadowColor: const Color(0xFF1CD381).withOpacity(0.3), + shadowColor: const Color(0xFF1CD381), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -224,7 +222,7 @@ class _MintNftCoordinatesPageState extends State { Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white, borderRadius: BorderRadius.circular(6), ), child: const Icon( @@ -260,7 +258,7 @@ class _MintNftCoordinatesPageState extends State { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: const Color(0xFF1CD381).withOpacity(0.1), + color: const Color(0xFF1CD381), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -286,12 +284,12 @@ class _MintNftCoordinatesPageState extends State { color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( - color: const Color(0xFFFAEB96).withOpacity(0.5), + color: const Color(0xFFFAEB96), width: 2, ), boxShadow: [ BoxShadow( - color: const Color(0xFF1CD381).withOpacity(0.05), + color: const Color(0xFF1CD381), blurRadius: 8, offset: const Offset(0, 2), ), @@ -337,7 +335,7 @@ class _MintNftCoordinatesPageState extends State { Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: const Color(0xFFFAEB96).withOpacity(0.3), + color: const Color(0xFFFAEB96), borderRadius: BorderRadius.circular(10), ), child: Icon( diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart index e535349..22397cd 100644 --- a/lib/pages/mint_nft/mint_nft_images.dart +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -42,12 +42,15 @@ class _MultipleImageUploadPageState extends State { if (images.isEmpty) return; logger.d('Selected ${images.length} images for upload'); + + // ignore: use_build_context_synchronously + final provider = Provider.of(context, listen: false); + setState(() { _processingImages = images.map((image) => File(image.path)).toList(); _isUploading = true; }); - final provider = Provider.of(context, listen: false); List newHashes = []; for (int i = 0; i < images.length; i++) { @@ -252,7 +255,7 @@ class _MultipleImageUploadPageState extends State { width: 120, height: 120, decoration: BoxDecoration( - color: Colors.green.withOpacity(0.8), + color: Colors.green, borderRadius: BorderRadius.circular(8), ), diff --git a/lib/pages/register_user_page.dart b/lib/pages/register_user_page.dart index f6d19ef..357c338 100644 --- a/lib/pages/register_user_page.dart +++ b/lib/pages/register_user_page.dart @@ -278,7 +278,7 @@ class _RegisterUserPageState extends State { Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: const Color(0xFF1CD381).withOpacity(0.1), + color: const Color(0xFF1CD381), borderRadius: BorderRadius.circular(8), ), child: const Icon( @@ -308,12 +308,12 @@ class _RegisterUserPageState extends State { color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( - color: const Color(0xFFFAEB96).withOpacity(0.5), + color: const Color(0xFFFAEB96), width: 2, ), boxShadow: [ BoxShadow( - color: const Color(0xFF1CD381).withOpacity(0.05), + color: const Color(0xFF1CD381), blurRadius: 8, offset: const Offset(0, 2), ), @@ -444,7 +444,7 @@ Widget _buildFormField({ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: const Color(0xFF1CD381).withOpacity(0.1), + color: const Color(0xFF1CD381), borderRadius: BorderRadius.circular(8), ), child: Icon( @@ -470,12 +470,12 @@ Widget _buildFormField({ color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( - color: const Color(0xFFFAEB96).withOpacity(0.5), + color: const Color(0xFFFAEB96), width: 2, ), boxShadow: [ BoxShadow( - color: const Color(0xFF1CD381).withOpacity(0.05), + color: const Color(0xFF1CD381), blurRadius: 8, offset: const Offset(0, 2), ), diff --git a/lib/providers/mint_nft_provider.dart b/lib/providers/mint_nft_provider.dart index 810be43..6b6b722 100644 --- a/lib/providers/mint_nft_provider.dart +++ b/lib/providers/mint_nft_provider.dart @@ -5,7 +5,6 @@ class MintNftProvider extends ChangeNotifier { double _longitude = 0; String _species = ""; String _details = ""; - String _detailsHash = ""; String _imageUri = ""; String _qrIpfsHash = ""; String _geoHash = ""; diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 4ee1e65..7150bb4 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -366,6 +366,7 @@ class WalletProvider extends ChangeNotifier { } } + // ignore: unused_element String _getCurrentSessionChainId() { final sessions = _web3App!.sessions.getAll(); if (!sessions.isNotEmpty) { diff --git a/lib/utils/constants/contractDetails.dart b/lib/utils/constants/contractDetails.dart deleted file mode 100644 index f2baf0f..0000000 --- a/lib/utils/constants/contractDetails.dart +++ /dev/null @@ -1,6 +0,0 @@ -// CareToken Address: 0x63bBFf441E7b4369ae7F8f8e21CC57e80A9B34fc -// PlanterToken Address: 0x86e63aad461b6C1FE175254b723b4A0e4fd61779 -// VerifierToken Address: 0xFF98f5DA63D5f4e148266273464BbB8537107cE2 -// LegacyToken Address: 0x72A04057221C7e24B447A608a3815E1C5A1823b2 -// TreeNft Address: 0xD0B9957663a7d6bA29638Ef3067d54f832E0f0ED -// OrganisationFactory: 0xB4601651c2882de08049527F323E38e0E568BB63 diff --git a/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart b/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart index bd05e11..a096e72 100644 --- a/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart +++ b/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart @@ -1,5 +1,7 @@ // ignore: constant_identifier_names -const String TreeNftContractABI = '''[ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +const String treeNftContractABI = '''[ { "type": "constructor", "inputs": [ @@ -1709,5 +1711,5 @@ const String TreeNftContractABI = '''[ } ]'''; -const String TreeNFtContractAddress = - "0xD0B9957663a7d6bA29638Ef3067d54f832E0f0ED"; +final String treeNFtContractAddress = + dotenv.env['TREE_NFT_CONTRACT_ADDRESS'] ?? ''; diff --git a/lib/utils/services/contract_read_services.dart b/lib/utils/services/contract_read_services.dart index 1b82124..d34c575 100644 --- a/lib/utils/services/contract_read_services.dart +++ b/lib/utils/services/contract_read_services.dart @@ -1,7 +1,7 @@ -import 'package:web3dart/web3dart.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/constants/contract_abis/tree_nft_contract_abi.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; class ContractReadResult { final bool success; @@ -69,10 +69,10 @@ class ContractReadFunctions { ]; final result = await walletProvider.readContract( - contractAddress: TreeNFtContractAddress, + contractAddress: treeNFtContractAddress, functionName: 'getNFTsByUserPaginated', params: args, - abi: TreeNftContractABI, + abi: treeNftContractABI, ); logger.i("NFTs read successfully: $result"); if (result == null || result.isEmpty) { @@ -117,9 +117,9 @@ class ContractReadFunctions { ); } final result = await walletProvider.readContract( - contractAddress: TreeNFtContractAddress, + contractAddress: treeNFtContractAddress, functionName: 'ping', - abi: TreeNftContractABI, + abi: treeNftContractABI, params: [], ); String pingResponse; @@ -172,9 +172,9 @@ class ContractReadFunctions { EthereumAddress.fromHex(currentAddress); final List args = [userAddress]; final result = await walletProvider.readContract( - contractAddress: TreeNFtContractAddress, + contractAddress: treeNFtContractAddress, functionName: 'getUserProfile', - abi: TreeNftContractABI, + abi: treeNftContractABI, params: args, ); final profile = result.length > 0 ? result[0] ?? [] : []; diff --git a/lib/utils/services/contract_write_functions.dart b/lib/utils/services/contract_write_functions.dart index 9fb0780..fb50c54 100644 --- a/lib/utils/services/contract_write_functions.dart +++ b/lib/utils/services/contract_write_functions.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/constants/contract_abis/tree_nft_contract_abi.dart'; @@ -79,10 +77,10 @@ class ContractWriteFunctions { photos, ]; final txHash = await walletProvider.writeContract( - contractAddress: TreeNFtContractAddress, + contractAddress: treeNFtContractAddress, functionName: 'mintNft', params: args, - abi: TreeNftContractABI, + abi: treeNftContractABI, chainId: walletProvider.currentChainId, ); @@ -120,10 +118,10 @@ class ContractWriteFunctions { } final List args = [name, profilePhotoHash]; final txHash = await walletProvider.writeContract( - contractAddress: TreeNFtContractAddress, + contractAddress: treeNFtContractAddress, functionName: 'registerUserProfile', params: args, - abi: TreeNftContractABI, + abi: treeNftContractABI, chainId: walletProvider.currentChainId, ); diff --git a/lib/utils/services/ipfs_services.dart b/lib/utils/services/ipfs_services.dart index 1c41d52..d216ea5 100644 --- a/lib/utils/services/ipfs_services.dart +++ b/lib/utils/services/ipfs_services.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; -String API_KEY = dotenv.get('API_KEY', fallback: ""); -String API_SECRET = dotenv.get('API_SECRET', fallback: ""); +String apiKey = dotenv.get('API_KEY', fallback: ""); +String apiSecret = dotenv.get('API_SECRET', fallback: ""); Future uploadToIPFS( File imageFile, Function(bool) setUploadingState) async { @@ -13,8 +13,8 @@ Future uploadToIPFS( var url = Uri.parse("https://api.pinata.cloud/pinning/pinFileToIPFS"); var request = http.MultipartRequest("POST", url); request.headers.addAll({ - "pinata_api_key": API_KEY, - "pinata_secret_api_key": API_SECRET, + "pinata_api_key": apiKey, + "pinata_secret_api_key": apiSecret, }); request.files.add(await http.MultipartFile.fromPath("file", imageFile.path)); diff --git a/lib/utils/services/switch_chain_utils.dart b/lib/utils/services/switch_chain_utils.dart index 1a13346..3e16a54 100644 --- a/lib/utils/services/switch_chain_utils.dart +++ b/lib/utils/services/switch_chain_utils.dart @@ -54,7 +54,7 @@ void showChainSelector(BuildContext context, WalletProvider walletProvider) { }, ), ); - }).toList(), + }), const SizedBox(height: 16), ], ), diff --git a/lib/widgets/map_widgets/flutter_map_widget.dart b/lib/widgets/map_widgets/flutter_map_widget.dart index 6e72585..ce2011d 100644 --- a/lib/widgets/map_widgets/flutter_map_widget.dart +++ b/lib/widgets/map_widgets/flutter_map_widget.dart @@ -9,11 +9,10 @@ class CoordinatesMap extends StatefulWidget { final Function(double lat, double lng)? onLocationSelected; const CoordinatesMap( - {Key? key, + {super.key, this.onLocationSelected, required double lat, - required double lng}) - : super(key: key); + required double lng}); @override State createState() => _CoordinatesMapState(); @@ -23,10 +22,6 @@ class _CoordinatesMapState extends State { late MapController _mapController; bool _mapLoaded = false; bool _hasError = false; - String? _errorMessage; - static const double _defaultLat = 28.9845; - static const double _defaultLng = 77.8956; - @override void initState() { super.initState(); @@ -37,8 +32,8 @@ class _CoordinatesMapState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, provider, _) { - final double latitude = provider.getLatitude() ?? _defaultLat; - final double longitude = provider.getLongitude() ?? _defaultLng; + final double latitude = provider.getLatitude(); + final double longitude = provider.getLongitude(); return Container( decoration: BoxDecoration( @@ -91,7 +86,6 @@ class _CoordinatesMapState extends State { onPressed: () { setState(() { _hasError = false; - _errorMessage = null; }); }, child: const Text("Retry"), @@ -138,7 +132,6 @@ class _CoordinatesMapState extends State { if (mounted) { setState(() { _hasError = true; - _errorMessage = 'Network connection issue'; }); } }, @@ -161,7 +154,7 @@ class _CoordinatesMapState extends State { ), if (!_mapLoaded) Container( - color: Colors.white.withOpacity(0.8), + color: Colors.white, child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -179,7 +172,7 @@ class _CoordinatesMapState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -202,7 +195,7 @@ class _CoordinatesMapState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black, blurRadius: 4, offset: const Offset(0, 2), ), @@ -226,7 +219,7 @@ class _CoordinatesMapState extends State { topLeft: Radius.circular(4), topRight: Radius.circular(4), ), - child: Container( + child: SizedBox( width: 40, height: 40, child: const Icon( @@ -257,7 +250,7 @@ class _CoordinatesMapState extends State { bottomLeft: Radius.circular(4), bottomRight: Radius.circular(4), ), - child: Container( + child: SizedBox( width: 40, height: 40, child: const Icon( @@ -281,7 +274,7 @@ class _CoordinatesMapState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.8), + color: Colors.blue, borderRadius: BorderRadius.circular(4), ), child: const Text( @@ -311,8 +304,10 @@ class StaticDisplayMap extends StatefulWidget { final double lng; const StaticDisplayMap( - {Key? key, this.onLocationSelected, required this.lat, required this.lng}) - : super(key: key); + {super.key, + this.onLocationSelected, + required this.lat, + required this.lng}); @override State createState() => _StaticDisplayMapState(); @@ -322,7 +317,6 @@ class _StaticDisplayMapState extends State { late MapController _mapController; bool _mapLoaded = false; bool _hasError = false; - String? _errorMessage; static const double _defaultLat = 28.9845; // Example: Roorkee, India static const double _defaultLng = 77.8956; @@ -400,7 +394,6 @@ class _StaticDisplayMapState extends State { onPressed: () { setState(() { _hasError = false; - _errorMessage = null; }); }, child: const Text("Retry"), @@ -440,7 +433,6 @@ class _StaticDisplayMapState extends State { }); }, onTap: (tapPosition, point) { - // For static display, you might want to disable tap or handle differently if (widget.onLocationSelected != null) { widget.onLocationSelected!(point.latitude, point.longitude); } @@ -454,7 +446,6 @@ class _StaticDisplayMapState extends State { if (mounted) { setState(() { _hasError = true; - _errorMessage = 'Network connection issue'; }); } }, @@ -477,7 +468,7 @@ class _StaticDisplayMapState extends State { ), if (!_mapLoaded) Container( - color: Colors.white.withOpacity(0.8), + color: Colors.white, child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -495,7 +486,7 @@ class _StaticDisplayMapState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -518,7 +509,7 @@ class _StaticDisplayMapState extends State { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black, blurRadius: 4, offset: const Offset(0, 2), ), @@ -542,7 +533,7 @@ class _StaticDisplayMapState extends State { topLeft: Radius.circular(4), topRight: Radius.circular(4), ), - child: Container( + child: SizedBox( width: 40, height: 40, child: const Icon( @@ -573,7 +564,7 @@ class _StaticDisplayMapState extends State { bottomLeft: Radius.circular(4), bottomRight: Radius.circular(4), ), - child: Container( + child: SizedBox( width: 40, height: 40, child: const Icon( @@ -597,7 +588,7 @@ class _StaticDisplayMapState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.8), + color: Colors.blue, borderRadius: BorderRadius.circular(4), ), child: const Text( diff --git a/lib/widgets/nft_display_utils/tree_nft_view_details_with_map.dart b/lib/widgets/nft_display_utils/tree_nft_view_details_with_map.dart index c4a73d3..bb9e6cb 100644 --- a/lib/widgets/nft_display_utils/tree_nft_view_details_with_map.dart +++ b/lib/widgets/nft_display_utils/tree_nft_view_details_with_map.dart @@ -34,7 +34,7 @@ class _NewNFTMapWidgetState extends State { borderRadius: BorderRadius.circular(12.0), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), + color: Colors.black, blurRadius: 8, offset: const Offset(0, 2), ), @@ -64,7 +64,7 @@ class _NewNFTMapWidgetState extends State { color: Colors.white, boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.1), + color: Colors.green, blurRadius: 8, offset: const Offset(0, 2), ), diff --git a/lib/widgets/nft_display_utils/tree_NFT_view_widget.dart b/lib/widgets/nft_display_utils/tree_nft_view_widget.dart similarity index 98% rename from lib/widgets/nft_display_utils/tree_NFT_view_widget.dart rename to lib/widgets/nft_display_utils/tree_nft_view_widget.dart index 4353a64..b8fff3f 100644 --- a/lib/widgets/nft_display_utils/tree_NFT_view_widget.dart +++ b/lib/widgets/nft_display_utils/tree_nft_view_widget.dart @@ -1,3 +1,4 @@ +// ignore: file_names import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; diff --git a/lib/widgets/nft_display_utils/user_nfts_widget.dart b/lib/widgets/nft_display_utils/user_nfts_widget.dart index 69d6559..4bd0bc2 100644 --- a/lib/widgets/nft_display_utils/user_nfts_widget.dart +++ b/lib/widgets/nft_display_utils/user_nfts_widget.dart @@ -105,10 +105,10 @@ class UserNftsWidget extends StatefulWidget { final String userAddress; const UserNftsWidget({ - Key? key, + super.key, required this.isOwnerCalling, required this.userAddress, - }) : super(key: key); + }); @override State createState() => _UserNftsWidgetState(); @@ -118,7 +118,9 @@ class _UserNftsWidgetState extends State { List _nfts = []; bool _isLoading = false; String? _errorMessage; + // ignore: unused_field int _currentPage = 0; + // ignore: unused_field final int _itemsPerPage = 10; int _totalCount = 0; bool _hasMore = true; @@ -193,7 +195,7 @@ class _UserNftsWidgetState extends State { return Card( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), elevation: 8, - shadowColor: Colors.black.withOpacity(0.5), + shadowColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: const BorderSide( @@ -338,44 +340,41 @@ class _UserNftsWidgetState extends State { @override Widget build(BuildContext context) { - return Container( - child: Column( - children: [ - // Header - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.isOwnerCalling ? "Your NFTs" : "User NFTs", + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + if (_totalCount > 0) Text( - widget.isOwnerCalling ? "Your NFTs" : "User NFTs", + '$_totalCount trees', style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.grey, ), ), - if (_totalCount > 0) - Text( - '$_totalCount trees', - style: const TextStyle( - fontSize: 16, - color: Colors.grey, - ), - ), - ], - ), + ], ), + ), - // Content - Expanded( - child: _errorMessage != null - ? _buildErrorWidget() - : _nfts.isEmpty && !_isLoading - ? _buildEmptyWidget() - : _buildNFTsList(), - ), - ], - ), + // Content + Expanded( + child: _errorMessage != null + ? _buildErrorWidget() + : _nfts.isEmpty && !_isLoading + ? _buildEmptyWidget() + : _buildNFTsList(), + ), + ], ); } diff --git a/lib/widgets/profile_widgets/profile_section_widget.dart b/lib/widgets/profile_widgets/profile_section_widget.dart index f8b7320..82aeeb1 100644 --- a/lib/widgets/profile_widgets/profile_section_widget.dart +++ b/lib/widgets/profile_widgets/profile_section_widget.dart @@ -34,9 +34,9 @@ class UserProfileData { try { dynamic actualData = data; return UserProfileData( - name: actualData[2].toString() ?? '', - userAddress: actualData[0].toString() ?? '', - profilePhotoIpfs: actualData[1].toString() ?? '', + name: actualData[2].toString(), + userAddress: actualData[0].toString(), + profilePhotoIpfs: actualData[1].toString(), dateJoined: _toInt(actualData[3]), verificationsRevoked: _toInt(actualData[4]), reportedSpam: _toInt(actualData[5]), @@ -46,9 +46,9 @@ class UserProfileData { legacyTokens: _toInt(actualData[8]), ); } catch (e) { - debugPrint("Error parsing Tree data: $e"); - debugPrint("Data received: $data"); - debugPrint("Data type: ${data.runtimeType}"); + logger.d("Error parsing Tree data: $e"); + logger.d("Data received: $data"); + logger.d("Data type: ${data.runtimeType}"); return UserProfileData( name: '', @@ -244,119 +244,118 @@ class _ProfileSectionWidgetState extends State { } Widget _tokenWidget() { - return Container( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(4.0), - child: SizedBox( - height: 40, - width: 150, - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 251, 251, 99), - border: Border.all( - color: Colors.black, - width: 2, - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black12, // shadow color - blurRadius: 6, // shadow softness - offset: Offset(0, 3), // shadow position - ), - ], + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 40, + width: 150, + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 251, 251, 99), + border: Border.all( + color: Colors.black, + width: 2, ), - child: Center( - child: Text( - 'Planter Tokens : ${_userProfileData!.planterTokens}'))), - ), - ), - Padding( - padding: const EdgeInsets.all(4.0), - child: SizedBox( - height: 40, - width: 150, - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 28, 211, 129), - border: Border.all( - color: Colors.black, - width: 2, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black12, // shadow color + blurRadius: 6, // shadow softness + offset: Offset(0, 3), // shadow position ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: Offset(0, 3), - ), - ], - ), - child: Center( - child: Text( - 'Care Tokens : ${_userProfileData!.careTokens}'))), - ), + ], + ), + child: Center( + child: Text( + 'Planter Tokens : ${_userProfileData!.planterTokens}'))), ), - Padding( - padding: const EdgeInsets.all(4.0), - child: SizedBox( - height: 40, - width: 150, - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 251, 251, 99), - border: Border.all( - color: Colors.black, - width: 2, - ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: Offset(0, 3), - ), - ], + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 40, + width: 150, + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 28, 211, 129), + border: Border.all( + color: Colors.black, + width: 2, ), - child: Center( - child: Text( - 'Verifier Tokens : ${_userProfileData!.verifierTokens}'))), - ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 6, + offset: Offset(0, 3), + ), + ], + ), + child: Center( + child: + Text('Care Tokens : ${_userProfileData!.careTokens}'))), ), - Padding( - padding: const EdgeInsets.all(4.0), - child: SizedBox( - height: 40, - width: 150, - child: Container( - decoration: BoxDecoration( - color: const Color.fromARGB(255, 28, 211, 129), - border: Border.all( - color: Colors.black, - width: 2, + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 40, + width: 150, + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 251, 251, 99), + border: Border.all( + color: Colors.black, + width: 2, + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 6, + offset: Offset(0, 3), ), - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black12, - blurRadius: 6, - offset: Offset(0, 3), - ), - ], + ], + ), + child: Center( + child: Text( + 'Verifier Tokens : ${_userProfileData!.verifierTokens}'))), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 40, + width: 150, + child: Container( + decoration: BoxDecoration( + color: const Color.fromARGB(255, 28, 211, 129), + border: Border.all( + color: Colors.black, + width: 2, ), - child: Center( - child: Text( - 'Legacy Tokens : ${_userProfileData!.legacyTokens}'))), - ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 6, + offset: Offset(0, 3), + ), + ], + ), + child: Center( + child: Text( + 'Legacy Tokens : ${_userProfileData!.legacyTokens}'))), ), - ], - ), + ), + ], ); } + // ignore: unused_element Widget _buildErrorState() { return Container( padding: const EdgeInsets.all(20), diff --git a/lib/widgets/wallet_not_connected_widget.dart b/lib/widgets/wallet_not_connected_widget.dart index 6a8ecec..ad78ed8 100644 --- a/lib/widgets/wallet_not_connected_widget.dart +++ b/lib/widgets/wallet_not_connected_widget.dart @@ -26,6 +26,7 @@ Widget buildWalletNotConnectedWidget(BuildContext context) { onPressed: () { final walletProvider = Provider.of(context, listen: false); + walletProvider.connectWallet(); }, child: const Text('Connect Wallet'), ), From ab84dd5df060c9e085790aa5e67062b13812314d Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sun, 24 Aug 2025 05:09:38 +0530 Subject: [PATCH 2/9] fix: remove error handling in the build comman --- .github/workflows/flutter.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml index 36ee323..38da893 100644 --- a/.github/workflows/flutter.yaml +++ b/.github/workflows/flutter.yaml @@ -32,4 +32,4 @@ jobs: run: dart format --output=none --set-exit-if-changed . - name: Flutter build (apk) - run: flutter build apk --release --no-tree-shake-icons 2>/dev/null | grep -v "deprecated" + run: flutter build apk --release --no-tree-shake-icons | grep -v "deprecated" From 3ad511b8bc14f213adb334268e213b8b271ad9ff Mon Sep 17 00:00:00 2001 From: IronJam13 Date: Tue, 26 Aug 2025 22:20:40 +0530 Subject: [PATCH 3/9] add: add individual nft read function --- lib/pages/tree_details_page.dart | 2 +- .../services/contract_read_services.dart | 66 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index ea732fb..39b674d 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -14,4 +14,4 @@ class TreeDetailsPage extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/utils/services/contract_read_services.dart b/lib/utils/services/contract_read_services.dart index d34c575..a2624f0 100644 --- a/lib/utils/services/contract_read_services.dart +++ b/lib/utils/services/contract_read_services.dart @@ -1,7 +1,7 @@ +import 'package:web3dart/web3dart.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/constants/contract_abis/tree_nft_contract_abi.dart'; -import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; class ContractReadResult { final bool success; @@ -186,4 +186,68 @@ class ContractReadFunctions { ); } } + + static Future getTreeNFTInfo({ + required WalletProvider walletProvider, + required int id, + required int offset, + required int limit, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected"); + return ContractReadResult.error( + errorMessage: 'Please connect your wallet first', + ); + } + + final String address = walletProvider.currentAddress.toString(); + if (!address.startsWith('0x')) { + return ContractReadResult.error( + errorMessage: 'Invalid wallet address format', + ); + } + final List args = [BigInt.from(id)]; + + final treeDetailsResult = await walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getTreeDetailsbyID', + params: args, + abi: treeNftContractABI, + ); + + final tree = + treeDetailsResult.length > 0 ? treeDetailsResult[0] ?? [] : []; + logger.d("Tree Info"); + logger.d(tree); + + final treeVerifiersResult = await walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getTreeNftVerifiersPaginated', + params: [BigInt.from(id), BigInt.from(offset), BigInt.from(limit)], + abi: treeNftContractABI); + + final verifiers = + treeVerifiersResult.length > 0 ? treeVerifiersResult[0] ?? [] : []; + logger.d("Tree Verifiers Info"); + logger.d(verifiers); + + final ownerResult = await walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'ownerOf', + params: [BigInt.from(id)], + abi: treeNftContractABI, + ); + + final owner = ownerResult.isNotEmpty ? ownerResult[0] : null; + return ContractReadResult.success( + data: {'details': tree, 'verifiers': verifiers, 'owner': owner}, + ); + } catch (e) { + logger.e("Error fetching the details of the Tree NFT", error: e); + return ContractReadResult.error( + errorMessage: 'Failed to read the details of the Tree: ${e.toString()}', + ); + } + } } From f7507f5a77aba38274f3086bfed63c1142d54b42 Mon Sep 17 00:00:00 2001 From: IronJam13 Date: Tue, 26 Aug 2025 22:24:01 +0530 Subject: [PATCH 4/9] add: tree details page --- lib/pages/tree_details_page.dart | 304 ++++++++++++++++++++++++++++++- 1 file changed, 296 insertions(+), 8 deletions(-) diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index 39b674d..463b323 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -1,17 +1,305 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_read_services.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/map_widgets/static_map_display_widget.dart'; -class TreeDetailsPage extends StatelessWidget { - final String treeId; +final TREE_VERIFIERS_OFFSET = 0; +final TREE_VERIFIERS_LIMIT = 10; + +class Tree { + final int id; + final int latitude; + final int longitude; + final int planting; + final int death; + final String species; + final String imageUri; + final String qrIpfsHash; + final String metadata; + final List photos; + final String geoHash; + final List ancestors; + final int lastCareTimestamp; + final int careCount; + final List verifiers; + final String owner; + + Tree({ + required this.id, + required this.latitude, + required this.longitude, + required this.planting, + required this.death, + required this.species, + required this.imageUri, + required this.qrIpfsHash, + required this.metadata, + required this.photos, + required this.geoHash, + required this.ancestors, + required this.lastCareTimestamp, + required this.careCount, + required this.verifiers, + required this.owner, + }); + + factory Tree.fromContractData( + List userData, List verifiers, String owner) { + logger.d("User data, Verifiers and Owner"); + logger.d(userData); + logger.d(verifiers); + logger.d(owner); + try { + if (userData is List && verifiers is List) { + return Tree( + id: _toInt(userData[0]), + latitude: _toInt(userData[1]), + longitude: _toInt(userData[2]), + planting: _toInt(userData[3]), + death: _toInt(userData[4]), + species: userData[5]?.toString() ?? '', + imageUri: userData[6]?.toString() ?? '', + qrIpfsHash: userData[7]?.toString() ?? '', + metadata: userData[8]?.toString() ?? '', + photos: userData[9] is List + ? List.from(userData[9].map((p) => p.toString())) + : [], + geoHash: userData[10]?.toString() ?? '', + ancestors: userData[11] is List + ? List.from(userData[11].map((a) => a.toString())) + : [], + lastCareTimestamp: _toInt(userData[12]), + careCount: _toInt(userData[13]), + verifiers: List.from(verifiers.map((a) => a.toString())), + owner: owner, + ); + } + throw Exception("Unexpected data structure: ${userData.runtimeType}"); + } catch (e) { + return Tree( + id: 0, + latitude: 0, + longitude: 0, + planting: 0, + death: 0, + species: 'Unknown', + imageUri: '', + qrIpfsHash: '', + metadata: '', + photos: [], + geoHash: '', + ancestors: [], + lastCareTimestamp: 0, + careCount: 0, + verifiers: [], + owner: '', + ); + } + } + + static int _toInt(dynamic value) { + if (value is BigInt) return value.toInt(); + if (value is int) return value; + return int.tryParse(value.toString()) ?? 0; + } +} +class TreeDetailsPage extends StatefulWidget { + final String treeId; const TreeDetailsPage({super.key, required this.treeId}); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Tree Details - $treeId')), - body: Center( - child: Text('Showing details for tree ID: $treeId'), + State createState() => _TreeDetailsPageState(); +} + +class _TreeDetailsPageState extends State { + String? _errorMessage = ""; + String? loggedInUser = ""; + bool canVerify = false; + bool _isLoading = false; + Tree? treeDetails; + + @override + void initState() { + super.initState(); + _loadTreeDetails(); + } + + static int _toInt(dynamic value) { + if (value is BigInt) return value.toInt(); + if (value is int) return value; + return int.tryParse(value.toString()) ?? 0; + } + + Future _loadTreeDetails() async { + final walletProvider = Provider.of(context, listen: false); + loggedInUser = walletProvider.currentAddress.toString(); + setState(() { + _isLoading = true; + }); + final result = await ContractReadFunctions.getTreeNFTInfo( + walletProvider: walletProvider, + id: _toInt(widget.treeId), + offset: TREE_VERIFIERS_OFFSET, + limit: TREE_VERIFIERS_LIMIT); + if (result.success && result.data != null) { + final List treesData = result.data['details'] ?? []; + final List verifiersData = result.data['verifiers'] ?? []; + final String owner = result.data['owner'].toString(); + treeDetails = Tree.fromContractData(treesData, verifiersData, owner); + canVerify = true; + for (var verifier in verifiersData) { + if (verifier.toString().toLowerCase() == loggedInUser) { + canVerify = false; + break; + } + } + } + setState(() { + _isLoading = false; + }); + } + + Widget _buildMapSection(double screenHeight, double screenWidth) { + final mapHeight = (screenHeight * 0.35).clamp(250.0, 350.0); + final mapWidth = (screenWidth * 0.9); + + return Center( + child: Container( + height: mapHeight, + width: mapWidth.toDouble(), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: StaticCoordinatesMap( + lat: treeDetails!.latitude / 1e6, + lng: treeDetails!.longitude / 1e6, + ), ), ); } -} \ No newline at end of file + + Widget _buildTreeNFTDetailsSection(double screenHeight, double screenWidth) { + final componentHeight = (screenHeight * 0.35).clamp(250.0, 350.0); + final componentWidth = (screenWidth * 0.9); + return Container( + height: componentHeight, + width: componentWidth, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + width: 60, + height: 30, + decoration: BoxDecoration( + border: Border.all(width: 2.0), + color: const Color.fromARGB(255, 28, 211, 129), + borderRadius: BorderRadius.circular(8.0), + ), + child: Center( + child: Text( + (treeDetails!.latitude / 1e6).toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 9, height: 1, color: Colors.white), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + width: 60, + height: 30, + decoration: BoxDecoration( + border: Border.all(width: 2.0), + color: Color.fromARGB(255, 251, 251, 99), + borderRadius: BorderRadius.circular(8.0), + ), + child: Center( + child: Text( + (treeDetails!.longitude / 1e6).toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 9, height: 1, color: Colors.black), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.all(2.0), + child: Container( + width: 60, + height: 30, + decoration: BoxDecoration( + border: Border.all(width: 2.0), + color: const Color(0xFFFF4E63), + borderRadius: BorderRadius.circular(8.0), + ), + child: Center( + child: Text( + (treeDetails!.species.toString()), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 9, height: 1, color: Colors.white), + ), + ), + ), + ), + ], + ), + Text("${treeDetails!.metadata}"), + Text("Care Taken: ${treeDetails!.careCount}"), + Text("Last Care Taken: ${treeDetails!.lastCareTimestamp}"), + treeDetails?.owner == loggedInUser + ? Text("Owner") + : Text("Not the owner"), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final screenWidth = MediaQuery.of(context).size.width; + return BaseScaffold( + title: "Tree NFT Details", + body: _isLoading + ? Text("Loading") + : Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: _buildMapSection(screenHeight, screenWidth), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: _buildTreeNFTDetailsSection( + screenHeight, screenWidth), + ) + ], + ), + )); + } +} From 6e2dfd4bf50b5667cb307227bb164563891c8040 Mon Sep 17 00:00:00 2001 From: IronJam13 Date: Tue, 26 Aug 2025 23:36:16 +0530 Subject: [PATCH 5/9] add: verifiers list --- lib/models/tree_details.dart | 96 +++ lib/pages/tree_details_page.dart | 503 +++++++++++---- .../services/contract_write_functions.dart | 72 +++ .../static_map_display_widget.dart | 587 ++++++++++++++++++ .../tree_nft_details_verifiers_widget.dart | 432 +++++++++++++ 5 files changed, 1561 insertions(+), 129 deletions(-) create mode 100644 lib/models/tree_details.dart create mode 100644 lib/widgets/map_widgets/static_map_display_widget.dart create mode 100644 lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart diff --git a/lib/models/tree_details.dart b/lib/models/tree_details.dart new file mode 100644 index 0000000..15b5e8d --- /dev/null +++ b/lib/models/tree_details.dart @@ -0,0 +1,96 @@ +import 'package:tree_planting_protocol/utils/logger.dart'; + +class Tree { + final int id; + final int latitude; + final int longitude; + final int planting; + final int death; + final String species; + final String imageUri; + final String qrIpfsHash; + final String metadata; + final List photos; + final String geoHash; + final List ancestors; + final int lastCareTimestamp; + final int careCount; + final List verifiers; + final String owner; + + Tree({ + required this.id, + required this.latitude, + required this.longitude, + required this.planting, + required this.death, + required this.species, + required this.imageUri, + required this.qrIpfsHash, + required this.metadata, + required this.photos, + required this.geoHash, + required this.ancestors, + required this.lastCareTimestamp, + required this.careCount, + required this.verifiers, + required this.owner, + }); + + factory Tree.fromContractData( + List userData, List verifiers, String owner) { + logger.d("User data, Verifiers and Owner"); + logger.d(userData); + logger.d(verifiers); + logger.d(owner); + try { + return Tree( + id: _toInt(userData[0]), + latitude: _toInt(userData[1]), + longitude: _toInt(userData[2]), + planting: _toInt(userData[3]), + death: _toInt(userData[4]), + species: userData[5]?.toString() ?? '', + imageUri: userData[6]?.toString() ?? '', + qrIpfsHash: userData[7]?.toString() ?? '', + metadata: userData[8]?.toString() ?? '', + photos: userData[9] is List + ? List.from(userData[9].map((p) => p.toString())) + : [], + geoHash: userData[10]?.toString() ?? '', + ancestors: userData[11] is List + ? List.from(userData[11].map((a) => a.toString())) + : [], + lastCareTimestamp: _toInt(userData[12]), + careCount: _toInt(userData[13]), + verifiers: List.from(verifiers.map((a) => a.toString())), + owner: owner, + ); + } catch (e) { + return Tree( + id: 0, + latitude: 0, + longitude: 0, + planting: 0, + death: 0, + species: 'Unknown', + imageUri: '', + qrIpfsHash: '', + metadata: '', + photos: [], + geoHash: '', + ancestors: [], + lastCareTimestamp: 0, + careCount: 0, + verifiers: [], + owner: '', + ); + } + } + + static int _toInt(dynamic value) { + if (value is BigInt) return value.toInt(); + if (value is int) return value; + return int.tryParse(value.toString()) ?? 0; + } +} diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index 463b323..fe78711 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -3,110 +3,15 @@ import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_read_services.dart'; +import 'package:tree_planting_protocol/utils/services/contract_write_functions.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/map_widgets/static_map_display_widget.dart'; +import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart'; +import 'package:tree_planting_protocol/models/tree_details.dart' hide Tree; final TREE_VERIFIERS_OFFSET = 0; final TREE_VERIFIERS_LIMIT = 10; -class Tree { - final int id; - final int latitude; - final int longitude; - final int planting; - final int death; - final String species; - final String imageUri; - final String qrIpfsHash; - final String metadata; - final List photos; - final String geoHash; - final List ancestors; - final int lastCareTimestamp; - final int careCount; - final List verifiers; - final String owner; - - Tree({ - required this.id, - required this.latitude, - required this.longitude, - required this.planting, - required this.death, - required this.species, - required this.imageUri, - required this.qrIpfsHash, - required this.metadata, - required this.photos, - required this.geoHash, - required this.ancestors, - required this.lastCareTimestamp, - required this.careCount, - required this.verifiers, - required this.owner, - }); - - factory Tree.fromContractData( - List userData, List verifiers, String owner) { - logger.d("User data, Verifiers and Owner"); - logger.d(userData); - logger.d(verifiers); - logger.d(owner); - try { - if (userData is List && verifiers is List) { - return Tree( - id: _toInt(userData[0]), - latitude: _toInt(userData[1]), - longitude: _toInt(userData[2]), - planting: _toInt(userData[3]), - death: _toInt(userData[4]), - species: userData[5]?.toString() ?? '', - imageUri: userData[6]?.toString() ?? '', - qrIpfsHash: userData[7]?.toString() ?? '', - metadata: userData[8]?.toString() ?? '', - photos: userData[9] is List - ? List.from(userData[9].map((p) => p.toString())) - : [], - geoHash: userData[10]?.toString() ?? '', - ancestors: userData[11] is List - ? List.from(userData[11].map((a) => a.toString())) - : [], - lastCareTimestamp: _toInt(userData[12]), - careCount: _toInt(userData[13]), - verifiers: List.from(verifiers.map((a) => a.toString())), - owner: owner, - ); - } - throw Exception("Unexpected data structure: ${userData.runtimeType}"); - } catch (e) { - return Tree( - id: 0, - latitude: 0, - longitude: 0, - planting: 0, - death: 0, - species: 'Unknown', - imageUri: '', - qrIpfsHash: '', - metadata: '', - photos: [], - geoHash: '', - ancestors: [], - lastCareTimestamp: 0, - careCount: 0, - verifiers: [], - owner: '', - ); - } - } - - static int _toInt(dynamic value) { - if (value is BigInt) return value.toInt(); - if (value is int) return value; - return int.tryParse(value.toString()) ?? 0; - } -} - class TreeDetailsPage extends StatefulWidget { final String treeId; const TreeDetailsPage({super.key, required this.treeId}); @@ -116,7 +21,6 @@ class TreeDetailsPage extends StatefulWidget { } class _TreeDetailsPageState extends State { - String? _errorMessage = ""; String? loggedInUser = ""; bool canVerify = false; bool _isLoading = false; @@ -125,7 +29,7 @@ class _TreeDetailsPageState extends State { @override void initState() { super.initState(); - _loadTreeDetails(); + loadTreeDetails(); } static int _toInt(dynamic value) { @@ -134,7 +38,7 @@ class _TreeDetailsPageState extends State { return int.tryParse(value.toString()) ?? 0; } - Future _loadTreeDetails() async { + Future loadTreeDetails() async { final walletProvider = Provider.of(context, listen: false); loggedInUser = walletProvider.currentAddress.toString(); setState(() { @@ -150,14 +54,17 @@ class _TreeDetailsPageState extends State { final List verifiersData = result.data['verifiers'] ?? []; final String owner = result.data['owner'].toString(); treeDetails = Tree.fromContractData(treesData, verifiersData, owner); + logger.d("Verifiers data: $verifiersData"); canVerify = true; for (var verifier in verifiersData) { - if (verifier.toString().toLowerCase() == loggedInUser) { + if (verifier[0].toString().toLowerCase() == + loggedInUser?.toLowerCase()) { canVerify = false; break; } } } + logger.d("Tree Details: ${treeDetails?.verifiers}"); setState(() { _isLoading = false; }); @@ -190,11 +97,10 @@ class _TreeDetailsPageState extends State { ); } - Widget _buildTreeNFTDetailsSection(double screenHeight, double screenWidth) { - final componentHeight = (screenHeight * 0.35).clamp(250.0, 350.0); + Widget _buildTreeNFTDetailsSection( + double screenHeight, double screenWidth, BuildContext context) { final componentWidth = (screenWidth * 0.9); - return Container( - height: componentHeight, + return SizedBox( width: componentWidth, child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -264,17 +170,352 @@ class _TreeDetailsPageState extends State { ), ], ), - Text("${treeDetails!.metadata}"), - Text("Care Taken: ${treeDetails!.careCount}"), - Text("Last Care Taken: ${treeDetails!.lastCareTimestamp}"), - treeDetails?.owner == loggedInUser - ? Text("Owner") - : Text("Not the owner"), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailCard("Description", treeDetails!.metadata), + const SizedBox(height: 12), + _buildDetailCard( + "Care Taken", "${treeDetails!.careCount} times"), + const SizedBox(height: 12), + _buildDetailCard("Last Care", + _formatTimestamp(treeDetails!.lastCareTimestamp)), + const SizedBox(height: 12), + ], + ), + ), + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: canVerify + ? [Colors.green.shade400, Colors.green.shade600] + : [Colors.grey.shade300, Colors.grey.shade400], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12.0), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Icon( + canVerify ? Icons.verified : Icons.lock, + color: Colors.white, + size: 32, + ), + const SizedBox(height: 8), + Text( + canVerify ? "Tree Verification" : "Verification Disabled", + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + canVerify + ? "Confirm this tree's authenticity" + : "You cannot verify this tree", + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: canVerify + ? () async { + _showVerificationDialog(); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: + canVerify ? Colors.green.shade600 : Colors.grey, + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(25), + ), + elevation: 0, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + canVerify ? Icons.check_circle : Icons.cancel, + size: 20, + ), + const SizedBox(width: 8), + const Text( + "Verify Tree", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), + ), + ], + ), + ), + treeVerifiersSection(loggedInUser, treeDetails, loadTreeDetails, context), ], ), ); } + String _formatTimestamp(int timestamp) { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return "${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}"; + } + + Widget _buildDetailCard(String title, String value) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ], + ), + ); + } + + void _showVerificationDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + title: Row( + children: [ + Icon(Icons.verified, color: Colors.green.shade600), + const SizedBox(width: 8), + const Text("Verify Tree"), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Are you sure you want to verify this tree? This action will:", + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 12), + _buildVerificationPoint("• Record verification on blockchain"), + _buildVerificationPoint("• Require gas fees for transaction"), + _buildVerificationPoint("• Cannot be undone once confirmed"), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + "Cancel", + style: TextStyle(color: Colors.grey.shade600), + ), + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + await _performVerification(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: const Text("Verify"), + ), + ], + ); + }, + ); + } + + Widget _buildVerificationPoint(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Text( + text, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ); + } + + Future _performVerification() async { + logger.d("Starting tree verification process"); + logger.d("Logged in user: $loggedInUser"); + logger.d("Tree ID: ${treeDetails!.id}"); + + try { + // Get wallet provider + WalletProvider walletProvider = + Provider.of(context, listen: false); + + // Validate wallet connection + if (!walletProvider.isConnected) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text("Please connect your wallet first"), + backgroundColor: Colors.orange.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + const SizedBox(width: 16), + const Text("Processing verification..."), + ], + ), + backgroundColor: Colors.blue.shade600, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 10), + ), + ); + + final result = await ContractWriteFunctions.verifyTree( + walletProvider: walletProvider, + treeId: treeDetails!.id, + description: "Tree verified by user", + photos: ["verification_photo"], + ); + + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + if (result.success) { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 8), + const Text("Tree verification submitted successfully!"), + ], + ), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + + await loadTreeDetails(); + } else { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(Icons.error, color: Colors.white), + const SizedBox(width: 8), + Expanded( + child: Text("Verification failed: ${result.errorMessage}")), + ], + ), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 5), + ), + ); + } + } catch (e) { + + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + String errorMessage = "Verification failed"; + if (e.toString().contains("No element")) { + errorMessage = + "Wallet connection issue. Please reconnect your wallet and try again."; + } else if (e.toString().contains("timeout")) { + errorMessage = "Transaction timeout. Please try again."; + } else if (e.toString().contains("user rejected")) { + errorMessage = "Transaction was cancelled by user."; + } else if (e.toString().contains("insufficient funds")) { + errorMessage = "Insufficient funds for gas fees."; + } else { + errorMessage = "Error: ${e.toString()}"; + } + + logger.e("Verification error: $e"); + + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon(Icons.error_outline, color: Colors.white), + const SizedBox(width: 8), + Expanded(child: Text(errorMessage)), + ], + ), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 5), + ), + ); + } + } + @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; @@ -282,23 +523,27 @@ class _TreeDetailsPageState extends State { return BaseScaffold( title: "Tree NFT Details", body: _isLoading - ? Text("Loading") - : Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: _buildMapSection(screenHeight, screenWidth), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: _buildTreeNFTDetailsSection( - screenHeight, screenWidth), - ) - ], + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: _buildMapSection(screenHeight, screenWidth), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: _buildTreeNFTDetailsSection( + screenHeight, screenWidth, context), + ), + const SizedBox(height: 20), // Extra bottom padding + ], + ), ), )); } diff --git a/lib/utils/services/contract_write_functions.dart b/lib/utils/services/contract_write_functions.dart index fb50c54..920b92b 100644 --- a/lib/utils/services/contract_write_functions.dart +++ b/lib/utils/services/contract_write_functions.dart @@ -138,4 +138,76 @@ class ContractWriteFunctions { ); } } + + static Future verifyTree( + {required WalletProvider walletProvider, + required int treeId, + required String description, + required List photos}) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for verifying tree"); + return ContractWriteResult.error( + errorMessage: 'Please connect your wallet before verifying.', + ); + } + + final List args = [BigInt.from(treeId), photos, description]; + final txHash = await walletProvider.writeContract( + contractAddress: treeNFtContractAddress, + functionName: 'verify', + params: args, + abi: treeNftContractABI, + chainId: walletProvider.currentChainId, + ); + + logger.i("Tree verification transaction sent: $txHash"); + + return ContractWriteResult.success( + transactionHash: txHash, + data: {'treeId': treeId}, + ); + } catch (e) { + logger.e("Error verifying Tree", error: e); + return ContractWriteResult.error( + errorMessage: e.toString(), + ); + } + } + + static Future removeVerification( + {required WalletProvider walletProvider, + required int treeId, + required String address}) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for removing verification"); + return ContractWriteResult.error( + errorMessage: + 'Please connect your wallet before removing verification.', + ); + } + + final List args = [BigInt.from(treeId), address]; + final txHash = await walletProvider.writeContract( + contractAddress: treeNFtContractAddress, + functionName: 'removeVerification', + params: args, + abi: treeNftContractABI, + chainId: walletProvider.currentChainId, + ); + + logger.i("Remove verification transaction sent: $txHash"); + + return ContractWriteResult.success( + transactionHash: txHash, + data: {'treeId': treeId, 'address': address}, + ); + } catch (e) { + logger.e("Error removing verification", error: e); + return ContractWriteResult.error( + errorMessage: e.toString(), + ); + } + } } diff --git a/lib/widgets/map_widgets/static_map_display_widget.dart b/lib/widgets/map_widgets/static_map_display_widget.dart new file mode 100644 index 0000000..a95c9c5 --- /dev/null +++ b/lib/widgets/map_widgets/static_map_display_widget.dart @@ -0,0 +1,587 @@ +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; + +class StaticCoordinatesMap extends StatefulWidget { + final double lat; + final double lng; + const StaticCoordinatesMap({super.key, required this.lat, required this.lng}); + + @override + State createState() => _CoordinatesMapState(); +} + +class _CoordinatesMapState extends State { + late MapController _mapController; + bool _mapLoaded = false; + bool _hasError = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _mapController = MapController(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + final double latitude = widget.lat; + final double longitude = widget.lng; + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _hasError + ? _buildErrorWidget() + : _buildMapWidget(latitude, longitude), + ), + ); + }, + ); + } + + Widget _buildErrorWidget() { + return Container( + color: Colors.grey[100], + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.map_outlined, + size: 60, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + "Map Unavailable", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "Please use the coordinate fields below", + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _hasError = false; + _errorMessage = null; + }); + }, + child: const Text("Retry"), + ), + ], + ), + ), + ); + } + + Widget _buildMapWidget(double latitude, double longitude) { + return Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(latitude, longitude), + initialZoom: 4.0, + minZoom: 3.0, + maxZoom: 18.0, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all, + ), + onMapReady: () { + setState(() { + _mapLoaded = true; + }); + }, + onTap: (tapPosition, point) { + final provider = + Provider.of(context, listen: false); + provider.setLatitude(point.latitude); + provider.setLongitude(point.longitude); + }, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'tree_planting_protocol', + errorTileCallback: (tile, error, stackTrace) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = 'Network connection issue'; + }); + } + }, + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(latitude, longitude), + width: 80, + height: 80, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], + ), + if (!_mapLoaded) + Container( + color: Colors.white, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text("Loading map..."), + ], + ), + ), + ), + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + Positioned( + right: 8, + top: 50, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom < 18.0) { + _mapController.move( + _mapController.camera.center, + currentZoom + 1, + ); + } + }, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.add, + color: Colors.black87, + size: 20, + ), + ), + ), + ), + Container( + height: 1, + color: Colors.grey[300], + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom > 3.0) { + _mapController.move( + _mapController.camera.center, + currentZoom - 1, + ); + } + }, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.remove, + color: Colors.black87, + size: 20, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ); + } + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } +} + +class StaticDisplayMap extends StatefulWidget { + final Function(double lat, double lng)? onLocationSelected; + final double lat; + final double lng; + + const StaticDisplayMap( + {Key? key, this.onLocationSelected, required this.lat, required this.lng}) + : super(key: key); + + @override + State createState() => _StaticDisplayMapState(); +} + +class _StaticDisplayMapState extends State { + late MapController _mapController; + bool _mapLoaded = false; + bool _hasError = false; + String? _errorMessage; + static const double _defaultLat = 28.9845; // Example: Roorkee, India + static const double _defaultLng = 77.8956; + + @override + void initState() { + super.initState(); + _mapController = MapController(); + } + + double _sanitizeCoordinate(double value, double defaultValue) { + if (value.isNaN || + value.isInfinite || + value == double.infinity || + value == double.negativeInfinity) { + logger.e( + 'Invalid coordinate detected: $value, using default: $defaultValue'); + return defaultValue; + } + return value; + } + + @override + Widget build(BuildContext context) { + double latitude = _sanitizeCoordinate(widget.lat, _defaultLat); + double longitude = _sanitizeCoordinate(widget.lng, _defaultLng); + latitude = latitude.clamp(-90.0, 90.0); + longitude = longitude.clamp(-180.0, 180.0); + + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: _hasError + ? _buildErrorWidget() + : _buildMapWidget(latitude, longitude), + ), + ); + } + + Widget _buildErrorWidget() { + return Container( + color: Colors.grey[100], + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.map_outlined, + size: 60, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + Text( + "Map Unavailable", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 8), + Text( + "Please use the coordinate fields below", + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + setState(() { + _hasError = false; + _errorMessage = null; + }); + }, + child: const Text("Retry"), + ), + ], + ), + ), + ); + } + + Widget _buildMapWidget(double latitude, double longitude) { + if (latitude.isNaN || + latitude.isInfinite || + longitude.isNaN || + longitude.isInfinite) { + logger.e( + 'ERROR: Invalid coordinates in _buildMapWidget - lat: $latitude, lng: $longitude'); + latitude = _defaultLat; + longitude = _defaultLng; + } + + return Stack( + children: [ + FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: LatLng(latitude, longitude), + initialZoom: 15.0, + minZoom: 3.0, + maxZoom: 18.0, + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all, + ), + onMapReady: () { + setState(() { + _mapLoaded = true; + }); + }, + onTap: (tapPosition, point) {}, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'tree_planting_protocol', + errorTileCallback: (tile, error, stackTrace) { + if (mounted) { + setState(() { + _hasError = true; + _errorMessage = 'Network connection issue'; + }); + } + }, + ), + MarkerLayer( + markers: [ + Marker( + point: LatLng(latitude, longitude), + width: 80, + height: 80, + child: const Icon( + Icons.location_pin, + color: Colors.red, + size: 40, + ), + ), + ], + ), + ], + ), + if (!_mapLoaded) + Container( + color: Colors.white, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text("Loading map..."), + ], + ), + ), + ), + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + Positioned( + right: 8, + top: 50, + child: Column( + children: [ + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom < 18.0) { + _mapController.move( + _mapController.camera.center, + currentZoom + 1, + ); + } + }, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(4), + topRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.add, + color: Colors.black87, + size: 20, + ), + ), + ), + ), + Container( + height: 1, + color: Colors.grey[300], + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom > 3.0) { + _mapController.move( + _mapController.camera.center, + currentZoom - 1, + ); + } + }, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(4), + bottomRight: Radius.circular(4), + ), + child: Container( + width: 40, + height: 40, + child: const Icon( + Icons.remove, + color: Colors.black87, + size: 20, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + Positioned( + bottom: 8, + left: 8, + right: 8, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + "Static display • Use zoom buttons or pinch to zoom", + style: TextStyle( + color: Colors.white, + fontSize: 11, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + } + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } +} diff --git a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart new file mode 100644 index 0000000..33774ad --- /dev/null +++ b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart @@ -0,0 +1,432 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_write_functions.dart'; + +class Tree { + final int id; + final int latitude; + final int longitude; + final int planting; + final int death; + final String species; + final String imageUri; + final String qrIpfsHash; + final String metadata; + final List photos; + final String geoHash; + final List ancestors; + final int lastCareTimestamp; + final int careCount; + final List verifiers; + final String owner; + + Tree({ + required this.id, + required this.latitude, + required this.longitude, + required this.planting, + required this.death, + required this.species, + required this.imageUri, + required this.qrIpfsHash, + required this.metadata, + required this.photos, + required this.geoHash, + required this.ancestors, + required this.lastCareTimestamp, + required this.careCount, + required this.verifiers, + required this.owner, + }); + + factory Tree.fromContractData( + List userData, List verifiers, String owner) { + logger.d("User data, Verifiers and Owner"); + logger.d(userData); + logger.d(verifiers); + logger.d(owner); + try { + return Tree( + id: _toInt(userData[0]), + latitude: _toInt(userData[1]), + longitude: _toInt(userData[2]), + planting: _toInt(userData[3]), + death: _toInt(userData[4]), + species: userData[5]?.toString() ?? '', + imageUri: userData[6]?.toString() ?? '', + qrIpfsHash: userData[7]?.toString() ?? '', + metadata: userData[8]?.toString() ?? '', + photos: userData[9] is List + ? List.from(userData[9].map((p) => p.toString())) + : [], + geoHash: userData[10]?.toString() ?? '', + ancestors: userData[11] is List + ? List.from(userData[11].map((a) => a.toString())) + : [], + lastCareTimestamp: _toInt(userData[12]), + careCount: _toInt(userData[13]), + verifiers: List.from(verifiers.map((a) => a.toString())), + owner: owner, + ); + } catch (e) { + return Tree( + id: 0, + latitude: 0, + longitude: 0, + planting: 0, + death: 0, + species: 'Unknown', + imageUri: '', + qrIpfsHash: '', + metadata: '', + photos: [], + geoHash: '', + ancestors: [], + lastCareTimestamp: 0, + careCount: 0, + verifiers: [], + owner: '', + ); + } + } + + static int _toInt(dynamic value) { + if (value is BigInt) return value.toInt(); + if (value is int) return value; + return int.tryParse(value.toString()) ?? 0; + } +} + +Future _removeVerifier( + String verifierAddress, BuildContext context, Tree? treeDetails, Function loadTreeDetails) async { + try { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + const Text("Remove verifier functionality not yet implemented"), + backgroundColor: Colors.orange.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + final provider = Provider.of(context, listen: false); + final result = await ContractWriteFunctions.removeVerification( + walletProvider: provider, + treeId: treeDetails!.id, + address: verifierAddress, + ); + + if (result.success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text("Verifier removed successfully!"), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + // Refresh the tree details + await loadTreeDetails(); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Failed to remove verifier: ${result.errorMessage}"), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + } + + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Error: $e"), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + } +} + +Widget treeVerifiersSection( + String? loggedInUser, Tree? treeDetails, Function loadTreeDetails ,BuildContext context) { + if (treeDetails?.verifiers == null || treeDetails!.verifiers.isEmpty) { + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + children: [ + Icon( + Icons.group_off, + color: Colors.grey.shade400, + size: 32, + ), + const SizedBox(height: 8), + Text( + "No Verifiers Yet", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + "This tree hasn't been verified by anyone", + style: TextStyle( + color: Colors.grey.shade500, + fontSize: 14, + ), + ), + ], + ), + ); + } + + final isOwner = treeDetails?.owner == loggedInUser; + + return Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.blue.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.verified_user, + color: Colors.blue.shade600, + size: 24, + ), + const SizedBox(width: 8), + Text( + "Tree Verifiers", + style: TextStyle( + color: Colors.blue.shade800, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + "${treeDetails!.verifiers.length}", + style: TextStyle( + color: Colors.blue.shade800, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + isOwner + ? "Tap the ✕ to remove a verifier (owner only)" + : "Users who have verified this tree", + style: TextStyle( + color: Colors.blue.shade600, + fontSize: 14, + ), + ), + const SizedBox(height: 16), + ...treeDetails!.verifiers.asMap().entries.map((entry) { + final index = entry.key; + final verifier = entry.value; + return _buildVerifierCard(verifier, index, isOwner, loadTreeDetails, treeDetails, context); + }), + ], + ), + ); +} + +Widget _buildVerifierCard( + String verifierAddress, int index, bool canRemove, Function loadTreeDetails, Tree? treeDetails, BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 8.0), + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + border: Border.all(color: Colors.blue.shade100), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + Icons.person, + color: Colors.blue.shade600, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Verifier ${index + 1}", + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + "${verifierAddress.substring(0, 6)}...${verifierAddress.substring(verifierAddress.length - 4)}", + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + if (canRemove) + GestureDetector( + onTap: () => + _showRemoveVerifierDialog(verifierAddress, index, context, treeDetails, loadTreeDetails), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.red.shade200), + ), + child: Icon( + Icons.close, + color: Colors.red.shade600, + size: 18, + ), + ), + ), + ], + ), + ); +} + +void _showRemoveVerifierDialog( + String verifierAddress, int index, BuildContext context, Tree? treeDetails, Function loadTreeDetails) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.0), + ), + title: Row( + children: [ + Icon(Icons.warning, color: Colors.orange.shade600), + const SizedBox(width: 8), + const Text("Remove Verifier"), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Are you sure you want to remove this verifier?", + style: TextStyle(fontSize: 16), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon(Icons.person, color: Colors.grey.shade600), + const SizedBox(width: 8), + Expanded( + child: Text( + verifierAddress, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Text( + "This action:", + style: TextStyle( + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + _buildRemovalPoint("• Will require gas fees"), + _buildRemovalPoint("• Cannot be undone"), + _buildRemovalPoint("• Removes verification permanently"), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + "Cancel", + style: TextStyle(color: Colors.grey.shade600), + ), + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + await _removeVerifier(verifierAddress, context, treeDetails, loadTreeDetails); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + child: const Text("Remove"), + ), + ], + ); + }, + ); +} + +Widget _buildRemovalPoint(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Text( + text, + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade700, + ), + ), + ); +} From 4a2aa5b702d6fb284ef78dbde2d44d498192dd09 Mon Sep 17 00:00:00 2001 From: IronJam13 Date: Wed, 27 Aug 2025 16:53:15 +0530 Subject: [PATCH 6/9] chore: remove verifier functionality --- lib/pages/tree_details_page.dart | 4 +- .../contract_abis/tree_nft_contract_abi.dart | 1718 +---------------- .../services/contract_read_services.dart | 1 + .../services/contract_write_functions.dart | 7 +- .../tree_nft_details_verifiers_widget.dart | 346 +++- 5 files changed, 265 insertions(+), 1811 deletions(-) diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index fe78711..832e003 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -273,7 +273,8 @@ class _TreeDetailsPageState extends State { ], ), ), - treeVerifiersSection(loggedInUser, treeDetails, loadTreeDetails, context), + treeVerifiersSection( + loggedInUser, treeDetails, loadTreeDetails, context), ], ), ); @@ -478,7 +479,6 @@ class _TreeDetailsPageState extends State { ); } } catch (e) { - // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).hideCurrentSnackBar(); diff --git a/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart b/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart index a096e72..c41b5cd 100644 --- a/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart +++ b/lib/utils/constants/contract_abis/tree_nft_contract_abi.dart @@ -1,1715 +1,15 @@ // ignore: constant_identifier_names import 'package:flutter_dotenv/flutter_dotenv.dart'; -const String treeNftContractABI = '''[ - { - "type": "constructor", - "inputs": [ - { - "name": "_careTokenContract", - "type": "address", - "internalType": "address" - }, - { - "name": "_planterTokenContract", - "type": "address", - "internalType": "address" - }, - { - "name": "_verifierTokenContract", - "type": "address", - "internalType": "address" - }, - { - "name": "_legacyTokenContract", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "approve", - "inputs": [ - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "balanceOf", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "careTokenContract", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract CareToken" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getAllNFTs", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "internalType": "struct Tree[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getApproved", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getNFTsByUser", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "internalType": "struct Tree[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getNFTsByUserPaginated", - "inputs": [ - { - "name": "user", - "type": "address", - "internalType": "address" - }, - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "trees", - "type": "tuple[]", - "internalType": "struct Tree[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "totalCount", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getRecentTreesPaginated", - "inputs": [ - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "paginatedTrees", - "type": "tuple[]", - "internalType": "struct Tree[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "totalCount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "hasMore", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getTreeDetailsbyID", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple", - "internalType": "struct Tree", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getTreeNftVerifiers", - "inputs": [ - { - "name": "_tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "internalType": "struct TreeNftVerification[]", - "components": [ - { - "name": "verifier", - "type": "address", - "internalType": "address" - }, - { - "name": "timestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "proofHashes", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "description", - "type": "string", - "internalType": "string" - }, - { - "name": "isHidden", - "type": "bool", - "internalType": "bool" - }, - { - "name": "treeNftId", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getTreeNftVerifiersPaginated", - "inputs": [ - { - "name": "_tokenId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "verifications", - "type": "tuple[]", - "internalType": "struct TreeNftVerification[]", - "components": [ - { - "name": "verifier", - "type": "address", - "internalType": "address" - }, - { - "name": "timestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "proofHashes", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "description", - "type": "string", - "internalType": "string" - }, - { - "name": "isHidden", - "type": "bool", - "internalType": "bool" - }, - { - "name": "treeNftId", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "totalCount", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "visiblecount", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getUserProfile", - "inputs": [ - { - "name": "userAddress", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "userDetails", - "type": "tuple", - "internalType": "struct UserDetails", - "components": [ - { - "name": "userAddress", - "type": "address", - "internalType": "address" - }, - { - "name": "profilePhotoIpfs", - "type": "string", - "internalType": "string" - }, - { - "name": "name", - "type": "string", - "internalType": "string" - }, - { - "name": "dateJoined", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "verificationsRevoked", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "reportedSpam", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "verifierTokens", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planterTokens", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "legacyTokens", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careTokens", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getVerifiedTreesByUser", - "inputs": [ - { - "name": "verifier", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "tuple[]", - "internalType": "struct Tree[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "getVerifiedTreesByUserPaginated", - "inputs": [ - { - "name": "verifier", - "type": "address", - "internalType": "address" - }, - { - "name": "offset", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "limit", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "trees", - "type": "tuple[]", - "internalType": "struct Tree[]", - "components": [ - { - "name": "id", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "planting", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "death", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "photos", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "ancestors", - "type": "address[]", - "internalType": "address[]" - }, - { - "name": "lastCareTimestamp", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "careCount", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "name": "totalCount", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isApprovedForAll", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - }, - { - "name": "operator", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "isVerified", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "verifier", - "type": "address", - "internalType": "address" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "legacyToken", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract LegacyToken" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "markDead", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "minimumTimeToMarkTreeDead", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "uint256", - "internalType": "uint256" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "mintNft", - "inputs": [ - { - "name": "latitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "longitude", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "species", - "type": "string", - "internalType": "string" - }, - { - "name": "imageUri", - "type": "string", - "internalType": "string" - }, - { - "name": "qrIpfsHash", - "type": "string", - "internalType": "string" - }, - { - "name": "metadata", - "type": "string", - "internalType": "string" - }, - { - "name": "geoHash", - "type": "string", - "internalType": "string" - }, - { - "name": "initialPhotos", - "type": "string[]", - "internalType": "string[]" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "name", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "owner", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "ownerOf", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "address" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "ping", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "pure" - }, - { - "type": "function", - "name": "planterTokenContract", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract PlanterToken" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "registerUserProfile", - "inputs": [ - { - "name": "_name", - "type": "string", - "internalType": "string" - }, - { - "name": "_profilePhotoHash", - "type": "string", - "internalType": "string" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "removeVerification", - "inputs": [ - { - "name": "_verificationId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "renounceOwnership", - "inputs": [], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "safeTransferFrom", - "inputs": [ - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "safeTransferFrom", - "inputs": [ - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "data", - "type": "bytes", - "internalType": "bytes" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "setApprovalForAll", - "inputs": [ - { - "name": "operator", - "type": "address", - "internalType": "address" - }, - { - "name": "approved", - "type": "bool", - "internalType": "bool" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "supportsInterface", - "inputs": [ - { - "name": "interfaceId", - "type": "bytes4", - "internalType": "bytes4" - } - ], - "outputs": [ - { - "name": "", - "type": "bool", - "internalType": "bool" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "symbol", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "tokenURI", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [ - { - "name": "", - "type": "string", - "internalType": "string" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "transferFrom", - "inputs": [ - { - "name": "from", - "type": "address", - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "transferOwnership", - "inputs": [ - { - "name": "newOwner", - "type": "address", - "internalType": "address" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "updateUserDetails", - "inputs": [ - { - "name": "_name", - "type": "string", - "internalType": "string" - }, - { - "name": "_profilePhotoHash", - "type": "string", - "internalType": "string" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "function", - "name": "verifierTokenContract", - "inputs": [], - "outputs": [ - { - "name": "", - "type": "address", - "internalType": "contract VerifierToken" - } - ], - "stateMutability": "view" - }, - { - "type": "function", - "name": "verify", - "inputs": [ - { - "name": "_tokenId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "_proofHashes", - "type": "string[]", - "internalType": "string[]" - }, - { - "name": "_description", - "type": "string", - "internalType": "string" - } - ], - "outputs": [], - "stateMutability": "nonpayable" - }, - { - "type": "event", - "name": "Approval", - "inputs": [ - { - "name": "owner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "approved", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "ApprovalForAll", - "inputs": [ - { - "name": "owner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "operator", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "approved", - "type": "bool", - "indexed": false, - "internalType": "bool" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "OwnershipTransferred", - "inputs": [ - { - "name": "previousOwner", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "newOwner", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "Transfer", - "inputs": [ - { - "name": "from", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "to", - "type": "address", - "indexed": true, - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - } - ], - "anonymous": false - }, - { - "type": "event", - "name": "VerificationRemoved", - "inputs": [ - { - "name": "verificationId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "treeNftId", - "type": "uint256", - "indexed": true, - "internalType": "uint256" - }, - { - "name": "verifier", - "type": "address", - "indexed": true, - "internalType": "address" - } - ], - "anonymous": false - }, - { - "type": "error", - "name": "ERC721IncorrectOwner", - "inputs": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - }, - { - "name": "owner", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC721InsufficientApproval", - "inputs": [ - { - "name": "operator", - "type": "address", - "internalType": "address" - }, - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "ERC721InvalidApprover", - "inputs": [ - { - "name": "approver", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC721InvalidOperator", - "inputs": [ - { - "name": "operator", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC721InvalidOwner", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC721InvalidReceiver", - "inputs": [ - { - "name": "receiver", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC721InvalidSender", - "inputs": [ - { - "name": "sender", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "ERC721NonexistentToken", - "inputs": [ - { - "name": "tokenId", - "type": "uint256", - "internalType": "uint256" - } - ] - }, - { - "type": "error", - "name": "InvalidCoordinates", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidInput", - "inputs": [] - }, - { - "type": "error", - "name": "InvalidTreeID", - "inputs": [] - }, - { - "type": "error", - "name": "MinimumMarkDeadTimeNotReached", - "inputs": [] - }, - { - "type": "error", - "name": "NotTreeOwner", - "inputs": [] - }, - { - "type": "error", - "name": "OwnableInvalidOwner", - "inputs": [ - { - "name": "owner", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "OwnableUnauthorizedAccount", - "inputs": [ - { - "name": "account", - "type": "address", - "internalType": "address" - } - ] - }, - { - "type": "error", - "name": "PaginationLimitExceeded", - "inputs": [] - }, - { - "type": "error", - "name": "TreeAlreadyDead", - "inputs": [] - }, - { - "type": "error", - "name": "UserAlreadyRegistered", - "inputs": [] - }, - { - "type": "error", - "name": "UserNotRegistered", - "inputs": [] - } - ]'''; +const String treeNftContractABI = + '''[{"type":"constructor","inputs":[{"name":"_careTokenContract","type":"address","internalType":"address"},{"name":"_planterTokenContract","type":"address","internalType":"address"},{"name":"_verifierTokenContract","type":"address","internalType":"address"},{"name":"_legacyTokenContract","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"approve","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"balanceOf","inputs":[{"name":"owner","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"careTokenContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract CareToken"}],"stateMutability":"view"},{"type":"function","name":"getAllNFTs","inputs":[],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getApproved","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getNFTsByUser","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getNFTsByUserPaginated","inputs":[{"name":"user","type":"address","internalType":"address"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"trees","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRecentTreesPaginated","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"paginatedTrees","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"},{"name":"hasMore","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getTreeDetailsbyID","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct Tree","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getTreeNftVerifiers","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct TreeNftVerification[]","components":[{"name":"verifier","type":"address","internalType":"address"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"description","type":"string","internalType":"string"},{"name":"isHidden","type":"bool","internalType":"bool"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getTreeNftVerifiersPaginated","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"verifications","type":"tuple[]","internalType":"struct TreeNftVerification[]","components":[{"name":"verifier","type":"address","internalType":"address"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"description","type":"string","internalType":"string"},{"name":"isHidden","type":"bool","internalType":"bool"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"},{"name":"visiblecount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUserProfile","inputs":[{"name":"userAddress","type":"address","internalType":"address"}],"outputs":[{"name":"userDetails","type":"tuple","internalType":"struct UserDetails","components":[{"name":"userAddress","type":"address","internalType":"address"},{"name":"profilePhotoIpfs","type":"string","internalType":"string"},{"name":"name","type":"string","internalType":"string"},{"name":"dateJoined","type":"uint256","internalType":"uint256"},{"name":"verificationsRevoked","type":"uint256","internalType":"uint256"},{"name":"reportedSpam","type":"uint256","internalType":"uint256"},{"name":"verifierTokens","type":"uint256","internalType":"uint256"},{"name":"planterTokens","type":"uint256","internalType":"uint256"},{"name":"legacyTokens","type":"uint256","internalType":"uint256"},{"name":"careTokens","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getVerifiedTreesByUser","inputs":[{"name":"verifier","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getVerifiedTreesByUserPaginated","inputs":[{"name":"verifier","type":"address","internalType":"address"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"trees","type":"tuple[]","internalType":"struct Tree[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"planting","type":"uint256","internalType":"uint256"},{"name":"death","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"ancestors","type":"address[]","internalType":"address[]"},{"name":"lastCareTimestamp","type":"uint256","internalType":"uint256"},{"name":"careCount","type":"uint256","internalType":"uint256"}]},{"name":"totalCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"isApprovedForAll","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"operator","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isVerified","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"verifier","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"legacyToken","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract LegacyToken"}],"stateMutability":"view"},{"type":"function","name":"markDead","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"minimumTimeToMarkTreeDead","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"mintNft","inputs":[{"name":"latitude","type":"uint256","internalType":"uint256"},{"name":"longitude","type":"uint256","internalType":"uint256"},{"name":"species","type":"string","internalType":"string"},{"name":"imageUri","type":"string","internalType":"string"},{"name":"qrIpfsHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"initialPhotos","type":"string[]","internalType":"string[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"ownerOf","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"ping","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"pure"},{"type":"function","name":"planterTokenContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract PlanterToken"}],"stateMutability":"view"},{"type":"function","name":"registerUserProfile","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_profilePhotoHash","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeVerification","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"},{"name":"verifier","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"safeTransferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"safeTransferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setApprovalForAll","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"approved","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"tokenURI","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateUserDetails","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_profilePhotoHash","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"verifierTokenContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract VerifierToken"}],"stateMutability":"view"},{"type":"function","name":"verify","inputs":[{"name":"_tokenId","type":"uint256","internalType":"uint256"},{"name":"_proofHashes","type":"string[]","internalType":"string[]"},{"name":"_description","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ApprovalForAll","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"operator","type":"address","indexed":true,"internalType":"address"},{"name":"approved","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Transfer","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VerificationRemoved","inputs":[{"name":"verificationId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"treeNftId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"verifier","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"ERC721IncorrectOwner","inputs":[{"name":"sender","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"},{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InsufficientApproval","inputs":[{"name":"operator","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ERC721InvalidApprover","inputs":[{"name":"approver","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidOperator","inputs":[{"name":"operator","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidReceiver","inputs":[{"name":"receiver","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721InvalidSender","inputs":[{"name":"sender","type":"address","internalType":"address"}]},{"type":"error","name":"ERC721NonexistentToken","inputs":[{"name":"tokenId","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidCoordinates","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidTreeID","inputs":[]},{"type":"error","name":"MinimumMarkDeadTimeNotReached","inputs":[]},{"type":"error","name":"NotTreeOwner","inputs":[]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]},{"type":"error","name":"PaginationLimitExceeded","inputs":[]},{"type":"error","name":"TreeAlreadyDead","inputs":[]},{"type":"error","name":"UserAlreadyRegistered","inputs":[]},{"type":"error","name":"UserNotRegistered","inputs":[]}]'''; final String treeNFtContractAddress = dotenv.env['TREE_NFT_CONTRACT_ADDRESS'] ?? ''; + +// CareToken Address: 0xF9C45610FEA0382Ab5d28c7CaEe44F6aC26Fe956 +// PlanterToken Address: 0x18a3BB9E8b6a692b3B29Dcf49Ce58f4bf2CB0E93 +// VerifierToken Address: 0x52db3eEff09D1dBE30007fA06AE14aF9849D29ba +// LegacyToken Address: 0xD5C0F25B883f018133d1Ce46fdb3365B660EF1db +// TreeNft Address: 0xeD3D3a4f30ad25d29BD6cB46Bb705a120809DB23 +// OrganisationFactory: 0x75da54F30d347040e977860a9C3495b2C52b4F23 diff --git a/lib/utils/services/contract_read_services.dart b/lib/utils/services/contract_read_services.dart index a2624f0..007e452 100644 --- a/lib/utils/services/contract_read_services.dart +++ b/lib/utils/services/contract_read_services.dart @@ -1,3 +1,4 @@ +// ignore: depend_on_referenced_packages import 'package:web3dart/web3dart.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; diff --git a/lib/utils/services/contract_write_functions.dart b/lib/utils/services/contract_write_functions.dart index 920b92b..7debf81 100644 --- a/lib/utils/services/contract_write_functions.dart +++ b/lib/utils/services/contract_write_functions.dart @@ -1,6 +1,7 @@ import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/constants/contract_abis/tree_nft_contract_abi.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; class ContractWriteResult { final bool success; @@ -188,7 +189,11 @@ class ContractWriteFunctions { ); } - final List args = [BigInt.from(treeId), address]; + final List args = [ + BigInt.from(treeId), + EthereumAddress.fromHex(address) + ]; + final txHash = await walletProvider.writeContract( contractAddress: treeNFtContractAddress, functionName: 'removeVerification', diff --git a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart index 33774ad..1063f57 100644 --- a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart +++ b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart @@ -4,6 +4,50 @@ import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_write_functions.dart'; +class Verifier { + final String address; + final int timestamp; + final List proofHashes; + final String description; + final bool isActive; + final int verificationId; + + Verifier({ + required this.address, + required this.timestamp, + required this.proofHashes, + required this.description, + required this.isActive, + required this.verificationId, + }); + + factory Verifier.fromList(List data) { + return Verifier( + address: data[0].toString(), + timestamp: data[1] is BigInt + ? (data[1] as BigInt).toInt() + : int.parse(data[1].toString()), + proofHashes: data[2] is List + ? List.from(data[2].map((p) => p.toString())) + : [], + description: data[3].toString(), + isActive: data[4] == true || data[4].toString().toLowerCase() == 'true', + verificationId: data[5] is BigInt + ? (data[5] as BigInt).toInt() + : int.parse(data[5].toString()), + ); + } + + String get formattedTimestamp { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return "${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}"; + } + + String get shortAddress { + return "${address.substring(0, 6)}...${address.substring(address.length - 4)}"; + } +} + class Tree { final int id; final int latitude; @@ -19,7 +63,7 @@ class Tree { final List ancestors; final int lastCareTimestamp; final int careCount; - final List verifiers; + final List verifiers; final String owner; Tree({ @@ -67,7 +111,7 @@ class Tree { : [], lastCareTimestamp: _toInt(userData[12]), careCount: _toInt(userData[13]), - verifiers: List.from(verifiers.map((a) => a.toString())), + verifiers: _parseVerifiers(verifiers), owner: owner, ); } catch (e) { @@ -97,59 +141,67 @@ class Tree { if (value is int) return value; return int.tryParse(value.toString()) ?? 0; } + + static List _parseVerifiers(List verifiersData) { + List verifiers = []; + + for (var verifierEntry in verifiersData) { + if (verifierEntry is List && verifierEntry.length >= 6) { + try { + verifiers.add(Verifier.fromList(verifierEntry)); + } catch (e) { + logger.e("Error parsing verifier: $e"); + } + } + } + + return verifiers; + } } -Future _removeVerifier( - String verifierAddress, BuildContext context, Tree? treeDetails, Function loadTreeDetails) async { +Future _removeVerifier(Verifier verifier, BuildContext context, + Tree? treeDetails, Function loadTreeDetails) async { + final messenger = ScaffoldMessenger.of(context); + logger.d("Removing verifier: ${verifier.address}"); try { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - const Text("Remove verifier functionality not yet implemented"), - backgroundColor: Colors.orange.shade600, - behavior: SnackBarBehavior.floating, - ), - ); final provider = Provider.of(context, listen: false); final result = await ContractWriteFunctions.removeVerification( walletProvider: provider, treeId: treeDetails!.id, - address: verifierAddress, + address: verifier.address.toString(), ); - if (result.success) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text("Verifier removed successfully!"), - backgroundColor: Colors.green.shade600, - behavior: SnackBarBehavior.floating, - ), - ); - // Refresh the tree details - await loadTreeDetails(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Failed to remove verifier: ${result.errorMessage}"), - backgroundColor: Colors.red.shade600, - behavior: SnackBarBehavior.floating, - ), - ); - } - + if (result.success) { + messenger.showSnackBar( + SnackBar( + content: const Text("Verifier removed successfully!"), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + await loadTreeDetails(); + } else { + messenger.showSnackBar( + SnackBar( + content: Text("Failed to remove verifier: ${result.errorMessage}"), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( + messenger.showSnackBar( SnackBar( content: Text("Error: $e"), - backgroundColor: Colors.red.shade600, - behavior: SnackBarBehavior.floating, + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, ), ); } } -Widget treeVerifiersSection( - String? loggedInUser, Tree? treeDetails, Function loadTreeDetails ,BuildContext context) { +Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, + Function loadTreeDetails, BuildContext context) { if (treeDetails?.verifiers == null || treeDetails!.verifiers.isEmpty) { return Container( width: double.infinity, @@ -250,15 +302,16 @@ Widget treeVerifiersSection( ...treeDetails!.verifiers.asMap().entries.map((entry) { final index = entry.key; final verifier = entry.value; - return _buildVerifierCard(verifier, index, isOwner, loadTreeDetails, treeDetails, context); + return _buildVerifierCard( + verifier, index, isOwner, loadTreeDetails, treeDetails, context); }), ], ), ); } -Widget _buildVerifierCard( - String verifierAddress, int index, bool canRemove, Function loadTreeDetails, Tree? treeDetails, BuildContext context) { +Widget _buildVerifierCard(Verifier verifier, int index, bool canRemove, + Function loadTreeDetails, Tree? treeDetails, BuildContext context) { return Container( margin: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.all(12.0), @@ -267,61 +320,132 @@ Widget _buildVerifierCard( borderRadius: BorderRadius.circular(8.0), border: Border.all(color: Colors.blue.shade100), ), - child: Row( + child: Column( children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(20), - ), - child: Icon( - Icons.person, - color: Colors.blue.shade600, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Verifier ${index + 1}", - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, - ), + Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(20), ), - const SizedBox(height: 2), - Text( - "${verifierAddress.substring(0, 6)}...${verifierAddress.substring(verifierAddress.length - 4)}", - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - fontFamily: 'monospace', + child: Icon( + Icons.person, + color: Colors.blue.shade600, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Verifier ${index + 1}", + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + verifier.shortAddress, + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + fontFamily: 'monospace', + ), + ), + ], + ), + ), + if (canRemove) + GestureDetector( + onTap: () => _showRemoveVerifierDialog( + verifier, index, context, treeDetails, loadTreeDetails), + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.red.shade200), + ), + child: Icon( + Icons.close, + color: Colors.red.shade600, + size: 18, + ), ), ), - ], - ), + ], ), - if (canRemove) - GestureDetector( - onTap: () => - _showRemoveVerifierDialog(verifierAddress, index, context, treeDetails, loadTreeDetails), + if (verifier.description.isNotEmpty || verifier.proofHashes.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), child: Container( - width: 32, - height: 32, + width: double.infinity, + padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.red.shade200), + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(6.0), ), - child: Icon( - Icons.close, - color: Colors.red.shade600, - size: 18, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (verifier.description.isNotEmpty) ...[ + Text( + "Description:", + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + Text( + verifier.description, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 4), + ], + Row( + children: [ + Icon(Icons.access_time, + size: 12, color: Colors.grey.shade500), + const SizedBox(width: 4), + Text( + verifier.formattedTimestamp, + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade500, + ), + ), + const Spacer(), + if (verifier.proofHashes.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "${verifier.proofHashes.length} proof${verifier.proofHashes.length != 1 ? 's' : ''}", + style: TextStyle( + fontSize: 10, + color: Colors.blue.shade700, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], ), ), ), @@ -330,8 +454,8 @@ Widget _buildVerifierCard( ); } -void _showRemoveVerifierDialog( - String verifierAddress, int index, BuildContext context, Tree? treeDetails, Function loadTreeDetails) { +void _showRemoveVerifierDialog(Verifier verifier, int index, + BuildContext context, Tree? treeDetails, Function loadTreeDetails) { showDialog( context: context, builder: (BuildContext context) { @@ -361,18 +485,41 @@ void _showRemoveVerifierDialog( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.person, color: Colors.grey.shade600), - const SizedBox(width: 8), - Expanded( - child: Text( - verifierAddress, - style: const TextStyle( - fontFamily: 'monospace', + Row( + children: [ + Icon(Icons.person, color: Colors.grey.shade600), + const SizedBox(width: 8), + Expanded( + child: Text( + verifier.address, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ], + ), + if (verifier.description.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + "Description: ${verifier.description}", + style: TextStyle( fontSize: 12, + color: Colors.grey.shade600, ), ), + ], + const SizedBox(height: 4), + Text( + "Verified: ${verifier.formattedTimestamp}", + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + ), ), ], ), @@ -401,7 +548,8 @@ void _showRemoveVerifierDialog( ElevatedButton( onPressed: () async { Navigator.of(context).pop(); - await _removeVerifier(verifierAddress, context, treeDetails, loadTreeDetails); + await _removeVerifier( + verifier, context, treeDetails, loadTreeDetails); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red.shade600, From 2117c9849094fd179f6abaae1f54786e61a63c11 Mon Sep 17 00:00:00 2001 From: IronJam13 Date: Sat, 30 Aug 2025 01:42:07 +0530 Subject: [PATCH 7/9] add: missing .env.stencil variables --- .env.stencil | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.env.stencil b/.env.stencil index a81de8d..3f246c3 100644 --- a/.env.stencil +++ b/.env.stencil @@ -3,4 +3,15 @@ API_KEY= API_SECRET= ALCHEMY_API_KEY= CONTRACT_ADDRESS= -APPLICATION_ID= \ No newline at end of file +APPLICATION_ID= + + +//CONTRACT ADDRESSES + + +TREE_NFT_CONTRACT_ADDRESS= +ORGANISATION_FACTORY_CONTRACT_ADDRESS= +CARE_TOKEN_CONTRACT_ADDRESS= +VERIFIER_TOKEN_CONTRACT_ADDRESS= +LEGACY_TOKEN_CONTRACT_ADDRESS= +PLANTER_TOKEN_CONTRACT_ADDRESS= From f823807120de0a9695e5cabd2d9871248c0373dd Mon Sep 17 00:00:00 2001 From: IronJam13 Date: Sat, 30 Aug 2025 02:16:24 +0530 Subject: [PATCH 8/9] fix: static map display --- lib/pages/tree_details_page.dart | 17 +- .../static_map_display_widget.dart | 655 ++++++------------ .../tree_nft_details_verifiers_widget.dart | 6 +- 3 files changed, 230 insertions(+), 448 deletions(-) diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index 832e003..2577af6 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -7,10 +7,12 @@ import 'package:tree_planting_protocol/utils/services/contract_write_functions.d import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/map_widgets/static_map_display_widget.dart'; import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart'; -import 'package:tree_planting_protocol/models/tree_details.dart' hide Tree; -final TREE_VERIFIERS_OFFSET = 0; -final TREE_VERIFIERS_LIMIT = 10; + +// ignore: constant_identifier_names +const TREE_VERIFIERS_OFFSET = 0; +// ignore: constant_identifier_names +const TREE_VERIFIERS_LIMIT = 10; class TreeDetailsPage extends StatefulWidget { final String treeId; @@ -90,8 +92,8 @@ class _TreeDetailsPageState extends State { ), clipBehavior: Clip.antiAlias, child: StaticCoordinatesMap( - lat: treeDetails!.latitude / 1e6, - lng: treeDetails!.longitude / 1e6, + lat: (treeDetails!.latitude / 1e6) - 90.0, + lng: (treeDetails!.longitude / 1e6) - 180.0, ), ), ); @@ -120,7 +122,7 @@ class _TreeDetailsPageState extends State { ), child: Center( child: Text( - (treeDetails!.latitude / 1e6).toString(), + ((treeDetails!.latitude / 1e6) - 90.0).toStringAsFixed(6), textAlign: TextAlign.center, style: const TextStyle( fontSize: 9, height: 1, color: Colors.white), @@ -140,7 +142,8 @@ class _TreeDetailsPageState extends State { ), child: Center( child: Text( - (treeDetails!.longitude / 1e6).toString(), + ((treeDetails!.longitude / 1e6) - 180.0) + .toStringAsFixed(6), textAlign: TextAlign.center, style: const TextStyle( fontSize: 9, height: 1, color: Colors.black), diff --git a/lib/widgets/map_widgets/static_map_display_widget.dart b/lib/widgets/map_widgets/static_map_display_widget.dart index a95c9c5..95e7bc8 100644 --- a/lib/widgets/map_widgets/static_map_display_widget.dart +++ b/lib/widgets/map_widgets/static_map_display_widget.dart @@ -1,9 +1,6 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; -import 'package:tree_planting_protocol/utils/logger.dart'; class StaticCoordinatesMap extends StatefulWidget { final double lat; @@ -11,14 +8,13 @@ class StaticCoordinatesMap extends StatefulWidget { const StaticCoordinatesMap({super.key, required this.lat, required this.lng}); @override - State createState() => _CoordinatesMapState(); + State createState() => _StaticCoordinatesMapState(); } -class _CoordinatesMapState extends State { +class _StaticCoordinatesMapState extends State { late MapController _mapController; bool _mapLoaded = false; bool _hasError = false; - String? _errorMessage; @override void initState() { @@ -26,308 +22,25 @@ class _CoordinatesMapState extends State { _mapController = MapController(); } - @override - Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, _) { - final double latitude = widget.lat; - final double longitude = widget.lng; - - return Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: _hasError - ? _buildErrorWidget() - : _buildMapWidget(latitude, longitude), - ), - ); - }, - ); - } - - Widget _buildErrorWidget() { - return Container( - color: Colors.grey[100], - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.map_outlined, - size: 60, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - "Map Unavailable", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.grey[600], - ), - ), - const SizedBox(height: 8), - Text( - "Please use the coordinate fields below", - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - setState(() { - _hasError = false; - _errorMessage = null; - }); - }, - child: const Text("Retry"), - ), - ], - ), - ), - ); - } - - Widget _buildMapWidget(double latitude, double longitude) { - return Stack( - children: [ - FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: LatLng(latitude, longitude), - initialZoom: 4.0, - minZoom: 3.0, - maxZoom: 18.0, - interactionOptions: const InteractionOptions( - flags: InteractiveFlag.all, - ), - onMapReady: () { - setState(() { - _mapLoaded = true; - }); - }, - onTap: (tapPosition, point) { - final provider = - Provider.of(context, listen: false); - provider.setLatitude(point.latitude); - provider.setLongitude(point.longitude); - }, - ), - children: [ - TileLayer( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'tree_planting_protocol', - errorTileCallback: (tile, error, stackTrace) { - if (mounted) { - setState(() { - _hasError = true; - _errorMessage = 'Network connection issue'; - }); - } - }, - ), - MarkerLayer( - markers: [ - Marker( - point: LatLng(latitude, longitude), - width: 80, - height: 80, - child: const Icon( - Icons.location_pin, - color: Colors.red, - size: 40, - ), - ), - ], - ), - ], - ), - if (!_mapLoaded) - Container( - color: Colors.white, - child: const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text("Loading map..."), - ], - ), - ), - ), - Positioned( - top: 8, - left: 8, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", - style: const TextStyle( - color: Colors.white, - fontSize: 10, - ), - ), - ), - ), - Positioned( - right: 8, - top: 50, - child: Column( - children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black, - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - final currentZoom = _mapController.camera.zoom; - if (currentZoom < 18.0) { - _mapController.move( - _mapController.camera.center, - currentZoom + 1, - ); - } - }, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(4), - topRight: Radius.circular(4), - ), - child: Container( - width: 40, - height: 40, - child: const Icon( - Icons.add, - color: Colors.black87, - size: 20, - ), - ), - ), - ), - Container( - height: 1, - color: Colors.grey[300], - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - final currentZoom = _mapController.camera.zoom; - if (currentZoom > 3.0) { - _mapController.move( - _mapController.camera.center, - currentZoom - 1, - ); - } - }, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(4), - bottomRight: Radius.circular(4), - ), - child: Container( - width: 40, - height: 40, - child: const Icon( - Icons.remove, - color: Colors.black87, - size: 20, - ), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ], - ); - } - @override void dispose() { _mapController.dispose(); super.dispose(); } -} - -class StaticDisplayMap extends StatefulWidget { - final Function(double lat, double lng)? onLocationSelected; - final double lat; - final double lng; - - const StaticDisplayMap( - {Key? key, this.onLocationSelected, required this.lat, required this.lng}) - : super(key: key); - - @override - State createState() => _StaticDisplayMapState(); -} - -class _StaticDisplayMapState extends State { - late MapController _mapController; - bool _mapLoaded = false; - bool _hasError = false; - String? _errorMessage; - static const double _defaultLat = 28.9845; // Example: Roorkee, India - static const double _defaultLng = 77.8956; - - @override - void initState() { - super.initState(); - _mapController = MapController(); - } - - double _sanitizeCoordinate(double value, double defaultValue) { - if (value.isNaN || - value.isInfinite || - value == double.infinity || - value == double.negativeInfinity) { - logger.e( - 'Invalid coordinate detected: $value, using default: $defaultValue'); - return defaultValue; - } - return value; - } @override Widget build(BuildContext context) { - double latitude = _sanitizeCoordinate(widget.lat, _defaultLat); - double longitude = _sanitizeCoordinate(widget.lng, _defaultLng); - latitude = latitude.clamp(-90.0, 90.0); - longitude = longitude.clamp(-180.0, 180.0); + final double latitude = widget.lat; + final double longitude = widget.lng; return Container( + height: 200, // Fixed height for consistency decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), ), child: ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), child: _hasError ? _buildErrorWidget() : _buildMapWidget(latitude, longitude), @@ -344,10 +57,10 @@ class _StaticDisplayMapState extends State { children: [ Icon( Icons.map_outlined, - size: 60, + size: 48, color: Colors.grey[400], ), - const SizedBox(height: 16), + const SizedBox(height: 12), Text( "Map Unavailable", style: TextStyle( @@ -356,24 +69,14 @@ class _StaticDisplayMapState extends State { color: Colors.grey[600], ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( - "Please use the coordinate fields below", + "Coordinates: ${widget.lat.toStringAsFixed(6)}, ${widget.lng.toStringAsFixed(6)}", style: TextStyle( fontSize: 12, color: Colors.grey[500], + fontFamily: 'monospace', ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - setState(() { - _hasError = false; - _errorMessage = null; - }); - }, - child: const Text("Retry"), ), ], ), @@ -382,34 +85,27 @@ class _StaticDisplayMapState extends State { } Widget _buildMapWidget(double latitude, double longitude) { - if (latitude.isNaN || - latitude.isInfinite || - longitude.isNaN || - longitude.isInfinite) { - logger.e( - 'ERROR: Invalid coordinates in _buildMapWidget - lat: $latitude, lng: $longitude'); - latitude = _defaultLat; - longitude = _defaultLng; - } - return Stack( children: [ FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: LatLng(latitude, longitude), - initialZoom: 15.0, + initialZoom: 12.0, minZoom: 3.0, maxZoom: 18.0, interactionOptions: const InteractionOptions( - flags: InteractiveFlag.all, + flags: InteractiveFlag.pinchZoom | + InteractiveFlag.doubleTapZoom | + InteractiveFlag.drag | + InteractiveFlag.flingAnimation, ), onMapReady: () { setState(() { _mapLoaded = true; }); + _mapController.move(LatLng(latitude, longitude), 15.0); }, - onTap: (tapPosition, point) {}, ), children: [ TileLayer( @@ -419,7 +115,6 @@ class _StaticDisplayMapState extends State { if (mounted) { setState(() { _hasError = true; - _errorMessage = 'Network connection issue'; }); } }, @@ -428,12 +123,46 @@ class _StaticDisplayMapState extends State { markers: [ Marker( point: LatLng(latitude, longitude), - width: 80, - height: 80, - child: const Icon( - Icons.location_pin, - color: Colors.red, - size: 40, + width: 60, + height: 60, + alignment: Alignment.topCenter, + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.green.shade600, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 3), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.park, + color: Colors.white, + size: 24, + ), + ), + Container( + width: 0, + height: 0, + decoration: const BoxDecoration( + border: Border( + left: + BorderSide(width: 6, color: Colors.transparent), + right: + BorderSide(width: 6, color: Colors.transparent), + top: BorderSide(width: 8, color: Colors.white), + ), + ), + ), + ], ), ), ], @@ -442,146 +171,196 @@ class _StaticDisplayMapState extends State { ), if (!_mapLoaded) Container( - color: Colors.white, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.green), + ), SizedBox(height: 16), - Text("Loading map..."), + Text( + "Loading tree location...", + style: TextStyle(color: Colors.grey), + ), ], ), ), ), Positioned( - top: 8, - left: 8, + top: 0, + left: 0, + right: 0, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", - style: const TextStyle( - color: Colors.white, - fontSize: 10, + color: Colors.green.shade600, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), - ), - ), - Positioned( - right: 8, - top: 50, - child: Column( - children: [ - Container( - decoration: BoxDecoration( + child: Row( + children: [ + const Icon( + Icons.location_on, color: Colors.white, - borderRadius: BorderRadius.circular(4), - boxShadow: [ - BoxShadow( - color: Colors.black, - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], + size: 18, ), - child: Column( - children: [ - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - final currentZoom = _mapController.camera.zoom; - if (currentZoom < 18.0) { - _mapController.move( - _mapController.camera.center, - currentZoom + 1, - ); - } - }, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(4), - topRight: Radius.circular(4), - ), - child: Container( - width: 40, - height: 40, - child: const Icon( - Icons.add, - color: Colors.black87, - size: 20, - ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Tree NFT Location", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.white, ), ), - ), - Container( - height: 1, - color: Colors.grey[300], - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - final currentZoom = _mapController.camera.zoom; - if (currentZoom > 3.0) { - _mapController.move( - _mapController.camera.center, - currentZoom - 1, - ); - } - }, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(4), - bottomRight: Radius.circular(4), - ), - child: Container( - width: 40, - height: 40, - child: const Icon( - Icons.remove, - color: Colors.black87, - size: 20, - ), + Text( + "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", + style: const TextStyle( + fontSize: 10, + color: Colors.white70, + fontFamily: 'monospace', ), ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.white), + ), + child: const Text( + "INTERACTIVE", + style: TextStyle( + fontSize: 8, + fontWeight: FontWeight.bold, + color: Colors.white, ), - ], + ), ), - ), - ], + ], + ), ), ), Positioned( - bottom: 8, - left: 8, - right: 8, + right: 12, + top: 60, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(4), + color: Colors.white, + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - child: const Text( - "Static display • Use zoom buttons or pinch to zoom", - style: TextStyle( - color: Colors.white, - fontSize: 11, - ), - textAlign: TextAlign.center, + child: Column( + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom < 18.0) { + _mapController.move( + _mapController.camera.center, + currentZoom + 1, + ); + } + }, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + child: SizedBox( + width: 36, + height: 36, + child: const Icon( + Icons.add, + color: Colors.black87, + size: 18, + ), + ), + ), + ), + Container( + height: 1, + color: Colors.grey[300], + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + final currentZoom = _mapController.camera.zoom; + if (currentZoom > 3.0) { + _mapController.move( + _mapController.camera.center, + currentZoom - 1, + ); + } + }, + borderRadius: BorderRadius.zero, + child: SizedBox( + width: 36, + height: 36, + child: const Icon( + Icons.remove, + color: Colors.black87, + size: 18, + ), + ), + ), + ), + Container( + height: 1, + color: Colors.grey[300], + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _mapController.move( + LatLng(latitude, longitude), + _mapController.camera.zoom, + ); + }, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(6), + ), + child: SizedBox( + width: 36, + height: 36, + child: const Icon( + Icons.my_location, + color: Colors.green, + size: 18, + ), + ), + ), + ), + ], ), ), ), ], ); } - - @override - void dispose() { - _mapController.dispose(); - super.dispose(); - } } diff --git a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart index 1063f57..13537b4 100644 --- a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart +++ b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart @@ -241,7 +241,7 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, ); } - final isOwner = treeDetails?.owner == loggedInUser; + final isOwner = treeDetails.owner == loggedInUser; return Container( width: double.infinity, @@ -279,7 +279,7 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, borderRadius: BorderRadius.circular(12), ), child: Text( - "${treeDetails!.verifiers.length}", + "${treeDetails.verifiers.length}", style: TextStyle( color: Colors.blue.shade800, fontWeight: FontWeight.bold, @@ -299,7 +299,7 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, ), ), const SizedBox(height: 16), - ...treeDetails!.verifiers.asMap().entries.map((entry) { + ...treeDetails.verifiers.asMap().entries.map((entry) { final index = entry.key; final verifier = entry.value; return _buildVerifierCard( From 8aaab9363dff5f4c6b5af7d19ffaaa1e026bac72 Mon Sep 17 00:00:00 2001 From: IronJam11 Date: Sat, 30 Aug 2025 03:41:01 +0530 Subject: [PATCH 9/9] fix: minor bug issues --- lib/pages/tree_details_page.dart | 490 ++++++++-- .../static_map_display_widget.dart | 27 +- .../tree_nft_details_verifiers_widget.dart | 883 ++++++++++++++---- 3 files changed, 1129 insertions(+), 271 deletions(-) diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index 2577af6..47604ae 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -1,13 +1,17 @@ +// ignore_for_file: use_build_context_synchronously + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_read_services.dart'; import 'package:tree_planting_protocol/utils/services/contract_write_functions.dart'; +import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/map_widgets/static_map_display_widget.dart'; import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart'; - +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; // ignore: constant_identifier_names const TREE_VERIFIERS_OFFSET = 0; @@ -66,7 +70,7 @@ class _TreeDetailsPageState extends State { } } } - logger.d("Tree Details: ${treeDetails?.verifiers}"); + logger.d("Tree Details hot: ${treeDetails?.verifiers}"); setState(() { _isLoading = false; }); @@ -325,77 +329,22 @@ class _TreeDetailsPageState extends State { void _showVerificationDialog() { showDialog( context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.0), - ), - title: Row( - children: [ - Icon(Icons.verified, color: Colors.green.shade600), - const SizedBox(width: 8), - const Text("Verify Tree"), - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Are you sure you want to verify this tree? This action will:", - style: TextStyle(fontSize: 16), - ), - const SizedBox(height: 12), - _buildVerificationPoint("• Record verification on blockchain"), - _buildVerificationPoint("• Require gas fees for transaction"), - _buildVerificationPoint("• Cannot be undone once confirmed"), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - "Cancel", - style: TextStyle(color: Colors.grey.shade600), - ), - ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - await _performVerification(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade600, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), - child: const Text("Verify"), - ), - ], - ); - }, - ); - } - - Widget _buildVerificationPoint(String text) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Text( - text, - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade700, - ), + builder: (context) => _VerificationModal( + treeDetails: treeDetails!, + onVerify: _performVerification, ), ); } - Future _performVerification() async { + Future _performVerification({ + required String description, + required List proofHashes, + }) async { logger.d("Starting tree verification process"); logger.d("Logged in user: $loggedInUser"); logger.d("Tree ID: ${treeDetails!.id}"); + logger.d("Description: $description"); + logger.d("Proof hashes: $proofHashes"); try { // Get wallet provider @@ -439,15 +388,13 @@ class _TreeDetailsPageState extends State { final result = await ContractWriteFunctions.verifyTree( walletProvider: walletProvider, treeId: treeDetails!.id, - description: "Tree verified by user", - photos: ["verification_photo"], + description: description, + photos: proofHashes, ); - // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (result.success) { - // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( @@ -464,7 +411,6 @@ class _TreeDetailsPageState extends State { await loadTreeDetails(); } else { - // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( @@ -482,7 +428,6 @@ class _TreeDetailsPageState extends State { ); } } catch (e) { - // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).hideCurrentSnackBar(); String errorMessage = "Verification failed"; @@ -500,8 +445,6 @@ class _TreeDetailsPageState extends State { } logger.e("Verification error: $e"); - - // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( @@ -544,10 +487,407 @@ class _TreeDetailsPageState extends State { child: _buildTreeNFTDetailsSection( screenHeight, screenWidth, context), ), - const SizedBox(height: 20), // Extra bottom padding + const SizedBox(height: 20), ], ), ), )); } } + +class _VerificationModal extends StatefulWidget { + final Tree treeDetails; + final Function( + {required String description, + required List proofHashes}) onVerify; + + const _VerificationModal({ + required this.treeDetails, + required this.onVerify, + }); + + @override + State<_VerificationModal> createState() => _VerificationModalState(); +} + +class _VerificationModalState extends State<_VerificationModal> { + final TextEditingController _descriptionController = TextEditingController(); + final List _selectedImages = []; + final List _uploadedHashes = []; + bool _isUploading = false; + final ImagePicker _picker = ImagePicker(); + + @override + void dispose() { + _descriptionController.dispose(); + super.dispose(); + } + + Future _pickImage() async { + if (_selectedImages.length >= 3) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Maximum 3 images allowed"), + backgroundColor: Colors.orange, + ), + ); + return; + } + + final XFile? image = await _picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1200, + maxHeight: 1200, + imageQuality: 85, + ); + + if (image != null) { + setState(() { + _selectedImages.add(File(image.path)); + }); + } + } + + Future _takePhoto() async { + if (_selectedImages.length >= 3) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("Maximum 3 images allowed"), + backgroundColor: Colors.orange, + ), + ); + return; + } + + final XFile? image = await _picker.pickImage( + source: ImageSource.camera, + maxWidth: 1200, + maxHeight: 1200, + imageQuality: 85, + ); + + if (image != null) { + setState(() { + _selectedImages.add(File(image.path)); + }); + } + } + + void _removeImage(int index) { + setState(() { + _selectedImages.removeAt(index); + }); + } + + Future _uploadImages() async { + if (_selectedImages.isEmpty) return; + + setState(() { + _isUploading = true; + _uploadedHashes.clear(); + }); + + try { + for (int i = 0; i < _selectedImages.length; i++) { + File image = _selectedImages[i]; + logger.d("Uploading image ${i + 1} of ${_selectedImages.length}"); + + final hash = await uploadToIPFS(image, (uploading) {}); + + if (hash != null) { + _uploadedHashes.add(hash); + logger.d("Successfully uploaded image ${i + 1}: $hash"); + } else { + logger.e("Failed to upload image ${i + 1}"); + throw Exception("Failed to upload image ${i + 1}"); + } + } + + logger.d( + "All images uploaded successfully. Total hashes: ${_uploadedHashes.length}"); + } catch (e) { + logger.e("Error uploading images: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Error uploading images: $e"), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isUploading = false; + }); + } + } + + void _showImageSourceDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("Select Image Source"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.camera_alt), + title: const Text("Camera"), + onTap: () { + Navigator.pop(context); + _takePhoto(); + }, + ), + ListTile( + leading: const Icon(Icons.photo_library), + title: const Text("Gallery"), + onTap: () { + Navigator.pop(context); + _pickImage(); + }, + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final dialogWidth = + screenSize.width * 0.9 > 500 ? 500.0 : screenSize.width * 0.9; + final dialogHeight = + screenSize.height * 0.8 > 700 ? 700.0 : screenSize.height * 0.8; + + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Container( + constraints: BoxConstraints( + maxHeight: dialogHeight, + maxWidth: dialogWidth, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + child: Row( + children: [ + Icon(Icons.verified, color: Colors.green.shade600, size: 28), + const SizedBox(width: 12), + const Expanded( + child: Text( + "Verify Tree", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close), + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Verification Description", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + TextField( + controller: _descriptionController, + maxLines: 3, + decoration: InputDecoration( + hintText: + "Describe your verification (e.g., tree health, location accuracy, etc.)", + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Colors.grey.shade50, + ), + ), + const SizedBox(height: 20), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + child: Text( + "Proof Images (Optional)", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + Flexible( + child: TextButton.icon( + onPressed: _selectedImages.length < 3 + ? _showImageSourceDialog + : null, + icon: const Icon(Icons.add_photo_alternate, + size: 18), + label: const Text("Add", + style: TextStyle(fontSize: 12)), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + if (_selectedImages.isNotEmpty) + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _selectedImages.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(right: 8), + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + _selectedImages[index], + width: 80, + height: 80, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 4, + right: 4, + child: GestureDetector( + onTap: () => _removeImage(index), + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + const SizedBox(height: 20), + if (_isUploading) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.shade50, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.blue.shade600), + ), + ), + const SizedBox(width: 12), + const Text("Uploading images to IPFS..."), + ], + ), + ), + ], + ), + ), + ), + Container( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + "Cancel", + style: TextStyle(color: Colors.grey.shade600), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isUploading || + _descriptionController.text.trim().isEmpty + ? null + : () async { + if (_selectedImages.isNotEmpty && + _uploadedHashes.length != + _selectedImages.length) { + await _uploadImages(); + } + Navigator.pop(context); + widget.onVerify( + description: _descriptionController.text.trim(), + proofHashes: _uploadedHashes, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + _selectedImages.isNotEmpty && + _uploadedHashes.length != _selectedImages.length + ? "Upload & Verify" + : "Verify Tree", + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/map_widgets/static_map_display_widget.dart b/lib/widgets/map_widgets/static_map_display_widget.dart index 95e7bc8..88e6d7c 100644 --- a/lib/widgets/map_widgets/static_map_display_widget.dart +++ b/lib/widgets/map_widgets/static_map_display_widget.dart @@ -216,42 +216,17 @@ class _StaticCoordinatesMapState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - "Tree NFT Location", - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), Text( "${latitude.toStringAsFixed(6)}, ${longitude.toStringAsFixed(6)}", style: const TextStyle( fontSize: 10, - color: Colors.white70, + color: Colors.black, fontFamily: 'monospace', ), ), ], ), ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.white), - ), - child: const Text( - "INTERACTIVE", - style: TextStyle( - fontSize: 8, - fontWeight: FontWeight.bold, - color: Colors.white, - ), - ), - ), ], ), ), diff --git a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart index 13537b4..5a91a90 100644 --- a/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart +++ b/lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/providers/theme_provider.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_write_functions.dart'; @@ -22,20 +24,27 @@ class Verifier { }); factory Verifier.fromList(List data) { - return Verifier( - address: data[0].toString(), - timestamp: data[1] is BigInt - ? (data[1] as BigInt).toInt() - : int.parse(data[1].toString()), - proofHashes: data[2] is List - ? List.from(data[2].map((p) => p.toString())) - : [], - description: data[3].toString(), - isActive: data[4] == true || data[4].toString().toLowerCase() == 'true', - verificationId: data[5] is BigInt - ? (data[5] as BigInt).toInt() - : int.parse(data[5].toString()), - ); + logger.d("Creating Verifier from data: $data"); + try { + return Verifier( + address: data[0].toString(), + timestamp: data[1] is BigInt + ? (data[1] as BigInt).toInt() + : int.parse(data[1].toString()), + proofHashes: data[2] is List + ? List.from(data[2].map((p) => p.toString())) + : [], + description: data[3].toString(), + isActive: data[4] == true || data[4].toString().toLowerCase() == 'true', + verificationId: data[5] is BigInt + ? (data[5] as BigInt).toInt() + : int.parse(data[5].toString()), + ); + } catch (e) { + logger.e("Error in Verifier.fromList: $e"); + logger.e("Data that caused error: $data"); + rethrow; + } } String get formattedTimestamp { @@ -87,12 +96,15 @@ class Tree { factory Tree.fromContractData( List userData, List verifiers, String owner) { - logger.d("User data, Verifiers and Owner"); - logger.d(userData); - logger.d(verifiers); - logger.d(owner); + logger.d("=== Tree.fromContractData ==="); + logger.d("User data: $userData"); + logger.d("Verifiers raw data: $verifiers"); + logger.d("Owner: $owner"); try { - return Tree( + final parsedVerifiers = _parseVerifiers(verifiers); + logger.d("Parsed verifiers count: ${parsedVerifiers.length}"); + + final tree = Tree( id: _toInt(userData[0]), latitude: _toInt(userData[1]), longitude: _toInt(userData[2]), @@ -111,10 +123,15 @@ class Tree { : [], lastCareTimestamp: _toInt(userData[12]), careCount: _toInt(userData[13]), - verifiers: _parseVerifiers(verifiers), + verifiers: parsedVerifiers, owner: owner, ); + + logger.d( + "Tree created successfully with ${tree.verifiers.length} verifiers"); + return tree; } catch (e) { + logger.e("Error creating Tree from contract data: $e"); return Tree( id: 0, latitude: 0, @@ -145,24 +162,137 @@ class Tree { static List _parseVerifiers(List verifiersData) { List verifiers = []; - for (var verifierEntry in verifiersData) { - if (verifierEntry is List && verifierEntry.length >= 6) { - try { - verifiers.add(Verifier.fromList(verifierEntry)); - } catch (e) { - logger.e("Error parsing verifier: $e"); + logger.d("Parsing verifiers data: $verifiersData"); + logger.d("Verifiers data length: ${verifiersData.length}"); + + for (int i = 0; i < verifiersData.length; i++) { + var verifierEntry = verifiersData[i]; + logger.d( + "Verifier entry $i: $verifierEntry (type: ${verifierEntry.runtimeType})"); + + try { + if (verifierEntry is String) { + logger.d("Parsing string verifier: $verifierEntry"); + verifiers.add(Verifier( + address: verifierEntry, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + proofHashes: [], + description: "Verified", + isActive: true, + verificationId: i, + )); + logger.d("Successfully parsed string verifier $i"); + } else if (verifierEntry is List) { + logger.d("Verifier entry $i length: ${verifierEntry.length}"); + logger.d("Verifier entry $i contents: $verifierEntry"); + + if (verifierEntry.isNotEmpty) { + if (verifierEntry.length == 1) { + verifiers.add(Verifier( + address: verifierEntry[0].toString(), + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + proofHashes: [], + description: "Verified", + isActive: true, + verificationId: i, + )); + } else if (verifierEntry.length >= 6) { + verifiers.add(Verifier.fromList(verifierEntry)); + } else { + verifiers.add(Verifier( + address: verifierEntry[0].toString(), + timestamp: verifierEntry.length > 1 + ? (verifierEntry[1] is BigInt + ? (verifierEntry[1] as BigInt).toInt() + : int.tryParse(verifierEntry[1].toString()) ?? + DateTime.now().millisecondsSinceEpoch ~/ 1000) + : DateTime.now().millisecondsSinceEpoch ~/ 1000, + proofHashes: + verifierEntry.length > 2 && verifierEntry[2] is List + ? List.from( + verifierEntry[2].map((p) => p.toString())) + : [], + description: verifierEntry.length > 3 + ? verifierEntry[3].toString() + : "Verified", + isActive: verifierEntry.length > 4 + ? (verifierEntry[4] == true || + verifierEntry[4].toString().toLowerCase() == 'true') + : true, + verificationId: verifierEntry.length > 5 + ? (verifierEntry[5] is BigInt + ? (verifierEntry[5] as BigInt).toInt() + : int.tryParse(verifierEntry[5].toString()) ?? i) + : i, + )); + } + logger.d("Successfully parsed verifier $i"); + } else { + logger.w("Verifier entry $i is an empty list"); + } + } else { + logger.w( + "Verifier entry $i is neither string nor list: ${verifierEntry.runtimeType}"); + } + } catch (e) { + logger.e("Error parsing verifier $i: $e"); + if (verifierEntry != null) { + verifiers.add(Verifier( + address: verifierEntry.toString(), + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + proofHashes: [], + description: "Verified (partial data)", + isActive: true, + verificationId: i, + )); } } } + logger.d("Total parsed verifiers: ${verifiers.length}"); return verifiers; } } +void _copyToClipboard(String text, BuildContext context) { + Clipboard.setData(ClipboardData(text: text)); + + final scaffoldMessenger = ScaffoldMessenger.of(context); + scaffoldMessenger.showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.white, size: 16), + const SizedBox(width: 8), + const Text("Address copied to clipboard!"), + ], + ), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 2), + ), + ); +} + +Map _getThemeColors(BuildContext context) { + final themeProvider = Provider.of(context); + return { + 'primary': themeProvider.isDarkMode + ? const Color.fromARGB(255, 1, 135, 12) + : const Color.fromARGB(255, 28, 211, 129), + 'primaryLight': themeProvider.isDarkMode + ? const Color.fromARGB(255, 1, 135, 12) + : const Color.fromARGB(255, 28, 211, 129), + 'primaryBorder': themeProvider.isDarkMode + ? const Color.fromARGB(255, 1, 135, 12) + : const Color.fromARGB(255, 28, 211, 129), + }; +} + Future _removeVerifier(Verifier verifier, BuildContext context, Tree? treeDetails, Function loadTreeDetails) async { - final messenger = ScaffoldMessenger.of(context); logger.d("Removing verifier: ${verifier.address}"); + try { final provider = Provider.of(context, listen: false); final result = await ContractWriteFunctions.removeVerification( @@ -171,38 +301,51 @@ Future _removeVerifier(Verifier verifier, BuildContext context, address: verifier.address.toString(), ); - if (result.success) { - messenger.showSnackBar( - SnackBar( - content: const Text("Verifier removed successfully!"), - backgroundColor: Colors.green.shade600, - behavior: SnackBarBehavior.floating, - ), - ); - await loadTreeDetails(); - } else { - messenger.showSnackBar( + if (context.mounted) { + final messenger = ScaffoldMessenger.of(context); + if (result.success) { + messenger.showSnackBar( + SnackBar( + content: const Text("Verifier removed successfully!"), + backgroundColor: Colors.green.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + await loadTreeDetails(); + } else { + messenger.showSnackBar( + SnackBar( + content: Text("Failed to remove verifier: ${result.errorMessage}"), + backgroundColor: Colors.red.shade600, + behavior: SnackBarBehavior.floating, + ), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text("Failed to remove verifier: ${result.errorMessage}"), + content: Text("Error: $e"), backgroundColor: Colors.red.shade600, behavior: SnackBarBehavior.floating, ), ); } - } catch (e) { - messenger.showSnackBar( - SnackBar( - content: Text("Error: $e"), - backgroundColor: Colors.red.shade600, - behavior: SnackBarBehavior.floating, - ), - ); } } Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, Function loadTreeDetails, BuildContext context) { + logger.d("=== treeVerifiersSection ==="); + logger.d("treeDetails: $treeDetails"); + logger.d("treeDetails?.verifiers: ${treeDetails?.verifiers}"); + logger.d("verifiers length: ${treeDetails?.verifiers.length}"); + + final themeColors = _getThemeColors(context); + if (treeDetails?.verifiers == null || treeDetails!.verifiers.isEmpty) { + logger.d("No verifiers found, showing empty state"); return Container( width: double.infinity, margin: const EdgeInsets.symmetric(vertical: 16.0), @@ -241,6 +384,8 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, ); } + logger.d( + "Rendering verifiers section with ${treeDetails.verifiers.length} verifiers"); final isOwner = treeDetails.owner == loggedInUser; return Container( @@ -248,9 +393,9 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, margin: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: Colors.blue.shade50, + color: themeColors['primaryLight'], borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: Colors.blue.shade200), + border: Border.all(color: themeColors['primaryBorder']!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -259,14 +404,14 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, children: [ Icon( Icons.verified_user, - color: Colors.blue.shade600, + color: themeColors['primary'], size: 24, ), const SizedBox(width: 8), Text( "Tree Verifiers", style: TextStyle( - color: Colors.blue.shade800, + color: themeColors['primary'], fontSize: 18, fontWeight: FontWeight.bold, ), @@ -275,13 +420,13 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.blue.shade100, + color: themeColors['primary']!, borderRadius: BorderRadius.circular(12), ), child: Text( "${treeDetails.verifiers.length}", style: TextStyle( - color: Colors.blue.shade800, + color: themeColors['primary'], fontWeight: FontWeight.bold, ), ), @@ -291,11 +436,11 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, const SizedBox(height: 12), Text( isOwner - ? "Tap the ✕ to remove a verifier (owner only)" - : "Users who have verified this tree", + ? "Tap any verifier to view details • Tap ✕ to remove (owner only)" + : "Tap any verifier to view verification details", style: TextStyle( - color: Colors.blue.shade600, - fontSize: 14, + color: themeColors['primary']!, + fontSize: 12, ), ), const SizedBox(height: 16), @@ -312,150 +457,518 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, Widget _buildVerifierCard(Verifier verifier, int index, bool canRemove, Function loadTreeDetails, Tree? treeDetails, BuildContext context) { + final themeColors = _getThemeColors(context); + return Container( margin: const EdgeInsets.only(bottom: 8.0), - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.0), - border: Border.all(color: Colors.blue.shade100), - ), - child: Column( + child: Stack( children: [ - Row( - children: [ - Container( - width: 40, - height: 40, + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _showVerifierDetailsModal(verifier, context, themeColors); + }, + borderRadius: BorderRadius.circular(12.0), + child: Container( + padding: const EdgeInsets.all(12.0), decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(20), - ), - child: Icon( - Icons.person, - color: Colors.blue.shade600, - size: 20, + color: Colors.white, + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: themeColors['primaryBorder']!), + boxShadow: [ + BoxShadow( + color: themeColors['primaryLight']!, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Text( - "Verifier ${index + 1}", - style: const TextStyle( - fontWeight: FontWeight.w600, - fontSize: 14, + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + themeColors['primary']!, + themeColors['primary']! + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(22), + boxShadow: [ + BoxShadow( + color: themeColors['primary']!, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.verified_user, + color: Colors.white, + size: 20, ), ), - const SizedBox(height: 2), - Text( - verifier.shortAddress, - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 12, - fontFamily: 'monospace', + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + _copyToClipboard(verifier.address, context); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + verifier.shortAddress, + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + fontFamily: 'monospace', + ), + ), + const SizedBox(width: 6), + Icon( + Icons.copy, + size: 12, + color: themeColors['primary']!, + ), + ], + ), + ), + const SizedBox(width: 8), + if (verifier.proofHashes.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: themeColors['primary']!, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.photo_library, + size: 10, + color: themeColors['primary'], + ), + const SizedBox(width: 2), + Text( + "${verifier.proofHashes.length}", + style: TextStyle( + fontSize: 10, + color: themeColors['primary'], + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.access_time, + size: 12, + color: Colors.grey.shade500, + ), + const SizedBox(width: 4), + Text( + verifier.formattedTimestamp, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], ), ), + Icon( + Icons.chevron_right, + color: Colors.grey.shade400, + size: 20, + ), ], ), ), - if (canRemove) - GestureDetector( - onTap: () => _showRemoveVerifierDialog( - verifier, index, context, treeDetails, loadTreeDetails), + ), + ), + if (canRemove) + Positioned( + top: -6, + right: -6, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _showRemoveVerifierDialog(verifier, index, context, + treeDetails, loadTreeDetails, themeColors); + }, + borderRadius: BorderRadius.circular(15), child: Container( - width: 32, - height: 32, + width: 30, + height: 30, decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.red.shade200), + color: Colors.red.shade500, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], ), - child: Icon( + child: const Icon( Icons.close, - color: Colors.red.shade600, - size: 18, + color: Colors.white, + size: 16, ), ), ), - ], + ), + ), + ], + ), + ); +} + +void _showVerifierDetailsModal( + Verifier verifier, BuildContext context, Map themeColors) { + final screenSize = MediaQuery.of(context).size; + final dialogWidth = + screenSize.width * 0.9 > 400 ? 400.0 : screenSize.width * 0.9; + final dialogHeight = + screenSize.height * 0.8 > 500 ? 500.0 : screenSize.height * 0.8; + + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - if (verifier.description.isNotEmpty || verifier.proofHashes.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(6.0), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (verifier.description.isNotEmpty) ...[ - Text( - "Description:", - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Colors.grey.shade700, + child: Container( + constraints: BoxConstraints( + maxHeight: dialogHeight, + maxWidth: dialogWidth, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + themeColors['primary']!, + themeColors['primary']! + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), ), - ), - Text( - verifier.description, - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, + child: const Icon( + Icons.verified_user, + color: Colors.white, + size: 20, ), ), - const SizedBox(height: 4), - ], - Row( - children: [ - Icon(Icons.access_time, - size: 12, color: Colors.grey.shade500), - const SizedBox(width: 4), - Text( - verifier.formattedTimestamp, + const SizedBox(width: 12), + Expanded( + child: Text( + "Verification Details", style: TextStyle( - fontSize: 10, - color: Colors.grey.shade500, + fontSize: 20, + fontWeight: FontWeight.bold, + color: themeColors['primary'], ), ), - const Spacer(), - if (verifier.proofHashes.isNotEmpty) - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.blue.shade100, - borderRadius: BorderRadius.circular(8), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon(Icons.close, color: themeColors['primary']), + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.account_circle, + color: Colors.grey.shade600, size: 16), + const SizedBox(width: 6), + Text( + "Verifier Address", + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + ], ), - child: Text( - "${verifier.proofHashes.length} proof${verifier.proofHashes.length != 1 ? 's' : ''}", - style: TextStyle( - fontSize: 10, - color: Colors.blue.shade700, - fontWeight: FontWeight.w500, + const SizedBox(height: 4), + GestureDetector( + onTap: () => + _copyToClipboard(verifier.address, context), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: themeColors['primaryLight'], + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: themeColors['primaryBorder']!), + ), + child: Row( + children: [ + Expanded( + child: Text( + verifier.address, + style: const TextStyle( + fontSize: 13, + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(width: 8), + Icon( + Icons.copy, + size: 16, + color: themeColors['primary'], + ), + ], + ), ), ), + ], + ), + const SizedBox(height: 12), + _buildDetailRow( + "Verified On", + verifier.formattedTimestamp, + Icons.access_time, + themeColors), + const SizedBox(height: 12), + if (verifier.description.isNotEmpty) ...[ + _buildDetailRow("Description", verifier.description, + Icons.description, themeColors), + const SizedBox(height: 12), + ], + if (verifier.proofHashes.isNotEmpty) ...[ + Row( + children: [ + Icon(Icons.photo_library, + color: Colors.grey.shade600, size: 20), + const SizedBox(width: 8), + Text( + "Proof Images (${verifier.proofHashes.length})", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + ], + ), + const SizedBox(height: 8), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: verifier.proofHashes.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(right: 8), + width: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: themeColors['primaryBorder']!), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + verifier.proofHashes[index], + fit: BoxFit.cover, + errorBuilder: + (context, error, stackTrace) => + Container( + color: Colors.grey.shade100, + child: Icon( + Icons.broken_image, + color: Colors.grey.shade400, + size: 30, + ), + ), + loadingBuilder: + (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: Colors.grey.shade100, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + themeColors['primary']), + ), + ), + ); + }, + ), + ), + ); + }, + ), ), + const SizedBox(height: 12), + ], + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: verifier.isActive + ? themeColors['primary']! + : Colors.red.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: verifier.isActive + ? themeColors['primary']! + : Colors.red.shade200, + ), + ), + child: Row( + children: [ + Icon( + verifier.isActive + ? Icons.check_circle + : Icons.cancel, + color: verifier.isActive + ? themeColors['primary'] + : Colors.red.shade600, + size: 20, + ), + const SizedBox(width: 8), + Text( + verifier.isActive + ? "Active Verification" + : "Inactive Verification", + style: TextStyle( + color: verifier.isActive + ? themeColors['primary'] + : Colors.red.shade700, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 20), ], ), - ], + ), + ), + Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: themeColors['primary'], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text("Close"), + ), + ), ), + ], + ), + ), + ); + }, + ); +} + +Widget _buildDetailRow( + String label, String value, IconData icon, Map themeColors) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: Colors.grey.shade600, size: 16), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, ), ), - ], - ), + ], + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + value, + style: const TextStyle( + fontSize: 13, + fontFamily: 'monospace', + ), + ), + ), + ], ); } -void _showRemoveVerifierDialog(Verifier verifier, int index, - BuildContext context, Tree? treeDetails, Function loadTreeDetails) { +void _showRemoveVerifierDialog( + Verifier verifier, + int index, + BuildContext context, + Tree? treeDetails, + Function loadTreeDetails, + Map themeColors) { showDialog( context: context, builder: (BuildContext context) { @@ -482,8 +995,9 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey.shade100, + color: themeColors['primaryLight'], borderRadius: BorderRadius.circular(8), + border: Border.all(color: themeColors['primaryBorder']!), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -492,17 +1006,46 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, children: [ Icon(Icons.person, color: Colors.grey.shade600), const SizedBox(width: 8), - Expanded( - child: Text( - verifier.address, - style: const TextStyle( - fontFamily: 'monospace', - fontSize: 12, - ), + const Text( + "Address:", + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 12, ), ), ], ), + const SizedBox(height: 4), + GestureDetector( + onTap: () => _copyToClipboard(verifier.address, context), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: + Border.all(color: themeColors['primaryBorder']!), + ), + child: Row( + children: [ + Expanded( + child: Text( + verifier.address, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 11, + ), + ), + ), + Icon( + Icons.copy, + size: 14, + color: themeColors['primary'], + ), + ], + ), + ), + ), if (verifier.description.isNotEmpty) ...[ const SizedBox(height: 8), Text(