diff --git a/.env.stencil b/.env.stencil index 32eedc7..d474d43 100644 --- a/.env.stencil +++ b/.env.stencil @@ -1,7 +1,7 @@ #API KEYS AND SECRETS -API_KEY= -API_SECRET= +PINATA_API_KEY= +PINATA_API_SECRET= ALCHEMY_API_KEY= #APPLICATION CONFIGURATION diff --git a/lib/components/bottom_navigation_widget.dart b/lib/components/bottom_navigation_widget.dart index 93ea61e..43a7381 100644 --- a/lib/components/bottom_navigation_widget.dart +++ b/lib/components/bottom_navigation_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/utils/constants/bottom_nav_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; class BottomNavigationWidget extends StatelessWidget { final String currentRoute; @@ -24,9 +25,9 @@ class BottomNavigationWidget extends StatelessWidget { return BottomNavigationBar( currentIndex: _getCurrentIndex(), type: BottomNavigationBarType.fixed, - selectedItemColor: Theme.of(context).colorScheme.primary, - unselectedItemColor: Theme.of(context).colorScheme.onSurfaceVariant, - backgroundColor: const Color.fromARGB(255, 37, 236, 147), + selectedItemColor: getThemeColors(context)['secondary'], + unselectedItemColor: getThemeColors(context)['textSecondary'], + backgroundColor: getThemeColors(context)['primary'], elevation: 8, onTap: (index) { final route = BottomNavConstants.items[index].route; diff --git a/lib/components/transaction_dialog.dart b/lib/components/transaction_dialog.dart new file mode 100644 index 0000000..686872c --- /dev/null +++ b/lib/components/transaction_dialog.dart @@ -0,0 +1,256 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; + +class TransactionDialog extends StatelessWidget { + final String title; + final String message; + final String? transactionHash; + final bool isSuccess; + final VoidCallback? onClose; + + const TransactionDialog({ + super.key, + required this.title, + required this.message, + this.transactionHash, + this.isSuccess = true, + this.onClose, + }); + + @override + Widget build(BuildContext context) { + final colors = getThemeColors(context); + + return Dialog( + backgroundColor: colors['background'], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + side: BorderSide( + color: colors['border']!, + width: buttonborderWidth, + ), + ), + child: Container( + constraints: const BoxConstraints(maxWidth: 400), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: colors['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with icon and title + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isSuccess ? colors['primary'] : colors['error'], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colors['border']!, + width: 2, + ), + ), + child: Icon( + isSuccess ? Icons.check_circle : Icons.error, + color: colors['textPrimary'], + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: colors['textPrimary'], + ), + ), + ), + ], + ), + const SizedBox(height: 20), + + // Message + Text( + message, + style: TextStyle( + fontSize: 14, + color: colors['textPrimary'], + height: 1.5, + ), + ), + + if (transactionHash != null && transactionHash!.isNotEmpty) ...[ + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colors['secondary'], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: colors['border']!, + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.receipt_long, + size: 16, + color: colors['textPrimary'], + ), + const SizedBox(width: 8), + Text( + 'Transaction Hash', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: colors['textPrimary'], + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + '${transactionHash!.substring(0, 10)}...${transactionHash!.substring(transactionHash!.length - 8)}', + style: TextStyle( + fontSize: 12, + fontFamily: 'monospace', + color: colors['textPrimary'], + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + Clipboard.setData( + ClipboardData(text: transactionHash!)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Hash copied!'), + duration: const Duration(seconds: 1), + backgroundColor: colors['primary'], + ), + ); + }, + icon: Icon( + Icons.copy, + size: 16, + color: colors['textPrimary'], + ), + ), + ], + ), + ], + ), + ), + ], + + const SizedBox(height: 24), + + // Close Button + SizedBox( + width: double.infinity, + height: 50, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + if (onClose != null) onClose!(); + }, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + color: + isSuccess ? colors['primary'] : colors['secondary'], + border: Border.all( + color: colors['border']!, + width: buttonborderWidth, + ), + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + child: Center( + child: Text( + 'Close', + style: TextStyle( + color: colors['textPrimary'], + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + static void showSuccess( + BuildContext context, { + required String title, + required String message, + String? transactionHash, + VoidCallback? onClose, + }) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => TransactionDialog( + title: title, + message: message, + transactionHash: transactionHash, + isSuccess: true, + onClose: onClose, + ), + ); + } + }); + } + + static void showError( + BuildContext context, { + required String title, + required String message, + VoidCallback? onClose, + }) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => TransactionDialog( + title: title, + message: message, + isSuccess: false, + onClose: onClose, + ), + ); + } + }); + } +} diff --git a/lib/components/universal_navbar.dart b/lib/components/universal_navbar.dart index 9b7d166..801d484 100644 --- a/lib/components/universal_navbar.dart +++ b/lib/components/universal_navbar.dart @@ -12,8 +12,23 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { final String? title; final List? actions; final Widget? leading; + final bool showBackButton; + final bool isLoading; + final VoidCallback? onBackPressed; + final VoidCallback? onReload; + final bool showReloadButton; - const UniversalNavbar({super.key, this.title, this.actions, this.leading}); + const UniversalNavbar({ + super.key, + this.title, + this.actions, + this.leading, + this.showBackButton = false, + this.isLoading = false, + this.onBackPressed, + this.onReload, + this.showReloadButton = false, + }); @override Size get preferredSize => const Size.fromHeight(120.0); @@ -46,6 +61,31 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { children: [ if (leading != null) ...[ leading!, + ] else if (showBackButton) ...[ + Container( + width: 36, + height: 36, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + size: 20, + ), + onPressed: + onBackPressed ?? () => Navigator.of(context).pop(), + tooltip: 'Go Back', + ), + ), ], Expanded( flex: 2, @@ -77,7 +117,17 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { ), ), const SizedBox(width: 8), - if (title != null) + if (isLoading) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ) + else if (title != null) Flexible( child: Text( title!, @@ -105,34 +155,30 @@ class UniversalNavbar extends StatelessWidget implements PreferredSizeWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - // Container( - // width: 36, - // height: 36, - // decoration: BoxDecoration( - // color: Colors.white.withOpacity(0.2), - // borderRadius: BorderRadius.circular(8), - // border: Border.all( - // color: Colors.white.withOpacity(0.3), - // width: 1, - // ), - // ), - // child: IconButton( - // padding: EdgeInsets.zero, - // icon: Icon( - // themeProvider.isDarkMode - // ? Icons.light_mode - // : Icons.dark_mode, - // color: Colors.white, - // size: 18, - // ), - // onPressed: () { - // themeProvider.toggleTheme(); - // }, - // tooltip: themeProvider.isDarkMode - // ? 'Switch to Light Mode' - // : 'Switch to Dark Mode', - // ), - // ), + if (showReloadButton && onReload != null) + Container( + width: 36, + height: 36, + margin: const EdgeInsets.only(right: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.refresh, + color: Colors.white, + size: 20, + ), + onPressed: isLoading ? null : onReload, + tooltip: 'Reload Data', + ), + ), const SizedBox(width: 6), if (actions != null) ...actions!, if (walletProvider.isConnected && diff --git a/lib/components/wallet_connect_dialog.dart b/lib/components/wallet_connect_dialog.dart index 4dc8082..8735c01 100644 --- a/lib/components/wallet_connect_dialog.dart +++ b/lib/components/wallet_connect_dialog.dart @@ -2,6 +2,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/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; class WalletConnectDialog extends StatelessWidget { final String uri; @@ -12,91 +14,216 @@ class WalletConnectDialog extends StatelessWidget { Widget build(BuildContext context) { final walletProvider = Provider.of(context, listen: false); - return AlertDialog( - title: const Text('Choose Wallet'), - content: SizedBox( - width: 300, + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Select your wallet app to connect:', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + // Title + Text( + 'Choose Wallet', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), ), const SizedBox(height: 8), + Text( + 'Select your wallet app to connect:', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 4), Text( 'On emulator: Consider using web wallets or copying URI', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), + style: TextStyle( + fontSize: 11, + color: getThemeColors(context)['textSecondary'], + ), ), - const SizedBox(height: 16), + const SizedBox(height: 20), // Wallet options - ...(walletProvider.walletOptions.map( - (wallet) => Container( - margin: const EdgeInsets.only(bottom: 8), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - 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()), - backgroundColor: Colors.red, + ...walletProvider.walletOptions.asMap().entries.map( + (entry) { + final index = entry.key; + final wallet = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: SizedBox( + width: double.infinity, + height: 50, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: InkWell( + onTap: () 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()), + backgroundColor: + // ignore: use_build_context_synchronously + getThemeColors(context)['error'], + ), + ); + } + }, + borderRadius: + BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + color: index % 2 == 0 + ? getThemeColors(context)['primary'] + : getThemeColors(context)['secondary'], + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + wallet.icon, + size: 20, + color: getThemeColors(context)['textPrimary'], + ), + const SizedBox(width: 12), + Text( + wallet.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], ), - ); - } + ), + ), + ), + ), + ); + }, + ), + + Padding( + padding: const EdgeInsets.only(top: 8), + child: SizedBox( + width: double.infinity, + height: 50, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: InkWell( + onTap: () 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( + SnackBar( + content: const Text('URI copied to clipboard!'), + // ignore: use_build_context_synchronously + backgroundColor: getThemeColors(context)['primary'], + ), + ); }, - style: ElevatedButton.styleFrom( - backgroundColor: wallet.color, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + color: getThemeColors(context)['secondary'], + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: + BorderRadius.circular(buttonCircularRadius), ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.copy, + size: 20, + color: getThemeColors(context)['textPrimary'], + ), + const SizedBox(width: 12), + Text( + 'Copy URI', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], ), ), - icon: Icon(wallet.icon, size: 20), - label: Text(wallet.name), ), ), ), - )), - Container( - margin: const EdgeInsets.only(top: 8), + ), + + const SizedBox(height: 16), + Center( child: SizedBox( width: double.infinity, - 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!'), - backgroundColor: Colors.green, + height: 45, + child: Material( + elevation: 2, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + child: Center( + child: Text( + 'Cancel', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textPrimary'], + ), + ), ), - ); - }, - icon: const Icon(Icons.copy, size: 20), - label: const Text('Copy URI'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), ), ), ), @@ -105,14 +232,6 @@ class WalletConnectDialog extends StatelessWidget { ], ), ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ], ); } } diff --git a/lib/main.dart b/lib/main.dart index be64716..d519938 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:tree_planting_protocol/pages/home_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_details.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_images.dart'; +import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_organisation.dart'; import 'package:tree_planting_protocol/pages/mint_nft/submit_nft_page.dart'; import 'package:tree_planting_protocol/pages/organisations_pages/create_organisation.dart'; import 'package:tree_planting_protocol/pages/organisations_pages/organisation_details_page.dart'; @@ -13,6 +14,7 @@ import 'package:tree_planting_protocol/pages/register_user_page.dart'; import 'package:tree_planting_protocol/pages/settings_page.dart'; import 'package:tree_planting_protocol/pages/tree_details_page.dart'; import 'package:tree_planting_protocol/pages/trees_page.dart'; +import 'package:tree_planting_protocol/pages/user_profile_page.dart'; import 'package:tree_planting_protocol/pages/mint_nft/mint_nft_coordinates.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; @@ -90,6 +92,22 @@ class MyApp extends StatelessWidget { return const CreateOrganisationPage(); }, ), + GoRoute( + path: '/user-profile/:address', + name: 'user_profile', + builder: (BuildContext context, GoRouterState state) { + final address = state.pathParameters['address'].toString(); + return UserProfilePage( + userAddress: address.isNotEmpty ? address : ''); + }, + ), + GoRoute( + path: RouteConstants.mintNftOrganisationPath, + name: RouteConstants.mintNftOrganisation, + builder: (BuildContext context, GoRouterState state) { + return const MintNftOrganisationPage(); + }, + ), GoRoute( path: RouteConstants.mintNftPath, name: RouteConstants.mintNft, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 55e6c8a..64438a8 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -21,7 +21,11 @@ class HomePage extends StatelessWidget { return SingleChildScrollView( child: Column( children: [ - SizedBox(width: 400, child: ProfileSectionWidget()), + SizedBox( + width: 400, + child: ProfileSectionWidget( + userAddress: walletProvider.currentAddress ?? '', + )), SizedBox( width: 400, height: 600, diff --git a/lib/pages/mint_nft/mint_nft_coordinates.dart b/lib/pages/mint_nft/mint_nft_coordinates.dart index 978eb7b..28d00d9 100644 --- a/lib/pages/mint_nft/mint_nft_coordinates.dart +++ b/lib/pages/mint_nft/mint_nft_coordinates.dart @@ -126,11 +126,25 @@ class _MintNftCoordinatesPageState extends State { } try { + logger.d( + "Attempting to parse coordinates: latitude='$latitude', longitude='$longitude'"); + final lat = double.parse(latitude); final lng = double.parse(longitude); - if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { + + logger.d("Successfully parsed coordinates: lat=$lat, lng=$lng"); + + if (lat < -90 || lat > 90) { + _showCustomSnackBar( + "Invalid latitude: $lat. Must be between -90 and 90.", + isError: true, + ); + return; + } + + if (lng < -180 || lng > 180) { _showCustomSnackBar( - "Please enter valid coordinates. Latitude: -90 to 90, Longitude: -180 to 180", + "Invalid longitude: $lng. Must be between -180 and 180.", isError: true, ); return; @@ -138,16 +152,61 @@ class _MintNftCoordinatesPageState extends State { String geohash; try { + logger.d( + "Validation passed - lat: $lat (${lat >= -90 && lat <= 90}), lng: $lng (${lng >= -180 && lng <= 180})"); logger.d("About to generate geohash for: lat=$lat, lng=$lng"); - geohash = geoHasher.encode(lat, lng, precision: 12); + + // Additional validation before geohash generation + if (lat.isNaN || lng.isNaN) { + throw Exception("Invalid numeric values: lat=$lat, lng=$lng"); + } + + if (lat < -90 || lat > 90) { + throw Exception("Latitude $lat is out of range (-90 to 90)"); + } + + if (lng < -180 || lng > 180) { + throw Exception("Longitude $lng is out of range (-180 to 180)"); + } + logger + .d("Trying geohash generation with different parameter orders..."); + + try { + logger.d("Attempting: encode(lat=$lat, lng=$lng)"); + geohash = geoHasher.encode(lat, lng, precision: 12); + logger.d("Success with lat, lng order"); + } catch (e1) { + logger.d("Failed with lat, lng order: $e1"); + try { + // Alternative order: longitude first, latitude second + logger.d("Attempting: encode(lng=$lng, lat=$lat)"); + geohash = geoHasher.encode(lng, lat, precision: 12); + logger.d("Success with lng, lat order"); + } catch (e2) { + logger.e("Both parameter orders failed:"); + logger.e(" lat, lng order error: $e1"); + logger.e(" lng, lat order error: $e2"); + throw Exception( + "Geohash generation failed with both parameter orders. lat,lng: $e1 | lng,lat: $e2"); + } + } logger.d("Generated geohash: $geohash"); } catch (geohashError) { - logger.d("Geohash error: $geohashError"); + logger.e("All geohash generation attempts failed:"); + logger.e(" Input latitude: $lat (from text: '$latitude')"); + logger.e(" Input longitude: $lng (from text: '$longitude')"); + logger.e(" Error: $geohashError"); + + // Use a simple fallback geohash if the library fails + logger.d("Creating fallback geohash..."); + geohash = + "fallback_${lat.abs().toStringAsFixed(6)}_${lng.abs().toStringAsFixed(6)}"; + logger.d("Fallback geohash created: $geohash"); + _showCustomSnackBar( - "Error generating location code: $geohashError", - isError: true, + "Warning: Using fallback location code due to geohash error. Coordinates saved successfully.", + isError: false, ); - return; } Provider.of(context, listen: false).setLatitude(lat); @@ -160,8 +219,10 @@ class _MintNftCoordinatesPageState extends State { _showCustomSnackBar("Coordinates submitted successfully!"); context.push(RouteConstants.mintNftDetailsPath); } catch (e) { + logger.e("Coordinate parsing error: $e"); + logger.e("Input values - latitude: '$latitude', longitude: '$longitude'"); _showCustomSnackBar( - "Please enter valid numeric coordinates. $latitude", + "Please enter valid numeric coordinates.\nLatitude: '$latitude'\nLongitude: '$longitude'\nError: $e", isError: true, ); } @@ -225,6 +286,7 @@ class _MintNftCoordinatesPageState extends State { return BaseScaffold( title: "Mint NFT Coordinates", + showBackButton: true, body: SingleChildScrollView( padding: EdgeInsets.symmetric( horizontal: screenWidth * 0.05, @@ -246,7 +308,19 @@ class _MintNftCoordinatesPageState extends State { width: double.infinity, constraints: BoxConstraints(maxWidth: screenWidth * 0.92), decoration: BoxDecoration( + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(24), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: getThemeColors(context)['shadow']!, + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -255,9 +329,10 @@ class _MintNftCoordinatesPageState extends State { width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( + color: getThemeColors(context)['primary'], borderRadius: const BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), + topLeft: Radius.circular(22), + topRight: Radius.circular(22), ), ), child: Row( @@ -265,7 +340,12 @@ class _MintNftCoordinatesPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(14), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), ), child: Icon( Icons.location_on, @@ -283,14 +363,14 @@ class _MintNftCoordinatesPageState extends State { style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: getThemeColors(context)['primary'], + color: getThemeColors(context)['textSecondary'], ), ), Text( 'Mark where your tree is planted', style: TextStyle( fontSize: 14, - color: getThemeColors(context)['textPrimary'], + color: getThemeColors(context)['textSecondary']!, ), ), ], @@ -309,17 +389,18 @@ class _MintNftCoordinatesPageState extends State { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: primaryYellowColor, + color: getThemeColors(context)['secondary'], borderRadius: BorderRadius.circular(12), border: Border.all( - color: getThemeColors(context)['secondary']!, + color: getThemeColors(context)['border']!, + width: 2, ), ), child: Row( children: [ Icon( Icons.info_outline, - color: getThemeColors(context)['primary'], + color: getThemeColors(context)['textPrimary'], size: 20, ), const SizedBox(width: 12), @@ -327,7 +408,7 @@ class _MintNftCoordinatesPageState extends State { child: Text( 'Tap on the map or enter coordinates manually below', style: TextStyle( - fontSize: 14, + fontSize: 13, color: getThemeColors(context)['textPrimary'], fontWeight: FontWeight.w500, ), @@ -342,59 +423,60 @@ class _MintNftCoordinatesPageState extends State { Expanded( child: _buildCoordinateField( controller: latitudeController, - label: 'Latitude', + label: 'Latitude (Y)', icon: Icons.straighten, - hint: '-90 to 90', + hint: 'e.g. 37.7749 (-90 to 90)', ), ), const SizedBox(width: 16), Expanded( child: _buildCoordinateField( controller: longitudeController, - label: 'Longitude', + label: 'Longitude (X)', icon: Icons.straighten, - hint: '-180 to 180', + hint: 'e.g. -122.4194 (-180 to 180)', ), ), ], ), const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: submitCoordinates, - style: ElevatedButton.styleFrom( - backgroundColor: getThemeColors(context)['primary'], - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Continue', - style: TextStyle( - color: getThemeColors(context)['textPrimary'], - fontSize: 18, - fontWeight: FontWeight.w600, - ), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: submitCoordinates, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textSecondary'], + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Colors.black, width: 2), ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: getThemeColors(context)['primary'], - borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Continue', + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 18, + fontWeight: FontWeight.w600, + ), ), - child: const Icon( + const SizedBox(width: 8), + Icon( Icons.arrow_forward, - size: 18, + size: 20, + color: getThemeColors(context)['textSecondary'], ), - ), - ], + ], + ), ), ), ), @@ -567,10 +649,10 @@ class _MintNftCoordinatesPageState extends State { const SizedBox(height: 8), Container( decoration: BoxDecoration( - color: Colors.white, + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(12), border: Border.all( - color: getThemeColors(context)['secondary']!, + color: getThemeColors(context)['border']!, width: 2, ), ), diff --git a/lib/pages/mint_nft/mint_nft_details.dart b/lib/pages/mint_nft/mint_nft_details.dart index 88f70f1..0f04520 100644 --- a/lib/pages/mint_nft/mint_nft_details.dart +++ b/lib/pages/mint_nft/mint_nft_details.dart @@ -4,8 +4,8 @@ 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/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/tree_species_constants.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; -import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_view_details_with_map.dart'; class MintNftDetailsPage extends StatefulWidget { const MintNftDetailsPage({super.key}); @@ -16,15 +16,25 @@ class MintNftDetailsPage extends StatefulWidget { class _MintNftCoordinatesPageState extends State { final descriptionController = TextEditingController(); - final speciesController = TextEditingController(); + String? selectedSpecies; + int numberOfTrees = 1; void submitDetails() { final description = descriptionController.text; - final species = speciesController.text; - if (description.isEmpty || species.isEmpty) { + if (description.isEmpty || + selectedSpecies == null || + selectedSpecies!.isEmpty) { _showCustomSnackBar( - "Please enter both description and species.", + "Please enter description and select a tree species.", + isError: true, + ); + return; + } + + if (numberOfTrees <= 0) { + _showCustomSnackBar( + "Please select at least 1 tree.", isError: true, ); return; @@ -32,10 +42,15 @@ class _MintNftCoordinatesPageState extends State { Provider.of(context, listen: false) .setDescription(description); - Provider.of(context, listen: false).setSpecies(species); + Provider.of(context, listen: false) + .setSpecies(selectedSpecies!); + Provider.of(context, listen: false) + .setNumberOfTrees(numberOfTrees); _showCustomSnackBar("Details submitted successfully!"); - context.push(RouteConstants.mintNftImagesPath); + + // Navigate to organization selection page + context.push(RouteConstants.mintNftOrganisationPath); } void _showCustomSnackBar(String message, {bool isError = false}) { @@ -78,10 +93,8 @@ class _MintNftCoordinatesPageState extends State { return BaseScaffold( title: "NFT Details", - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop(), - ), + showBackButton: true, + onBackPressed: () => context.pop(), body: SingleChildScrollView( padding: EdgeInsets.symmetric( horizontal: screenWidth * 0.05, @@ -103,14 +116,19 @@ class _MintNftCoordinatesPageState extends State { width: double.infinity, constraints: BoxConstraints(maxWidth: screenWidth * 0.92), decoration: BoxDecoration( + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(24), - // boxShadow: [ - // BoxShadow( - // color: const Color(0xFF1CD381), - // blurRadius: 20, - // offset: const Offset(0, 8), - // ), - // ], + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: getThemeColors(context)['shadow']!, + blurRadius: 10, + offset: const Offset(0, 8), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -119,15 +137,10 @@ class _MintNftCoordinatesPageState extends State { width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( - // gradient: LinearGradient( - // colors: [ - // const Color(0xFF1CD381), - // const Color(0xFF1CD381), - // ], - // ), + color: getThemeColors(context)['primary'], borderRadius: const BorderRadius.only( - topLeft: Radius.circular(24), - topRight: Radius.circular(24), + topLeft: Radius.circular(22), + topRight: Radius.circular(22), ), ), child: Row( @@ -137,10 +150,14 @@ class _MintNftCoordinatesPageState extends State { decoration: BoxDecoration( color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(14), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), ), child: Icon( Icons.edit_note, - color: getThemeColors(context)['icon'], + color: getThemeColors(context)['primary'], size: 28, ), ), @@ -154,14 +171,14 @@ class _MintNftCoordinatesPageState extends State { style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, - color: getThemeColors(context)['textPrimary'], + color: getThemeColors(context)['textSecondary'], ), ), Text( 'Tell us about your tree', style: TextStyle( fontSize: 14, - color: getThemeColors(context)['textPrimary'], + color: getThemeColors(context)['textSecondary']!, ), ), ], @@ -175,13 +192,9 @@ class _MintNftCoordinatesPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildFormField( - controller: speciesController, - label: 'Tree Species', - hint: 'e.g., Oak, Pine, Maple...', - icon: Icons.eco, - maxLines: 1, - ), + _buildSpeciesDropdown(), + const SizedBox(height: 20), + _buildNumberOfTreesPicker(), const SizedBox(height: 20), _buildFormField( controller: descriptionController, @@ -192,42 +205,43 @@ class _MintNftCoordinatesPageState extends State { minLines: 3, ), const SizedBox(height: 32), - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: submitDetails, - style: ElevatedButton.styleFrom( - backgroundColor: getThemeColors(context)['primary'], - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - shadowColor: primaryGreenColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Continue', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - ), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(16), + child: SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: submitDetails, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textSecondary'], + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Colors.black, width: 2), ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: getThemeColors(context)['primary'], - borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Continue', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textSecondary'], + ), ), - child: const Icon( + const SizedBox(width: 8), + Icon( Icons.arrow_forward, - size: 18, + size: 20, + color: getThemeColors(context)['textSecondary'], ), - ), - ], + ], + ), ), ), ), @@ -246,6 +260,7 @@ class _MintNftCoordinatesPageState extends State { required IconData icon, int maxLines = 1, int? minLines, + TextInputType keyboardType = TextInputType.text, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -277,24 +292,18 @@ class _MintNftCoordinatesPageState extends State { const SizedBox(height: 12), Container( decoration: BoxDecoration( - color: Colors.white, + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(16), border: Border.all( - color: const Color(0xFFFAEB96), + color: getThemeColors(context)['border']!, width: 2, ), - // boxShadow: [ - // BoxShadow( - // // color: const Color(0xFF1CD381), - // blurRadius: 8, - // offset: const Offset(0, 2), - // ), - // ], ), child: TextField( controller: controller, maxLines: maxLines, minLines: minLines, + keyboardType: keyboardType, style: TextStyle( fontSize: 16, color: getThemeColors(context)['textPrimary'], @@ -320,40 +329,211 @@ class _MintNftCoordinatesPageState extends State { ); } - // ignore: unused_element - Widget _buildPreviewSection() { + Widget _buildSpeciesDropdown() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.eco, + color: getThemeColors(context)['icon'], + size: 18, + ), + ), + const SizedBox(width: 8), + const Text( + 'Tree Species', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF1CD381), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFFAEB96), + width: 2, + ), + ), + child: DropdownButtonFormField( + value: selectedSpecies, + decoration: InputDecoration( + hintText: 'Select tree species...', + hintStyle: TextStyle( + color: getThemeColors(context)['background'], + fontSize: 14, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(16), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.all(20), + filled: true, + fillColor: getThemeColors(context)['background'], + ), + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['textPrimary'], + height: 1.4, + ), + items: TreeSpeciesConstants.getAllSpecies().map((String species) { + return DropdownMenuItem( + value: species, + child: Text(species), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + selectedSpecies = newValue; + }); + }, + ), + ), + ], + ); + } + + Widget _buildNumberOfTreesPicker() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.format_list_numbered, + color: getThemeColors(context)['icon'], + size: 18, + ), + ), + const SizedBox(width: 8), + const Text( + 'Number of Trees', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF1CD381), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + height: 120, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: const Color(0xFFFAEB96), + width: 2, + ), + ), child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ + // Decrease button Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: const Color(0xFFFAEB96), - borderRadius: BorderRadius.circular(10), + margin: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () { + if (numberOfTrees > 1) { + setState(() { + numberOfTrees--; + }); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + shape: const CircleBorder(), + padding: const EdgeInsets.all(12), + ), + child: const Icon( + Icons.remove, + color: Colors.white, + size: 20, + ), ), - child: Icon( - Icons.preview, - color: primaryGreenColor, - size: 20, + ), + + // Number display + Expanded( + flex: 2, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['primary']!, + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '$numberOfTrees', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['primary'], + ), + ), + Text( + numberOfTrees == 1 ? 'Tree' : 'Trees', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), ), ), - const SizedBox(width: 12), - Text( - 'Live Preview', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: primaryGreenColor, + + // Increase button + Container( + margin: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () { + if (numberOfTrees < 100) { + // Set a reasonable max limit + setState(() { + numberOfTrees++; + }); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + shape: const CircleBorder(), + padding: const EdgeInsets.all(12), + ), + child: const Icon( + Icons.add, + color: Colors.white, + size: 20, + ), ), ), ], ), ), - const NewNFTMapWidget(), ], ); } @@ -361,7 +541,6 @@ class _MintNftCoordinatesPageState extends State { @override void dispose() { descriptionController.dispose(); - speciesController.dispose(); super.dispose(); } } diff --git a/lib/pages/mint_nft/mint_nft_images.dart b/lib/pages/mint_nft/mint_nft_images.dart index 1090913..195f305 100644 --- a/lib/pages/mint_nft/mint_nft_images.dart +++ b/lib/pages/mint_nft/mint_nft_images.dart @@ -5,6 +5,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/ipfs_services.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; @@ -149,6 +150,7 @@ class _MultipleImageUploadPageState extends State { Widget build(BuildContext context) { return BaseScaffold( title: "Mint Tree NFT", + showBackButton: true, body: Column( children: [ Container( @@ -171,27 +173,68 @@ class _MultipleImageUploadPageState extends State { Row( children: [ Expanded( - child: ElevatedButton.icon( - onPressed: - _isUploading ? null : _pickAndUploadImages, - icon: const Icon(Icons.add_photo_alternate), - label: Text( - 'Add Photos', - style: TextStyle(color: primaryGreenColor), + flex: 2, + child: Material( + elevation: 4, + borderRadius: + BorderRadius.circular(buttonCircularRadius), + child: ElevatedButton.icon( + onPressed: + _isUploading ? null : _pickAndUploadImages, + icon: const Icon(Icons.add_photo_alternate, + size: 20), + label: const Text('Add Photos'), + style: ElevatedButton.styleFrom( + backgroundColor: + getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textSecondary'], + elevation: 0, + padding: + const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + buttonCircularRadius), + side: const BorderSide( + color: Colors.black, width: 2), + ), + ), ), ), ), - const SizedBox(width: 16), - if (_uploadedHashes.isNotEmpty) - ElevatedButton.icon( - onPressed: _isUploading ? null : _removeAllImages, - icon: const Icon(Icons.delete_sweep), - label: const Text('Remove All'), - style: ElevatedButton.styleFrom( - foregroundColor: - getThemeColors(context)['secondaryButton'], + if (_uploadedHashes.isNotEmpty) ...[ + const SizedBox(width: 16), + Expanded( + child: Material( + elevation: 4, + borderRadius: + BorderRadius.circular(buttonCircularRadius), + child: OutlinedButton.icon( + onPressed: + _isUploading ? null : _removeAllImages, + icon: const Icon(Icons.delete_sweep, size: 20), + label: const Text('Clear All'), + style: OutlinedButton.styleFrom( + foregroundColor: + getThemeColors(context)['textSecondary'], + backgroundColor: getThemeColors( + context)['secondaryButton'], + elevation: 0, + padding: + const EdgeInsets.symmetric(vertical: 16), + side: BorderSide( + color: getThemeColors(context)['border']!, + width: 2, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + buttonCircularRadius), + ), + ), + ), ), ), + ], ], ), const SizedBox(height: 16), @@ -336,15 +379,37 @@ class _MultipleImageUploadPageState extends State { style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), - ElevatedButton( - onPressed: _uploadedHashes.isEmpty - ? null - : () { - context.push('/mint-nft/submit-nft'); - }, - child: Text( - "Submit NFT", - style: Theme.of(context).textTheme.titleMedium, + Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: ElevatedButton( + onPressed: _uploadedHashes.isEmpty + ? null + : () { + context.push('/mint-nft/submit-nft'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textSecondary'], + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + minimumSize: const Size(double.infinity, 50), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + side: + const BorderSide(color: Colors.black, width: 2), + ), + ), + child: Text( + "Submit NFT", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textSecondary'], + ), + ), ), ), const SizedBox(height: 16), diff --git a/lib/pages/mint_nft/mint_nft_organisation.dart b/lib/pages/mint_nft/mint_nft_organisation.dart new file mode 100644 index 0000000..2d5fd60 --- /dev/null +++ b/lib/pages/mint_nft/mint_nft_organisation.dart @@ -0,0 +1,737 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/utils/constants/route_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_contract_read_functions.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; + +class MintNftOrganisationPage extends StatefulWidget { + const MintNftOrganisationPage({super.key}); + + @override + State createState() => + _MintNftOrganisationPageState(); +} + +class _MintNftOrganisationPageState extends State { + List _userOrganisations = []; + String? _selectedOrganisation; + bool _isLoading = true; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _loadUserOrganisations(); + } + + Future _loadUserOrganisations() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final walletProvider = + Provider.of(context, listen: false); + + final result = await ContractReadFunctions.getOrganisationsByUser( + walletProvider: walletProvider, + ); + + if (result.success && result.data != null) { + final data = result.data as Map; + final organisations = data['organisations'] as List; + + setState(() { + _userOrganisations = organisations.map((e) => e.toString()).toList(); + _isLoading = false; + }); + + logger.d("User organisations loaded: $_userOrganisations"); + } else { + setState(() { + _errorMessage = result.errorMessage ?? 'Failed to load organisations'; + _isLoading = false; + }); + } + } catch (e) { + logger.e("Error loading organisations: $e"); + setState(() { + _errorMessage = 'Error loading organisations: $e'; + _isLoading = false; + }); + } + } + + void _submitOrganisation() { + if (_selectedOrganisation == null || _selectedOrganisation!.isEmpty) { + _showCustomSnackBar( + "Please select an organisation", + isError: true, + ); + return; + } + Provider.of(context, listen: false) + .setOrganisationAddress(_selectedOrganisation!); + + _showCustomSnackBar("Organisation selected successfully!"); + context.push(RouteConstants.mintNftImagesPath); + } + + void _showCustomSnackBar(String message, {bool isError = false}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + isError ? Icons.error_outline : Icons.check_circle_outline, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + message, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + backgroundColor: + isError ? Colors.red.shade400 : getThemeColors(context)['primary'], + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(16), + ), + ); + } + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + + return BaseScaffold( + title: "Select Organisation", + showBackButton: true, + isLoading: _isLoading, + body: _isLoading + ? _buildLoadingState() + : _errorMessage != null + ? _buildErrorState() + : _userOrganisations.isEmpty + ? _buildEmptyState() + : SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: screenWidth * 0.05, + vertical: 20, + ), + child: Column( + children: [ + _buildFormSection(screenWidth, context), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildLoadingState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + getThemeColors(context)['primary']!), + ), + const SizedBox(height: 16), + Text( + 'Loading your organisations...', + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + ); + } + + Widget _buildErrorState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: getThemeColors(context)['error'], + ), + const SizedBox(height: 16), + Text( + 'Failed to Load Organisations', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 8), + Text( + _errorMessage ?? 'Unknown error occurred', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: _loadUserOrganisations, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: getThemeColors(context)['textSecondary'], + elevation: 4, + padding: + const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + side: const BorderSide(color: Colors.black, width: 2), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary']!, + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 3), + ), + child: Icon( + Icons.business_outlined, + size: 64, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 24), + Text( + 'No Organisations Found', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 12), + Text( + 'You are not a member of any organisations yet. You can mint individually or create/join an organisation.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + height: 1.5, + ), + ), + const SizedBox(height: 32), + // Mint Individually button (primary action) + Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + logger.i("=== MINT INDIVIDUALLY FROM EMPTY STATE ==="); + logger.i("Clearing organisation address to empty string"); + Provider.of(context, listen: false) + .setOrganisationAddress(""); + final address = + Provider.of(context, listen: false) + .organisationAddress; + logger.i("Organisation address after clearing: '$address'"); + logger.i("Address length: ${address.length}"); + context.push(RouteConstants.mintNftImagesPath); + }, + icon: Icon(Icons.person, + size: 20, color: getThemeColors(context)['textPrimary']), + label: Text( + 'Mint Individually', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: getThemeColors(context)['textPrimary'], + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + side: const BorderSide(color: Colors.black, width: 2), + ), + ), + ), + ), + ), + const SizedBox(height: 16), + const Divider(height: 32), + Text( + 'Or manage organisations', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: OutlinedButton.icon( + onPressed: () { + context.push('/create-organisation'); + }, + icon: Icon(Icons.add, + size: 20, + color: getThemeColors(context)['textPrimary']), + label: Text( + 'Create', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.w600, + ), + ), + style: OutlinedButton.styleFrom( + foregroundColor: getThemeColors(context)['textPrimary'], + backgroundColor: getThemeColors(context)['secondary'], + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + side: BorderSide( + color: Colors.black, + width: 2, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: OutlinedButton.icon( + onPressed: () { + context.push('/organisations'); + }, + icon: Icon(Icons.search, + size: 20, + color: getThemeColors(context)['textPrimary']), + label: Text( + 'View All', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.w600, + ), + ), + style: OutlinedButton.styleFrom( + foregroundColor: getThemeColors(context)['textPrimary'], + backgroundColor: getThemeColors(context)['secondary'], + elevation: 0, + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 12), + side: BorderSide( + color: Colors.black, + width: 2, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildFormSection(double screenWidth, BuildContext context) { + return Container( + width: double.infinity, + constraints: BoxConstraints(maxWidth: screenWidth * 0.92), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: getThemeColors(context)['shadow']!, + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Icon( + Icons.business, + color: getThemeColors(context)['primary'], + size: 28, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Choose Organisation', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textSecondary'], + ), + ), + Text( + 'Select organisation or mint individually', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textSecondary']!, + ), + ), + ], + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: getThemeColors(context)['textPrimary'], + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Minting on behalf of an organisation will create a proposal that needs approval from organisation members', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + Text( + 'Your Organisations', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: _selectedOrganisation, + hint: Text( + 'Select an organisation', + style: TextStyle( + color: getThemeColors(context)['textPrimary']!, + fontSize: 14, + ), + ), + isExpanded: true, + icon: Icon( + Icons.arrow_drop_down, + color: getThemeColors(context)['primary'], + size: 30, + ), + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + items: _userOrganisations.map((String organisation) { + return DropdownMenuItem( + value: organisation, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${organisation.substring(0, 6)}...${organisation.substring(organisation.length - 4)}', + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 2), + Text( + organisation, + style: TextStyle( + fontSize: 10, + color: + getThemeColors(context)['textPrimary']!, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + setState(() { + _selectedOrganisation = newValue; + }); + }, + ), + ), + ), + if (_selectedOrganisation != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['primary']!, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + Icons.check_circle, + color: getThemeColors(context)['primary'], + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Selected Organisation:', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + _selectedOrganisation!, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + fontFamily: 'monospace', + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ], + const SizedBox(height: 32), + Column( + children: [ + Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () { + // Clear organization address and skip to images + logger + .i("=== MINT INDIVIDUALLY BUTTON CLICKED ==="); + logger.i( + "Clearing organisation address to empty string"); + Provider.of(context, listen: false) + .setOrganisationAddress(""); + final address = Provider.of( + context, + listen: false) + .organisationAddress; + logger.i( + "Organisation address after clearing: '$address'"); + logger.i("Address length: ${address.length}"); + context.push(RouteConstants.mintNftImagesPath); + }, + icon: const Icon(Icons.person, size: 20), + label: const Text('Mint Individually'), + style: OutlinedButton.styleFrom( + foregroundColor: + getThemeColors(context)['textPrimary'], + backgroundColor: + getThemeColors(context)['secondary'], + elevation: 0, + padding: const EdgeInsets.symmetric( + vertical: 16, horizontal: 24), + side: BorderSide( + color: getThemeColors(context)['border']!, + width: 2, + ), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _submitOrganisation, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textSecondary'], + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + side: const BorderSide( + color: Colors.black, width: 2), + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Continue', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + SizedBox(width: 8), + Icon(Icons.arrow_forward, size: 20), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/mint_nft/submit_nft_page.dart b/lib/pages/mint_nft/submit_nft_page.dart index 9b4e48e..8d94828 100644 --- a/lib/pages/mint_nft/submit_nft_page.dart +++ b/lib/pages/mint_nft/submit_nft_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/components/transaction_dialog.dart'; import 'package:tree_planting_protocol/providers/mint_nft_provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; @@ -7,6 +8,7 @@ import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_view_details_with_map.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart'; class SubmitNFTPage extends StatefulWidget { const SubmitNFTPage({super.key}); @@ -22,52 +24,21 @@ class _SubmitNFTPageState extends State { String? lastTransactionHash; Map? lastTransactionData; - void _showSuccessDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.check_circle, - color: getThemeColors(context)['primary']), - const SizedBox(width: 8), - Text(title), - ], - ), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, + void _showSuccessDialog(String title, String message, + {String? transactionHash}) { + TransactionDialog.showSuccess( + context, + title: title, + message: message, + transactionHash: transactionHash, ); } void _showErrorDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.error, color: getThemeColors(context)['error']), - const SizedBox(width: 8), - Text(title), - ], - ), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, + TransactionDialog.showError( + context, + title: title, + message: message, ); } @@ -76,21 +47,63 @@ class _SubmitNFTPageState extends State { final mintNftProvider = Provider.of(context, listen: false); + logger.i("=== STARTING MINT TRANSACTION ==="); + logger.i( + "Organisation address from provider: '${mintNftProvider.organisationAddress}'"); + logger.i( + "Organisation address length: ${mintNftProvider.organisationAddress.length}"); + logger.i( + "Organisation address isEmpty: ${mintNftProvider.organisationAddress.isEmpty}"); + logger.i( + "Organisation address isNotEmpty: ${mintNftProvider.organisationAddress.isNotEmpty}"); + setState(() { isMinting = true; errorMessage = null; }); try { - final result = await ContractWriteFunctions.mintNft( - walletProvider: walletProvider, - latitude: mintNftProvider.getLatitude(), - longitude: mintNftProvider.getLongitude(), - species: mintNftProvider.getSpecies(), - photos: mintNftProvider.getInitialPhotos(), - geoHash: mintNftProvider.getGeoHash(), - metadata: mintNftProvider.getDetails(), - ); + // Check if organisation is selected + final bool isOrganisationMint = + mintNftProvider.organisationAddress.isNotEmpty; + + logger + .i("Organisation address: '${mintNftProvider.organisationAddress}'"); + logger.i("Is organisation mint: $isOrganisationMint"); + + dynamic result; + + if (isOrganisationMint) { + // Call plantTreeProposal for organisation + logger.i( + ">>> TAKING ORGANISATION PATH: ${mintNftProvider.organisationAddress}"); + result = await OrganisationContractWriteFunctions.plantTreeProposal( + organisationContractAddress: mintNftProvider.organisationAddress, + walletProvider: walletProvider, + latitude: mintNftProvider.getLatitude(), + longitude: mintNftProvider.getLongitude(), + species: mintNftProvider.getSpecies(), + photos: mintNftProvider.getInitialPhotos(), + geoHash: mintNftProvider.getGeoHash(), + numberOfTrees: mintNftProvider.getNumberOfTrees(), + metadata: mintNftProvider.getDetails(), + ); + } else { + // Call mintNft for individual minting + logger.i(">>> TAKING INDIVIDUAL PATH - Minting individually"); + result = await ContractWriteFunctions.mintNft( + walletProvider: walletProvider, + latitude: mintNftProvider.getLatitude(), + longitude: mintNftProvider.getLongitude(), + species: mintNftProvider.getSpecies(), + photos: mintNftProvider.getInitialPhotos(), + geoHash: mintNftProvider.getGeoHash(), + numberOfTrees: mintNftProvider.getNumberOfTrees(), + metadata: mintNftProvider.getDetails(), + ); + } + + if (!mounted) return; setState(() { isMinting = false; @@ -107,17 +120,25 @@ class _SubmitNFTPageState extends State { if (result.success) { _showSuccessDialog( - 'Transaction Sent!', - 'Transaction hash: ${result.transactionHash!.substring(0, 10)}...\n\n' - 'The NFT will be minted once the transaction is confirmed.\n\n' - 'Species: ${result.data['species']}\n' - 'Photos: ${result.data['photos'].length} uploaded', + isOrganisationMint ? 'Proposal Submitted!' : 'Transaction Sent!', + isOrganisationMint + ? 'Your tree planting proposal has been submitted to the organisation for approval.\n\n' + 'Species: ${result.data['species']}\n' + 'Photos: ${result.data['photos'].length} uploaded' + : 'The NFT will be minted once the transaction is confirmed.\n\n' + 'Species: ${result.data['species']}\n' + 'Photos: ${result.data['photos'].length} uploaded', + transactionHash: result.transactionHash, ); + + // Clear the organisation address after successful submission + mintNftProvider.clearData(); } else { _showErrorDialog('Transaction Failed', result.errorMessage!); } } catch (e) { logger.e("Unexpected error in _mintTreeNft", error: e); + if (!mounted) return; setState(() { isMinting = false; errorMessage = 'Unexpected error: ${e.toString()}'; @@ -133,22 +154,27 @@ class _SubmitNFTPageState extends State { margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: getThemeColors(context)['primary'], - borderRadius: BorderRadius.circular(8), - border: Border.all(color: getThemeColors(context)['primaryBorder']!), + color: getThemeColors(context)['primary']!, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(Icons.receipt_long, color: getThemeColors(context)['icon']!), + Icon(Icons.receipt_long, + color: getThemeColors(context)['primary']!), const SizedBox(width: 8), - const Text( + Text( "Last Transaction:", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, + color: getThemeColors(context)['textPrimary'], ), ), ], @@ -156,24 +182,34 @@ class _SubmitNFTPageState extends State { const SizedBox(height: 8), Text( "Hash: ${lastTransactionHash!.substring(0, 20)}...", - style: const TextStyle( + style: TextStyle( fontFamily: 'monospace', fontSize: 12, + color: getThemeColors(context)['textPrimary'], ), ), if (lastTransactionData != null) ...[ const SizedBox(height: 8), Text( "Species: ${lastTransactionData!['species']}", - style: const TextStyle(fontSize: 12), + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), ), Text( "Photos: ${lastTransactionData!['photos']?.length ?? 0}", - style: const TextStyle(fontSize: 12), + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), ), Text( "Location: (${lastTransactionData!['latitude']?.toStringAsFixed(6)}, ${lastTransactionData!['longitude']?.toStringAsFixed(6)})", - style: const TextStyle(fontSize: 12), + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), ), ], ], @@ -189,8 +225,11 @@ class _SubmitNFTPageState extends State { padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: getThemeColors(context)['error']!, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: getThemeColors(context)['error']!), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['error']!, + width: 2, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -200,11 +239,12 @@ class _SubmitNFTPageState extends State { Icon(Icons.error_outline, color: getThemeColors(context)['error']!), const SizedBox(width: 8), - const Text( + Text( "Error:", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, + color: getThemeColors(context)['error'], ), ), ], @@ -213,7 +253,7 @@ class _SubmitNFTPageState extends State { Text( errorMessage!, style: TextStyle( - color: getThemeColors(context)['error']!, + color: getThemeColors(context)['textPrimary'], fontSize: 14, ), ), @@ -224,8 +264,14 @@ class _SubmitNFTPageState extends State { @override Widget build(BuildContext context) { + final mintNftProvider = Provider.of(context); + final bool isOrganisationMint = + mintNftProvider.organisationAddress.isNotEmpty; + return BaseScaffold( title: "Submit NFT", + showBackButton: true, + isLoading: isMinting, body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -235,38 +281,61 @@ class _SubmitNFTPageState extends State { _buildErrorInfo(), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: isMinting ? null : _mintTreeNft, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isMinting ? null : _mintTreeNft, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: getThemeColors(context)['textSecondary'], + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.black, width: 2), + ), ), - ), - child: isMinting - ? const Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: - AlwaysStoppedAnimation(Colors.white), + child: isMinting + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + getThemeColors( + context)['textSecondary']!), + ), ), + const SizedBox(width: 8), + Text( + isOrganisationMint + ? "Submitting Proposal..." + : "Minting...", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: + getThemeColors(context)['textSecondary'], + ), + ), + ], + ) + : Text( + isOrganisationMint ? "Submit Proposal" : "Mint NFT", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textSecondary'], ), - SizedBox(width: 8), - Text("Minting...", style: TextStyle(fontSize: 16)), - ], - ) - : const Text( - "Mint NFT", - style: TextStyle(fontSize: 16), - ), + ), + ), ), ), ), diff --git a/lib/pages/organisations_pages/create_organisation.dart b/lib/pages/organisations_pages/create_organisation.dart index de0f320..b7a9fb7 100644 --- a/lib/pages/organisations_pages/create_organisation.dart +++ b/lib/pages/organisations_pages/create_organisation.dart @@ -161,10 +161,9 @@ class _CreateOrganisationPageState extends State { return BaseScaffold( title: "Organisation Details", - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => context.pop(), - ), + showBackButton: true, + onBackPressed: () => context.pop(), + isLoading: _isUploading, body: SingleChildScrollView( padding: EdgeInsets.symmetric( horizontal: screenWidth * 0.05, diff --git a/lib/pages/organisations_pages/organisation_details_page.dart b/lib/pages/organisations_pages/organisation_details_page.dart index 6576d2f..9c907e1 100644 --- a/lib/pages/organisations_pages/organisation_details_page.dart +++ b/lib/pages/organisations_pages/organisation_details_page.dart @@ -1,22 +1,626 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/components/transaction_dialog.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart'; +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/organisation_details_page/tabs/tabs.dart'; -class OrganisationDetailsPage extends StatelessWidget { +class OrganisationDetailsPage extends StatefulWidget { final String organisationAddress; const OrganisationDetailsPage({super.key, required this.organisationAddress}); @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Organisation Details"), - ), - body: Center( - child: Text( - "Organisation Address: $organisationAddress", - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + State createState() => + _OrganisationDetailsPageState(); +} + +class _OrganisationDetailsPageState extends State + with SingleTickerProviderStateMixin { + String organisationName = ""; + String organisationDescription = ""; + String organisationLogoHash = ""; + List organisationOwners = []; + List organisationMembers = []; + int timeOfCreation = 0; + bool isMember = false; + bool isOwner = false; + bool _isLoading = false; + String _errorMessage = ""; + final TextEditingController _addMemberController = TextEditingController(); + + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this); + fetchOrganisationDetails(); + } + + @override + void dispose() { + _addMemberController.dispose(); + _tabController.dispose(); + super.dispose(); + } + + Future fetchOrganisationDetails() async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + final walletProvider = Provider.of(context, listen: false); + try { + final result = + await OrganisationContractReadFunctions.getOrganisationsByUser( + walletProvider: walletProvider, + organisationContractAddress: widget.organisationAddress, + ); + + logger.d("Organisation details fetch result: ${result.data}"); + + if (result.success) { + final data = result.data; + setState(() { + isMember = data['isMember']; + isOwner = data['isOwner']; + organisationName = data['organisationName']; + organisationDescription = data['organisationDescription']; + organisationLogoHash = data['organisationLogoHash']; + organisationOwners = List.from(data['owners'] ?? []); + organisationMembers = List.from(data['members'] ?? []); + timeOfCreation = data['timeOfCreation'] ?? 0; + }); + } else { + setState(() { + _errorMessage = + result.errorMessage ?? "Failed to load organisation details"; + }); + logger.e("Error fetching organisation details: ${result.errorMessage}"); + } + } catch (e) { + setState(() { + _errorMessage = "Error loading organisation details: $e"; + }); + logger.e("Error fetching organisation details: $e"); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future addMember(String address) async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + final walletProvider = Provider.of(context, listen: false); + try { + final result = await OrganisationContractWriteFunctions.addMember( + walletProvider: walletProvider, + organisationContractAddress: widget.organisationAddress, + userAddress: address, + ); + + if (result.success) { + // ignore: use_build_context_synchronously + TransactionDialog.showSuccess( + context, + title: 'Member Added!', + message: + 'The member has been successfully added to the organisation.', + transactionHash: result.transactionHash, + ); + } else { + // ignore: use_build_context_synchronously + TransactionDialog.showError( + context, + title: 'Failed to Add Member', + message: result.errorMessage ?? 'An unknown error occurred', + ); + setState(() { + _errorMessage = result.errorMessage ?? "Failed to add member"; + }); + logger.e("Error adding member: ${result.errorMessage}"); + } + } catch (e) { + setState(() { + _errorMessage = "Error adding member: $e"; + }); + logger.e("Error adding member: $e"); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future removeMember(String address) async { + setState(() { + _isLoading = true; + _errorMessage = ""; + }); + + final walletProvider = Provider.of(context, listen: false); + try { + final result = await OrganisationContractWriteFunctions.removeMember( + walletProvider: walletProvider, + organisationContractAddress: widget.organisationAddress, + userAddress: address, + ); + + if (result.success) { + // ignore: use_build_context_synchronously + TransactionDialog.showSuccess( + context, + title: 'Member Removed!', + message: + 'The member has been successfully removed from the organisation.', + transactionHash: result.transactionHash, + ); + } else { + // ignore: use_build_context_synchronously + TransactionDialog.showError( + context, + title: 'Failed to Remove Member', + message: result.errorMessage ?? 'An unknown error occurred', + ); + setState(() { + _errorMessage = result.errorMessage ?? "Failed to add member"; + }); + logger.e("Error adding member: ${result.errorMessage}"); + } + } catch (e) { + setState(() { + _errorMessage = "Error adding member: $e"; + }); + logger.e("Error adding member: $e"); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + void _showAddMemberModal() { + _addMemberController.clear(); + showDialog( + context: context, + builder: (context) => Dialog( + backgroundColor: getThemeColors(context)['background'], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + side: BorderSide( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + ), + child: Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Add Member', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon( + Icons.close, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + 'Enter the wallet address of the new member:', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 12), + TextField( + controller: _addMemberController, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontFamily: 'monospace', + ), + decoration: InputDecoration( + hintText: '0x...', + hintStyle: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + filled: true, + fillColor: getThemeColors(context)['secondary'], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: getThemeColors(context)['primary']!, + width: 2, + ), + ), + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: + getThemeColors(context)['secondaryBackground'], + foregroundColor: getThemeColors(context)['textPrimary'], + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + side: BorderSide( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + ), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: () { + final address = _addMemberController.text.trim(); + if (address.isNotEmpty) { + Navigator.of(context).pop(); + addMember(address.toString()); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + ), + child: const Text('Add Member'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + String _truncateAddress(String address) { + if (address.length <= 10) return address; + return '${address.substring(0, 6)}...${address.substring(address.length - 4)}'; + } + + Widget _buildTabBar() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: getThemeColors(context)['textPrimary'], + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all(color: Colors.black, width: 2), + ), + dividerColor: Colors.transparent, + tabs: const [ + Tab( + icon: Icon(Icons.info_outline), + text: 'Info', + ), + Tab( + icon: Icon(Icons.group), + text: 'Members', + ), + Tab( + icon: Icon(Icons.verified_user), + text: 'Verifications', + ), + Tab( + icon: Icon(Icons.nature), + text: 'Proposals', + ), + ], + ), + ); + } + + Widget _buildOrganisationHeader() { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 3), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipOval( + child: organisationLogoHash.isNotEmpty + ? Image.network( + organisationLogoHash, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: getThemeColors(context)['primary'], + child: const Icon( + Icons.business, + size: 40, + color: Colors.white, + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: getThemeColors(context)['secondary'], + child: const CircularProgressIndicator( + color: Colors.black, + ), + ); + }, + ) + : Container( + color: getThemeColors(context)['primary'], + child: const Icon( + Icons.business, + size: 40, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + organisationName.isNotEmpty ? organisationName : 'Organisation', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary'], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: Text( + _truncateAddress(widget.organisationAddress), + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isOwner) + _buildStatusBadge("Owner", getThemeColors(context)['primary']!), + if (isOwner && isMember) const SizedBox(width: 8), + if (isMember && !isOwner) + _buildStatusBadge( + "Member", getThemeColors(context)['secondary']!), + if (!isMember && !isOwner) + _buildStatusBadge("Visitor", Colors.grey), + ], + ), + ], + ), + ); + } + + Widget _buildStatusBadge(String text, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.black, width: 2), + ), + child: Text( + text, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), ), ); } + + Widget _buildLoadingState() { + return Container( + padding: const EdgeInsets.all(40), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + getThemeColors(context)['primary']!), + ), + const SizedBox(height: 16), + Text( + 'Loading organisation details...', + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['primary'], + ), + ), + ], + ), + ), + ); + } + + Widget _buildErrorState() { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: getThemeColors(context)['error'], + ), + const SizedBox(height: 16), + Text( + 'Failed to load organisation', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['error'], + ), + ), + const SizedBox(height: 8), + Text( + _errorMessage, + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['error'], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: fetchOrganisationDetails, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BaseScaffold( + title: organisationName.isNotEmpty + ? organisationName + : "Organisation Details", + showBackButton: true, + isLoading: _isLoading, + showReloadButton: true, + onReload: fetchOrganisationDetails, + body: _isLoading + ? _buildLoadingState() + : _errorMessage.isNotEmpty + ? _buildErrorState() + : Column( + children: [ + _buildOrganisationHeader(), + const SizedBox(height: 16), + _buildTabBar(), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + InfoTab( + organisationDescription: organisationDescription, + timeOfCreation: timeOfCreation, + ), + MembersTab( + organisationOwners: organisationOwners, + organisationMembers: organisationMembers, + isOwner: isOwner, + onAddMember: _showAddMemberModal, + onRemoveMember: removeMember, + ), + VerificationRequestsTab( + organisationAddress: widget.organisationAddress, + ), + PlantingProposalsTab( + organisationAddress: widget.organisationAddress, + ), + ], + ), + ), + ], + ), + ); + } } diff --git a/lib/pages/organisations_pages/user_organisations_page.dart b/lib/pages/organisations_pages/user_organisations_page.dart index 619551e..c48f2dc 100644 --- a/lib/pages/organisations_pages/user_organisations_page.dart +++ b/lib/pages/organisations_pages/user_organisations_page.dart @@ -5,7 +5,7 @@ import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; -import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_read_functions.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_contract_read_functions.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; class OrganisationsPage extends StatefulWidget { diff --git a/lib/pages/register_user_page.dart b/lib/pages/register_user_page.dart index 0059682..c9cc03e 100644 --- a/lib/pages/register_user_page.dart +++ b/lib/pages/register_user_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/components/transaction_dialog.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart'; @@ -32,51 +33,21 @@ class _RegisterUserPageState extends State { super.dispose(); } - void _showSuccessDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - const Icon(Icons.check_circle, color: Colors.green), - const SizedBox(width: 8), - Text(title), - ], - ), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, + void _showSuccessDialog(String title, String message, + {String? transactionHash}) { + TransactionDialog.showSuccess( + context, + title: title, + message: message, + transactionHash: transactionHash, ); } void _showErrorDialog(String title, String message) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.error, color: getThemeColors(context)['error']), - const SizedBox(width: 8), - Text(title), - ], - ), - content: Text(message), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), - ), - ], - ); - }, + TransactionDialog.showError( + context, + title: title, + message: message, ); } @@ -151,9 +122,8 @@ class _RegisterUserPageState extends State { if (result.success) { _showSuccessDialog( 'Registration Successful!', - 'User registered successfully!\n\n' - 'Transaction hash: ${result.transactionHash!.substring(0, 10)}...\n\n' - 'Welcome to the Tree Planting Protocol!', + 'User registered successfully! Welcome to the Tree Planting Protocol!', + transactionHash: result.transactionHash, ); } else { _showErrorDialog('Registration Failed', result.errorMessage!); @@ -174,6 +144,8 @@ class _RegisterUserPageState extends State { Widget build(BuildContext context) { return BaseScaffold( title: "Register", + showBackButton: true, + isLoading: _isLoading, body: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( @@ -306,7 +278,7 @@ class _RegisterUserPageState extends State { color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( - color: getThemeColors(context)['secondaryBorder']!, + color: getThemeColors(context)['primaryBorder']!, width: 2, ), boxShadow: [ @@ -449,7 +421,7 @@ Widget _buildFormField({ ), child: Icon( icon, - color: getThemeColors(context)['primary'], + color: getThemeColors(context)['icon'], size: 18, ), ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 3029e4d..a3b0d03 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,8 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.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/widgets/basic_scaffold.dart'; import 'package:tree_planting_protocol/utils/services/switch_chain_utils.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/planter_token_contract/planter_token_read_services.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @@ -12,33 +17,465 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { + final TextEditingController _tokenAddressController = TextEditingController(); + final TextEditingController _userAddressController = TextEditingController(); + bool _isLoadingToken = false; + Map? _tokenDetails; + String? _tokenError; + + @override + void dispose() { + _tokenAddressController.dispose(); + _userAddressController.dispose(); + super.dispose(); + } + + Future _checkPlanterToken() async { + if (_tokenAddressController.text.trim().isEmpty) { + setState(() { + _tokenError = 'Please enter a token contract address'; + }); + return; + } + + setState(() { + _isLoadingToken = true; + _tokenError = null; + _tokenDetails = null; + }); + + try { + final walletProvider = + Provider.of(context, listen: false); + + final result = await PlanterTokenReadFunctions.getPlanterTokenDetails( + walletProvider: walletProvider, + tokenContractAddress: _tokenAddressController.text.trim(), + ); + + if (result.success && result.data != null) { + setState(() { + _tokenDetails = result.data; + _isLoadingToken = false; + }); + } else { + setState(() { + _tokenError = result.errorMessage ?? 'Failed to fetch token details'; + _isLoadingToken = false; + }); + } + } catch (e) { + setState(() { + _tokenError = 'Error: ${e.toString()}'; + _isLoadingToken = false; + }); + } + } + + void _viewUserProfile() { + final address = _userAddressController.text.trim(); + if (address.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Please enter a user address'), + backgroundColor: getThemeColors(context)['error'], + ), + ); + return; + } + + // Navigate to user profile page with address + context.push('/user-profile/$address'); + } + + String _formatAddress(String? address) { + if (address == null || address.isEmpty) { + return 'Unknown'; + } + if (address.length <= 18) { + return address; + } + return '${address.substring(0, 10)}...${address.substring(address.length - 8)}'; + } + @override Widget build(BuildContext context) { return BaseScaffold( - title: "Settings", - body: Column( - mainAxisAlignment: MainAxisAlignment.center, + title: "Settings & Tools", + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Consumer( - builder: (ctx, walletProvider, __) => Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + _buildSectionCard( + title: 'Wallet Information', + child: Consumer( + builder: (ctx, walletProvider, __) { + final address = walletProvider.userAddress ?? ''; + final shortAddress = address.isNotEmpty && address.length > 18 + ? '${address.substring(0, 10)}...${address.substring(address.length - 8)}' + : address.isNotEmpty + ? address + : 'Not connected'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow( + 'Address:', + shortAddress, + copyable: address.isNotEmpty ? address : null, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Network:', + '${walletProvider.currentChainName} (${walletProvider.currentChainId})', + ), + const SizedBox(height: 12), + _buildActionButton( + 'Switch Chain', + Icons.swap_horiz, + () => showChainSelector(context, walletProvider), + getThemeColors(context)['primary']!, + ), + ], + ); + }, + ), + ), + + const SizedBox(height: 20), + + // Planter Token Checker Section + _buildSectionCard( + title: 'Check Planter Token', + subtitle: 'View owner and planter address of any token contract', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'User Address: ${walletProvider.userAddress}', + TextField( + controller: _tokenAddressController, + decoration: InputDecoration( + hintText: 'Enter token contract address', + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + borderSide: BorderSide( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + borderSide: BorderSide( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + borderSide: BorderSide( + color: getThemeColors(context)['primary']!, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), ), - Text( - 'Current Chain: ${walletProvider.currentChainName} (${walletProvider.currentChainId})', - style: const TextStyle(fontSize: 20), + const SizedBox(height: 12), + _buildActionButton( + 'Check Token', + Icons.search, + _isLoadingToken ? null : _checkPlanterToken, + getThemeColors(context)['secondary']!, ), - ElevatedButton( - onPressed: () => showChainSelector(context, walletProvider), - child: const Text('Switch Chain'), + if (_isLoadingToken) ...[ + const SizedBox(height: 16), + const Center(child: CircularProgressIndicator()), + ], + if (_tokenError != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: getThemeColors(context)['error']!, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getThemeColors(context)['error']!, + width: 1, + ), + ), + child: Text( + _tokenError!, + style: TextStyle( + color: getThemeColors(context)['error'], + ), + ), + ), + ], + if (_tokenDetails != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary'], + borderRadius: + BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Token Details', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + _buildInfoRow( + 'Name:', + _tokenDetails!['name'] ?? 'Unknown', + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Symbol:', + _tokenDetails!['symbol'] ?? 'Unknown', + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Owner:', + _formatAddress(_tokenDetails!['owner'] as String?), + copyable: _tokenDetails!['owner'] as String?, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Planter:', + _formatAddress( + _tokenDetails!['planterAddress'] as String?), + copyable: + _tokenDetails!['planterAddress'] as String?, + ), + ], + ), + ), + ], + ], + ), + ), + + const SizedBox(height: 20), + + // User Profile Viewer Section + _buildSectionCard( + title: 'View User Profile', + subtitle: 'Check profile and NFTs of any user by address', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _userAddressController, + decoration: InputDecoration( + hintText: 'Enter user wallet address', + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + borderSide: BorderSide( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + borderSide: BorderSide( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + borderSide: BorderSide( + color: getThemeColors(context)['primary']!, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 12), + _buildActionButton( + 'View Profile', + Icons.person, + _viewUserProfile, + getThemeColors(context)['primary']!, ), ], ), - ) + ), ], - )); + ), + ), + ); + } + + Widget _buildSectionCard({ + required String title, + String? subtitle, + required Widget child, + }) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 13, + ), + ), + ], + const SizedBox(height: 16), + child, + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value, {String? copyable}) { + return Row( + children: [ + Text( + label, + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + value, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 14, + fontFamily: 'monospace', + ), + ), + ), + if (copyable != null) + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + Clipboard.setData(ClipboardData(text: copyable)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Copied to clipboard!'), + duration: const Duration(seconds: 1), + backgroundColor: getThemeColors(context)['primary'], + ), + ); + }, + icon: Icon( + Icons.copy, + size: 16, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ); + } + + Widget _buildActionButton( + String label, + IconData icon, + VoidCallback? onPressed, + Color color, + ) { + return SizedBox( + width: double.infinity, + height: 50, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + color: onPressed == null ? Colors.grey : color, + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: getThemeColors(context)['textPrimary'], + size: 20, + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ); } } diff --git a/lib/pages/tree_details_page.dart b/lib/pages/tree_details_page.dart index fa47bfe..bd10eea 100644 --- a/lib/pages/tree_details_page.dart +++ b/lib/pages/tree_details_page.dart @@ -1,6 +1,7 @@ // ignore_for_file: use_build_context_synchronously import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/components/transaction_dialog.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; @@ -31,12 +32,103 @@ class _TreeDetailsPageState extends State { String? loggedInUser = ""; bool canVerify = false; bool _isLoading = false; + bool _isLoadingMore = false; Tree? treeDetails; + int _verifiersOffset = 0; + final int _verifiersLimit = 10; + int _totalVerifiersCount = 0; + int _visibleVerifiersCount = 0; + final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); loadTreeDetails(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent * 0.8 && + !_isLoadingMore && + _verifiersOffset + _verifiersLimit < _visibleVerifiersCount) { + _loadMoreVerifiers(); + } + } + + Future _loadMoreVerifiers() async { + if (_isLoadingMore) return; + + setState(() { + _isLoadingMore = true; + _verifiersOffset += _verifiersLimit; + }); + + final walletProvider = Provider.of(context, listen: false); + final result = await ContractReadFunctions.getTreeNFTInfo( + walletProvider: walletProvider, + id: toInt(widget.treeId), + offset: _verifiersOffset, + limit: _verifiersLimit, + ); + + if (result.success && result.data != null) { + final List verifiersData = result.data['verifiers'] ?? []; + // Parse the verifiers using the same method from Tree model + final List newVerifiers = _parseVerifiers(verifiersData); + setState(() { + treeDetails?.verifiers.addAll(newVerifiers); + _isLoadingMore = false; + }); + } else { + setState(() { + _isLoadingMore = false; + }); + } + } + + static List _parseVerifiers(List verifiersData) { + List verifiers = []; + for (int i = 0; i < verifiersData.length; i++) { + var verifierEntry = verifiersData[i]; + + try { + if (verifierEntry is String) { + verifiers.add(Verifier( + address: verifierEntry, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + proofHashes: [], + description: "Verified", + isActive: true, + verificationId: i, + )); + } else if (verifierEntry is List) { + 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)); + } + } + } + } catch (e) { + logger.e("Error parsing verifier at index $i: $e"); + } + } + return verifiers; } Future loadTreeDetails() async { @@ -44,17 +136,25 @@ class _TreeDetailsPageState extends State { loggedInUser = walletProvider.currentAddress.toString(); setState(() { _isLoading = true; + _verifiersOffset = 0; }); final result = await ContractReadFunctions.getTreeNFTInfo( - walletProvider: walletProvider, - id: toInt(widget.treeId), - offset: TREE_VERIFIERS_OFFSET, - limit: TREE_VERIFIERS_LIMIT); + walletProvider: walletProvider, + id: toInt(widget.treeId), + offset: _verifiersOffset, + limit: _verifiersLimit, + ); 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(); + final int totalCount = result.data['totalCount'] ?? 0; + final int visibleCount = result.data['visibleCount'] ?? 0; + treeDetails = Tree.fromContractData(treesData, verifiersData, owner); + _totalVerifiersCount = totalCount; + _visibleVerifiersCount = visibleCount; + canVerify = true; for (var verifier in verifiersData) { if (verifier[0].toString().toLowerCase() == @@ -194,92 +294,109 @@ class _TreeDetailsPageState extends State { ], ), ), - Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(vertical: 16.0), - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: canVerify - ? getThemeColors(context)['primary'] - : getThemeColors(context)['secondary'], - borderRadius: BorderRadius.circular(12.0), - boxShadow: [ - BoxShadow( - color: getThemeColors(context)['textPrimary']!, - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - children: [ - Icon( - canVerify ? Icons.verified : Icons.lock, - color: getThemeColors(context)['textPrimary']!, - size: 32, - ), - const SizedBox(height: 8), - Text( - canVerify ? "Tree Verification" : "Verification Disabled", - style: TextStyle( + Material( + elevation: 4, + borderRadius: BorderRadius.circular(12.0), + child: Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 0), + padding: + const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0), + decoration: BoxDecoration( + color: canVerify + ? getThemeColors(context)['primary'] + : getThemeColors(context)['secondary'], + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: Column( + children: [ + Icon( + canVerify ? Icons.verified : Icons.lock, color: getThemeColors(context)['textPrimary']!, - fontSize: 18, - fontWeight: FontWeight.bold, + size: 32, ), - ), - const SizedBox(height: 4), - Text( - canVerify - ? "Confirm this tree's authenticity" - : "You cannot verify this tree", - style: TextStyle( - color: getThemeColors(context)['textPrimary'], - fontSize: 14, + const SizedBox(height: 8), + Text( + canVerify ? "Tree Verification" : "Verification Disabled", + style: TextStyle( + color: getThemeColors(context)['textPrimary']!, + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: canVerify - ? () async { - _showVerificationDialog(); - } - : null, - style: ElevatedButton.styleFrom( - backgroundColor: getThemeColors(context)['background'], - foregroundColor: canVerify - ? getThemeColors(context)['primary'] - : getThemeColors(context)['secondary'], - padding: const EdgeInsets.symmetric( - horizontal: 32, vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25), + const SizedBox(height: 4), + Text( + canVerify + ? "Confirm this tree's authenticity" + : "You cannot verify this tree", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 14, ), - elevation: 0, + textAlign: TextAlign.center, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - canVerify ? Icons.check_circle : Icons.cancel, - size: 20, + const SizedBox(height: 16), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(25), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + border: Border.all(color: Colors.black, width: 2), ), - const SizedBox(width: 8), - const Text( - "Verify Tree", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + child: ElevatedButton( + onPressed: canVerify + ? () async { + _showVerificationDialog(); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: + getThemeColors(context)['background'], + foregroundColor: canVerify + ? getThemeColors(context)['textPrimary'] + : getThemeColors(context)['textSecondary'], + padding: const EdgeInsets.symmetric( + horizontal: 32, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(23), + ), + 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), + loggedInUser, + treeDetails, + loadTreeDetails, + context, + currentCount: treeDetails?.verifiers.length ?? 0, + totalCount: _totalVerifiersCount, + visibleCount: _visibleVerifiersCount, + ), ], ), ); @@ -291,36 +408,40 @@ class _TreeDetailsPageState extends State { } Widget _buildDetailCard(String title, String value) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(16.0), - decoration: BoxDecoration( - color: getThemeColors(context)['background'], - borderRadius: BorderRadius.circular(12.0), - border: Border.all( - color: getThemeColors(context)['primaryBorder']!, width: 1.5), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: getThemeColors(context)['textPrimary'], + return Material( + elevation: 4, + borderRadius: BorderRadius.circular(12.0), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12.0), + border: Border.all(color: Colors.black, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + letterSpacing: 0.5, + ), ), - ), - const SizedBox(height: 4), - Text( - value, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: getThemeColors(context)['textPrimary'], + const SizedBox(height: 6), + Text( + value, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: getThemeColors(context)['textPrimary'], + ), ), - ), - ], + ], + ), ), ); } @@ -385,37 +506,20 @@ class _TreeDetailsPageState extends State { ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (result.success) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - Icon(Icons.check_circle, - color: getThemeColors(context)['icon']), - const SizedBox(width: 8), - const Text("Tree verification submitted successfully!"), - ], - ), - backgroundColor: Colors.green.shade600, - behavior: SnackBarBehavior.floating, - ), + TransactionDialog.showSuccess( + context, + title: 'Verification Submitted!', + message: 'Your tree verification has been submitted successfully!', + transactionHash: result.transactionHash, + onClose: () async { + await loadTreeDetails(); + }, ); - - await loadTreeDetails(); } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - Icon(Icons.error, color: getThemeColors(context)['error']), - const SizedBox(width: 8), - Expanded( - child: Text("Verification failed: ${result.errorMessage}")), - ], - ), - backgroundColor: getThemeColors(context)['error'], - behavior: SnackBarBehavior.floating, - duration: const Duration(seconds: 5), - ), + TransactionDialog.showError( + context, + title: 'Verification Failed', + message: result.errorMessage ?? 'An unknown error occurred', ); } } catch (e) { @@ -459,9 +563,14 @@ class _TreeDetailsPageState extends State { final screenWidth = MediaQuery.of(context).size.width; return BaseScaffold( title: "Tree NFT Details", + showBackButton: true, + isLoading: _isLoading, + showReloadButton: true, + onReload: loadTreeDetails, body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( + controller: _scrollController, physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.all(8.0), @@ -478,6 +587,30 @@ class _TreeDetailsPageState extends State { child: _buildTreeNFTDetailsSection( screenHeight, screenWidth, context), ), + if (_isLoadingMore) + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + width: 20, + height: 20, + child: + CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 12), + Text( + "Loading more verifiers...", + style: TextStyle( + color: + getThemeColors(context)['textSecondary'], + fontSize: 14, + ), + ), + ], + ), + ), const SizedBox(height: 20), ], ), @@ -616,25 +749,71 @@ class _VerificationModalState extends State<_VerificationModal> { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text("Select Image Source"), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.black, width: 2), + ), + backgroundColor: getThemeColors(context)['background'], + title: Text( + "Select Image Source", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + ), + ), content: Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - leading: const Icon(Icons.camera_alt), - title: const Text("Camera"), - onTap: () { - Navigator.pop(context); - _takePhoto(); - }, + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: ListTile( + leading: Icon(Icons.camera_alt, + color: getThemeColors(context)['primary']), + title: Text( + "Camera", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), + onTap: () { + Navigator.pop(context); + _takePhoto(); + }, + ), + ), ), - ListTile( - leading: const Icon(Icons.photo_library), - title: const Text("Gallery"), - onTap: () { - Navigator.pop(context); - _pickImage(); - }, + const SizedBox(height: 12), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: ListTile( + leading: Icon(Icons.photo_library, + color: getThemeColors(context)['primary']), + title: Text( + "Gallery", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), + onTap: () { + Navigator.pop(context); + _pickImage(); + }, + ), + ), ), ], ), @@ -654,33 +833,52 @@ class _VerificationModalState extends State<_VerificationModal> { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Colors.black, width: 2), ), child: Container( constraints: BoxConstraints( maxHeight: dialogHeight, maxWidth: dialogWidth, ), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.black, width: 2), + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 0), + padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + border: const Border( + bottom: BorderSide(color: Colors.black, width: 2), + ), + ), child: Row( children: [ - Icon(Icons.verified, color: Colors.green.shade600, size: 28), + Icon(Icons.verified, + color: getThemeColors(context)['textPrimary'], size: 28), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( "Verify Tree", style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], ), ), ), IconButton( onPressed: () => Navigator.pop(context), - icon: const Icon(Icons.close), + icon: Icon(Icons.close, + color: getThemeColors(context)['textPrimary']), ), ], ), @@ -691,25 +889,45 @@ class _VerificationModalState extends State<_VerificationModal> { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( "Verification Description", style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, + color: getThemeColors(context)['textPrimary'], ), ), const SizedBox(height: 8), TextField( controller: _descriptionController, maxLines: 3, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), decoration: InputDecoration( hintText: "Describe your verification (e.g., tree health, location accuracy, etc.)", + hintStyle: TextStyle( + color: getThemeColors(context)['textSecondary'], + ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), + borderSide: + const BorderSide(color: Colors.black, width: 2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: + const BorderSide(color: Colors.black, width: 2), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide( + color: getThemeColors(context)['primary']!, + width: 2), ), filled: true, - fillColor: Colors.grey.shade50, + fillColor: getThemeColors(context)['background'], ), ), const SizedBox(height: 20), @@ -729,20 +947,38 @@ class _VerificationModalState extends State<_VerificationModal> { ), ), 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, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.black, width: 2), + ), + child: TextButton.icon( + onPressed: _selectedImages.length < 3 + ? _showImageSourceDialog + : null, + icon: Icon(Icons.add_photo_alternate, + size: 18, + color: getThemeColors( + context)['textPrimary']), + label: Text("Add", + style: TextStyle( + fontSize: 12, + color: getThemeColors( + context)['textPrimary'], + )), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ), ), ), ), @@ -760,10 +996,15 @@ class _VerificationModalState extends State<_VerificationModal> { itemBuilder: (context, index) { return Container( margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: + Border.all(color: Colors.black, width: 2), + ), child: Stack( children: [ ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(6), child: Image.file( _selectedImages[index], width: 80, @@ -782,6 +1023,8 @@ class _VerificationModalState extends State<_VerificationModal> { color: getThemeColors(context)['error'], shape: BoxShape.circle, + border: Border.all( + color: Colors.black, width: 1), ), child: const Icon( Icons.close, @@ -802,8 +1045,9 @@ class _VerificationModalState extends State<_VerificationModal> { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: getThemeColors(context)['background'], + color: getThemeColors(context)['secondary'], borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), ), child: Row( children: [ @@ -813,11 +1057,16 @@ class _VerificationModalState extends State<_VerificationModal> { child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( - getThemeColors(context)['primary']), + getThemeColors(context)['textPrimary']), ), ), const SizedBox(width: 12), - const Text("Uploading images to IPFS..."), + Text( + "Uploading images to IPFS...", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), ], ), ), @@ -827,51 +1076,82 @@ class _VerificationModalState extends State<_VerificationModal> { ), Container( padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Colors.black, width: 2), + ), + ), child: Row( children: [ Expanded( - child: TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - "Cancel", - style: TextStyle( - color: getThemeColors(context)['textPrimary']), + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + "Cancel", + style: TextStyle( + color: getThemeColors(context)['textPrimary']), + ), + ), ), ), ), 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: getThemeColors(context)['primary'], - foregroundColor: getThemeColors(context)['background'], - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: ElevatedButton( + onPressed: _isUploading || + _descriptionController.text.trim().isEmpty + ? null + : () async { + if (_selectedImages.isNotEmpty && + _uploadedHashes.length != + _selectedImages.length) { + await _uploadImages(); + } + if (!mounted) return; + Navigator.pop(context); + widget.onVerify( + description: + _descriptionController.text.trim(), + proofHashes: _uploadedHashes, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textPrimary'], + padding: const EdgeInsets.symmetric(vertical: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: Text( + _selectedImages.isNotEmpty && + _uploadedHashes.length != + _selectedImages.length + ? "Upload & Verify" + : "Verify Tree", + style: const TextStyle(fontWeight: FontWeight.bold), + ), ), - ), - child: Text( - _selectedImages.isNotEmpty && - _uploadedHashes.length != _selectedImages.length - ? "Upload & Verify" - : "Verify Tree", - style: const TextStyle(fontWeight: FontWeight.bold), ), ), ), diff --git a/lib/pages/trees_page.dart b/lib/pages/trees_page.dart index dc208ef..61a1ae8 100644 --- a/lib/pages/trees_page.dart +++ b/lib/pages/trees_page.dart @@ -1,32 +1,209 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; -import 'package:tree_planting_protocol/utils/constants/navbar_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; +import 'package:tree_planting_protocol/widgets/nft_display_utils/recent_trees_widget.dart'; +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; -class AllTreesPage extends StatelessWidget { +class AllTreesPage extends StatefulWidget { const AllTreesPage({super.key}); + @override + State createState() => _AllTreesPageState(); +} + +class _AllTreesPageState extends State { @override Widget build(BuildContext context) { - return BaseScaffold( - title: appName, - body: Center( + return Consumer( + builder: (context, walletProvider, child) { + return BaseScaffold( + title: "All Trees", + body: walletProvider.isConnected + ? _buildTreesPageContent(context) + : _buildConnectWalletPrompt(context), + ); + }, + ); + } + + Widget _buildTreesPageContent(BuildContext context) { + return Column( + children: [ + // Header with Mint NFT Button + Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.eco, + size: 28, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 8), + Text( + 'Discover Trees', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const Spacer(), + ElevatedButton.icon( + onPressed: () { + context.push('/mint-nft'); + }, + icon: const Icon(Icons.add, size: 20), + label: const Text( + 'Mint NFT', + style: TextStyle(fontWeight: FontWeight.bold), + ), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + elevation: buttonBlurRadius, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + ), + ], + ), + ), + // Recent Trees Widget + const Expanded( + child: RecentTreesWidget(), + ), + ], + ); + } + + Widget _buildConnectWalletPrompt(BuildContext context) { + return Center( + child: Container( + margin: const EdgeInsets.all(24), + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: buttonBlurRadius, + offset: const Offset(0, 2), + ), + ], + ), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ + Icon( + Icons.account_balance_wallet_outlined, + size: 64, + color: getThemeColors(context)['primary'], + ), + const SizedBox(height: 24), Text( - 'This page will display all the recent and nearby trees.', - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), + 'Connect Your Wallet', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), textAlign: TextAlign.center, ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () { - context.push('/mint-nft'); + const SizedBox(height: 16), + Text( + 'To view recent trees and interact with the blockchain, you need to connect your wallet first.', + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['textPrimary'], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton.icon( + onPressed: () async { + final walletProvider = Provider.of( + context, + listen: false, + ); + try { + await walletProvider.connectWallet(); + if (!mounted) return; + if (walletProvider.isConnected) { + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Wallet connected successfully!'), + // ignore: use_build_context_synchronously + backgroundColor: getThemeColors(context)['primary'], + behavior: SnackBarBehavior.floating, + ), + ); + } + } catch (e) { + if (!mounted) return; + // ignore: use_build_context_synchronously + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to connect wallet: $e'), + // ignore: use_build_context_synchronously + backgroundColor: getThemeColors(context)['error'], + behavior: SnackBarBehavior.floating, + ), + ); + } }, - child: const Text('Mint a new NFT'), + icon: const Icon(Icons.account_balance_wallet), + label: const Text( + 'Connect Wallet', + style: TextStyle(fontWeight: FontWeight.bold), + ), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + elevation: buttonBlurRadius, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.info_outline, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 8), + Text( + 'Supported wallets: MetaMask, WalletConnect', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], ), ], ), diff --git a/lib/pages/user_profile_page.dart b/lib/pages/user_profile_page.dart new file mode 100644 index 0000000..05a8985 --- /dev/null +++ b/lib/pages/user_profile_page.dart @@ -0,0 +1,43 @@ +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/constants/navbar_constants.dart'; + +import 'package:tree_planting_protocol/widgets/basic_scaffold.dart'; +import 'package:tree_planting_protocol/widgets/profile_widgets/profile_section_widget.dart'; +import 'package:tree_planting_protocol/widgets/nft_display_utils/user_nfts_widget.dart'; + +class UserProfilePage extends StatelessWidget { + final String userAddress; + + const UserProfilePage({super.key, required this.userAddress}); + + @override + Widget build(BuildContext context) { + return BaseScaffold( + title: appName, + showBackButton: true, + body: Consumer( + builder: (context, walletProvider, child) { + return SingleChildScrollView( + child: Column( + children: [ + SizedBox( + width: 400, + child: ProfileSectionWidget(userAddress: userAddress)), + SizedBox( + width: 400, + height: 600, + child: UserNftsWidget( + isOwnerCalling: true, userAddress: userAddress), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/providers/mint_nft_provider.dart b/lib/providers/mint_nft_provider.dart index 6b6b722..0277053 100644 --- a/lib/providers/mint_nft_provider.dart +++ b/lib/providers/mint_nft_provider.dart @@ -3,15 +3,18 @@ import 'package:flutter/material.dart'; class MintNftProvider extends ChangeNotifier { double _latitude = 0; double _longitude = 0; + int _numberOfTrees = 0; String _species = ""; String _details = ""; String _imageUri = ""; String _qrIpfsHash = ""; String _geoHash = ""; + String organisationAddress = ""; List _initialPhotos = []; double getLatitude() => _latitude; double getLongitude() => _longitude; + int getNumberOfTrees() => _numberOfTrees; String getSpecies() => _species; String getImageUri() => _imageUri; String getQrIpfsHash() => _qrIpfsHash; @@ -34,6 +37,11 @@ class MintNftProvider extends ChangeNotifier { notifyListeners(); } + void setOrganisationAddress(String address) { + organisationAddress = address; + notifyListeners(); + } + void setDescription(String details) { _details = details; notifyListeners(); @@ -59,6 +67,11 @@ class MintNftProvider extends ChangeNotifier { notifyListeners(); } + void setNumberOfTrees(int numberOfTrees) { + _numberOfTrees = numberOfTrees; + notifyListeners(); + } + void clearData() { _latitude = 0; _longitude = 0; @@ -67,6 +80,9 @@ class MintNftProvider extends ChangeNotifier { _qrIpfsHash = ""; _geoHash = ""; _initialPhotos.clear(); + organisationAddress = ""; + _numberOfTrees = 0; + _details = ""; notifyListeners(); } } diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 971f1c5..22a272c 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -397,14 +397,14 @@ class WalletProvider extends ChangeNotifier { // ignore: unused_element String _getCurrentSessionChainId() { final sessions = _web3App!.sessions.getAll(); - if (!sessions.isNotEmpty) { + if (sessions.isEmpty) { throw Exception('No active WalletConnect session'); } final accounts = sessions.first.namespaces['eip155']?.accounts; if (accounts != null && accounts.isNotEmpty) { return accounts.first.split(':')[1]; } - return '11155111'; // Default to Sepolia if no accounts found + return '11155111'; } Future switchChain(String newChainId) async { @@ -483,6 +483,42 @@ class WalletProvider extends ChangeNotifier { throw Exception('Wallet not connected'); } + final sessions = _web3App!.sessions.getAll(); + if (sessions.isEmpty) { + logger.w( + 'No active WalletConnect sessions found, wallet may be disconnected'); + _updateConnection( + isConnected: false, + address: null, + chainId: null, + message: 'Session expired - please reconnect', + ); + throw Exception( + 'WalletConnect session expired. Please reconnect your wallet.'); + } + + final session = sessions.first; + + if (DateTime.now().millisecondsSinceEpoch / 1000 > session.expiry) { + logger.w('WalletConnect session has expired'); + _updateConnection( + isConnected: false, + address: null, + chainId: null, + message: 'Session expired - please reconnect', + ); + throw Exception( + 'WalletConnect session has expired. Please reconnect your wallet.'); + } + + final accounts = session.namespaces['eip155']?.accounts; + if (accounts == null || accounts.isEmpty) { + logger.w('No accounts found in WalletConnect session'); + throw Exception( + 'No accounts found in wallet session. Please reconnect your wallet.'); + } + + logger.d('WalletConnect session validated successfully'); _updateStatus('Preparing transaction...'); final abiList = json.decode(abi) as List; @@ -544,11 +580,6 @@ class WalletProvider extends ChangeNotifier { } _updateStatus('Opening wallet for transaction approval...'); - final sessions = _web3App!.sessions.getAll(); - if (sessions.isEmpty) { - throw Exception('No active WalletConnect session'); - } - final session = sessions.first; final requestParams = SessionRequestParams( method: 'eth_sendTransaction', params: [transaction], diff --git a/lib/utils/constants/contract_abis/organisation_contract_details.dart b/lib/utils/constants/contract_abis/organisation_contract_details.dart index 4e38d9b..c27b7f0 100644 --- a/lib/utils/constants/contract_abis/organisation_contract_details.dart +++ b/lib/utils/constants/contract_abis/organisation_contract_details.dart @@ -1,6 +1,1146 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; -const String organisationContractAbi = - '''[{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_description","type":"string","internalType":"string"},{"name":"_photoIpfsHash","type":"string","internalType":"string"},{"name":"_creator","type":"address","internalType":"address"},{"name":"_factoryAddress","type":"address","internalType":"address"},{"name":"_treeNFTContractAddress","type":"address","internalType":"address"},{"name":"_founder","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"addMember","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changePaginationLimit","inputs":[{"name":"_limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"checkMembership","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"checkOwnership","inputs":[{"name":"user","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"description","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"founder","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getMemberCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMembers","inputs":[],"outputs":[{"name":"","type":"address[]","internalType":"address[]"}],"stateMutability":"view"},{"type":"function","name":"getOrganisationInfo","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"string","internalType":"string"},{"name":"","type":"string","internalType":"string"},{"name":"","type":"string","internalType":"string"},{"name":"","type":"address[]","internalType":"address[]"},{"name":"","type":"address[]","internalType":"address[]"},{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getOwners","inputs":[],"outputs":[{"name":"","type":"address[]","internalType":"address[]"}],"stateMutability":"view"},{"type":"function","name":"getTreePlantingProposal","inputs":[{"name":"proposalID","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct TreePlantingProposal","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"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":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"status","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getTreePlantingProposals","inputs":[{"name":"status","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct TreePlantingProposal[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"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":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"status","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getTreePlantingProposalsByStatus","inputs":[{"name":"status","type":"uint256","internalType":"uint256"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"proposals","type":"tuple[]","internalType":"struct TreePlantingProposal[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"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":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"},{"name":"metadata","type":"string","internalType":"string"},{"name":"status","type":"uint256","internalType":"uint256"}]},{"name":"totalMatching","type":"uint256","internalType":"uint256"},{"name":"hasMore","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getVerificationRequest","inputs":[{"name":"verificationID","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct OrganisationVerificationRequest","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"initialMember","type":"address","internalType":"address"},{"name":"organisationContract","type":"address","internalType":"address"},{"name":"status","type":"uint256","internalType":"uint256"},{"name":"description","type":"string","internalType":"string"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getVerificationRequests","inputs":[{"name":"status","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple[]","internalType":"struct OrganisationVerificationRequest[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"initialMember","type":"address","internalType":"address"},{"name":"organisationContract","type":"address","internalType":"address"},{"name":"status","type":"uint256","internalType":"uint256"},{"name":"description","type":"string","internalType":"string"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getVerificationRequestsByStatus","inputs":[{"name":"status","type":"uint256","internalType":"uint256"},{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"requests","type":"tuple[]","internalType":"struct OrganisationVerificationRequest[]","components":[{"name":"id","type":"uint256","internalType":"uint256"},{"name":"initialMember","type":"address","internalType":"address"},{"name":"organisationContract","type":"address","internalType":"address"},{"name":"status","type":"uint256","internalType":"uint256"},{"name":"description","type":"string","internalType":"string"},{"name":"timestamp","type":"uint256","internalType":"uint256"},{"name":"proofHashes","type":"string[]","internalType":"string[]"},{"name":"treeNftId","type":"uint256","internalType":"uint256"}]},{"name":"totalMatching","type":"uint256","internalType":"uint256"},{"name":"hasMore","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"leaveOrganisation","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"makeOwner","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"members","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"organisationFactoryContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract OrganisationFactory"}],"stateMutability":"view"},{"type":"function","name":"owners","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"paginationLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"photoIpfsHash","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"plantTreeProposal","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":"photos","type":"string[]","internalType":"string[]"},{"name":"geoHash","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeMember","inputs":[{"name":"member","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"requestVerification","inputs":[{"name":"_description","type":"string","internalType":"string"},{"name":"_proofHashes","type":"string[]","internalType":"string[]"},{"name":"_treeNftID","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"timeOfCreation","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeNFTContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract TreeNft"}],"stateMutability":"view"},{"type":"function","name":"voteOnTreePlantingProposal","inputs":[{"name":"proposalID","type":"uint256","internalType":"uint256"},{"name":"vote","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"voteOnVerificationRequest","inputs":[{"name":"verificationID","type":"uint256","internalType":"uint256"},{"name":"vote","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"UserAddedToOrganisation","inputs":[{"name":"user","type":"address","indexed":true,"internalType":"address"},{"name":"organisationContract","type":"address","indexed":true,"internalType":"address"},{"name":"timestamp","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"by_user","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"UserRemovedFromOrganisation","inputs":[{"name":"user","type":"address","indexed":true,"internalType":"address"},{"name":"organisationContract","type":"address","indexed":true,"internalType":"address"},{"name":"timestamp","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"by_user","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AlreadyMember","inputs":[]},{"type":"error","name":"AlreadyOwner","inputs":[]},{"type":"error","name":"AlreadyProcessed","inputs":[]},{"type":"error","name":"AlreadyVoted","inputs":[]},{"type":"error","name":"InvalidAddressInput","inputs":[]},{"type":"error","name":"InvalidCoordinates","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidNameInput","inputs":[]},{"type":"error","name":"InvalidProposalId","inputs":[]},{"type":"error","name":"InvalidVerificationId","inputs":[]},{"type":"error","name":"NeedAnotherOwner","inputs":[]},{"type":"error","name":"NotOrganisationMember","inputs":[]},{"type":"error","name":"OnlyOwner","inputs":[]},{"type":"error","name":"PaginationLimitExceeded","inputs":[]}]'''; +const String organisationContractAbi = '''[ + { + "type": "constructor", + "inputs": [ + { + "name": "_name", + "type": "string", + "internalType": "string" + }, + { + "name": "_description", + "type": "string", + "internalType": "string" + }, + { + "name": "_photoIpfsHash", + "type": "string", + "internalType": "string" + }, + { + "name": "_creator", + "type": "address", + "internalType": "address" + }, + { + "name": "_factoryAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_treeNFTContractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "_founder", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addMember", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "changePaginationLimit", + "inputs": [ + { + "name": "_limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "checkMembership", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "checkOwnership", + "inputs": [ + { + "name": "user", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "description", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "founder", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMemberCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMembers", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "memberList", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOrganisationInfo", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "string", + "internalType": "string" + }, + { + "name": "", + "type": "string", + "internalType": "string" + }, + { + "name": "", + "type": "string", + "internalType": "string" + }, + { + "name": "", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOwners", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "ownerList", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTreePlantingProposal", + "inputs": [ + { + "name": "proposalID", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct TreePlantingProposal", + "components": [ + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "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": "qrPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "photos", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "geoHash", + "type": "string", + "internalType": "string" + }, + { + "name": "metadata", + "type": "string", + "internalType": "string" + }, + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initiator", + "type": "address", + "internalType": "address" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTreePlantingProposals", + "inputs": [ + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "proposals", + "type": "tuple[]", + "internalType": "struct TreePlantingProposal[]", + "components": [ + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "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": "qrPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "photos", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "geoHash", + "type": "string", + "internalType": "string" + }, + { + "name": "metadata", + "type": "string", + "internalType": "string" + }, + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initiator", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTreePlantingProposalsByStatus", + "inputs": [ + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "proposals", + "type": "tuple[]", + "internalType": "struct TreePlantingProposal[]", + "components": [ + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "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": "qrPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "photos", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "geoHash", + "type": "string", + "internalType": "string" + }, + { + "name": "metadata", + "type": "string", + "internalType": "string" + }, + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initiator", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "totalMatching", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "hasMore", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getVerificationRequest", + "inputs": [ + { + "name": "verificationID", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "tuple", + "internalType": "struct OrganisationVerificationRequest", + "components": [ + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initialMember", + "type": "address", + "internalType": "address" + }, + { + "name": "organisationContract", + "type": "address", + "internalType": "address" + }, + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proofHashes", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "treeNftId", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getVerificationRequests", + "inputs": [ + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "requests", + "type": "tuple[]", + "internalType": "struct OrganisationVerificationRequest[]", + "components": [ + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initialMember", + "type": "address", + "internalType": "address" + }, + { + "name": "organisationContract", + "type": "address", + "internalType": "address" + }, + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proofHashes", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "treeNftId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getVerificationRequestsByStatus", + "inputs": [ + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "requests", + "type": "tuple[]", + "internalType": "struct OrganisationVerificationRequest[]", + "components": [ + { + "name": "id", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "initialMember", + "type": "address", + "internalType": "address" + }, + { + "name": "organisationContract", + "type": "address", + "internalType": "address" + }, + { + "name": "status", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "timestamp", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "proofHashes", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "treeNftId", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalMatching", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "hasMore", + "type": "bool", + "internalType": "bool" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "leaveOrganisation", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "makeOwner", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "members", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "organisationFactoryContract", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract OrganisationFactory" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owners", + "inputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "paginationLimit", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "photoIpfsHash", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "string", + "internalType": "string" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "plantTreeProposal", + "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": "_qrPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "_metadata", + "type": "string", + "internalType": "string" + }, + { + "name": "photos", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "geoHash", + "type": "string", + "internalType": "string" + }, + { + "name": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeMember", + "inputs": [ + { + "name": "member", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "requestVerification", + "inputs": [ + { + "name": "_description", + "type": "string", + "internalType": "string" + }, + { + "name": "_proofHashes", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "_treeNftID", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "timeOfCreation", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "treeNFTContract", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract TreeNft" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "voteOnTreePlantingProposal", + "inputs": [ + { + "name": "proposalID", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "vote", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "voteOnVerificationRequest", + "inputs": [ + { + "name": "verificationID", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "vote", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "UserAddedToOrganisation", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "organisationContract", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "timestamp", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "by_user", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "event", + "name": "UserRemovedFromOrganisation", + "inputs": [ + { + "name": "user", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "organisationContract", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "timestamp", + "type": "uint256", + "indexed": false, + "internalType": "uint256" + }, + { + "name": "by_user", + "type": "address", + "indexed": false, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "AlreadyMember", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyOwner", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyProcessed", + "inputs": [] + }, + { + "type": "error", + "name": "AlreadyVoted", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidAddressInput", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidCoordinates", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInput", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidNameInput", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidProposalId", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidVerificationId", + "inputs": [] + }, + { + "type": "error", + "name": "NeedAnotherOwner", + "inputs": [] + }, + { + "type": "error", + "name": "NotOrganisationMember", + "inputs": [] + }, + { + "type": "error", + "name": "OnlyOwner", + "inputs": [] + }, + { + "type": "error", + "name": "PaginationLimitExceeded", + "inputs": [] + } + ]'''; final String organisationContractAddress = dotenv.env['ORGANISATION_FACTORY_CONTRACT_ADDRESS'] ?? ''; diff --git a/lib/utils/constants/contract_abis/organisation_factory_contract_details.dart b/lib/utils/constants/contract_abis/organisation_factory_contract_details.dart index 7d985ac..b5acac3 100644 --- a/lib/utils/constants/contract_abis/organisation_factory_contract_details.dart +++ b/lib/utils/constants/contract_abis/organisation_factory_contract_details.dart @@ -1,7 +1,840 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; -const String organisationFactoryContractAbi = - '''[{"type":"constructor","inputs":[{"name":"_treeNFTContract","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"addMemberToOrganisation","inputs":[{"name":"_member","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createOrganisation","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_description","type":"string","internalType":"string"},{"name":"_photoIpfsHash","type":"string","internalType":"string"}],"outputs":[{"name":"organisationId","type":"uint256","internalType":"uint256"},{"name":"organisationAddress","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"getAllOrganisationDetails","inputs":[],"outputs":[{"name":"organizationDetails","type":"tuple[]","internalType":"struct OrganisationDetails[]","components":[{"name":"contractAddress","type":"address","internalType":"address"},{"name":"name","type":"string","internalType":"string"},{"name":"description","type":"string","internalType":"string"},{"name":"photoIpfsHash","type":"string","internalType":"string"},{"name":"owners","type":"address[]","internalType":"address[]"},{"name":"members","type":"address[]","internalType":"address[]"},{"name":"ownerCount","type":"uint256","internalType":"uint256"},{"name":"memberCount","type":"uint256","internalType":"uint256"},{"name":"isActive","type":"bool","internalType":"bool"},{"name":"timeOfCreation","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getAllOrganisations","inputs":[],"outputs":[{"name":"","type":"address[]","internalType":"address[]"}],"stateMutability":"view"},{"type":"function","name":"getMyOrganisations","inputs":[],"outputs":[{"name":"","type":"address[]","internalType":"address[]"}],"stateMutability":"view"},{"type":"function","name":"getOrganisationCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getOrganisationInfo","inputs":[{"name":"_organisationAddress","type":"address","internalType":"address"}],"outputs":[{"name":"organisationAddress","type":"address","internalType":"address"},{"name":"name","type":"string","internalType":"string"},{"name":"description","type":"string","internalType":"string"},{"name":"photoIpfsHash","type":"string","internalType":"string"},{"name":"owners","type":"address[]","internalType":"address[]"},{"name":"members","type":"address[]","internalType":"address[]"},{"name":"timeOfCreation","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getTreeNFTContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getUserOrganisations","inputs":[{"name":"_user","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"address[]","internalType":"address[]"}],"stateMutability":"view"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"removeOrganisation","inputs":[{"name":"_organisationAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"s_organisationAddressToOrganisation","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"address","internalType":"contract Organisation"}],"stateMutability":"view"},{"type":"function","name":"s_userToOrganisations","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"treeNFTContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"updateTreeNFTContract","inputs":[{"name":"_newTreeNFTContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"InvalidDescriptionInput","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidNameInput","inputs":[]},{"type":"error","name":"InvalidOrganisation","inputs":[]},{"type":"error","name":"OrganisationDoesNotExist","inputs":[]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]}]'''; - +const String organisationFactoryContractAbi = '''[ + { + "type": "constructor", + "inputs": [ + { + "name": "_treeNFTContract", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "addMemberToOrganisation", + "inputs": [ + { + "name": "_member", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "createOrganisation", + "inputs": [ + { + "name": "_name", + "type": "string", + "internalType": "string" + }, + { + "name": "_description", + "type": "string", + "internalType": "string" + }, + { + "name": "_photoIpfsHash", + "type": "string", + "internalType": "string" + } + ], + "outputs": [ + { + "name": "organisationId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "organisationAddress", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "getAllOrganisationDetails", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "organizationDetails", + "type": "tuple[]", + "internalType": "struct OrganisationDetails[]", + "components": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "organisationPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "owners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "members", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "ownerCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "memberCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "timeOfCreation", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getAllOrganisations", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMyOrganisations", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMyOrganisationsAsMember", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "tuple[]", + "internalType": "struct OrganisationDetails[]", + "components": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "organisationPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "owners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "members", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "ownerCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "memberCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "timeOfCreation", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getMyOrganisationsAsOwner", + "inputs": [ + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "tuple[]", + "internalType": "struct OrganisationDetails[]", + "components": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "organisationPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "owners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "members", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "ownerCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "memberCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "timeOfCreation", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOrganisationCount", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getOrganisationInfo", + "inputs": [ + { + "name": "_organisationAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "organisationAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "photoIpfsHash", + "type": "string", + "internalType": "string" + }, + { + "name": "owners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "members", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "timeOfCreation", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getTreeNFTContract", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUserOrganisations", + "inputs": [ + { + "name": "_user", + "type": "address", + "internalType": "address" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUserOrganisationsAsMember", + "inputs": [ + { + "name": "_user", + "type": "address", + "internalType": "address" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "tuple[]", + "internalType": "struct OrganisationDetails[]", + "components": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "organisationPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "owners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "members", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "ownerCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "memberCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "timeOfCreation", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUserOrganisationsAsOwner", + "inputs": [ + { + "name": "_user", + "type": "address", + "internalType": "address" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "orgs", + "type": "tuple[]", + "internalType": "struct OrganisationDetails[]", + "components": [ + { + "name": "contractAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "name", + "type": "string", + "internalType": "string" + }, + { + "name": "description", + "type": "string", + "internalType": "string" + }, + { + "name": "organisationPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "owners", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "members", + "type": "address[]", + "internalType": "address[]" + }, + { + "name": "ownerCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "memberCount", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "isActive", + "type": "bool", + "internalType": "bool" + }, + { + "name": "timeOfCreation", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "owner", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "promoteToOwner", + "inputs": [ + { + "name": "_member", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "removeOrganisation", + "inputs": [ + { + "name": "_organisationAddress", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "s_organisationAddressToOrganisation", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "contract Organisation" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "s_userToOrganisations", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "transferOwnership", + "inputs": [ + { + "name": "newOwner", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "treeNFTContract", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "updateTreeNFTContract", + "inputs": [ + { + "name": "_newTreeNFTContract", + "type": "address", + "internalType": "address" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "event", + "name": "OwnershipTransferred", + "inputs": [ + { + "name": "previousOwner", + "type": "address", + "indexed": true, + "internalType": "address" + }, + { + "name": "newOwner", + "type": "address", + "indexed": true, + "internalType": "address" + } + ], + "anonymous": false + }, + { + "type": "error", + "name": "InvalidDescriptionInput", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidInput", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidNameInput", + "inputs": [] + }, + { + "type": "error", + "name": "InvalidOrganisation", + "inputs": [] + }, + { + "type": "error", + "name": "OrganisationDoesNotExist", + "inputs": [] + }, + { + "type": "error", + "name": "OwnableInvalidOwner", + "inputs": [ + { + "name": "owner", + "type": "address", + "internalType": "address" + } + ] + }, + { + "type": "error", + "name": "OwnableUnauthorizedAccount", + "inputs": [ + { + "name": "account", + "type": "address", + "internalType": "address" + } + ] + } + ]'''; final String organisationFactoryContractAddress = dotenv.env['ORGANISATION_FACTORY_CONTRACT_ADDRESS'] ?? ''; diff --git a/lib/utils/constants/contract_abis/tree_nft_contract_details.dart b/lib/utils/constants/contract_abis/tree_nft_contract_details.dart index ab2f0d5..3639103 100644 --- a/lib/utils/constants/contract_abis/tree_nft_contract_details.dart +++ b/lib/utils/constants/contract_abis/tree_nft_contract_details.dart @@ -1,8 +1,1636 @@ // 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":"_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":[]}]'''; +const String treeNftContractABI = '''[ + { + "type": "constructor", + "inputs": [ + { + "name": "_careTokenContract", + "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": [ + { + "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": "qrPhoto", + "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": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + } + ] + }, + { + "name": "totalCount", + "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": "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": "qrPhoto", + "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": "numberOfTrees", + "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": "qrPhoto", + "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": "numberOfTrees", + "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": "qrPhoto", + "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": "numberOfTrees", + "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": "verifierPlanterTokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "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": "profilePhoto", + "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": "legacyTokens", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "careTokens", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "getUserVerifierTokenDetails", + "inputs": [ + { + "name": "userAddress", + "type": "address", + "internalType": "address" + }, + { + "name": "offset", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "limit", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "verifierTokenDetails", + "type": "tuple[]", + "internalType": "struct VerificationDetails[]", + "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": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "verifierPlanterTokenAddress", + "type": "address", + "internalType": "address" + } + ] + }, + { + "name": "totalCount", + "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": "qrPhoto", + "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": "numberOfTrees", + "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": "qrPhoto", + "type": "string", + "internalType": "string" + }, + { + "name": "metadata", + "type": "string", + "internalType": "string" + }, + { + "name": "geoHash", + "type": "string", + "internalType": "string" + }, + { + "name": "initialPhotos", + "type": "string[]", + "internalType": "string[]" + }, + { + "name": "numberOfTrees", + "type": "uint256", + "internalType": "uint256" + } + ], + "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": "registerUserProfile", + "inputs": [ + { + "name": "_name", + "type": "string", + "internalType": "string" + }, + { + "name": "_profilePhoto", + "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": "removeVerificationOptimized", + "inputs": [ + { + "name": "_verificationId", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_verifierArrayIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_verifiedTreesArrayIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_userVerificationIndex", + "type": "uint256", + "internalType": "uint256" + }, + { + "name": "_verifierTokenAddrIndex", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "renounceOwnership", + "inputs": [], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "type": "function", + "name": "s_userToPlanterTokenAddress", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "type": "function", + "name": "s_userToVerifierTokenAddresses", + "inputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + }, + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "outputs": [ + { + "name": "", + "type": "address", + "internalType": "address" + } + ], + "stateMutability": "view" + }, + { + "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": "_profilePhoto", + "type": "string", + "internalType": "string" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + }, + { + "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": "AlreadyVerified", + "inputs": [] + }, + { + "type": "error", + "name": "CannotVerifyOwnTree", + "inputs": [] + }, + { + "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": "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": [] + }, + { + "type": "error", + "name": "VerificationNotFound", + "inputs": [] + } + ] '''; final String treeNFtContractAddress = dotenv.env['TREE_NFT_CONTRACT_ADDRESS'] ?? ''; diff --git a/lib/utils/constants/route_constants.dart b/lib/utils/constants/route_constants.dart index 4b06121..0e3877a 100644 --- a/lib/utils/constants/route_constants.dart +++ b/lib/utils/constants/route_constants.dart @@ -2,12 +2,14 @@ class RouteConstants { static const String home = '/'; static const String allTrees = '/trees'; static const String mintNft = '/mint-nft'; + static const String mintNftOrganisation = '/mint-nft-organisation'; static const String mintNftDetails = '/mint-nft/details'; static const String mintNftImages = '/mint-nft/images'; static const String homePath = '/'; static const String allTreesPath = '/trees'; static const String mintNftPath = '/mint-nft'; + static const String mintNftOrganisationPath = '/mint-nft-organisation'; static const String mintNftDetailsPath = '/mint-nft/details'; static const String mintNftImagesPath = '/mint-nft/images'; } diff --git a/lib/utils/constants/tree_species_constants.dart b/lib/utils/constants/tree_species_constants.dart new file mode 100644 index 0000000..3b86d35 --- /dev/null +++ b/lib/utils/constants/tree_species_constants.dart @@ -0,0 +1,118 @@ +class TreeSpeciesConstants { + static const List commonTreeSpecies = [ + 'Oak', + 'Pine', + 'Spruce', + 'Maple', + 'Birch', + 'Cedar', + 'Fir', + 'Elm', + 'Ash', + 'Beech', + 'Poplar', + 'Willow', + 'Cherry', + 'Walnut', + 'Hickory', + 'Cypress', + 'Redwood', + 'Sequoia', + 'Eucalyptus', + 'Acacia', + 'Mahogany', + 'Teak', + 'Bamboo', + 'Palm', + 'Coconut Palm', + 'Olive', + 'Apple', + 'Orange', + 'Lemon', + 'Mango', + 'Avocado', + 'Fig', + 'Banyan', + 'Neem', + 'Sal', + 'Rosewood', + 'Sandalwood', + 'Chestnut', + 'Sycamore', + 'Magnolia', + 'Dogwood', + 'Juniper', + 'Hemlock', + 'Larch', + 'Yew', + 'Other' + ]; + + static const Map> speciesByCategory = { + 'Deciduous Trees': [ + 'Oak', + 'Maple', + 'Birch', + 'Elm', + 'Ash', + 'Beech', + 'Poplar', + 'Willow', + 'Cherry', + 'Walnut', + 'Hickory', + 'Chestnut', + 'Sycamore', + 'Magnolia', + 'Dogwood' + ], + 'Coniferous Trees': [ + 'Pine', + 'Spruce', + 'Cedar', + 'Fir', + 'Cypress', + 'Redwood', + 'Sequoia', + 'Juniper', + 'Hemlock', + 'Larch', + 'Yew' + ], + 'Tropical Trees': [ + 'Eucalyptus', + 'Acacia', + 'Mahogany', + 'Teak', + 'Bamboo', + 'Palm', + 'Coconut Palm', + 'Banyan', + 'Neem', + 'Sal', + 'Rosewood', + 'Sandalwood' + ], + 'Fruit Trees': [ + 'Apple', + 'Orange', + 'Lemon', + 'Mango', + 'Avocado', + 'Fig', + 'Olive' + ] + }; + + static List getAllSpecies() { + return commonTreeSpecies; + } + + static List getSpeciesByCategory(String category) { + return speciesByCategory[category] ?? []; + } + + static List getCategories() { + return speciesByCategory.keys.toList(); + } +} diff --git a/lib/utils/constants/ui/color_constants.dart b/lib/utils/constants/ui/color_constants.dart index 3eef26d..cc7feec 100644 --- a/lib/utils/constants/ui/color_constants.dart +++ b/lib/utils/constants/ui/color_constants.dart @@ -56,5 +56,11 @@ Map getThemeColors(BuildContext context) { 'primaryShadow': themeProvider.isDarkMode ? const Color.fromARGB(255, 255, 255, 255) : const Color.fromARGB(255, 0, 0, 0), + 'secondaryBorder': themeProvider.isDarkMode + ? const Color.fromARGB(255, 1, 135, 12) + : const Color.fromARGB(255, 28, 211, 129), + 'shadow': themeProvider.isDarkMode + ? const Color.fromARGB(255, 0, 0, 0) + : const Color.fromARGB(255, 128, 128, 128), }; } diff --git a/lib/utils/services/clipboard_service.dart b/lib/utils/services/clipboard_service.dart new file mode 100644 index 0000000..8ffbc1d --- /dev/null +++ b/lib/utils/services/clipboard_service.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; + +void copyAddress(String address, BuildContext context) { + Clipboard.setData(ClipboardData(text: address)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Address copied to clipboard'), + backgroundColor: getThemeColors(context)['primary'], + duration: Duration(seconds: 2), + ), + ); +} diff --git a/lib/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart b/lib/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart index 8b13789..584a7ae 100644 --- a/lib/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart +++ b/lib/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart @@ -1 +1,298 @@ +import 'package:tree_planting_protocol/utils/constants/contract_abis/organisation_contract_details.dart'; +// 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'; +class ContractReadResult { + final bool success; + final String? transactionHash; + final String? errorMessage; + final dynamic data; + + ContractReadResult({ + required this.success, + this.transactionHash, + this.errorMessage, + this.data, + }); + + ContractReadResult.success({ + String? transactionHash, + dynamic data, + }) : this( + success: true, + transactionHash: transactionHash, + data: data, + ); + + ContractReadResult.error({ + required String errorMessage, + }) : this( + success: false, + errorMessage: errorMessage, + ); +} + +class OrganisationContractReadFunctions { + static Future getOrganisationsByUser({ + required WalletProvider walletProvider, + required String organisationContractAddress, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for reading organisations"); + return ContractReadResult.error( + errorMessage: + 'Please connect your wallet before reading organisations.', + ); + } + + final String address = walletProvider.currentAddress.toString(); + if (!address.startsWith('0x')) { + return ContractReadResult.error( + errorMessage: 'Invalid wallet address format', + ); + } + + // Get organisation basic info + final orgDetailsResult = await walletProvider.readContract( + contractAddress: organisationContractAddress, + functionName: 'getOrganisationInfo', + params: [], + abi: organisationContractAbi, + ); + + if (orgDetailsResult == null || orgDetailsResult.isEmpty) { + return ContractReadResult.error( + errorMessage: 'No data returned from contract', + ); + } + + final organisationAddress = (orgDetailsResult[0] as EthereumAddress).hex; + final organisationName = orgDetailsResult[1] as String; + final organisationDescription = orgDetailsResult[2] as String; + final organisationLogoHash = orgDetailsResult[3] as String; + final timeOfCreation = (orgDetailsResult[6] as BigInt).toInt(); + + // Get owners with pagination + final ownersResult = await walletProvider.readContract( + contractAddress: organisationContractAddress, + functionName: 'getOwners', + params: [BigInt.from(0), BigInt.from(100)], // offset, limit + abi: organisationContractAbi, + ); + + final owners = ownersResult.isNotEmpty + ? (ownersResult[0] as List) + .map((e) => (e as EthereumAddress).hex) + .toList() + : []; + + // Get members with pagination + final membersResult = await walletProvider.readContract( + contractAddress: organisationContractAddress, + functionName: 'getMembers', + params: [BigInt.from(0), BigInt.from(100)], // offset, limit + abi: organisationContractAbi, + ); + + final members = membersResult.isNotEmpty + ? (membersResult[0] as List) + .map((e) => (e as EthereumAddress).hex) + .toList() + : []; + + final isOwner = + owners.any((o) => o.toLowerCase() == address.toLowerCase()); + final isMember = + members.any((m) => m.toLowerCase() == address.toLowerCase()); + + return ContractReadResult.success( + data: { + 'organisationAddress': organisationAddress, + 'organisationName': organisationName, + 'organisationDescription': organisationDescription, + 'organisationLogoHash': organisationLogoHash, + 'owners': owners, + 'members': members, + 'timeOfCreation': timeOfCreation, + 'isMember': isMember, + 'isOwner': isOwner, + }, + ); + } catch (e) { + logger.e("Error reading organisations", error: e); + return ContractReadResult.error( + errorMessage: 'Failed to read organisation info: ${e.toString()}', + ); + } + } + + static Future getVerificationRequestsByStatus({ + required WalletProvider walletProvider, + required String organisationContractAddress, + required int status, // 0: Pending, 1: Approved, 2: Rejected + required int offset, + required int limit, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected"); + return ContractReadResult.error( + errorMessage: 'Please connect your wallet.', + ); + } + + final String address = walletProvider.currentAddress.toString(); + if (!address.startsWith('0x')) { + return ContractReadResult.error( + errorMessage: 'Invalid wallet address format', + ); + } + + final args = [ + BigInt.from(status), + BigInt.from(offset), + BigInt.from(limit) + ]; + final contractResult = await walletProvider.readContract( + contractAddress: organisationContractAddress, + functionName: 'getVerificationRequestsByStatus', + params: args, + abi: organisationContractAbi, + ); + + if (contractResult == null || contractResult.isEmpty) { + return ContractReadResult.error( + errorMessage: 'No data returned from contract', + ); + } + + // Parse the contract result according to the return structure: + // returns (OrganisationVerificationRequest[] memory requests, uint256 totalMatching, bool hasMore) + final requests = + contractResult[0] as List; // OrganisationVerificationRequest[] + final totalMatching = (contractResult[1] as BigInt).toInt(); + final hasMore = contractResult[2] as bool; + + // Parse each OrganisationVerificationRequest + final List> parsedRequests = requests.map((request) { + final requestList = request as List; + return { + 'id': (requestList[0] as BigInt).toInt(), + 'initialMember': (requestList[1] as EthereumAddress).hex, + 'organisationContract': (requestList[2] as EthereumAddress).hex, + 'status': (requestList[3] as BigInt).toInt(), + 'description': requestList[4] as String, + 'timestamp': (requestList[5] as BigInt).toInt(), + 'proofHashes': + (requestList[6] as List).map((hash) => hash as String).toList(), + 'treeNftId': (requestList[7] as BigInt).toInt(), + }; + }).toList(); + + return ContractReadResult.success( + data: { + 'requests': parsedRequests, + 'totalMatching': totalMatching, + 'hasMore': hasMore, + 'status': status, + 'offset': offset, + 'limit': limit, + }, + ); + } catch (e) { + logger.e("Error reading verification requests", error: e); + return ContractReadResult.error( + errorMessage: 'Failed to read verification requests: ${e.toString()}', + ); + } + } + + static Future getTreePlantingProposalsByStatus({ + required WalletProvider walletProvider, + required String organisationContractAddress, + required int status, // 0: Pending, 1: Approved, 2: Rejected + required int offset, + required int limit, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for reading tree planting proposals"); + return ContractReadResult.error( + errorMessage: 'Please connect your wallet before reading proposals.', + ); + } + + final String address = walletProvider.currentAddress.toString(); + if (!address.startsWith('0x')) { + return ContractReadResult.error( + errorMessage: 'Invalid wallet address format', + ); + } + + final args = [ + BigInt.from(status), + BigInt.from(offset), + BigInt.from(limit) + ]; + final contractResult = await walletProvider.readContract( + contractAddress: organisationContractAddress, + functionName: 'getTreePlantingProposalsByStatus', + params: args, + abi: organisationContractAbi, + ); + + if (contractResult == null || contractResult.isEmpty) { + return ContractReadResult.error( + errorMessage: 'No data returned from contract', + ); + } + + // Parse the contract result according to the return structure: + // returns (TreePlantingProposal[] memory proposals, uint256 totalMatching, bool hasMore) + final proposals = contractResult[0] as List; // TreePlantingProposal[] + final totalMatching = (contractResult[1] as BigInt).toInt(); + final hasMore = contractResult[2] as bool; + + // Parse each TreePlantingProposal + final List> parsedProposals = + proposals.map((proposal) { + final proposalList = proposal as List; + return { + 'id': (proposalList[0] as BigInt).toInt(), + 'latitude': (proposalList[1] as BigInt).toInt(), + 'longitude': (proposalList[2] as BigInt).toInt(), + 'species': proposalList[3] as String, + 'imageUri': proposalList[4] as String, + 'qrPhoto': proposalList[5] as String, + 'photos': (proposalList[6] as List) + .map((photo) => photo as String) + .toList(), + 'geoHash': proposalList[7] as String, + 'metadata': proposalList[8] as String, + 'status': (proposalList[9] as BigInt).toInt(), + 'numberOfTrees': (proposalList[10] as BigInt).toInt(), + 'initiator': (proposalList[11] as EthereumAddress).hex, + }; + }).toList(); + + return ContractReadResult.success( + data: { + 'proposals': parsedProposals, + 'totalMatching': totalMatching, + 'hasMore': hasMore, + 'status': status, + 'offset': offset, + 'limit': limit, + }, + ); + } catch (e) { + logger.e("Error reading tree planting proposals", error: e); + return ContractReadResult.error( + errorMessage: 'Failed to read tree planting proposals: ${e.toString()}', + ); + } + } +} diff --git a/lib/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart b/lib/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart index 8b13789..9e74543 100644 --- a/lib/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart +++ b/lib/utils/services/contract_functions/organisation_contract/organisation_write_functions.dart @@ -1 +1,219 @@ +import 'package:tree_planting_protocol/providers/wallet_provider.dart'; +import 'package:tree_planting_protocol/utils/constants/contract_abis/organisation_contract_details.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:walletconnect_flutter_v2/walletconnect_flutter_v2.dart'; +class ContractWriteResult { + final bool success; + final String? transactionHash; + final String? errorMessage; + final dynamic data; + + ContractWriteResult({ + required this.success, + this.transactionHash, + this.errorMessage, + this.data, + }); + + ContractWriteResult.success({ + required String transactionHash, + dynamic data, + }) : this( + success: true, + transactionHash: transactionHash, + data: data, + ); + + ContractWriteResult.error({ + required String errorMessage, + }) : this( + success: false, + errorMessage: errorMessage, + ); +} + +class OrganisationContractWriteFunctions { + static Future addMember({ + required WalletProvider walletProvider, + required String organisationContractAddress, + required String userAddress, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for updating organisation details"); + return ContractWriteResult.error( + errorMessage: + 'Please connect your wallet before updating organisation details.', + ); + } + + final List args = [EthereumAddress.fromHex(userAddress)]; + final txHash = await walletProvider.writeContract( + contractAddress: organisationContractAddress, + functionName: 'addMember', + params: args, + abi: organisationContractAbi, + chainId: walletProvider.currentChainId, + ); + + logger.i("Organisation update transaction sent: $txHash"); + + return ContractWriteResult.success(transactionHash: txHash, data: { + 'userAddress': [EthereumAddress.fromHex(userAddress)], + }); + } catch (e) { + logger.e("Error updating organisation details: $e"); + return ContractWriteResult.error( + errorMessage: 'Failed to update organisation details: $e', + ); + } + } + + static Future removeMember({ + required WalletProvider walletProvider, + required String organisationContractAddress, + required String userAddress, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for updating organisation details"); + return ContractWriteResult.error( + errorMessage: + 'Please connect your wallet before updating organisation details.', + ); + } + + final List args = [EthereumAddress.fromHex(userAddress)]; + final txHash = await walletProvider.writeContract( + contractAddress: organisationContractAddress, + functionName: 'removeMember', + params: args, + abi: organisationContractAbi, + chainId: walletProvider.currentChainId, + ); + + logger.i("Organisation update transaction sent: $txHash"); + + return ContractWriteResult.success(transactionHash: txHash, data: { + 'userAddress': EthereumAddress.fromHex(userAddress), + }); + } catch (e) { + logger.e("Error updating organisation details: $e"); + return ContractWriteResult.error( + errorMessage: 'Failed to update organisation details: $e', + ); + } + } + + static Future plantTreeProposal({ + required String organisationContractAddress, + required WalletProvider walletProvider, + required double latitude, + required double longitude, + required String species, + required List photos, + required String geoHash, + required int numberOfTrees, + required String metadata, + String additionalData = "", + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected "); + return ContractWriteResult.error( + errorMessage: 'Please connect your wallet', + ); + } + + final lat = BigInt.from((latitude + 90.0) * 1e6); + final lng = BigInt.from((longitude + 180.0) * 1e6); + final numberOfTreesBigInt = BigInt.from(numberOfTrees); + + final List args = [ + lat, + lng, + species, + photos.isNotEmpty ? photos[0] : "", + "", + metadata, + photos, + geoHash, + numberOfTreesBigInt, + ]; + + final txHash = await walletProvider.writeContract( + contractAddress: organisationContractAddress, + functionName: 'plantTreeProposal', + params: args, + abi: organisationContractAbi, + chainId: walletProvider.currentChainId, + ); + + logger.i("Transaction sent: $txHash"); + + return ContractWriteResult.success( + transactionHash: txHash, + data: { + 'latitude': latitude, + 'longitude': longitude, + 'species': species, + 'photos': photos, + 'geoHash': geoHash, + }, + ); + } catch (e) { + logger.e("Error sending transaction: $e"); + return ContractWriteResult.error( + errorMessage: 'Failed to send transaction: $e', + ); + } + } + + static Future requestVerification({ + required String organisationContractAddress, + required WalletProvider walletProvider, + required List photos, + required String name, + required int nftId, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected"); + return ContractWriteResult.error( + errorMessage: 'Please connect your wallet ', + ); + } + + final List args = [ + name, + photos, + BigInt.from(nftId), + ]; + + final txHash = await walletProvider.writeContract( + contractAddress: organisationContractAddress, + functionName: 'requestVerification', + params: args, + abi: organisationContractAbi, + chainId: walletProvider.currentChainId, + ); + + logger.i("Request verification transaction sent: $txHash"); + + return ContractWriteResult.success( + transactionHash: txHash, + data: { + 'name': name, + 'photos': photos, + 'nftId': nftId, + }, + ); + } catch (e) { + logger.e("Error updating organisation details: $e"); + return ContractWriteResult.error( + errorMessage: 'Failed to update organisation details: $e', + ); + } + } +} diff --git a/lib/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_read_functions.dart b/lib/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_contract_read_functions.dart similarity index 94% rename from lib/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_read_functions.dart rename to lib/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_contract_read_functions.dart index dbb5f84..41fe8e1 100644 --- a/lib/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_read_functions.dart +++ b/lib/utils/services/contract_functions/organisation_factory_contract.dart/organisation_factory_contract_read_functions.dart @@ -54,6 +54,8 @@ class ContractReadFunctions { final EthereumAddress userAddress = EthereumAddress.fromHex(address); final List args = [ userAddress, + BigInt.from(0), // offset + BigInt.from(100), // limit - fetch up to 100 organisations ]; final result = await walletProvider.readContract( contractAddress: organisationFactoryContractAddress, @@ -71,6 +73,7 @@ class ContractReadFunctions { final totalCount = result.length > 1 ? int.parse(result[1].toString()) : 0; logger.d(organisations); + logger.d("Total organisations count: $totalCount"); return ContractReadResult.success( data: { 'organisations': organisations, diff --git a/lib/utils/services/contract_functions/planter_token_contract/planter_token_read_services.dart b/lib/utils/services/contract_functions/planter_token_contract/planter_token_read_services.dart new file mode 100644 index 0000000..7db4c32 --- /dev/null +++ b/lib/utils/services/contract_functions/planter_token_contract/planter_token_read_services.dart @@ -0,0 +1,206 @@ +// 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'; + +class PlanterTokenReadResult { + final bool success; + final String? errorMessage; + final dynamic data; + + PlanterTokenReadResult({ + required this.success, + this.errorMessage, + this.data, + }); + + PlanterTokenReadResult.success({ + dynamic data, + }) : this( + success: true, + data: data, + ); + + PlanterTokenReadResult.error({ + required String errorMessage, + }) : this( + success: false, + errorMessage: errorMessage, + ); +} + +class PlanterTokenReadFunctions { + // ERC20 token ABI for basic functions + static const String _planterTokenAbi = ''' + [ + { + "inputs": [], + "name": "planterAddress", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getPlanterAddress", + "outputs": [{"internalType": "address", "name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "address", "name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + } + ] + '''; + + static Future getPlanterTokenDetails({ + required WalletProvider walletProvider, + required String tokenContractAddress, + }) async { + try { + if (!walletProvider.isConnected) { + logger.e("Wallet not connected for reading planter token"); + return PlanterTokenReadResult.error( + errorMessage: 'Please connect your wallet.', + ); + } + + // Create contract ABI + final contractAbi = + ContractAbi.fromJson(_planterTokenAbi, 'PlanterToken'); + + // Call owner function + final ownerResult = await walletProvider.readContract( + contractAddress: tokenContractAddress, + abi: contractAbi, + functionName: 'owner', + params: [], + ); + + // Call getPlanterAddress function + final planterResult = await walletProvider.readContract( + contractAddress: tokenContractAddress, + abi: contractAbi, + functionName: 'getPlanterAddress', + params: [], + ); + + // Call name function + final nameResult = await walletProvider.readContract( + contractAddress: tokenContractAddress, + abi: contractAbi, + functionName: 'name', + params: [], + ); + + // Call symbol function + final symbolResult = await walletProvider.readContract( + contractAddress: tokenContractAddress, + abi: contractAbi, + functionName: 'symbol', + params: [], + ); + + if (!ownerResult.success || + !planterResult.success || + !nameResult.success || + !symbolResult.success) { + return PlanterTokenReadResult.error( + errorMessage: 'Failed to fetch token details from contract.', + ); + } + + logger.d("Planter Token Details fetched successfully"); + logger.d("Owner: ${ownerResult.data[0]}"); + logger.d("Planter: ${planterResult.data[0]}"); + logger.d("Name: ${nameResult.data[0]}"); + logger.d("Symbol: ${symbolResult.data[0]}"); + + return PlanterTokenReadResult.success( + data: { + 'owner': (ownerResult.data[0] as EthereumAddress).hex, + 'planterAddress': (planterResult.data[0] as EthereumAddress).hex, + 'name': nameResult.data[0].toString(), + 'symbol': symbolResult.data[0].toString(), + 'contractAddress': tokenContractAddress, + }, + ); + } catch (e) { + logger.e("Error fetching planter token details: $e"); + return PlanterTokenReadResult.error( + errorMessage: 'Failed to fetch token details: ${e.toString()}', + ); + } + } + + static Future getTokenBalance({ + required WalletProvider walletProvider, + required String tokenContractAddress, + required String accountAddress, + }) async { + try { + if (!walletProvider.isConnected) { + return PlanterTokenReadResult.error( + errorMessage: 'Please connect your wallet.', + ); + } + + final account = EthereumAddress.fromHex(accountAddress); + final contractAbi = + ContractAbi.fromJson(_planterTokenAbi, 'PlanterToken'); + + final balanceResult = await walletProvider.readContract( + contractAddress: tokenContractAddress, + abi: contractAbi, + functionName: 'balanceOf', + params: [account], + ); + + if (!balanceResult.success) { + return PlanterTokenReadResult.error( + errorMessage: 'Failed to fetch balance from contract.', + ); + } + + logger.d("Token balance fetched: ${balanceResult.data[0]}"); + + return PlanterTokenReadResult.success( + data: { + 'balance': balanceResult.data[0].toString(), + 'accountAddress': accountAddress, + 'contractAddress': tokenContractAddress, + }, + ); + } catch (e) { + logger.e("Error fetching token balance: $e"); + return PlanterTokenReadResult.error( + errorMessage: 'Failed to fetch balance: ${e.toString()}', + ); + } + } +} diff --git a/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart b/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart index 1c5f59a..7df8a6c 100644 --- a/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart +++ b/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart @@ -37,6 +37,7 @@ class ContractReadResult { class ContractReadFunctions { static Future getNFTsByUserPaginated({ required WalletProvider walletProvider, + required String userAddress, int offset = 0, int limit = 10, }) async { @@ -47,13 +48,13 @@ class ContractReadFunctions { errorMessage: 'Please connect your wallet before reading NFTs.', ); } - final String address = walletProvider.currentAddress.toString(); - if (!address.startsWith('0x')) { + if (!userAddress.startsWith('0x')) { return ContractReadResult.error( errorMessage: 'Invalid wallet address format', ); } - final EthereumAddress userAddress = EthereumAddress.fromHex(address); + final EthereumAddress finalUserAddress = + EthereumAddress.fromHex(userAddress); if (offset < 0 || limit <= 0 || limit > 100) { return ContractReadResult.error( errorMessage: @@ -61,7 +62,7 @@ class ContractReadFunctions { ); } final List args = [ - userAddress, + finalUserAddress, BigInt.from(offset), BigInt.from(limit), ]; @@ -96,14 +97,16 @@ class ContractReadFunctions { } } - static Future ping({ + static Future getProfileDetails({ required WalletProvider walletProvider, + required String currentAddress, }) async { try { if (!walletProvider.isConnected) { - logger.e("Wallet not connected for ping"); + logger.e("Wallet not connected for reading user data"); return ContractReadResult.error( - errorMessage: 'Please connect your wallet before pinging.', + errorMessage: + 'Please connect your wallet before fetching user details from blockchain', ); } final String address = walletProvider.currentAddress.toString(); @@ -112,39 +115,53 @@ class ContractReadFunctions { errorMessage: 'Invalid wallet address format', ); } - final result = await walletProvider.readContract( + final EthereumAddress userAddress = + EthereumAddress.fromHex(currentAddress); + final List argsProfile = [userAddress]; + final List argsVerifierTokens = [ + userAddress, + BigInt.from(0), // offset + BigInt.from(100), // limit - fetch up to 100 verifier tokens + ]; + final userVerifierTokensResult = await walletProvider.readContract( contractAddress: treeNFtContractAddress, - functionName: 'ping', + functionName: 'getUserVerifierTokenDetails', abi: treeNftContractABI, - params: [], - ); - String pingResponse; - if (result != null) { - if (result is List && result.isNotEmpty) { - pingResponse = - result[0]?.toString() ?? 'Ping successful - no return value'; - } else { - pingResponse = result.toString(); - } - } else { - pingResponse = 'Ping successful - no return value'; - } - return ContractReadResult.success( - data: { - 'result': pingResponse, - }, + params: argsVerifierTokens, ); + final userProfileResult = await walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getUserProfile', + abi: treeNftContractABI, + params: argsProfile, + ); + final profile = + userProfileResult.length > 0 ? userProfileResult[0] ?? [] : []; + final verifierTokens = userVerifierTokensResult.length > 0 + ? userVerifierTokensResult[0] ?? [] + : []; + final totalCount = userVerifierTokensResult.length > 1 + ? userVerifierTokensResult[1] + : BigInt.zero; + logger.d("User Profile"); + logger.d(profile); + logger.d("Verifier Tokens Total Count: $totalCount"); + return ContractReadResult.success(data: { + 'profile': profile, + 'verifierTokens': verifierTokens, + 'totalCount': totalCount + }); } catch (e) { - logger.e("Error pinging contract", error: e); - String detailedError = 'Ping failed: ${e.toString()}'; + logger.e("Error reading User profile", error: e); return ContractReadResult.error( - errorMessage: detailedError, + errorMessage: 'Failed to read User Profile: ${e.toString()}', ); } } - static Future getProfileDetails({ + static Future getProfileDetailsByAddress({ required WalletProvider walletProvider, + required String userAddress, }) async { try { if (!walletProvider.isConnected) { @@ -154,26 +171,56 @@ class ContractReadFunctions { 'Please connect your wallet before fetching user details from blockchain', ); } - final String address = walletProvider.currentAddress.toString(); - if (!address.startsWith('0x')) { + + if (!userAddress.startsWith('0x')) { return ContractReadResult.error( errorMessage: 'Invalid wallet address format', ); } - final String currentAddress = walletProvider.currentAddress!.toString(); - final EthereumAddress userAddress = - EthereumAddress.fromHex(currentAddress); - final List args = [userAddress]; - final result = await walletProvider.readContract( + + final EthereumAddress targetAddress = + EthereumAddress.fromHex(userAddress); + final List argsProfile = [targetAddress]; + final List argsVerifierTokens = [ + targetAddress, + BigInt.from(0), // offset + BigInt.from(100), // limit - fetch up to 100 verifier tokens + ]; + + final userVerifierTokensResult = await walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getUserVerifierTokenDetails', + abi: treeNftContractABI, + params: argsVerifierTokens, + ); + + final userProfileResult = await walletProvider.readContract( contractAddress: treeNFtContractAddress, functionName: 'getUserProfile', abi: treeNftContractABI, - params: args, + params: argsProfile, ); - final profile = result.length > 0 ? result[0] ?? [] : []; - return ContractReadResult.success(data: {'profile': profile}); + + final profile = + userProfileResult.length > 0 ? userProfileResult[0] ?? [] : []; + final verifierTokens = userVerifierTokensResult.length > 0 + ? userVerifierTokensResult[0] ?? [] + : []; + final totalCount = userVerifierTokensResult.length > 1 + ? userVerifierTokensResult[1] + : BigInt.zero; + + logger.d("User Profile for $userAddress"); + logger.d(profile); + logger.d("Verifier Tokens Total Count: $totalCount"); + + return ContractReadResult.success(data: { + 'profile': profile, + 'verifierTokens': verifierTokens, + 'totalCount': totalCount + }); } catch (e) { - logger.e("Error reading User profile", error: e); + logger.e("Error reading User profile for $userAddress", error: e); return ContractReadResult.error( errorMessage: 'Failed to read User Profile: ${e.toString()}', ); @@ -208,8 +255,6 @@ class ContractReadFunctions { ); final tree = treeDetailsResult.length > 0 ? treeDetailsResult[0] ?? [] : []; - logger.d("Tree Info"); - logger.d(tree); final treeVerifiersResult = await walletProvider.readContract( contractAddress: treeNFtContractAddress, functionName: 'getTreeNftVerifiersPaginated', @@ -217,8 +262,15 @@ class ContractReadFunctions { abi: treeNftContractABI); final verifiers = treeVerifiersResult.length > 0 ? treeVerifiersResult[0] ?? [] : []; + final totalCount = treeVerifiersResult.length > 1 + ? (treeVerifiersResult[1] as BigInt).toInt() + : 0; + final visibleCount = treeVerifiersResult.length > 2 + ? (treeVerifiersResult[2] as BigInt).toInt() + : 0; logger.d("Tree Verifiers Info"); logger.d(verifiers); + logger.d("Total verifications: $totalCount, Visible: $visibleCount"); final ownerResult = await walletProvider.readContract( contractAddress: treeNFtContractAddress, functionName: 'ownerOf', @@ -227,7 +279,13 @@ class ContractReadFunctions { ); final owner = ownerResult.isNotEmpty ? ownerResult[0] : null; return ContractReadResult.success( - data: {'details': tree, 'verifiers': verifiers, 'owner': owner}, + data: { + 'details': tree, + 'verifiers': verifiers, + 'owner': owner, + 'totalCount': totalCount, + 'visibleCount': visibleCount, + }, ); } catch (e) { logger.e("Error fetching the details of the Tree NFT", error: e); @@ -236,4 +294,91 @@ class ContractReadFunctions { ); } } + + static Future getRecentTreesPaginated({ + required WalletProvider walletProvider, + 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', + ); + } + + if (offset < 0 || limit <= 0 || limit > 50) { + return ContractReadResult.error( + errorMessage: + 'Invalid pagination parameters. Offset must be >= 0 and limit must be between 1-50', + ); + } + + final List args = [BigInt.from(offset), BigInt.from(limit)]; + final contractResult = await walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getRecentTreesPaginated', + params: args, + abi: treeNftContractABI, + ); + + if (contractResult == null || contractResult.isEmpty) { + return ContractReadResult.error( + errorMessage: 'No data returned from contract', + ); + } + final trees = contractResult[0] as List; + final totalCount = (contractResult[1] as BigInt).toInt(); + final hasMore = contractResult[2] as bool; + + final List> parsedTrees = trees.map((tree) { + final treeList = tree as List; + return { + 'id': (treeList[0] as BigInt).toInt(), + 'latitude': (treeList[1] as BigInt).toInt(), + 'longitude': (treeList[2] as BigInt).toInt(), + 'planting': (treeList[3] as BigInt).toInt(), + 'death': (treeList[4] as BigInt).toInt(), + 'species': treeList[5] as String, + 'imageUri': treeList[6] as String, + 'qrPhoto': treeList[7] as String, + 'metadata': treeList[8] as String, + 'photos': + (treeList[9] as List).map((photo) => photo as String).toList(), + 'geoHash': treeList[10] as String, + 'ancestors': (treeList[11] as List) + .map((ancestor) => (ancestor as EthereumAddress).hex) + .toList(), + 'lastCareTimestamp': (treeList[12] as BigInt).toInt(), + 'careCount': (treeList[13] as BigInt).toInt(), + 'numberOfTrees': (treeList[14] as BigInt).toInt(), + }; + }).toList(); + + logger + .d("Recent trees fetched successfully: ${parsedTrees.length} trees"); + + return ContractReadResult.success( + data: { + 'trees': parsedTrees, + 'totalCount': totalCount, + 'hasMore': hasMore, + 'offset': offset, + 'limit': limit, + }, + ); + } catch (e) { + logger.e("Error fetching recent trees", error: e); + return ContractReadResult.error( + errorMessage: 'Failed to read recent trees: ${e.toString()}', + ); + } + } } diff --git a/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart b/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart index fd55286..ed3e065 100644 --- a/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart +++ b/lib/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_write_functions.dart @@ -41,6 +41,7 @@ class ContractWriteFunctions { required String species, required List photos, required String geoHash, + required int numberOfTrees, required String metadata, String additionalData = "", }) async { @@ -63,6 +64,7 @@ class ContractWriteFunctions { } final lat = BigInt.from((latitude + 90.0) * 1e6); final lng = BigInt.from((longitude + 180.0) * 1e6); + final numberOfTreesBigInt = BigInt.from(numberOfTrees); logger.i("Minting NFT with coordinates: Lat: $lat, Lng: $lng"); logger @@ -76,6 +78,7 @@ class ContractWriteFunctions { metadata, geoHash, photos, + numberOfTreesBigInt, ]; final txHash = await walletProvider.writeContract( contractAddress: treeNFtContractAddress, diff --git a/lib/utils/services/ipfs_services.dart b/lib/utils/services/ipfs_services.dart index d216ea5..ade2787 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 apiKey = dotenv.get('API_KEY', fallback: ""); -String apiSecret = dotenv.get('API_SECRET', fallback: ""); +String apiKey = dotenv.get('PINATA_API_KEY', fallback: ""); +String apiSecret = dotenv.get('PINATA_API_SECRET', fallback: ""); Future uploadToIPFS( File imageFile, Function(bool) setUploadingState) async { diff --git a/lib/widgets/basic_scaffold.dart b/lib/widgets/basic_scaffold.dart index 31857e6..22c9f81 100644 --- a/lib/widgets/basic_scaffold.dart +++ b/lib/widgets/basic_scaffold.dart @@ -17,16 +17,27 @@ class BaseScaffold extends StatelessWidget { final bool extendBodyBehindAppBar; final bool showBottomNavigation; final Widget? leading; + final bool showBackButton; + final bool isLoading; + final VoidCallback? onBackPressed; + final VoidCallback? onReload; + final bool showReloadButton; - const BaseScaffold( - {super.key, - required this.body, - this.title, - this.actions, - this.floatingActionButton, - this.extendBodyBehindAppBar = false, - this.showBottomNavigation = true, - this.leading}); + const BaseScaffold({ + super.key, + required this.body, + this.title, + this.actions, + this.floatingActionButton, + this.extendBodyBehindAppBar = false, + this.showBottomNavigation = true, + this.leading, + this.showBackButton = false, + this.isLoading = false, + this.onBackPressed, + this.onReload, + this.showReloadButton = false, + }); @override Widget build(BuildContext context) { @@ -59,8 +70,16 @@ class BaseScaffold extends StatelessWidget { } return Scaffold( - appBar: - UniversalNavbar(title: title, actions: actions, leading: leading), + appBar: UniversalNavbar( + title: title, + actions: actions, + leading: leading, + showBackButton: showBackButton, + isLoading: isLoading, + onBackPressed: onBackPressed, + onReload: onReload, + showReloadButton: showReloadButton, + ), extendBodyBehindAppBar: extendBodyBehindAppBar, body: bodyContent, floatingActionButton: floatingActionButton, diff --git a/lib/widgets/map_widgets/flutter_map_widget.dart b/lib/widgets/map_widgets/flutter_map_widget.dart index 936698f..19d0b2a 100644 --- a/lib/widgets/map_widgets/flutter_map_widget.dart +++ b/lib/widgets/map_widgets/flutter_map_widget.dart @@ -276,7 +276,7 @@ class _CoordinatesMapState extends State { child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.blue, + color: getThemeColors(context)['primary'], borderRadius: BorderRadius.circular(4), ), child: Text( diff --git a/lib/widgets/map_widgets/static_map_display_widget.dart b/lib/widgets/map_widgets/static_map_display_widget.dart index fb5d8d9..df65e77 100644 --- a/lib/widgets/map_widgets/static_map_display_widget.dart +++ b/lib/widgets/map_widgets/static_map_display_widget.dart @@ -91,7 +91,7 @@ class _StaticCoordinatesMapState extends State { mapController: _mapController, options: MapOptions( initialCenter: LatLng(latitude, longitude), - initialZoom: 12.0, + initialZoom: 10.0, minZoom: 3.0, maxZoom: 18.0, interactionOptions: const InteractionOptions( @@ -104,7 +104,6 @@ class _StaticCoordinatesMapState extends State { setState(() { _mapLoaded = true; }); - _mapController.move(LatLng(latitude, longitude), 15.0); }, ), children: [ diff --git a/lib/widgets/nft_display_utils/recent_trees_widget.dart b/lib/widgets/nft_display_utils/recent_trees_widget.dart new file mode 100644 index 0000000..426c65e --- /dev/null +++ b/lib/widgets/nft_display_utils/recent_trees_widget.dart @@ -0,0 +1,595 @@ +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/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart'; + +class RecentTreesWidget extends StatefulWidget { + const RecentTreesWidget({super.key}); + + @override + State createState() => _RecentTreesWidgetState(); +} + +class _RecentTreesWidgetState extends State { + List> _trees = []; + bool _isLoading = false; + String? _errorMessage; + int _currentOffset = 0; + final int _itemsPerPage = 10; + int _totalCount = 0; + bool _hasMore = true; + + @override + void initState() { + super.initState(); + _loadTrees(); + } + + Future _loadTrees({bool loadMore = false}) async { + if (_isLoading) return; + + setState(() { + _isLoading = true; + if (!loadMore) { + _errorMessage = null; + _trees.clear(); + _currentOffset = 0; + } + }); + + try { + final walletProvider = + Provider.of(context, listen: false); + + final result = await ContractReadFunctions.getRecentTreesPaginated( + walletProvider: walletProvider, + offset: loadMore ? _currentOffset : 0, + limit: _itemsPerPage, + ); + + if (result.success && result.data != null) { + final List treesData = result.data['trees'] ?? []; + final int totalCount = result.data['totalCount'] ?? 0; + final bool hasMore = result.data['hasMore'] ?? false; + + final List> newTrees = + List>.from(treesData); + + setState(() { + if (loadMore) { + _trees.addAll(newTrees); + _currentOffset += newTrees.length; + } else { + _trees = newTrees; + _currentOffset = newTrees.length; + } + _totalCount = totalCount; + _hasMore = hasMore; + }); + + logger.d("Loaded ${newTrees.length} trees, total: ${_trees.length}"); + } else { + setState(() { + _errorMessage = result.errorMessage ?? 'Failed to load recent trees'; + }); + } + } catch (e) { + setState(() { + _errorMessage = 'Error loading recent trees: $e'; + }); + logger.e("Error loading recent trees: $e"); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future _refreshTrees() async { + await _loadTrees(); + } + + double _convertCoordinate(int coordinate) { + // Convert from fixed-point representation to decimal degrees + return (coordinate / 1000000.0) - 90.0; + } + + String _formatDate(int timestamp) { + if (timestamp == 0) return "Unknown"; + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return "${date.day}/${date.month}/${date.year}"; + } + + Widget _buildTreeCard(Map tree) { + final bool isAlive = tree['death'] == 0 || + tree['death'] > DateTime.now().millisecondsSinceEpoch ~/ 1000; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: buttonBlurRadius, + offset: const Offset(0, 2), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Tree Image + if (tree['imageUri'] != null && + tree['imageUri'].toString().isNotEmpty) + Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + tree['imageUri'], + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: getThemeColors(context)['secondary'], + child: Center( + child: Icon( + Icons.image_not_supported, + size: 40, + color: getThemeColors(context)['textPrimary'], + ), + ), + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + color: getThemeColors(context)['secondary'], + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + getThemeColors(context)['primary']!), + ), + ), + ); + }, + ), + ), + ), + + const SizedBox(height: 12), + + // Tree ID and Status Row + Row( + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.black, width: 1), + ), + child: Text( + 'ID: ${tree['id']}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const Spacer(), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isAlive + ? getThemeColors(context)['success'] ?? Colors.green + : getThemeColors(context)['error']!, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.black, width: 1), + ), + child: Text( + isAlive ? 'Alive' : 'Deceased', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Species + Text( + tree['species'] ?? 'Unknown Species', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + + const SizedBox(height: 8), + + // Location + Row( + children: [ + Icon( + Icons.location_on, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Expanded( + child: Text( + 'Location: ${_convertCoordinate(tree['latitude']).toStringAsFixed(6)}, ${_convertCoordinate(tree['longitude']).toStringAsFixed(6)}', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + ), + ], + ), + + const SizedBox(height: 4), + + // Planting Date + Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Text( + 'Planted: ${_formatDate(tree['planting'])}', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + + const SizedBox(height: 4), + + // Care Information + Row( + children: [ + Icon( + Icons.favorite, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Text( + 'Care Count: ${tree['careCount'] ?? 0}', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(width: 16), + Icon( + Icons.nature, + size: 16, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 4), + Text( + 'Trees: ${tree['numberOfTrees'] ?? 1}', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + + if (tree['geoHash'] != null && + tree['geoHash'].toString().isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.map, + size: 16, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 4), + Text( + 'GeoHash: ${tree['geoHash']}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + fontFamily: 'monospace', + ), + ), + ], + ), + ], + + const SizedBox(height: 16), + + // Action Buttons + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + context.push('/trees/${tree['id']}'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + elevation: buttonBlurRadius, + ), + child: const Text( + 'View Details', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Map view coming soon!'), + backgroundColor: getThemeColors(context)['secondary'], + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['secondary'], + foregroundColor: getThemeColors(context)['textPrimary'], + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + side: BorderSide( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + elevation: buttonBlurRadius, + ), + child: const Text( + 'View on Map', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildErrorWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: getThemeColors(context)['error'], + ), + const SizedBox(height: 16), + Text( + _errorMessage!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _refreshTrees, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + ), + child: const Text('Retry'), + ), + ], + ), + ); + } + + Widget _buildEmptyWidget() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.eco, + size: 64, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(height: 16), + Text( + "No recent trees found", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 8), + Text( + "Be the first to plant a tree and contribute to our ecosystem!", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _refreshTrees, + icon: const Icon(Icons.refresh), + label: const Text('Refresh'), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 2), + ), + ), + ], + ), + ); + } + + Widget _buildLoadingIndicator() { + return Padding( + padding: const EdgeInsets.all(16), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + getThemeColors(context)['primary']!), + ), + const SizedBox(height: 8), + Text( + 'Loading more trees...', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Tree count badge + if (_totalCount > 0) + Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary'], + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: Text( + '$_totalCount trees planted', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + ), + ], + ), + ), + + // Content + Expanded( + child: _errorMessage != null + ? _buildErrorWidget() + : _trees.isEmpty && !_isLoading + ? _buildEmptyWidget() + : RefreshIndicator( + onRefresh: _refreshTrees, + color: getThemeColors(context)['primary'], + child: NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (!_isLoading && + _hasMore && + scrollInfo.metrics.pixels == + scrollInfo.metrics.maxScrollExtent) { + _loadTrees(loadMore: true); + } + return false; + }, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _trees.length + + (_hasMore && !_isLoading ? 0 : 0) + + (_isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index < _trees.length) { + return _buildTreeCard(_trees[index]); + } else if (_isLoading) { + return _buildLoadingIndicator(); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ), + + // Loading indicator at bottom when loading more + if (_isLoading && _trees.isNotEmpty) _buildLoadingIndicator(), + ], + ); + } +} 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 f92cc1f..ebab9cb 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,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:tree_planting_protocol/components/transaction_dialog.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; @@ -266,41 +267,44 @@ Future _removeVerifier(Verifier verifier, BuildContext context, ); if (context.mounted) { - final messenger = ScaffoldMessenger.of(context); if (result.success) { - messenger.showSnackBar( - SnackBar( - content: const Text("Verifier removed successfully!"), - backgroundColor: getThemeColors(context)['primary'], - behavior: SnackBarBehavior.floating, - ), + TransactionDialog.showSuccess( + context, + title: 'Verifier Removed!', + message: 'The verifier has been successfully removed.', + transactionHash: result.transactionHash, + onClose: () async { + await loadTreeDetails(); + }, ); - await loadTreeDetails(); } else { - messenger.showSnackBar( - SnackBar( - content: Text("Failed to remove verifier: ${result.errorMessage}"), - backgroundColor: getThemeColors(context)['error'], - behavior: SnackBarBehavior.floating, - ), + TransactionDialog.showError( + context, + title: 'Failed to Remove Verifier', + message: result.errorMessage ?? 'An unknown error occurred', ); } } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Error: $e"), - backgroundColor: getThemeColors(context)['error'], - behavior: SnackBarBehavior.floating, - ), + TransactionDialog.showError( + context, + title: 'Error', + message: e.toString(), ); } } } -Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, - Function loadTreeDetails, BuildContext context) { +Widget treeVerifiersSection( + String? loggedInUser, + Tree? treeDetails, + Function loadTreeDetails, + BuildContext context, { + int currentCount = 0, + int totalCount = 0, + int visibleCount = 0, +}) { final themeColors = getThemeColors(context); if (treeDetails?.verifiers == null || treeDetails!.verifiers.isEmpty) { @@ -312,7 +316,7 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, decoration: BoxDecoration( color: getThemeColors(context)['background']!, borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: getThemeColors(context)['border']!), + border: Border.all(color: getThemeColors(context)['border']!, width: 2), ), child: Column( children: [ @@ -349,29 +353,52 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, margin: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - color: themeColors['primary'], + color: themeColors['background'], borderRadius: BorderRadius.circular(12.0), - border: Border.all(color: themeColors['primaryBorder']!), + border: Border.all(color: Colors.black, width: 2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon( - Icons.verified_user, - color: primaryYellowColor, - size: 24, + Row( + children: [ + Icon( + Icons.verified_user, + color: themeColors['primary'], + size: 24, + ), + const SizedBox(width: 8), + Text( + "Tree Verifiers", + style: TextStyle( + color: getThemeColors(context)['textPrimary']!, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], ), - const SizedBox(width: 8), - Text( - "Tree Verifiers", - style: TextStyle( - color: getThemeColors(context)['textPrimary']!, - fontSize: 18, - fontWeight: FontWeight.bold, + if (visibleCount > 0) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: themeColors['primary'], + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.black, width: 2), + ), + child: Text( + "$currentCount of $visibleCount", + style: TextStyle( + color: themeColors['textPrimary'], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), ), - ), ], ), const SizedBox(height: 12), @@ -380,10 +407,22 @@ Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, ? "Tap any verifier to view details • Tap ✕ to remove (owner only)" : "Tap any verifier to view verification details", style: TextStyle( - color: themeColors['primary']!, + color: themeColors['textSecondary']!, fontSize: 12, ), ), + if (visibleCount > currentCount) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + "Scroll down to load more verifiers", + style: TextStyle( + color: themeColors['textSecondary']!, + fontSize: 11, + fontStyle: FontStyle.italic, + ), + ), + ), const SizedBox(height: 16), ...treeDetails.verifiers.asMap().entries.map((entry) { final index = entry.key; @@ -595,29 +634,46 @@ void _showVerifierDetailsModal( return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), + side: const BorderSide(color: Colors.black, width: 2), ), child: Container( constraints: BoxConstraints( maxHeight: dialogHeight, maxWidth: dialogWidth, ), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.black, width: 2), + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), + ), + border: const Border( + bottom: BorderSide(color: Colors.black, width: 2), + ), + ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( - color: getThemeColors(context)['primary'], + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.black, width: 2), ), child: Icon( Icons.verified_user, - color: getThemeColors(context)['icon'], + color: getThemeColors(context)['primary'], size: 20, ), ), @@ -635,7 +691,7 @@ void _showVerifierDetailsModal( IconButton( onPressed: () => Navigator.pop(context), icon: Icon(Icons.close, - color: getThemeColors(context)['icon']), + color: getThemeColors(context)['textPrimary']), ), ], ), @@ -674,20 +730,21 @@ void _showVerifierDetailsModal( width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: getThemeColors(context)['primary'], + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(6), - border: Border.all( - color: getThemeColors( - context)['primaryBorder']!), + border: + Border.all(color: Colors.black, width: 2), ), child: Row( children: [ Expanded( child: Text( verifier.address, - style: const TextStyle( + style: TextStyle( fontSize: 13, fontFamily: 'monospace', + color: getThemeColors( + context)['textPrimary'], ), ), ), @@ -695,7 +752,7 @@ void _showVerifierDetailsModal( Icon( Icons.copy, size: 16, - color: getThemeColors(context)['icon'], + color: getThemeColors(context)['primary'], ), ], ), @@ -745,11 +802,11 @@ void _showVerifierDetailsModal( height: 80, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - border: Border.all( - color: themeColors['primaryBorder']!), + border: + Border.all(color: Colors.black, width: 2), ), child: ClipRRect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(6), child: Image.network( verifier.proofHashes[index], fit: BoxFit.cover, @@ -791,14 +848,10 @@ void _showVerifierDetailsModal( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: verifier.isActive - ? getThemeColors(context)['primary']! - : Colors.red.shade50, + ? getThemeColors(context)['primary'] + : getThemeColors(context)['secondary'], borderRadius: BorderRadius.circular(8), - border: Border.all( - color: verifier.isActive - ? getThemeColors(context)['primary']! - : Colors.red.shade200, - ), + border: Border.all(color: Colors.black, width: 2), ), child: Row( children: [ @@ -806,12 +859,19 @@ void _showVerifierDetailsModal( verifier.isActive ? Icons.check_circle : Icons.cancel, - color: verifier.isActive - ? getThemeColors(context)['primary'] - : Colors.red.shade600, + color: getThemeColors(context)['textPrimary'], size: 20, ), const SizedBox(width: 8), + Text( + verifier.isActive + ? "Active Verification" + : "Verification Removed", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.w600, + ), + ), ], ), ), @@ -822,22 +882,38 @@ void _showVerifierDetailsModal( ), Container( padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: Colors.black, width: 2), + ), + ), child: SizedBox( width: double.infinity, - child: ElevatedButton( - onPressed: () => Navigator.pop(context), - style: ElevatedButton.styleFrom( - backgroundColor: getThemeColors(context)['background'], - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: + getThemeColors(context)['textPrimary'], + padding: const EdgeInsets.symmetric(vertical: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text( + "Close", + style: TextStyle(fontWeight: FontWeight.bold), + ), ), - ), - child: Text( - "Close", - style: TextStyle( - color: getThemeColors(context)['textPrimary']), ), ), ), @@ -897,43 +973,54 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0), + side: const BorderSide(color: Colors.black, width: 2), ), + backgroundColor: getThemeColors(context)['background'], title: Row( children: [ - Icon(Icons.warning, color: getThemeColors(context)['icon']), + Icon(Icons.warning, color: getThemeColors(context)['error']), const SizedBox(width: 8), - const Text("Remove Verifier"), + Text( + "Remove Verifier", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( "Are you sure you want to remove this verifier?", - style: TextStyle(fontSize: 16), + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['textPrimary'], + ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: getThemeColors(context)['primary'], + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(8), - border: Border.all( - color: getThemeColors(context)['primaryBorder']!), + border: Border.all(color: Colors.black, width: 2), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(Icons.person, color: Colors.grey.shade600), + Icon(Icons.person, + color: getThemeColors(context)['primary']), const SizedBox(width: 8), - const Text( + Text( "Address:", style: TextStyle( fontWeight: FontWeight.w600, fontSize: 12, + color: getThemeColors(context)['textPrimary'], ), ), ], @@ -944,26 +1031,26 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( - color: Colors.white, + color: getThemeColors(context)['background'], borderRadius: BorderRadius.circular(6), - border: Border.all( - color: getThemeColors(context)['primaryBorder']!), + border: Border.all(color: Colors.black, width: 2), ), child: Row( children: [ Expanded( child: Text( verifier.address, - style: const TextStyle( + style: TextStyle( fontFamily: 'monospace', fontSize: 11, + color: getThemeColors(context)['textPrimary'], ), ), ), Icon( Icons.copy, size: 14, - color: getThemeColors(context)['icon'], + color: getThemeColors(context)['primary'], ), ], ), @@ -975,7 +1062,7 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, "Description: ${verifier.description}", style: TextStyle( fontSize: 12, - color: Colors.grey.shade600, + color: getThemeColors(context)['textSecondary'], ), ), ], @@ -984,7 +1071,7 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, "Verified: ${verifier.formattedTimestamp}", style: TextStyle( fontSize: 11, - color: Colors.grey.shade500, + color: getThemeColors(context)['textSecondary'], ), ), ], @@ -998,33 +1085,57 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, color: getThemeColors(context)['textPrimary'], ), ), - _buildRemovalPoint("• Will require gas fees"), - _buildRemovalPoint("• Cannot be undone"), - _buildRemovalPoint("• Removes verification permanently"), + _buildRemovalPoint("• Will require gas fees", context), + _buildRemovalPoint("• Cannot be undone", context), + _buildRemovalPoint("• Removes verification permanently", context), ], ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - "Cancel", - style: TextStyle(color: Colors.grey.shade600), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + "Cancel", + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), + ), ), ), - ElevatedButton( - onPressed: () async { - Navigator.of(context).pop(); - await _removeVerifier( - verifier, context, treeDetails, loadTreeDetails); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red.shade600, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.black, width: 2), + ), + child: ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + await _removeVerifier( + verifier, context, treeDetails, loadTreeDetails); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['error'], + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: const Text("Remove", + style: TextStyle(fontWeight: FontWeight.bold)), ), ), - child: const Text("Remove"), ), ], ); @@ -1032,14 +1143,14 @@ void _showRemoveVerifierDialog(Verifier verifier, int index, ); } -Widget _buildRemovalPoint(String text) { +Widget _buildRemovalPoint(String text, BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Text( text, style: TextStyle( fontSize: 14, - color: Colors.grey.shade700, + color: getThemeColors(context)['textSecondary'], ), ), ); 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 66af69a..6e0acca 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 @@ -58,72 +58,274 @@ class _NewNFTMapWidgetState extends State { maxWidth: containerMaxWidth, minHeight: 120, ), - padding: EdgeInsets.all(screenWidth * 0.04), decoration: BoxDecoration( - border: Border.all(color: Colors.green, width: 2), - borderRadius: BorderRadius.circular(12.0), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + borderRadius: BorderRadius.circular(16.0), color: getThemeColors(context)['background'], boxShadow: [ BoxShadow( - color: getThemeColors(context)['primary']!, + color: getThemeColors(context)['shadow']!, blurRadius: 8, offset: const Offset(0, 2), ), ], ), - child: Consumer( - builder: (ctx, provider, _) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildInfoRow( - 'Latitude:', - provider.getLatitude().toString(), - screenWidth, - ), - const SizedBox(height: 8), - _buildInfoRow( - 'Longitude:', - provider.getLongitude().toString(), - screenWidth, - ), - const SizedBox(height: 8), - _buildInfoRow( - 'GeoHash:', - provider.getGeoHash(), - screenWidth, - ), - const SizedBox(height: 8), - _buildInfoRow( - 'Species:', - provider.getSpecies(), - screenWidth, + child: Column( + children: [ + // Header + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(14), + topRight: Radius.circular(14), ), - const SizedBox(height: 8), - _buildInfoRow( - 'Description:', - _formatDescription(provider.getDetails(), screenWidth), - screenWidth, - isDescription: true, - ), - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: provider.getInitialPhotos().length, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Image.network( - provider.getInitialPhotos()[index], - height: 100, + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Icon( + Icons.preview, + color: getThemeColors(context)['primary'], + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'NFT Preview', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textSecondary'], + ), + ), + ], + ), + ), + // Content + Padding( + padding: EdgeInsets.all(screenWidth * 0.04), + child: Consumer( + builder: (ctx, provider, _) { + final organisationAddress = provider.organisationAddress; + final hasOrganisation = organisationAddress.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hasOrganisation) ...[ + Container( width: double.infinity, - fit: BoxFit.cover, + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary']!, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + Icons.business, + color: getThemeColors(context)['primary'], + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Organisation Minting', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: getThemeColors( + context)['textPrimary'], + ), + ), + const SizedBox(height: 4), + Text( + '${organisationAddress.substring(0, 6)}...${organisationAddress.substring(organisationAddress.length - 4)}', + style: TextStyle( + fontSize: 12, + fontFamily: 'monospace', + color: getThemeColors( + context)['textPrimary']!, + ), + ), + ], + ), + ), + ], + ), + ), + ], + _buildInfoRow( + 'Latitude:', + provider.getLatitude().toString(), + screenWidth, + icon: Icons.north, + color: getThemeColors(context)['primary']!, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Longitude:', + provider.getLongitude().toString(), + screenWidth, + icon: Icons.east, + color: getThemeColors(context)['primary']!, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'GeoHash:', + provider.getGeoHash(), + screenWidth, + icon: Icons.tag, + color: getThemeColors(context)['secondary']!, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Species:', + provider.getSpecies(), + screenWidth, + icon: Icons.park, + color: getThemeColors(context)['primary']!, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Number of Trees:', + provider.getNumberOfTrees().toString(), + screenWidth, + icon: Icons.format_list_numbered, + color: getThemeColors(context)['secondary']!, + ), + const SizedBox(height: 8), + _buildInfoRow( + 'Description:', + _formatDescription( + provider.getDetails(), screenWidth), + screenWidth, + isDescription: true, + icon: Icons.description, + color: getThemeColors(context)['primary']!, + ), + if (provider.getInitialPhotos().isNotEmpty) ...[ + const SizedBox(height: 16), + Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: getThemeColors(context)['primary']!, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + getThemeColors(context)['primary']!, + width: 1, + ), + ), + child: Icon( + Icons.photo_library, + color: getThemeColors(context)['primary'], + size: 16, + ), + ), + const SizedBox(width: 8), + Text( + 'Uploaded Photos (${provider.getInitialPhotos().length})', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['primary'], + ), + ), + ], ), - ); - }), - ], - ); - }, + const SizedBox(height: 8), + ], + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: provider.getInitialPhotos().length, + itemBuilder: (context, index) { + return Container( + margin: + const EdgeInsets.symmetric(vertical: 6.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + getThemeColors(context)['primary']!, + width: 2, + ), + ), + clipBehavior: Clip.antiAlias, + child: Stack( + children: [ + Image.network( + provider.getInitialPhotos()[index], + height: 150, + width: double.infinity, + fit: BoxFit.cover, + ), + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: getThemeColors( + context)['primary'], + borderRadius: + BorderRadius.circular(20), + border: Border.all( + color: Colors.black, + width: 2, + ), + ), + child: Text( + 'Photo ${index + 1}', + style: TextStyle( + color: getThemeColors( + context)['textSecondary'], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ); + }), + ], + ); + }, + ), + ), + ], ), ), ], @@ -131,34 +333,68 @@ class _NewNFTMapWidgetState extends State { ); } - Widget _buildInfoRow(String label, String value, double screenWidth, - {bool isDescription = false}) { + Widget _buildInfoRow( + String label, + String value, + double screenWidth, { + bool isDescription = false, + IconData? icon, + Color? color, + }) { final fontSize = screenWidth < 360 ? 14.0 : 16.0; + final labelColor = color ?? getThemeColors(context)['primary']!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.w600, - color: getThemeColors(context)['textPrimary']!, - ), + Row( + children: [ + if (icon != null) ...[ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: labelColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.black, + width: 2, + ), + ), + child: Icon( + icon, + color: labelColor, + size: 16, + ), + ), + const SizedBox(width: 8), + ], + Text( + label, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: labelColor, + ), + ), + ], ), - const SizedBox(height: 4), + const SizedBox(height: 8), Container( width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 14), decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade200), + color: labelColor, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.black, + width: 2, + ), ), child: Text( value, style: TextStyle( fontSize: fontSize, + fontWeight: FontWeight.w500, color: getThemeColors(context)['textPrimary']!, height: isDescription ? 1.4 : 1.2, ), diff --git a/lib/widgets/nft_display_utils/user_nfts_widget.dart b/lib/widgets/nft_display_utils/user_nfts_widget.dart index 6326e34..9a1075f 100644 --- a/lib/widgets/nft_display_utils/user_nfts_widget.dart +++ b/lib/widgets/nft_display_utils/user_nfts_widget.dart @@ -153,6 +153,7 @@ class _UserNftsWidgetState extends State { walletProvider: walletProvider, offset: 0, limit: 10, + userAddress: widget.userAddress, ); if (result.success && result.data != null) { @@ -251,7 +252,7 @@ class _UserNftsWidgetState extends State { const SizedBox(width: 4), Expanded( child: Text( - 'Location: ${tree.latitude / 1000000}, ${tree.longitude / 1000000}', + 'Location: ${(tree.latitude / 1000000) - 90}, ${(tree.longitude / 1000000) - 180}', style: TextStyle( color: getThemeColors(context)['textPrimary']), ), @@ -358,6 +359,7 @@ class _UserNftsWidgetState extends State { @override Widget build(BuildContext context) { + final walletProvider = Provider.of(context); return Column( children: [ Padding( @@ -366,7 +368,9 @@ class _UserNftsWidgetState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - widget.isOwnerCalling ? "Your NFTs" : "User NFTs", + widget.userAddress == walletProvider.currentAddress + ? 'Your Trees' + : "User's Trees", style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, @@ -434,7 +438,9 @@ class _UserNftsWidgetState extends State { ), const SizedBox(height: 16), Text( - widget.isOwnerCalling + widget.userAddress == + Provider.of(context, listen: false) + .currentAddress ? "You don't have any tree NFTs yet" : "This user doesn't have any tree NFTs yet", textAlign: TextAlign.center, diff --git a/lib/widgets/organisation_details_page/tabs/info_tab.dart b/lib/widgets/organisation_details_page/tabs/info_tab.dart new file mode 100644 index 0000000..343b1d8 --- /dev/null +++ b/lib/widgets/organisation_details_page/tabs/info_tab.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; + +class InfoTab extends StatelessWidget { + final String organisationDescription; + final int timeOfCreation; + + const InfoTab({ + super.key, + required this.organisationDescription, + required this.timeOfCreation, + }); + + String _formatDate(int timestamp) { + if (timestamp == 0) return "Unknown"; + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return "${date.day}/${date.month}/${date.year}"; + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'About', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['secondary'], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: Text( + organisationDescription.isNotEmpty + ? organisationDescription + : 'No description available', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + height: 1.4, + ), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 20, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 8), + Text( + 'Created: ${_formatDate(timeOfCreation)}', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + )); + } +} diff --git a/lib/widgets/organisation_details_page/tabs/members_tab.dart b/lib/widgets/organisation_details_page/tabs/members_tab.dart new file mode 100644 index 0000000..6c48e64 --- /dev/null +++ b/lib/widgets/organisation_details_page/tabs/members_tab.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; + +class MembersTab extends StatelessWidget { + final List organisationOwners; + final List organisationMembers; + final bool isOwner; + final VoidCallback onAddMember; + final Function(String) onRemoveMember; + + const MembersTab({ + super.key, + required this.organisationOwners, + required this.organisationMembers, + required this.isOwner, + required this.onAddMember, + required this.onRemoveMember, + }); + + String _truncateAddress(String address) { + if (address.length <= 10) return address; + return '${address.substring(0, 6)}...${address.substring(address.length - 4)}'; + } + + void _copyAddress(BuildContext context, String address) { + Clipboard.setData(ClipboardData(text: address)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Address copied to clipboard'), + backgroundColor: getThemeColors(context)['primary'], + duration: Duration(seconds: 2), + ), + ); + } + + void _showRemoveMemberConfirmation(BuildContext context, String address) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: getThemeColors(context)['background'], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + side: BorderSide( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + ), + title: Text( + 'Remove Member', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + ), + ), + content: Text( + 'Are you sure you want to remove this member?\n\n${_truncateAddress(address)}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Cancel', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + ), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + onRemoveMember(address); + }, + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['error'], + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + ), + child: const Text('Remove'), + ), + ], + ), + ); + } + + Widget _buildMemberTile( + BuildContext context, String address, bool isMemberOwner) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isMemberOwner + ? getThemeColors(context)['primary'] + : getThemeColors(context)['secondary'], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: Row( + children: [ + Icon( + isMemberOwner ? Icons.admin_panel_settings : Icons.person, + size: 16, + color: isMemberOwner + ? getThemeColors(context)['primary'] + : getThemeColors(context)['textPrimary'], + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _truncateAddress(address), + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + fontFamily: 'monospace', + ), + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () => _copyAddress(context, address), + icon: Icon( + Icons.copy, + size: 16, + color: getThemeColors(context)['textPrimary'], + ), + tooltip: 'Copy address', + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), + if (isOwner && !isMemberOwner) ...[ + const SizedBox(width: 4), + IconButton( + onPressed: () => _showRemoveMemberConfirmation(context, address), + icon: Icon( + Icons.remove_circle, + size: 16, + color: getThemeColors(context)['error'], + ), + tooltip: 'Remove member', + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), + ], + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + boxShadow: [ + BoxShadow( + color: Colors.black, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (organisationOwners.isNotEmpty) ...[ + Row( + children: [ + Icon( + Icons.admin_panel_settings, + size: 20, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 8), + Text( + 'Owners (${organisationOwners.length})', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + const SizedBox(height: 12), + ...organisationOwners + .map((owner) => _buildMemberTile(context, owner, true)), + const SizedBox(height: 20), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.group, + size: 20, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 8), + Text( + 'Members (${organisationMembers.length})', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + if (isOwner) + ElevatedButton.icon( + onPressed: onAddMember, + icon: const Icon(Icons.add, size: 16), + label: const Text('Add Member'), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 8), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + side: const BorderSide(color: Colors.black, width: 1), + ), + ), + ], + ), + const SizedBox(height: 12), + if (organisationMembers.isNotEmpty) + ...organisationMembers + .map((member) => _buildMemberTile(context, member, false)) + else + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['secondaryBackground'], + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: Text( + 'No members yet', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 14, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/organisation_details_page/tabs/planting_proposals_tab.dart b/lib/widgets/organisation_details_page/tabs/planting_proposals_tab.dart new file mode 100644 index 0000000..ad384ff --- /dev/null +++ b/lib/widgets/organisation_details_page/tabs/planting_proposals_tab.dart @@ -0,0 +1,450 @@ +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/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart'; + +class PlantingProposalsTab extends StatefulWidget { + final String organisationAddress; + + const PlantingProposalsTab({ + super.key, + required this.organisationAddress, + }); + + @override + State createState() => _PlantingProposalsTabState(); +} + +class _PlantingProposalsTabState extends State + with SingleTickerProviderStateMixin { + late TabController _statusTabController; + final Map>> _proposalsByStatus = { + 0: [], // Pending + 1: [], // Approved + 2: [], // Rejected + }; + final Map _isLoading = {0: false, 1: false, 2: false}; + final Map _errorMessages = {0: '', 1: '', 2: ''}; + + @override + void initState() { + super.initState(); + _statusTabController = TabController(length: 3, vsync: this); + _loadAllProposals(); + } + + @override + void dispose() { + _statusTabController.dispose(); + super.dispose(); + } + + Future _loadAllProposals() async { + for (int status = 0; status <= 2; status++) { + await _loadProposalsByStatus(status); + } + } + + Future _loadProposalsByStatus(int status) async { + setState(() { + _isLoading[status] = true; + _errorMessages[status] = ''; + }); + + try { + final walletProvider = + Provider.of(context, listen: false); + final result = await OrganisationContractReadFunctions + .getTreePlantingProposalsByStatus( + walletProvider: walletProvider, + organisationContractAddress: widget.organisationAddress, + status: status, + offset: 0, + limit: 50, + ); + + if (result.success && result.data != null) { + setState(() { + _proposalsByStatus[status] = + List>.from(result.data['proposals'] ?? []); + }); + } else { + setState(() { + _errorMessages[status] = + result.errorMessage ?? 'Failed to load tree planting proposals'; + }); + } + } catch (e) { + setState(() { + _errorMessages[status] = 'Error loading tree planting proposals: $e'; + }); + logger.e('Error loading tree planting proposals for status $status: $e'); + } finally { + setState(() { + _isLoading[status] = false; + }); + } + } + + String _getStatusText(int status) { + switch (status) { + case 0: + return 'Pending'; + case 1: + return 'Approved'; + case 2: + return 'Rejected'; + default: + return 'Unknown'; + } + } + + Color _getStatusColor(int status) { + switch (status) { + case 0: + return Colors.orange; + case 1: + return Colors.green; + case 2: + return Colors.red; + default: + return Colors.grey; + } + } + + String _truncateAddress(String address) { + if (address.length <= 10) return address; + return '${address.substring(0, 6)}...${address.substring(address.length - 4)}'; + } + + double _convertCoordinate(int coordinate) { + // Convert from fixed-point representation to decimal degrees + return coordinate / 1000000.0; + } + + Widget _buildProposalCard(Map proposal) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: getThemeColors(context)['shadow']!.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getStatusColor(proposal['status']), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'ID: ${proposal['id']}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${proposal['numberOfTrees']} trees', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + proposal['species'] ?? 'Unknown Species', + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.location_on, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Text( + 'Lat: ${_convertCoordinate(proposal['latitude']).toStringAsFixed(6)}, ' + 'Lng: ${_convertCoordinate(proposal['longitude']).toStringAsFixed(6)}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.person, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Text( + 'Initiator: ${_truncateAddress(proposal['initiator'])}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + if (proposal['geoHash'] != null && + proposal['geoHash'].isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.map, + size: 16, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 4), + Text( + 'GeoHash: ${proposal['geoHash']}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + fontFamily: 'monospace', + ), + ), + ], + ), + ], + if (proposal['photos'] != null && + (proposal['photos'] as List).isNotEmpty) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.photo_library, + size: 16, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 4), + Text( + '${(proposal['photos'] as List).length} photos', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + ], + ], + ), + ); + } + + Widget _buildStatusTabContent(int status) { + if (_isLoading[status]!) { + return Container( + padding: const EdgeInsets.all(40), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + getThemeColors(context)['primary']!, + ), + ), + const SizedBox(height: 16), + Text( + 'Loading ${_getStatusText(status).toLowerCase()} proposals...', + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['primary'], + ), + ), + ], + ), + ), + ); + } + + if (_errorMessages[status]!.isNotEmpty) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: getThemeColors(context)['error'], + ), + const SizedBox(height: 16), + Text( + 'Error loading proposals', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['error'], + ), + ), + const SizedBox(height: 8), + Text( + _errorMessages[status]!, + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['error'], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _loadProposalsByStatus(status), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + ), + child: const Text('Retry'), + ), + ], + ), + ); + } + + final proposals = _proposalsByStatus[status]!; + if (proposals.isEmpty) { + return Container( + padding: const EdgeInsets.all(40), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.nature_outlined, + size: 48, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(height: 16), + Text( + 'No ${_getStatusText(status).toLowerCase()} proposals', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 8), + Text( + 'No tree planting proposals found with ${_getStatusText(status).toLowerCase()} status.', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: proposals.length, + itemBuilder: (context, index) { + return _buildProposalCard(proposals[index]); + }, + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: TabBar( + controller: _statusTabController, + labelColor: Colors.white, + unselectedLabelColor: getThemeColors(context)['textPrimary'], + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: BorderRadius.circular(12), + ), + dividerColor: Colors.transparent, + tabs: [ + Tab( + icon: Icon(Icons.pending, size: 16), + text: 'Pending (${_proposalsByStatus[0]?.length ?? 0})', + ), + Tab( + icon: Icon(Icons.check_circle, size: 16), + text: 'Approved (${_proposalsByStatus[1]?.length ?? 0})', + ), + Tab( + icon: Icon(Icons.cancel, size: 16), + text: 'Rejected (${_proposalsByStatus[2]?.length ?? 0})', + ), + ], + ), + ), + SizedBox( + height: 500, // Fixed height for the TabBarView + child: TabBarView( + controller: _statusTabController, + children: [ + _buildStatusTabContent(0), // Pending + _buildStatusTabContent(1), // Approved + _buildStatusTabContent(2), // Rejected + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/organisation_details_page/tabs/tabs.dart b/lib/widgets/organisation_details_page/tabs/tabs.dart new file mode 100644 index 0000000..9c856fd --- /dev/null +++ b/lib/widgets/organisation_details_page/tabs/tabs.dart @@ -0,0 +1,4 @@ +export 'info_tab.dart'; +export 'members_tab.dart'; +export 'verification_requests_tab.dart'; +export 'planting_proposals_tab.dart'; diff --git a/lib/widgets/organisation_details_page/tabs/verification_requests_tab.dart b/lib/widgets/organisation_details_page/tabs/verification_requests_tab.dart new file mode 100644 index 0000000..80db2e7 --- /dev/null +++ b/lib/widgets/organisation_details_page/tabs/verification_requests_tab.dart @@ -0,0 +1,420 @@ +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/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/organisation_contract/organisation_read_functions.dart'; + +class VerificationRequestsTab extends StatefulWidget { + final String organisationAddress; + + const VerificationRequestsTab({ + super.key, + required this.organisationAddress, + }); + + @override + State createState() => + _VerificationRequestsTabState(); +} + +class _VerificationRequestsTabState extends State + with SingleTickerProviderStateMixin { + late TabController _statusTabController; + final Map>> _requestsByStatus = { + 0: [], // Pending + 1: [], // Approved + 2: [], // Rejected + }; + final Map _isLoading = {0: false, 1: false, 2: false}; + final Map _errorMessages = {0: '', 1: '', 2: ''}; + + @override + void initState() { + super.initState(); + _statusTabController = TabController(length: 3, vsync: this); + _loadAllRequests(); + } + + @override + void dispose() { + _statusTabController.dispose(); + super.dispose(); + } + + Future _loadAllRequests() async { + for (int status = 0; status <= 2; status++) { + await _loadRequestsByStatus(status); + } + } + + Future _loadRequestsByStatus(int status) async { + setState(() { + _isLoading[status] = true; + _errorMessages[status] = ''; + }); + + try { + final walletProvider = + Provider.of(context, listen: false); + final result = await OrganisationContractReadFunctions + .getVerificationRequestsByStatus( + walletProvider: walletProvider, + organisationContractAddress: widget.organisationAddress, + status: status, + offset: 0, + limit: 50, + ); + + if (result.success && result.data != null) { + setState(() { + _requestsByStatus[status] = + List>.from(result.data['requests'] ?? []); + }); + } else { + setState(() { + _errorMessages[status] = + result.errorMessage ?? 'Failed to load verification requests'; + }); + } + } catch (e) { + setState(() { + _errorMessages[status] = 'Error loading verification requests: $e'; + }); + logger.e('Error loading verification requests for status $status: $e'); + } finally { + setState(() { + _isLoading[status] = false; + }); + } + } + + String _getStatusText(int status) { + switch (status) { + case 0: + return 'Pending'; + case 1: + return 'Approved'; + case 2: + return 'Rejected'; + default: + return 'Unknown'; + } + } + + Color _getStatusColor(int status) { + switch (status) { + case 0: + return Colors.orange; + case 1: + return Colors.green; + case 2: + return Colors.red; + default: + return Colors.grey; + } + } + + String _truncateAddress(String address) { + if (address.length <= 10) return address; + return '${address.substring(0, 6)}...${address.substring(address.length - 4)}'; + } + + String _formatDate(int timestamp) { + final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + return "${date.day}/${date.month}/${date.year}"; + } + + Widget _buildRequestCard(Map request) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: getThemeColors(context)['shadow']!.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getStatusColor(request['status']), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'ID: ${request['id']}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const Spacer(), + Icon( + Icons.verified_user, + size: 16, + color: getThemeColors(context)['primary'], + ), + ], + ), + const SizedBox(height: 12), + Text( + request['description'] ?? 'No description', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.person, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Text( + 'Member: ${_truncateAddress(request['initialMember'])}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(width: 4), + Text( + 'Date: ${_formatDate(request['timestamp'])}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + if (request['treeNftId'] != null && request['treeNftId'] > 0) ...[ + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.nature, + size: 16, + color: getThemeColors(context)['primary'], + ), + const SizedBox(width: 4), + Text( + 'Tree NFT ID: ${request['treeNftId']}', + style: TextStyle( + fontSize: 12, + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ], + ), + ); + } + + Widget _buildStatusTabContent(int status) { + if (_isLoading[status]!) { + return Container( + padding: const EdgeInsets.all(40), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + getThemeColors(context)['primary']!, + ), + ), + const SizedBox(height: 16), + Text( + 'Loading ${_getStatusText(status).toLowerCase()} requests...', + style: TextStyle( + fontSize: 16, + color: getThemeColors(context)['primary'], + ), + ), + ], + ), + ), + ); + } + + if (_errorMessages[status]!.isNotEmpty) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: getThemeColors(context)['error'], + ), + const SizedBox(height: 16), + Text( + 'Error loading requests', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['error'], + ), + ), + const SizedBox(height: 8), + Text( + _errorMessages[status]!, + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['error'], + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _loadRequestsByStatus(status), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: Colors.white, + ), + child: const Text('Retry'), + ), + ], + ), + ); + } + + final requests = _requestsByStatus[status]!; + if (requests.isEmpty) { + return Container( + padding: const EdgeInsets.all(40), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.verified_user_outlined, + size: 48, + color: getThemeColors(context)['secondary'], + ), + const SizedBox(height: 16), + Text( + 'No ${_getStatusText(status).toLowerCase()} requests', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + const SizedBox(height: 8), + Text( + 'No verification requests found with ${_getStatusText(status).toLowerCase()} status.', + style: TextStyle( + fontSize: 14, + color: getThemeColors(context)['textPrimary'], + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: requests.length, + itemBuilder: (context, index) { + return _buildRequestCard(requests[index]); + }, + ); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 0), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 1, + ), + ), + child: TabBar( + controller: _statusTabController, + labelColor: Colors.white, + unselectedLabelColor: getThemeColors(context)['textPrimary'], + indicatorSize: TabBarIndicatorSize.tab, + indicator: BoxDecoration( + color: getThemeColors(context)['primary'], + borderRadius: BorderRadius.circular(12), + ), + dividerColor: Colors.transparent, + tabs: [ + Tab( + icon: Icon(Icons.pending, size: 16), + text: 'Pending (${_requestsByStatus[0]?.length ?? 0})', + ), + Tab( + icon: Icon(Icons.check_circle, size: 16), + text: 'Approved (${_requestsByStatus[1]?.length ?? 0})', + ), + Tab( + icon: Icon(Icons.cancel, size: 16), + text: 'Rejected (${_requestsByStatus[2]?.length ?? 0})', + ), + ], + ), + ), + SizedBox( + height: 500, + child: TabBarView( + controller: _statusTabController, + children: [ + _buildStatusTabContent(0), // Pending + _buildStatusTabContent(1), // Approved + _buildStatusTabContent(2), // Rejected + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/profile_widgets/profile_section_widget.dart b/lib/widgets/profile_widgets/profile_section_widget.dart index ceac22d..715b777 100644 --- a/lib/widgets/profile_widgets/profile_section_widget.dart +++ b/lib/widgets/profile_widgets/profile_section_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:tree_planting_protocol/providers/wallet_provider.dart'; @@ -7,75 +8,206 @@ import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; import 'package:tree_planting_protocol/utils/logger.dart'; import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart'; +class VerificationDetails { + final String verifier; + final int timestamp; + final List proofHashes; + final String description; + final bool isHidden; + final int numberOfTrees; + final String verifierPlanterTokenAddress; + + VerificationDetails({ + required this.verifier, + required this.timestamp, + required this.proofHashes, + required this.description, + required this.isHidden, + required this.numberOfTrees, + required this.verifierPlanterTokenAddress, + }); + + factory VerificationDetails.fromContractData(dynamic data) { + logger.d("VerificationDetails.fromContractData - Raw data: $data"); + logger.d( + "VerificationDetails.fromContractData - Data type: ${data.runtimeType}"); + logger.d( + "VerificationDetails.fromContractData - Data length: ${data is List ? data.length : 'N/A'}"); + + try { + if (data is List && data.length >= 7) { + logger.d("VerificationDetails - Parsing fields:"); + logger.d(" verifier: ${data[0]} (${data[0].runtimeType})"); + logger.d(" timestamp: ${data[1]} (${data[1].runtimeType})"); + logger.d(" proofHashes: ${data[2]} (${data[2].runtimeType})"); + logger.d(" description: ${data[3]} (${data[3].runtimeType})"); + logger.d(" isHidden: ${data[4]} (${data[4].runtimeType})"); + logger.d(" numberOfTrees: ${data[5]} (${data[5].runtimeType})"); + logger.d( + " verifierPlanterTokenAddress: ${data[6]} (${data[6].runtimeType})"); + } + + final verificationDetails = VerificationDetails( + verifier: data[0]?.toString() ?? '', + timestamp: _toIntStatic(data[1]), + proofHashes: data[2] is List + ? (data[2] as List).map((e) => e?.toString() ?? '').toList() + : [], + description: data[3]?.toString() ?? '', + isHidden: + data[4] == true || data[4]?.toString().toLowerCase() == 'true', + numberOfTrees: _toIntStatic(data[5]), + verifierPlanterTokenAddress: data[6]?.toString() ?? '', + ); + + logger.d("VerificationDetails parsed successfully:"); + logger.d(" verifier: ${verificationDetails.verifier}"); + logger.d(" timestamp: ${verificationDetails.timestamp}"); + logger.d(" proofHashes: ${verificationDetails.proofHashes}"); + logger.d(" description: ${verificationDetails.description}"); + logger.d(" isHidden: ${verificationDetails.isHidden}"); + logger.d(" numberOfTrees: ${verificationDetails.numberOfTrees}"); + logger.d( + " verifierPlanterTokenAddress: ${verificationDetails.verifierPlanterTokenAddress}"); + + return verificationDetails; + } catch (e) { + logger.e("Error parsing VerificationDetails: $e"); + logger.e("Data received: $data"); + logger.e("Data type: ${data.runtimeType}"); + return VerificationDetails( + verifier: '', + timestamp: 0, + proofHashes: [], + description: '', + isHidden: false, + numberOfTrees: 0, + verifierPlanterTokenAddress: '', + ); + } + } + + static int _toIntStatic(dynamic value) { + try { + logger.d("_toIntStatic: Converting value: $value (${value.runtimeType})"); + if (value == null) { + logger.d("_toIntStatic: Value is null, returning 0"); + return 0; + } + if (value is BigInt) { + final result = value.toInt(); + logger.d("_toIntStatic: Converted BigInt $value to int $result"); + return result; + } + if (value is int) { + logger.d("_toIntStatic: Value is already int: $value"); + return value; + } + final parsed = int.tryParse(value.toString()); + logger.d( + "_toIntStatic: Parsed ${value.runtimeType} '$value' to int: $parsed"); + return parsed ?? 0; + } catch (e) { + logger.e( + "_toIntStatic: Error converting $value (${value.runtimeType}) to int: $e"); + return 0; + } + } +} + class UserProfileData { String name; final String userAddress; - final String profilePhotoIpfs; + final String profilePhoto; final int dateJoined; final int verificationsRevoked; final int reportedSpam; - final int verifierTokens; final int careTokens; - final int planterTokens; final int legacyTokens; UserProfileData( {required this.name, required this.userAddress, - required this.profilePhotoIpfs, + required this.profilePhoto, required this.dateJoined, required this.verificationsRevoked, required this.reportedSpam, - required this.verifierTokens, required this.careTokens, - required this.planterTokens, required this.legacyTokens}); factory UserProfileData.fromContractData(dynamic data) { - logger.d(data); + logger.d("Raw contract data: $data"); + logger.d("Data type: ${data.runtimeType}"); + logger.d("Data length: ${data is List ? data.length : 'N/A'}"); + try { dynamic actualData = data; - return UserProfileData( - name: actualData[2].toString(), - userAddress: actualData[0].toString(), - profilePhotoIpfs: actualData[1].toString(), + + if (actualData is! List || actualData.length < 8) { + logger.e( + "Invalid data: Expected List with at least 8 elements, got ${actualData.runtimeType} with length ${actualData is List ? actualData.length : 'N/A'}"); + throw Exception("Invalid contract data structure"); + } + final userProfile = UserProfileData( + userAddress: actualData[0]?.toString() ?? '', + profilePhoto: actualData[1]?.toString() ?? '', + name: actualData[2]?.toString() ?? '', dateJoined: _toInt(actualData[3]), verificationsRevoked: _toInt(actualData[4]), reportedSpam: _toInt(actualData[5]), - verifierTokens: _toInt(actualData[6]), - careTokens: _toInt(actualData[9]), - planterTokens: _toInt(actualData[7]), - legacyTokens: _toInt(actualData[8]), + legacyTokens: _toInt(actualData[6]), + careTokens: _toInt(actualData[7]), ); + return userProfile; } catch (e) { - logger.d("Error parsing Tree data: $e"); - logger.d("Data received: $data"); - logger.d("Data type: ${data.runtimeType}"); - return UserProfileData( name: '', userAddress: '', - profilePhotoIpfs: '', + profilePhoto: '', dateJoined: 0, verificationsRevoked: 0, reportedSpam: 0, - verifierTokens: 0, careTokens: 0, - planterTokens: 0, legacyTokens: 0, ); } } static int _toInt(dynamic value) { - if (value is BigInt) return value.toInt(); - if (value is int) return value; - return int.tryParse(value.toString()) ?? 0; + try { + if (value == null) { + logger.d("_toInt: value is null, returning 0"); + return 0; + } + if (value is BigInt) { + logger.d("_toInt: Converting BigInt $value to int"); + return value.toInt(); + } + if (value is int) { + logger.d("_toInt: Value is already int: $value"); + return value; + } + if (value is String) { + final parsed = int.tryParse(value); + logger.d("_toInt: Parsing string '$value' to int: $parsed"); + return parsed ?? 0; + } + final parsed = int.tryParse(value.toString()); + logger.d( + "_toInt: Converting ${value.runtimeType} '$value' to int: $parsed"); + return parsed ?? 0; + } catch (e) { + logger.e( + "_toInt: Error converting $value (${value.runtimeType}) to int: $e"); + return 0; + } } } class ProfileSectionWidget extends StatefulWidget { - const ProfileSectionWidget({super.key}); + final String userAddress; + + const ProfileSectionWidget({super.key, required this.userAddress}); @override State createState() => _ProfileSectionWidgetState(); @@ -85,7 +217,18 @@ class _ProfileSectionWidgetState extends State { bool _isLoading = false; String? _errorMessage = ""; UserProfileData? _userProfileData; + List _verifierTokens = []; bool _isNotRegistered = false; + int _displayedTokensCount = 5; + String? _expandedTokenAddress; + + // For tree icon variety + String _getTreeIcon(int index) { + // Use modulo to cycle through tree-1.png to tree-13.png + // Add offset based on previous index to avoid adjacent duplicates + final treeNumber = ((index * 3) % 13) + 1; // Multiply by 3 for more variety + return 'assets/tree-navbar-images/tree-$treeNumber.png'; + } @override void initState() { @@ -108,15 +251,66 @@ class _ProfileSectionWidgetState extends State { final walletProvider = Provider.of(context, listen: false); + logger.d("Calling ContractReadFunctions.getProfileDetails..."); + final String currentAddress = widget.userAddress; final result = await ContractReadFunctions.getProfileDetails( walletProvider: walletProvider, + currentAddress: currentAddress, ); + logger.d("Contract call completed:"); + logger.d(" - Success: ${result.success}"); + logger.d(" - Data not null: ${result.data != null}"); + logger.d(" - Full result data: ${result.data}"); + + if (result.data != null) { + logger.d("Result data keys: ${result.data.keys}"); + logger.d("Profile exists: ${result.data.containsKey('profile')}"); + logger.d( + "VerifierTokens exists: ${result.data.containsKey('verifierTokens')}"); + } + if (result.success && result.data != null) { final List data = result.data['profile'] ?? []; + final List verifierTokensData = result.data['verifierTokens'] ?? []; + logger.d("Profile data from contract result: $data"); + logger.d( + "Verifier tokens data from contract result: $verifierTokensData"); + logger + .d("Verifier tokens data type: ${verifierTokensData.runtimeType}"); + logger.d("Verifier tokens data isEmpty: ${verifierTokensData.isEmpty}"); setState(() { _userProfileData = UserProfileData.fromContractData(data); + + logger.d("Processing verifier tokens data..."); + logger.d( + "Raw verifier tokens data type: ${verifierTokensData.runtimeType}"); + logger.d( + "Raw verifier tokens data length: ${verifierTokensData.length}"); + + for (int i = 0; i < verifierTokensData.length; i++) { + logger.d("Processing verifier token $i: ${verifierTokensData[i]}"); + } + + _verifierTokens = verifierTokensData.map((tokenData) { + logger.d("Mapping token data: $tokenData"); + return VerificationDetails.fromContractData(tokenData); + }).toList(); + + logger.d( + "State updated with UserProfileData: ${_userProfileData?.name}, Care: ${_userProfileData?.careTokens}, Legacy: ${_userProfileData?.legacyTokens}"); + logger.d("Verifier tokens loaded: ${_verifierTokens.length} tokens"); + + for (int i = 0; i < _verifierTokens.length; i++) { + final token = _verifierTokens[i]; + logger.d("Verifier token $i:"); + logger.d(" - verifier: ${token.verifier}"); + logger.d(" - numberOfTrees: ${token.numberOfTrees}"); + logger.d(" - description: ${token.description}"); + logger.d(" - timestamp: ${token.timestamp}"); + logger.d(" - isHidden: ${token.isHidden}"); + } }); } else { final errorMsg = result.errorMessage?.toLowerCase() ?? ''; @@ -132,6 +326,7 @@ class _ProfileSectionWidgetState extends State { setState(() { _errorMessage = result.errorMessage ?? 'Failed to load profile data'; + _userProfileData = null; }); } } @@ -147,7 +342,8 @@ class _ProfileSectionWidgetState extends State { }); } else { setState(() { - _errorMessage = 'Errorloading User profile details: $e'; + _errorMessage = 'Error loading User profile details: $e'; + _userProfileData = null; }); } } finally { @@ -171,32 +367,75 @@ class _ProfileSectionWidgetState extends State { border: Border.all(color: Colors.black, width: 4), ), child: ClipOval( - child: _userProfileData!.profilePhotoIpfs.isNotEmpty - ? Image.network( - _userProfileData!.profilePhotoIpfs, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.person, - size: 40, - color: Colors.black, - ); - }, - loadingBuilder: (context, child, loadingProgress) { - if (loadingProgress == null) return child; - return const CircularProgressIndicator( - color: Colors.black, + child: Builder( + builder: (context) { + logger.d( + "UI: Profile photo URL: '${_userProfileData!.profilePhoto}'"); + logger.d( + "UI: Profile photo isEmpty: ${_userProfileData!.profilePhoto.isEmpty}"); + + return _userProfileData!.profilePhoto.isNotEmpty + ? Image.network( + _userProfileData!.profilePhoto, + fit: BoxFit.cover, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + errorBuilder: (context, error, stackTrace) { + logger.e( + "Profile photo loading error for URL: ${_userProfileData!.profilePhoto}"); + logger.e("Error: $error"); + logger.e("Stack trace: $stackTrace"); + + // Try alternative IPFS gateway if original fails + String originalUrl = + _userProfileData!.profilePhoto; + if (originalUrl.contains('pinata.cloud')) { + String ipfsHash = + originalUrl.split('/ipfs/').last; + String alternativeUrl = + 'https://ipfs.io/ipfs/$ipfsHash'; + logger.d( + "Trying alternative IPFS gateway: $alternativeUrl"); + + return Image.network( + alternativeUrl, + fit: BoxFit.cover, + errorBuilder: (context, error2, stackTrace2) { + logger.e( + "Alternative IPFS gateway also failed: $error2"); + return const Icon( + Icons.person, + size: 40, + color: Colors.black, + ); + }, + ); + } + + return const Icon( + Icons.person, + size: 40, + color: Colors.black, + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const CircularProgressIndicator( + color: Colors.black, + ); + }, + ) + : Container( + color: Colors.green.shade300, + child: const Icon( + Icons.person, + size: 40, + color: Colors.white, + ), ); - }, - ) - : Container( - color: Colors.green.shade300, - child: const Icon( - Icons.person, - size: 40, - color: Colors.white, - ), - ), + }, + ), ), ), const SizedBox(width: 16), @@ -249,7 +488,7 @@ class _ProfileSectionWidgetState extends State { padding: const EdgeInsets.all(4.0), child: SizedBox( height: 40, - width: 150, + width: 165, child: Material( elevation: 4, borderRadius: BorderRadius.circular(buttonCircularRadius), @@ -263,12 +502,15 @@ class _ProfileSectionWidgetState extends State { borderRadius: BorderRadius.circular(buttonCircularRadius), ), child: Center( - child: Text( - 'Planter Tokens : ${_userProfileData!.planterTokens}', - style: TextStyle( - color: getThemeColors(context)['textPrimary'], - fontWeight: FontWeight.bold, - )), + child: Builder(builder: (context) { + return Text( + 'Reported Spam : ${_userProfileData!.reportedSpam}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 12, + )); + }), ), ), ), @@ -277,7 +519,7 @@ class _ProfileSectionWidgetState extends State { padding: const EdgeInsets.all(4.0), child: SizedBox( height: 40, - width: 150, + width: 165, child: Material( elevation: 4, borderRadius: BorderRadius.circular(buttonCircularRadius), @@ -291,11 +533,15 @@ class _ProfileSectionWidgetState extends State { borderRadius: BorderRadius.circular(buttonCircularRadius), ), child: Center( - child: Text('Care Tokens : ${_userProfileData!.careTokens}', - style: TextStyle( - color: getThemeColors(context)['textPrimary'], - fontWeight: FontWeight.bold, - )), + child: Builder(builder: (context) { + return Text( + 'Care Tokens : ${_userProfileData!.careTokens}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 12, + )); + }), ), ), ), @@ -304,7 +550,7 @@ class _ProfileSectionWidgetState extends State { padding: const EdgeInsets.all(4.0), child: SizedBox( height: 40, - width: 150, + width: 165, child: Material( elevation: 4, borderRadius: BorderRadius.circular(buttonCircularRadius), @@ -318,12 +564,15 @@ class _ProfileSectionWidgetState extends State { borderRadius: BorderRadius.circular(buttonCircularRadius), ), child: Center( - child: Text( - 'Verifier Tokens : ${_userProfileData!.verifierTokens}', - style: TextStyle( - color: getThemeColors(context)['textPrimary'], - fontWeight: FontWeight.bold, - )), + child: Builder(builder: (context) { + return Text( + 'Verifications Revoked : ${_userProfileData!.verificationsRevoked}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 12, + )); + }), ), ), ), @@ -332,7 +581,7 @@ class _ProfileSectionWidgetState extends State { padding: const EdgeInsets.all(4.0), child: SizedBox( height: 40, - width: 150, + width: 165, child: Material( elevation: 4, borderRadius: BorderRadius.circular(buttonCircularRadius), @@ -346,12 +595,17 @@ class _ProfileSectionWidgetState extends State { borderRadius: BorderRadius.circular(buttonCircularRadius), ), child: Center( - child: Text( - 'Legacy Tokens : ${_userProfileData!.legacyTokens}', - style: TextStyle( - color: getThemeColors(context)['textPrimary'], - fontWeight: FontWeight.bold, - )), + child: Builder(builder: (context) { + logger.d( + "UI: Displaying Legacy Tokens: ${_userProfileData!.legacyTokens}"); + return Text( + 'Legacy Tokens : ${_userProfileData!.legacyTokens}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 12, + )); + }), ), ), ), @@ -360,6 +614,328 @@ class _ProfileSectionWidgetState extends State { ); } + Widget _verifierTokensWidget() { + logger.d( + "Building verifier tokens widget, tokens count: ${_verifierTokens.length}"); + + final tokensToDisplay = + _verifierTokens.take(_displayedTokensCount).toList(); + final hasMoreTokens = _verifierTokens.length > _displayedTokensCount; + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Text( + 'Verifier Tokens', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const SizedBox(height: 12), + + // Tokens display + if (_verifierTokens.isEmpty) + Center( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Text( + 'No verifier tokens found', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 14, + ), + ), + ), + ) + else + // Bubble-style grid layout - wraps automatically + Wrap( + spacing: 10, + runSpacing: 10, + alignment: WrapAlignment.start, + children: List.generate( + tokensToDisplay.length, + (index) => _buildTokenBubble(tokensToDisplay[index], index), + ), + ), + + // Load More button + if (hasMoreTokens) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: SizedBox( + height: 40, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: InkWell( + onTap: () { + setState(() { + _displayedTokensCount += 5; + }); + }, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 8), + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: + BorderRadius.circular(buttonCircularRadius), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Load More', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.expand_more, + size: 16, + color: getThemeColors(context)['textPrimary'], + ), + ], + ), + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildTokenBubble(VerificationDetails token, int index) { + final double tokenAmount = token.numberOfTrees / 1e18; + + // Format with appropriate decimal places based on size + String formattedAmount; + if (tokenAmount >= 1000000) { + formattedAmount = tokenAmount.toStringAsFixed(0); + } else if (tokenAmount >= 1000) { + formattedAmount = tokenAmount.toStringAsFixed(1); + } else if (tokenAmount >= 1) { + formattedAmount = tokenAmount.toStringAsFixed(2); + } else { + formattedAmount = tokenAmount.toStringAsFixed(4); + } + + final bool isExpanded = + _expandedTokenAddress == token.verifierPlanterTokenAddress; + + // Alternating colors for bubble effect + final color = index % 2 == 0 + ? getThemeColors(context)['primary'] + : getThemeColors(context)['secondary']; + + return GestureDetector( + onTap: () { + setState(() { + if (isExpanded) { + _expandedTokenAddress = null; + } else { + _expandedTokenAddress = token.verifierPlanterTokenAddress; + } + }); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + constraints: BoxConstraints( + minWidth: isExpanded ? 280 : 80, + maxWidth: isExpanded ? 300 : 80, + minHeight: isExpanded ? 120 : 80, + maxHeight: isExpanded ? 140 : 80, + ), + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(isExpanded ? 16 : 40), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color, + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: BorderRadius.circular(isExpanded ? 16 : 40), + ), + child: ClipRect( + child: isExpanded + ? _buildExpandedTokenContent(token, formattedAmount) + : _buildCollapsedTokenContent(formattedAmount, index), + ), + ), + ), + ), + ); + } + + Widget _buildCollapsedTokenContent(String amount, int index) { + // Format large numbers (e.g., 1000000 -> 1M, 1500 -> 1.5K) + String formatAmount(String amt) { + try { + double value = double.parse(amt); + if (value >= 1000000) { + return '${(value / 1000000).toStringAsFixed(1)}M'; + } else if (value >= 1000) { + return '${(value / 1000).toStringAsFixed(1)}K'; + } + return amt; + } catch (e) { + return amt; + } + } + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _getTreeIcon(index), + width: 32, + height: 32, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + // Fallback to coin emoji if image fails to load + return const Text( + '🪙', + style: TextStyle(fontSize: 24), + ); + }, + ), + const SizedBox(height: 4), + Text( + formatAmount(amount), + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 10, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ); + } + + Widget _buildExpandedTokenContent(VerificationDetails token, String amount) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Token Amount', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 10, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + amount, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontWeight: FontWeight.bold, + fontSize: 16, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + Clipboard.setData( + ClipboardData(text: token.verifierPlanterTokenAddress)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Address copied!'), + duration: const Duration(seconds: 1), + backgroundColor: getThemeColors(context)['primary'], + ), + ); + }, + icon: Icon( + Icons.copy, + size: 16, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Contract Address', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 9, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 2), + Text( + '${token.verifierPlanterTokenAddress.substring(0, 10)}...${token.verifierPlanterTokenAddress.substring(token.verifierPlanterTokenAddress.length - 8)}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 10, + fontFamily: 'monospace', + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + if (token.description.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + token.description.length > 40 + ? '${token.description.substring(0, 40)}...' + : token.description, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 9, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ); + } + // ignore: unused_element Widget _buildErrorState() { return Container( @@ -411,26 +987,31 @@ class _ProfileSectionWidgetState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - width: 150, - height: 20, - decoration: BoxDecoration( - color: Colors.green.shade50, - shape: BoxShape.circle, - ), - child: Icon( - Icons.person_add, - size: 40, - color: Colors.green.shade400, + Material( + elevation: 4, + shape: const CircleBorder(), + child: Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: getThemeColors(context)['primary'], + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 3), + ), + child: Icon( + Icons.person_add, + size: 60, + color: getThemeColors(context)['textPrimary'], + ), ), ), - const SizedBox(height: 34), + const SizedBox(height: 24), Text( 'Welcome to Tree Planting Protocol!', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Colors.green.shade700, + color: getThemeColors(context)['textPrimary'], ), textAlign: TextAlign.center, ), @@ -439,44 +1020,90 @@ class _ProfileSectionWidgetState extends State { 'You haven\'t registered yet. Create your profile to start your tree planting journey!', style: TextStyle( fontSize: 16, - color: Colors.grey.shade600, + color: getThemeColors(context)['textPrimary'], height: 1.4, ), textAlign: TextAlign.center, ), - const SizedBox(height: 20), + const SizedBox(height: 32), SizedBox( width: double.infinity, height: 50, - child: ElevatedButton.icon( - onPressed: () { - context.push('/register-user'); - }, - icon: const Icon(Icons.app_registration, size: 20), - label: const Text( - 'Register Now', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all(color: Colors.black, width: 2), ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade600, - foregroundColor: Colors.white, - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(25), + child: ElevatedButton.icon( + onPressed: () { + context.push('/register-user'); + }, + icon: Icon( + Icons.app_registration, + size: 20, + color: getThemeColors(context)['textPrimary'], + ), + label: Text( + 'Register Now', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: getThemeColors(context)['textPrimary'], + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + foregroundColor: getThemeColors(context)['textPrimary'], + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius - 2), + ), + ), ), ), ), ), const SizedBox(height: 16), - TextButton.icon( - onPressed: () => _loadUserProfileData(), - icon: const Icon(Icons.refresh, size: 18), - label: const Text('Check Again'), - style: TextButton.styleFrom( - foregroundColor: Colors.green.shade600, + SizedBox( + width: double.infinity, + height: 50, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all(color: Colors.black, width: 2), + ), + child: TextButton.icon( + onPressed: () => _loadUserProfileData(), + icon: Icon( + Icons.refresh, + size: 18, + color: getThemeColors(context)['textPrimary'], + ), + label: Text( + 'Check Again', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: getThemeColors(context)['textPrimary'], + ), + ), + style: TextButton.styleFrom( + backgroundColor: getThemeColors(context)['secondary'], + foregroundColor: getThemeColors(context)['textPrimary'], + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(buttonCircularRadius - 2), + ), + ), + ), + ), ), ), ], @@ -509,21 +1136,37 @@ class _ProfileSectionWidgetState extends State { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - child: _isLoading - ? _buildLoadingState() - : _isNotRegistered - ? _buildNotRegisteredState() - : Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _profileOverview(), - SizedBox( - width: 15, - ), - _tokenWidget() - ], - )); + logger.d( + "Building ProfileSectionWidget - isLoading: $_isLoading, isNotRegistered: $_isNotRegistered, userProfileData: ${_userProfileData?.name}"); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: _isLoading + ? _buildLoadingState() + : _isNotRegistered + ? _buildNotRegisteredState() + : _userProfileData == null + ? _buildErrorState() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // First row: Profile overview and token stats + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _profileOverview(), + const SizedBox(width: 15), + _tokenWidget(), + ], + ), + ), + const SizedBox(height: 20), + // Second row: Verifier tokens + _verifierTokensWidget(), + ], + ), + ); } } diff --git a/lib/widgets/profile_widgets/user_profile_viewer_widget.dart b/lib/widgets/profile_widgets/user_profile_viewer_widget.dart new file mode 100644 index 0000000..cc8482e --- /dev/null +++ b/lib/widgets/profile_widgets/user_profile_viewer_widget.dart @@ -0,0 +1,661 @@ +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/utils/constants/ui/color_constants.dart'; +import 'package:tree_planting_protocol/utils/constants/ui/dimensions.dart'; +import 'package:tree_planting_protocol/utils/logger.dart'; +import 'package:tree_planting_protocol/utils/services/contract_functions/tree_nft_contract/tree_nft_contract_read_services.dart'; +import 'package:tree_planting_protocol/widgets/profile_widgets/profile_section_widget.dart'; + +class UserProfileViewerWidget extends StatefulWidget { + final String userAddress; + + const UserProfileViewerWidget({ + super.key, + required this.userAddress, + }); + + @override + State createState() => + _UserProfileViewerWidgetState(); +} + +class _UserProfileViewerWidgetState extends State { + bool _isLoading = false; + String? _errorMessage; + UserProfileData? _userProfileData; + List _verifierTokens = []; + bool _isNotRegistered = false; + int _displayedTokensCount = 5; + String? _expandedTokenAddress; + + String _getTreeIcon(int index) { + final treeNumber = ((index * 3) % 13) + 1; + return 'assets/tree-navbar-images/tree-$treeNumber.png'; + } + + @override + void initState() { + super.initState(); + _loadUserProfileData(); + } + + Future _loadUserProfileData({bool loadMore = false}) async { + if (_isLoading) return; + + setState(() { + _isLoading = true; + _isNotRegistered = false; + if (!loadMore) { + _errorMessage = null; + } + }); + + try { + final walletProvider = + Provider.of(context, listen: false); + + final result = await ContractReadFunctions.getProfileDetailsByAddress( + walletProvider: walletProvider, + userAddress: widget.userAddress, + ); + + if (result.success && result.data != null) { + final List data = result.data['profile'] ?? []; + final List verifierTokensData = result.data['verifierTokens'] ?? []; + + setState(() { + _isLoading = false; + if (data.isEmpty) { + _isNotRegistered = true; + } else { + _userProfileData = UserProfileData.fromContractData(data); + _verifierTokens = verifierTokensData + .map((token) => VerificationDetails.fromContractData(token)) + .toList(); + } + }); + } else { + setState(() { + _isLoading = false; + _errorMessage = result.errorMessage ?? 'Failed to load user profile'; + if (result.errorMessage?.contains('not registered') ?? false) { + _isNotRegistered = true; + } + }); + } + } catch (e) { + logger.e("Error loading user profile", error: e); + setState(() { + _isLoading = false; + _errorMessage = 'Error: ${e.toString()}'; + }); + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (_isNotRegistered) { + return _buildNotRegisteredWidget(); + } + + if (_errorMessage != null) { + return _buildErrorWidget(); + } + + if (_userProfileData == null) { + return const Center( + child: Text('No profile data available'), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProfileHeader(), + const SizedBox(height: 16), + _buildProfileInfo(), + const SizedBox(height: 20), + if (_verifierTokens.isNotEmpty) ...[ + _buildVerifierTokensWidget(), + const SizedBox(height: 20), + ], + ], + ); + } + + Widget _buildProfileHeader() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: getThemeColors(context)['border']!, + width: 3, + ), + ), + child: ClipOval( + child: _userProfileData!.profilePhoto.isNotEmpty + ? Image.network( + _userProfileData!.profilePhoto, + fit: BoxFit.cover, + headers: { + 'Access-Control-Allow-Origin': '*', + }, + errorBuilder: (context, error, stackTrace) { + // Try alternative IPFS gateway if original fails + String originalUrl = _userProfileData!.profilePhoto; + if (originalUrl.contains('pinata.cloud')) { + String ipfsHash = originalUrl.split('/ipfs/').last; + String alternativeUrl = + 'https://ipfs.io/ipfs/$ipfsHash'; + + return Image.network( + alternativeUrl, + fit: BoxFit.cover, + errorBuilder: (context, error2, stackTrace2) { + return Icon( + Icons.person, + size: 40, + color: getThemeColors(context)['textSecondary'], + ); + }, + ); + } + + return Icon( + Icons.person, + size: 40, + color: getThemeColors(context)['textSecondary'], + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return CircularProgressIndicator( + color: getThemeColors(context)['primary'], + ); + }, + ) + : Container( + color: getThemeColors(context)['secondary'], + child: Icon( + Icons.person, + size: 40, + color: getThemeColors(context)['textPrimary'], + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + _userProfileData!.name.isNotEmpty + ? _userProfileData!.name + : 'Anonymous User', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + _buildAddressRow(), + ], + ), + ); + } + + Widget _buildAddressRow() { + final shortAddress = widget.userAddress.length > 18 + ? '${widget.userAddress.substring(0, 10)}...${widget.userAddress.substring(widget.userAddress.length - 8)}' + : widget.userAddress; + + return Row( + children: [ + Text( + shortAddress, + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 14, + fontFamily: 'monospace', + ), + ), + const SizedBox(width: 8), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + Clipboard.setData(ClipboardData(text: widget.userAddress)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Address copied to clipboard!'), + duration: const Duration(seconds: 1), + backgroundColor: getThemeColors(context)['primary'], + ), + ); + }, + icon: Icon( + Icons.copy, + size: 16, + color: getThemeColors(context)['textPrimary'], + ), + ), + ], + ); + } + + Widget _buildProfileInfo() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatColumn( + 'Care Tokens', + _userProfileData!.careTokens.toString(), + Icons.favorite, + ), + _buildStatColumn( + 'Legacy Tokens', + _userProfileData!.legacyTokens.toString(), + Icons.stars, + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatColumn( + 'Reports', + _userProfileData!.reportedSpam.toString(), + Icons.flag, + ), + _buildStatColumn( + 'Revoked', + _userProfileData!.verificationsRevoked.toString(), + Icons.remove_circle, + ), + ], + ), + if (_userProfileData!.dateJoined > 0) ...[ + const SizedBox(height: 16), + Row( + children: [ + Icon( + Icons.calendar_today, + size: 16, + color: getThemeColors(context)['textSecondary'], + ), + const SizedBox(width: 8), + Text( + 'Joined: ${DateTime.fromMillisecondsSinceEpoch(_userProfileData!.dateJoined * 1000).toString().split(' ')[0]}', + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 14, + ), + ), + ], + ), + ], + ], + ), + ); + } + + Widget _buildStatColumn(String label, String value, IconData icon) { + return Column( + children: [ + Icon( + icon, + size: 32, + color: getThemeColors(context)['primary'], + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 12, + ), + ), + ], + ); + } + + Widget _buildVerifierTokensWidget() { + if (_verifierTokens.isEmpty) return const SizedBox.shrink(); + + final tokensToShow = _verifierTokens.take(_displayedTokensCount).toList(); + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Verifier Tokens', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 12, + runSpacing: 12, + children: tokensToShow + .asMap() + .entries + .map((entry) => _buildTokenBubble(entry.value, entry.key)) + .toList(), + ), + if (_verifierTokens.length > _displayedTokensCount) ...[ + const SizedBox(height: 16), + Center( + child: TextButton( + onPressed: () { + setState(() { + _displayedTokensCount += 5; + }); + }, + child: Text( + 'Load More (${_verifierTokens.length - _displayedTokensCount} remaining)', + style: TextStyle( + color: getThemeColors(context)['primary'], + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ], + ), + ); + } + + Widget _buildTokenBubble(VerificationDetails token, int index) { + final isExpanded = + _expandedTokenAddress == token.verifierPlanterTokenAddress; + final bubbleColor = index % 2 == 0 + ? getThemeColors(context)['primary']! + : getThemeColors(context)['secondary']!; + + return GestureDetector( + onTap: () { + setState(() { + _expandedTokenAddress = + isExpanded ? null : token.verifierPlanterTokenAddress; + }); + }, + child: Material( + elevation: 4, + borderRadius: BorderRadius.circular(buttonCircularRadius), + child: ClipRect( + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + width: isExpanded ? 280 : 80, + height: isExpanded ? 120 : 80, + decoration: BoxDecoration( + color: bubbleColor, + border: Border.all( + color: getThemeColors(context)['border']!, + width: buttonborderWidth, + ), + borderRadius: BorderRadius.circular(buttonCircularRadius), + ), + child: isExpanded + ? _buildExpandedTokenContent(token) + : _buildCollapsedTokenContent(token, index), + ), + ), + ), + ); + } + + Widget _buildCollapsedTokenContent(VerificationDetails token, int index) { + final formattedAmount = _formatTokenAmount(token.numberOfTrees / 1e18); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + _getTreeIcon(index), + width: 32, + height: 32, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.park, + size: 32, + color: getThemeColors(context)['textPrimary'], + ); + }, + ), + const SizedBox(height: 4), + Text( + formattedAmount, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 14, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ); + } + + Widget _buildExpandedTokenContent(VerificationDetails token) { + final formattedAmount = + _formatDetailedTokenAmount(token.numberOfTrees / 1e18); + + return SingleChildScrollView( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Amount: $formattedAmount', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Verifier: ${token.verifierPlanterTokenAddress.substring(0, 8)}...${token.verifierPlanterTokenAddress.substring(token.verifierPlanterTokenAddress.length - 6)}', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 10, + fontFamily: 'monospace', + ), + ), + if (token.description.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + token.description, + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 10, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + const SizedBox(height: 4), + Text( + 'Date: ${DateTime.fromMillisecondsSinceEpoch(token.timestamp * 1000).toString().split('.')[0]}', + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 9, + ), + ), + ], + ), + ); + } + + String _formatTokenAmount(double amount) { + if (amount >= 1000000) { + return '${(amount / 1000000).toStringAsFixed(1)}M'; + } else if (amount >= 1000) { + return '${(amount / 1000).toStringAsFixed(1)}K'; + } + return amount.toStringAsFixed(0); + } + + String _formatDetailedTokenAmount(double amount) { + if (amount >= 1000000) { + return '${(amount / 1000000).toStringAsFixed(2)}M'; + } else if (amount >= 1000) { + return '${(amount / 1000).toStringAsFixed(2)}K'; + } else if (amount >= 1) { + return amount.toStringAsFixed(2); + } + return amount.toStringAsFixed(4); + } + + Widget _buildNotRegisteredWidget() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['background'], + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['border']!, + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.person_off, + size: 64, + color: getThemeColors(context)['textSecondary'], + ), + const SizedBox(height: 16), + Text( + 'User Not Registered', + style: TextStyle( + color: getThemeColors(context)['textPrimary'], + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'This address is not registered in the system', + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildErrorWidget() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: getThemeColors(context)['error']!, + borderRadius: BorderRadius.circular(buttonCircularRadius), + border: Border.all( + color: getThemeColors(context)['error']!, + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 64, + color: getThemeColors(context)['error'], + ), + const SizedBox(height: 16), + Text( + 'Error Loading Profile', + style: TextStyle( + color: getThemeColors(context)['error'], + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + _errorMessage ?? 'Unknown error', + style: TextStyle( + color: getThemeColors(context)['textSecondary'], + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _loadUserProfileData(), + style: ElevatedButton.styleFrom( + backgroundColor: getThemeColors(context)['primary'], + ), + child: const Text('Retry'), + ), + ], + ), + ); + } +}