-
Notifications
You must be signed in to change notification settings - Fork 19
NFT Display page #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NFT Display page #13
Conversation
WalkthroughAdds env variables and CI workflow. Introduces env-driven contract address usage. Implements tree details loading with contract reads, verification submission/removal via contract writes, and a new static map widget. Refactors UI components for opacity, layout, and constructors. Adds a Tree model, verifiers UI, and connects wallet button action. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant UI as TreeDetailsPage (Stateful)
participant Wallet as WalletProvider
participant Read as ContractReadFunctions
participant Env as dotenv
Note over UI,Read: Load Tree Details
User->>UI: Open Tree NFT details
UI->>Wallet: getUserAddress()
UI->>Env: Read TREE_NFT_CONTRACT_ADDRESS
UI->>Read: getTreeNFTInfo(id, offset, limit)
Read->>Wallet: readContract(getTreeDetailsbyID)
Read->>Wallet: readContract(getTreeNftVerifiersPaginated)
Read->>Wallet: readContract(ownerOf)
Read-->>UI: {details, verifiers, owner}
UI->>UI: Parse -> Tree model, canVerify
UI-->>User: Render map, details, verifiers
sequenceDiagram
autonumber
actor Verifier as User
participant UI as TreeDetailsPage
participant Modal as _VerificationModal
participant IPFS as uploadToIPFS
participant Write as ContractWriteFunctions
participant Wallet as WalletProvider
Note over UI,Modal: Submit Verification
Verifier->>UI: Tap "Verify"
UI->>Modal: Open (collect desc + photos)
Modal->>IPFS: uploadToIPFS(photo[0..n])
IPFS-->>Modal: [hashes]
Modal->>Write: verifyTree(treeId, desc, hashes)
Write->>Wallet: writeContract(verify)
Wallet-->>Write: tx result / error
alt success
Write-->>Modal: success
Modal-->>UI: close, notify
UI->>Read: refresh getTreeNFTInfo(...)
UI-->>Verifier: Updated verifiers list
else failure
Write-->>Modal: error
Modal-->>Verifier: Show SnackBar error
end
sequenceDiagram
autonumber
actor User
participant Widget as WalletNotConnectedWidget
participant Provider as WalletProvider
User->>Widget: Press "Connect Wallet"
Widget->>Provider: connectWallet()
Provider-->>Widget: connection status
Widget-->>User: Proceed based on status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (24)
lib/providers/wallet_provider.dart (5)
13-15: Missing web3dart imports break build where DeployedContract/Web3Client/EthereumAddress are used.
Add the proper import(s) so read/write paths compile.Apply:
import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:web3dart/web3dart.dart';
257-258: Expose userAddress properly (currently always null).
Downstream code likely expects this to reflect the session address.- get userAddress => null; + String? get userAddress => _currentAddress;
382-397: Chain switching only mutates local state; wallet remains on old chain. Also uses wrong name lookup.
This will show “switched” UI while transactions still execute on the prior chain. Implementwallet_switchEthereumChain(and fallback towallet_addEthereumChain) via WalletConnect, and only update state on success. Also fix${chainInfo['name']}which is indexing the map incorrectly.Future<bool> switchChain(String newChainId) async { logger.d('[switchChain] Requested chain id: $newChainId'); if (!_isConnected) { logger.e('[switchChain] Wallet not connected.'); throw Exception('Wallet not connected'); } - if (_currentChainId == newChainId) { + if (_currentChainId == newChainId) { logger.d('[switchChain] Already on chain $newChainId, skipping switch.'); return true; } - _updateStatus('Switching to ${chainInfo['name']}...'); - _currentChainId = newChainId; - notifyListeners(); - return true; + final sessions = _web3App!.sessions.getAll(); + if (sessions.isEmpty) throw Exception('No active WalletConnect session'); + final session = sessions.first; + + final hexChain = '0x${int.parse(newChainId).toRadixString(16)}'; + final targetInfo = _chainInfo[newChainId]; + _updateStatus( + 'Switching to ${targetInfo?['name'] ?? newChainId}...'); + try { + await _web3App!.request( + topic: session.topic, + chainId: 'eip155:${_currentChainId ?? _correctChainId}', + request: const SessionRequestParams( + // placeholder, replaced below + method: '', + params: [], + ).copyWith( + method: 'wallet_switchEthereumChain', + params: [ + {'chainId': hexChain} + ], + ), + ); + } catch (e) { + // Try add-and-switch if the chain is not known to the wallet. + if (targetInfo != null) { + try { + await _web3App!.request( + topic: session.topic, + chainId: 'eip155:${_currentChainId ?? _correctChainId}', + request: SessionRequestParams( + method: 'wallet_addEthereumChain', + params: [ + { + 'chainId': hexChain, + 'chainName': targetInfo['name'], + 'rpcUrls': [targetInfo['rpcUrl']], + 'nativeCurrency': targetInfo['nativeCurrency'], + 'blockExplorerUrls': [targetInfo['blockExplorerUrl']], + } + ], + ), + ); + } catch (e2) { + _updateStatus('Failed to switch chain: $e2'); + rethrow; + } + } else { + _updateStatus('Unsupported chain id: $newChainId'); + rethrow; + } + } + _currentChainId = newChainId; + notifyListeners(); + return true; }
405-442: Ensure HTTP client is closed on all paths in readContract; also accept both String and List ABIs consistently.
Currently,httpClientis only closed on success, leaking on errors.Future<dynamic> readContract({ @@ }) async { try { if (!_isConnected || _web3App == null || _currentChainId == null) { throw Exception('Wallet not connected'); } _updateStatus('Reading from contract...'); - List<dynamic> abiList; - if (abi is String) { - abiList = json.decode(abi); - } else if (abi is List) { - abiList = abi; - } else { - throw Exception('Invalid ABI format'); - } + final List<dynamic> abiList = abi is String + ? (json.decode(abi) as List<dynamic>) + : (abi is List ? abi as List<dynamic> : throw Exception('Invalid ABI format')); @@ - final httpClient = http.Client(); - final ethClient = Web3Client(rpcUrl!, httpClient); - final result = await ethClient.call( - contract: contract, - function: function, - params: params, - ); - - httpClient.close(); - _updateStatus('Contract read successful'); - - return result; + final httpClient = http.Client(); + try { + final ethClient = Web3Client(rpcUrl!, httpClient); + final result = await ethClient.call( + contract: contract, + function: function, + params: params, + ); + _updateStatus('Contract read successful'); + return result; + } finally { + httpClient.close(); + }
460-503: Harden writeContract: handle ABI types consistently and close HTTP client in finally.
Also avoidas Stringcast onrpcUrlwhen you can assert non-null.- final abiList = json.decode(abi) as List<dynamic>; + final List<dynamic> abiList = abi is String + ? (json.decode(abi) as List<dynamic>) + : (abi is List ? abi as List<dynamic> : throw Exception('Invalid ABI format')); @@ - final rpcUrl = getChainDetails(targetChainId).first['rpcUrl'] as String?; - final httpClient = http.Client(); - final ethClient = Web3Client(rpcUrl as String, httpClient); + final rpcUrl = getChainDetails(targetChainId).first['rpcUrl'] as String?; + final httpClient = http.Client(); + final ethClient = Web3Client(rpcUrl!, httpClient); @@ - final gasPrice = await ethClient.getGasPrice(); - httpClient.close(); + final gasPrice = await ethClient.getGasPrice(); + httpClient.close(); // ok to close early; request below uses WalletConnectlib/utils/services/ipfs_services.dart (1)
9-31: Always reset uploading state; add timeout; surface error.
If the request throws,setUploadingState(false)never runs. Wrap with try/finally and consider a timeout.Future<String?> uploadToIPFS( File imageFile, Function(bool) setUploadingState) async { - setUploadingState(true); - - var url = Uri.parse("https://api.pinata.cloud/pinning/pinFileToIPFS"); - var request = http.MultipartRequest("POST", url); - request.headers.addAll({ - "pinata_api_key": apiKey, - "pinata_secret_api_key": apiSecret, - }); - - request.files.add(await http.MultipartFile.fromPath("file", imageFile.path)); - var response = await request.send(); - - setUploadingState(false); - - if (response.statusCode == 200) { - var jsonResponse = json.decode(await response.stream.bytesToString()); - return "https://gateway.pinata.cloud/ipfs/${jsonResponse['IpfsHash']}"; - } else { - return null; - } + setUploadingState(true); + try { + if (apiKey.isEmpty || apiSecret.isEmpty) { + return null; + } + final url = Uri.parse("https://api.pinata.cloud/pinning/pinFileToIPFS"); + final request = http.MultipartRequest("POST", url) + ..headers.addAll({ + "pinata_api_key": apiKey, + "pinata_secret_api_key": apiSecret, + }) + ..files.add(await http.MultipartFile.fromPath("file", imageFile.path)); + final response = + await request.send().timeout(const Duration(seconds: 30)); + if (response.statusCode == 200) { + final jsonResponse = json.decode(await response.stream.bytesToString()); + return "https://gateway.pinata.cloud/ipfs/${jsonResponse['IpfsHash']}"; + } + return null; + } finally { + setUploadingState(false); + } }lib/pages/mint_nft/mint_nft_images.dart (1)
56-85: Guard setState() and provider usage with mounted checks after awaitsMultiple setState() calls are executed after awaited operations; if the widget unmounts mid-upload, this will throw.
Add early returns:
@@ - setState(() { + if (!mounted) return; + setState(() { _uploadingIndex = i; }); @@ - setState(() { + if (!mounted) return; + setState(() { _uploadedHashes.add(hash); }); @@ - setState(() { + if (!mounted) return; + setState(() { _isUploading = false; _uploadingIndex = -1; _processingImages.clear(); });Also applies to: 91-97
lib/widgets/profile_widgets/profile_section_widget.dart (2)
200-213: Substring on possibly-empty address can crashIf parsing fails, userAddress may be '', causing RangeError on substring. Use a safe formatter.
- Text( - '${_userProfileData!.userAddress.substring(0, 6)}...${_userProfileData!.userAddress.substring(_userProfileData!.userAddress.length - 4)}', + Text( + _shortAddress(_userProfileData!.userAddress),Add helper within the State class:
+ String _shortAddress(String addr) { + if (addr.isEmpty || addr.length <= 10) return addr; + return '${addr.substring(0, 6)}...${addr.substring(addr.length - 4)}'; + }
504-521: Surface errors instead of dereferencing null profile dataWhen fetch fails (but isn’t “not registered”), build still tries to render profile/token widgets using null _userProfileData. Use _buildErrorState and drop the ignore.
- child: _isLoading - ? _buildLoadingState() - : _isNotRegistered - ? _buildNotRegisteredState() - : Row( + child: _isLoading + ? _buildLoadingState() + : _isNotRegistered + ? _buildNotRegisteredState() + : (_errorMessage != null || _userProfileData == null) + ? _buildErrorState() + : Row(- // ignore: unused_elementAlso applies to: 358-358
lib/pages/register_user_page.dart (2)
279-289: Icon becomes invisible: foreground matches backgroundContainer and icon both use Color(0xFF1CD381), making the camera icon unreadable.
- decoration: BoxDecoration( - color: const Color(0xFF1CD381), + decoration: BoxDecoration( + color: const Color(0xFF1CD381), borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.camera_alt, - color: Color(0xFF1CD381), + color: Colors.white, size: 18, ),
447-453: Same contrast issue in form field labelsForeground icon color equals the new solid background.
- decoration: BoxDecoration( - color: const Color(0xFF1CD381), + decoration: BoxDecoration( + color: const Color(0xFF1CD381), borderRadius: BorderRadius.circular(8), ), child: Icon( icon, - color: const Color(0xFF1CD381), + color: Colors.white, size: 18, ),lib/components/wallet_connect_dialog.dart (2)
43-55: Replace lint suppressions with mounted checksDon’t silence use_build_context_synchronously; guard UI calls after awaits.
- // ignore: use_build_context_synchronously - Navigator.of(context).pop(); + if (!context.mounted) return; + Navigator.of(context).pop(); } catch (e) { - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( + if (context.mounted) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), backgroundColor: Colors.red, ), ); + }
81-89: Same here for “Copy URI” flowGuard dialog dismissal and SnackBar with context.mounted.
- // ignore: use_build_context_synchronously - Navigator.of(context).pop(); - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( + if (!context.mounted) return; + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('URI copied to clipboard!'), backgroundColor: Colors.green, ), );lib/components/universal_navbar.dart (1)
269-273: Incorrect use of Flexible inside a SizedBox (will throw at runtime)Flexible must be a direct child of a Flex (Row/Column). This will assert.
- SizedBox( - width: 10, - child: Flexible( - child: Text( + Flexible( + child: Text( formatAddress(walletProvider.currentAddress!), style: TextStyle( color: Colors.green[700], fontWeight: FontWeight.w600, fontSize: 10, ), overflow: TextOverflow.ellipsis, - ), ),lib/pages/mint_nft/mint_nft_details.dart (3)
140-147: Fix invisible header icon (white-on-white).The icon color is white inside a white container, making it invisible.
Apply:
- child: const Icon( - Icons.edit_note, - color: Colors.white, - size: 28, - ), + child: const Icon( + Icons.edit_note, + color: Color(0xFF1CD381), // contrast against white bg + size: 28, + ),
223-231: Arrow icon is invisible (inherits white on white).The ElevatedButton sets foregroundColor to white; the arrow icon (no explicit color) sits on a white chip.
Apply:
- child: const Icon( - Icons.arrow_forward, - size: 18, - ), + child: const Icon( + Icons.arrow_forward, + color: Color(0xFF1CD381), + size: 18, + ),
259-269: Green icon on green badge (no contrast).Icon and background both use Color(0xFF1CD381).
Apply one of:
- decoration: BoxDecoration( - color: const Color(0xFF1CD381), + decoration: BoxDecoration( + color: const Color(0xFF1CD381), borderRadius: BorderRadius.circular(8), ), - child: Icon( - icon, - color: const Color(0xFF1CD381), + child: Icon( + icon, + color: Colors.white, size: 18, ),lib/widgets/map_widgets/flutter_map_widget.dart (1)
343-347: Type error: clamp returns num, assigned to double.This will fail strong analysis; cast to double.
- double latitude = _sanitizeCoordinate(widget.lat, _defaultLat); - double longitude = _sanitizeCoordinate(widget.lng, _defaultLng); - latitude = latitude.clamp(-90.0, 90.0); - longitude = longitude.clamp(-180.0, 180.0); + double latitude = _sanitizeCoordinate(widget.lat, _defaultLat); + double longitude = _sanitizeCoordinate(widget.lng, _defaultLng); + latitude = latitude.clamp(-90.0, 90.0).toDouble(); + longitude = longitude.clamp(-180.0, 180.0).toDouble();lib/widgets/nft_display_utils/user_nfts_widget.dart (5)
134-156: Broken pagination: always fetching the first page.
offsetandlimitare hardcoded to0,10, so “load more” re-requests page 0. Use_currentPage/_itemsPerPageand only increment on success.Apply:
final result = await ContractReadFunctions.getNFTsByUserPaginated( walletProvider: walletProvider, - offset: 0, - limit: 10, + offset: _currentPage * _itemsPerPage, + limit: _itemsPerPage, ); @@ - _currentPage++; + _currentPage++; _hasMore = _nfts.length < _totalCount;Also applies to: 165-173
150-154: Wrong user scope: ignoresuserAddressprop.The read function pulls NFTs for the connected wallet only. This widget accepts
userAddress, so viewing another user’s NFTs won’t work.Either:
- Update
getNFTsByUserPaginatedto accept a target address (see proposed change in contract_read_services.dart), and passwidget.userAddresshere; or- Rename/remove
userAddressfrom this widget to match behavior.Proposed call after API change:
- final result = await ContractReadFunctions.getNFTsByUserPaginated( - walletProvider: walletProvider, - offset: _currentPage * _itemsPerPage, - limit: _itemsPerPage, - ); + final result = await ContractReadFunctions.getNFTsByUserPaginated( + walletProvider: walletProvider, + addressHex: widget.userAddress, + offset: _currentPage * _itemsPerPage, + limit: _itemsPerPage, + );
250-254: Latitude/longitude decoding inconsistent with mint encoding.Contract stores lat/lng as
(lat+90)*1e6and(lng+180)*1e6. The display should subtract offsets.- 'Location: ${tree.latitude / 1000000}, ${tree.longitude / 1000000}', + 'Location: ${(tree.latitude / 1e6) - 90.0}, ${(tree.longitude / 1e6) - 180.0}',
283-295: Alive/Deceased label contradicts badge color.If
death == 0, it’s alive; current label shows “Deceased” because0 < now. Handle the sentinel.- color: tree.death == 0 ? Colors.green : Colors.red, + color: tree.death == 0 || + tree.death > DateTime.now().millisecondsSinceEpoch ~/ 1000 + ? Colors.green + : Colors.red, @@ - tree.death < DateTime.now().millisecondsSinceEpoch ~/ 1000 - ? 'Deceased' - : 'Alive', + (tree.death == 0 || + tree.death > DateTime.now().millisecondsSinceEpoch ~/ 1000) + ? 'Alive' + : 'Deceased',Also applies to: 287-289
8-101: DuplicateTreemodel defined in multiple places.This file defines
Tree, andtree_nft_details_verifiers_widget.dartdefines anotherTreewith different fields. Divergence is likely and parsing bugs will creep in.Extract a single
Treemodel (e.g.,lib/models/tree_details.dart) and import it across widgets. Remove this local definition.lib/utils/services/contract_read_services.dart (1)
38-42: Allow reading NFTs for arbitrary addresses.Current API always reads for the connected wallet. Add an optional
addressHexto support viewing another user’s portfolio (used byUserNftsWidget).- static Future<ContractReadResult> getNFTsByUserPaginated({ - required WalletProvider walletProvider, - int offset = 0, - int limit = 10, - }) async { + static Future<ContractReadResult> getNFTsByUserPaginated({ + required WalletProvider walletProvider, + String? addressHex, + int offset = 0, + int limit = 10, + }) async { @@ - final String address = walletProvider.currentAddress.toString(); + final String address = + (addressHex ?? walletProvider.currentAddress?.toString() ?? '').trim(); if (!address.startsWith('0x')) { return ContractReadResult.error( errorMessage: 'Invalid wallet address format', ); } @@ - final EthereumAddress userAddress = EthereumAddress.fromHex(address); + final EthereumAddress userAddress = EthereumAddress.fromHex(address);Also mirror the pagination validation used here into other read methods as needed.
Also applies to: 51-59, 66-77
🧹 Nitpick comments (39)
lib/providers/wallet_provider.dart (8)
39-39: Getter type is nullable but source is not.
_correctChainIdis non-null; expose as non-nullable.- String? get correctChainId => _correctChainId; + String get correctChainId => _correctChainId;
43-43: Remove stray debug getterg.
This shadows nothing, leaks internal state with a meaningless name, and invites misuse.- bool get g => _isConnecting;
114-114: No-op string expression; replace with logging.
The bare string does nothing. Uselogger.- ('Session connected: ${event.session.topic}'); + logger.d('Session connected: ${event.session.topic}');
371-373: Incorrect emptiness check.
if (!sessions.isNotEmpty)is confusing; useisEmpty.- if (!sessions.isNotEmpty) { + if (sessions.isEmpty) {
583-587: No-op replaceAll; clean up.
replaceAll('://','://')does nothing.- if (!deepLinkBase.endsWith('://')) { - deepLinkBase = deepLinkBase.replaceAll('://', '://'); - } + if (!deepLinkBase.endsWith('://')) { + deepLinkBase = '$deepLinkBase://'; + }
399-441: Consider EIP-1559 fields for better wallet compatibility.
IncludingmaxFeePerGasandmaxPriorityFeePerGasimproves UX on chains that prefer them over legacygasPrice. Optional but recommended.
363-367: Dead method placeholder.
getSupportedChainsWithStatusthrows then returns; either implement or remove.Want me to wire this to compute “current” vs “supported” with reasons?
1-735: Prune unused WalletConnect RPC methods in both requiredNamespaces and optionalNamespaces
Only request the RPC calls you actually invoke—‘eth_sendTransaction’ and ‘personal_sign’—and remove the others (eth_signTransaction, eth_sign, eth_signTypedData, wallet_switchEthereumChain and all read-only methods like eth_call, eth_getBalance, eth_getTransactionCount, eth_getTransactionReceipt, eth_estimateGas) to minimize permission scope.lib/utils/services/switch_chain_utils.dart (1)
66-73: SnackBar says “switched” even if wallet didn’t switch.
GivenWalletProvider.switchChaincurrently only updates local state, the wallet remains on the old chain. After fixingswitchChainto usewallet_switchEthereumChain, this will be accurate; until then, consider deferring the success SnackBar until the request returns without throwing.lib/widgets/wallet_not_connected_widget.dart (1)
25-32: Optional: disable while connecting to prevent duplicate sessions.
WireonPressedto null whenwalletProvider.isConnectingand show a progress indicator.lib/models/wallet_chain_option.dart (3)
4-5: Handle missing ALCHEMY_API_KEY explicitly.
Empty key yields 401s; fail fast or fall back to public RPCs.-final String alchemyApiKey = dotenv.env['ALCHEMY_API_KEY'] ?? ''; +final String alchemyApiKey = dotenv.env['ALCHEMY_API_KEY'] ?? ''; +// Optional: assert or warn when empty +// assert(alchemyApiKey.isNotEmpty, 'ALCHEMY_API_KEY is missing');
22-30: Make static wallet options const.
Saves allocs and clarifies immutability.-final List<WalletOption> walletOptionsList = [ - WalletOption( +final List<WalletOption> walletOptionsList = const [ + WalletOption( name: 'MetaMask', deepLink: 'metamask://wc?uri=', fallbackUrl: 'https://metamask.app.link/wc?uri=', icon: Icons.account_balance_wallet, color: Colors.orange, ), ];
32-58: Avoid duplicating RPC URLs across rpcUrls and chainInfoList.
SourcechainInfoList['rpcUrl']fromrpcUrlsto keep a single truth.lib/pages/mint_nft/mint_nft_images.dart (1)
254-262: Restore translucency for “completed” overlay to preserve image legibilitySolid green fully obscures the thumbnail; keep partial opacity.
- color: Colors.green, + color: Colors.green.withOpacity(0.8),lib/widgets/profile_widgets/profile_section_widget.dart (2)
49-52: Fix log message copy: references “Tree data” in profile parserMinor but confusing during debugging.
- logger.d("Error parsing Tree data: $e"); + logger.d("Error parsing UserProfile data: $e");
246-355: Reduce duplication in token tilesFour near-identical blocks; create a small builder to keep copy changes consistent.
Example:
+ Widget _tokenTile(Color bg, String label, int value) => Padding( + padding: const EdgeInsets.all(4.0), + child: SizedBox( + height: 40, width: 150, + child: Container( + decoration: BoxDecoration( + color: bg, + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 6, offset: Offset(0,3))], + ), + child: Center(child: Text('$label : $value')), + ), + ), + ); - return Column( + return Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - ... four blocks ... + _tokenTile(const Color.fromARGB(255, 251, 251, 99), 'Planter Tokens', _userProfileData!.planterTokens), + _tokenTile(const Color.fromARGB(255, 28, 211, 129), 'Care Tokens', _userProfileData!.careTokens), + _tokenTile(const Color.fromARGB(255, 251, 251, 99), 'Verifier Tokens', _userProfileData!.verifierTokens), + _tokenTile(const Color.fromARGB(255, 28, 211, 129), 'Legacy Tokens', _userProfileData!.legacyTokens), ], );lib/pages/register_user_page.dart (1)
316-319: Heavy, fully opaque colored shadows look offUse a translucent shadow for depth without tinting the surface green.
- BoxShadow( - color: const Color(0xFF1CD381), + BoxShadow( + color: const Color(0xFF1CD381).withOpacity(0.08), blurRadius: 8, offset: const Offset(0, 2), ),Also applies to: 478-481
lib/components/universal_navbar.dart (1)
250-254: Overly dark shadows on light surfacesSolid black shadows appear harsh; use a translucent black for a softer elevation.
- BoxShadow( - color: Colors.black, + BoxShadow( + color: Colors.black26, blurRadius: 4, offset: const Offset(0, 2), ),Also applies to: 375-379
lib/pages/mint_nft/mint_nft_details.dart (1)
206-211: shadowColor is unused with elevation: 0.Either increase elevation or drop shadowColor to avoid confusion.
elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - shadowColor: const Color(0xFF1CD381),lib/widgets/map_widgets/static_map_display_widget.dart (2)
219-225: Improve coordinate banner contrast.Black text on dark green is low contrast; switch to white for readability.
- style: const TextStyle( - fontSize: 10, - color: Colors.black, - fontFamily: 'monospace', - ), + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontFamily: 'monospace', + ),
139-144: Soften heavy drop shadows.Solid black shadows look harsh; use translucency for a subtler, consistent material feel.
- BoxShadow( - color: Colors.black, + BoxShadow( + color: Colors.black26, blurRadius: 6, offset: const Offset(0, 2), ),- BoxShadow( - color: Colors.black, + BoxShadow( + color: Colors.black26, blurRadius: 4, offset: const Offset(0, 2), ),Also applies to: 243-246
lib/widgets/map_widgets/flutter_map_widget.dart (1)
105-108: Align initialZoom with minZoom.
initialZoom: 1.0is belowminZoom: 3.0; set them consistently to avoid immediate clamping/jank.- initialZoom: 1.0, + initialZoom: 3.0, minZoom: 3.0,lib/models/tree_details.dart (1)
69-88: Don’t swallow errors silently.Log the exception to aid debugging.
- } catch (e) { + } catch (e, st) { + logger.e('Tree.fromContractData failed: $e', stackTrace: st); return Tree(lib/widgets/nft_display_utils/tree_nft_view_details_with_map.dart (3)
37-41: Reduce overly strong shadows.Pure black/green shadows read heavy; use translucent variants to match other components.
- BoxShadow( - color: Colors.black, + BoxShadow( + color: Colors.black26, blurRadius: 8, offset: const Offset(0, 2), ),- BoxShadow( - color: Colors.green, + BoxShadow( + color: Colors.green.withOpacity(0.25), blurRadius: 8, offset: const Offset(0, 2), ),Also applies to: 67-71
115-121: Add loading/error handling for images.Network images should provide feedback and avoid layout jumps.
- child: Image.network( - provider.getInitialPhotos()[index], - height: 100, - width: double.infinity, - fit: BoxFit.cover, - ), + child: Image.network( + provider.getInitialPhotos()[index], + height: 100, + width: double.infinity, + fit: BoxFit.cover, + loadingBuilder: (ctx, child, progress) => + progress == null + ? child + : const Center(child: CircularProgressIndicator(strokeWidth: 2)), + errorBuilder: (ctx, _, __) => Container( + color: Colors.grey.shade200, + alignment: Alignment.center, + child: const Icon(Icons.broken_image, color: Colors.grey), + ), + ),
45-51: Redundant.toDouble()calls.If
getLatitude()/getLongitude()already returndouble, drop.toDouble()to avoid noise.lib/widgets/nft_display_utils/tree_nft_view_widget.dart (1)
1-1: Drop unnecessary lint suppression.The file name already follows snake_case. Remove the
file_namesignore.-// ignore: file_nameslib/pages/mint_nft/mint_nft_coordinates.dart (1)
621-657: Remove unused preview (and related import) instead of suppressing lints.
_buildPreviewSectionis unused and guarded by// ignore: unused_element. Delete it; if you still want it, gate withkDebugModeand render it. If deleted, theNewNFTWidgetimport becomes unnecessary too.- import 'package:tree_planting_protocol/widgets/nft_display_utils/tree_nft_view_widget.dart';- // ignore: unused_element - Widget _buildPreviewSection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: const Color(0xFFFAEB96), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.preview, - color: const Color(0xFF1CD381), - size: 20, - ), - ), - const SizedBox(width: 12), - const Text( - 'Live Preview', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF1CD381), - ), - ), - ], - ), - ), - const NewNFTWidget(), - ], - ); - }Also applies to: 9-9
lib/utils/constants/contract_abis/tree_nft_contract_abi.dart (2)
4-6: Consider moving the large ABI string to an asset JSON.Keeping a >10KB ABI inline bloats the binary and diffs. Loading from assets (then caching) improves maintainability.
10-16: Remove hard-coded address notes from source; keep in .env.stencil or docs.These comments tend to drift and risk confusion with env-driven config.
-// CareToken Address: 0xF9C45610FEA0382Ab5d28c7CaEe44F6aC26Fe956 -// PlanterToken Address: 0x18a3BB9E8b6a692b3B29Dcf49Ce58f4bf2CB0E93 -// VerifierToken Address: 0x52db3eEff09D1dBE30007fA06AE14aF9849D29ba -// LegacyToken Address: 0xD5C0F25B883f018133d1Ce46fdb3365B660EF1db -// TreeNft Address: 0xeD3D3a4f30ad25d29BD6cB46Bb705a120809DB23 -// OrganisationFactory: 0x75da54F30d347040e977860a9C3495b2C52b4F23.env.stencil (2)
9-9: Use dotenv-compatible comments and trim extra blank lines.Switch
//to#and remove extra blank lines flagged by the linter.- - -//CONTRACT ADDRESSES +# CONTRACT ADDRESSES -Also applies to: 8-8, 11-11
5-6: Remove unusedCONTRACT_ADDRESSand reconcile factory vars
CONTRACT_ADDRESSisn’t referenced in any code (only per-contract keys likeTREE_NFT_CONTRACT_ADDRESSare used); remove it from.env.stencil.ORGANISATION_FACTORY_CONTRACT_ADDRESSis declared but never consumed—either drop it or implement its usage, and ensure its spelling exactly matches the key referenced in code.lib/widgets/nft_display_utils/user_nfts_widget.dart (1)
439-445: Bottom detection can miss due to float equality; add threshold.Use a small threshold and
>=to trigger reliably.- if (!_isLoading && - _hasMore && - scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) { + const threshold = 100.0; + if (!_isLoading && + _hasMore && + scrollInfo.metrics.pixels >= + scrollInfo.metrics.maxScrollExtent - threshold) {lib/utils/services/contract_read_services.dart (2)
191-253: Parallelize independent reads and validate pagination.
getTreeNFTInforuns 3 sequential reads and lacks offset/limit validation. Parallelizing reduces latency; validating inputs guards RPC errors.static Future<ContractReadResult> getTreeNFTInfo({ @@ - try { + try { if (!walletProvider.isConnected) { @@ - final List<dynamic> args = [BigInt.from(id)]; + if (id < 0 || offset < 0 || limit <= 0 || limit > 100) { + return ContractReadResult.error( + errorMessage: + 'Invalid parameters. id>=0, offset>=0, 1<=limit<=100', + ); + } + final List<dynamic> args = [BigInt.from(id)]; @@ - final treeDetailsResult = await walletProvider.readContract( - contractAddress: treeNFtContractAddress, - functionName: 'getTreeDetailsbyID', - params: args, - abi: treeNftContractABI, - ); + final detailsF = walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getTreeDetailsbyID', + params: args, + abi: treeNftContractABI, + ); @@ - final treeVerifiersResult = await walletProvider.readContract( - contractAddress: treeNFtContractAddress, - functionName: 'getTreeNftVerifiersPaginated', - params: [BigInt.from(id), BigInt.from(offset), BigInt.from(limit)], - abi: treeNftContractABI); + final verifiersF = walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'getTreeNftVerifiersPaginated', + params: [BigInt.from(id), BigInt.from(offset), BigInt.from(limit)], + abi: treeNftContractABI, + ); @@ - final ownerResult = await walletProvider.readContract( - contractAddress: treeNFtContractAddress, - functionName: 'ownerOf', - params: [BigInt.from(id)], - abi: treeNftContractABI, - ); + final ownerF = walletProvider.readContract( + contractAddress: treeNFtContractAddress, + functionName: 'ownerOf', + params: [BigInt.from(id)], + abi: treeNftContractABI, + ); + + final results = await Future.wait([detailsF, verifiersF, ownerF]); + final treeDetailsResult = results[0]; + final treeVerifiersResult = results[1]; + final ownerResult = results[2];
85-94: Result shape handling could be stricter.If the contract ever returns tuples vs arrays,
result[0]may itself be a tuple. Consider normalizing toList<dynamic>explicitly.lib/utils/services/contract_write_functions.dart (3)
64-66: Avoid truncation when encoding coordinates.Use
.round()beforeBigInt.fromto reduce bias from flooring.- final lat = BigInt.from((latitude + 90.0) * 1e6); - final lng = BigInt.from((longitude + 180.0) * 1e6); + final lat = BigInt.from(((latitude + 90.0) * 1e6).round()); + final lng = BigInt.from(((longitude + 180.0) * 1e6).round());
143-171: Lightweight input validation for verify.Validate
treeId > 0and non-emptydescriptionto fail fast client-side.static Future<ContractWriteResult> verifyTree( @@ - try { + try { + if (treeId <= 0) { + return ContractWriteResult.error( + errorMessage: 'Invalid treeId', + ); + } + if (description.trim().isEmpty) { + return ContractWriteResult.error( + errorMessage: 'Description is required', + ); + }
179-217: Validate address format beforeEthereumAddress.fromHex.Provide a clearer error if the address is malformed.
static Future<ContractWriteResult> removeVerification( @@ - final List<dynamic> args = [ + if (!address.startsWith('0x') || address.length != 42) { + return ContractWriteResult.error( + errorMessage: 'Invalid verifier address format', + ); + } + final List<dynamic> args = [ BigInt.from(treeId), EthereumAddress.fromHex(address) ];lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart (1)
292-336:treeDetails!force unwrap in removal path.
_removeVerifierusestreeDetails!.id. While currently guarded by UI flow, keep it defensive.- treeId: treeDetails!.id, + treeId: (treeDetails?.id ?? -1),And early-return with error if
treeDetails == null || treeDetails.id < 0.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (28)
.env.stencil(1 hunks).github/workflows/flutter.yaml(1 hunks)lib/components/universal_navbar.dart(7 hunks)lib/components/wallet_connect_dialog.dart(2 hunks)lib/models/tree_details.dart(1 hunks)lib/models/wallet_chain_option.dart(3 hunks)lib/pages/home_page.dart(0 hunks)lib/pages/mint_nft/mint_nft_coordinates.dart(2 hunks)lib/pages/mint_nft/mint_nft_details.dart(8 hunks)lib/pages/mint_nft/mint_nft_images.dart(2 hunks)lib/pages/register_user_page.dart(4 hunks)lib/pages/tree_details_page.dart(1 hunks)lib/providers/mint_nft_provider.dart(0 hunks)lib/providers/wallet_provider.dart(1 hunks)lib/utils/constants/contractDetails.dart(0 hunks)lib/utils/constants/contract_abis/tree_nft_contract_abi.dart(1 hunks)lib/utils/services/contract_read_services.dart(5 hunks)lib/utils/services/contract_write_functions.dart(4 hunks)lib/utils/services/ipfs_services.dart(2 hunks)lib/utils/services/switch_chain_utils.dart(1 hunks)lib/widgets/map_widgets/flutter_map_widget.dart(15 hunks)lib/widgets/map_widgets/static_map_display_widget.dart(1 hunks)lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart(1 hunks)lib/widgets/nft_display_utils/tree_nft_view_details_with_map.dart(2 hunks)lib/widgets/nft_display_utils/tree_nft_view_widget.dart(1 hunks)lib/widgets/nft_display_utils/user_nfts_widget.dart(4 hunks)lib/widgets/profile_widgets/profile_section_widget.dart(3 hunks)lib/widgets/wallet_not_connected_widget.dart(1 hunks)
💤 Files with no reviewable changes (3)
- lib/pages/home_page.dart
- lib/utils/constants/contractDetails.dart
- lib/providers/mint_nft_provider.dart
🧰 Additional context used
🪛 actionlint (1.7.7)
.github/workflows/flutter.yaml
15-15: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🪛 YAMLlint (1.37.1)
.github/workflows/flutter.yaml
[error] 17-17: trailing spaces
(trailing-spaces)
[error] 18-18: trailing spaces
(trailing-spaces)
[error] 23-23: trailing spaces
(trailing-spaces)
[warning] 35-35: too many spaces after colon
(colons)
🪛 dotenv-linter (3.3.0)
.env.stencil
[warning] 5-5: [UnorderedKey] The CONTRACT_ADDRESS key should go before the WALLETCONNECT_PROJECT_ID key
(UnorderedKey)
[warning] 6-6: [UnorderedKey] The APPLICATION_ID key should go before the CONTRACT_ADDRESS key
(UnorderedKey)
[warning] 8-8: [ExtraBlankLine] Extra blank line detected
(ExtraBlankLine)
[warning] 9-9: [IncorrectDelimiter] The //CONTRACT ADDRESSES key has incorrect delimiter
(IncorrectDelimiter)
[warning] 9-9: [KeyWithoutValue] The //CONTRACT ADDRESSES key should be with a value or have an equal sign
(KeyWithoutValue)
[warning] 9-9: [LeadingCharacter] Invalid leading character detected
(LeadingCharacter)
[warning] 9-9: [TrailingWhitespace] Trailing whitespace detected
(TrailingWhitespace)
[warning] 11-11: [ExtraBlankLine] Extra blank line detected
(ExtraBlankLine)
[warning] 13-13: [UnorderedKey] The ORGANISATION_FACTORY_CONTRACT_ADDRESS key should go before the TREE_NFT_CONTRACT_ADDRESS key
(UnorderedKey)
[warning] 14-14: [UnorderedKey] The CARE_TOKEN_CONTRACT_ADDRESS key should go before the ORGANISATION_FACTORY_CONTRACT_ADDRESS key
(UnorderedKey)
[warning] 16-16: [UnorderedKey] The LEGACY_TOKEN_CONTRACT_ADDRESS key should go before the ORGANISATION_FACTORY_CONTRACT_ADDRESS key
(UnorderedKey)
[warning] 17-17: [UnorderedKey] The PLANTER_TOKEN_CONTRACT_ADDRESS key should go before the TREE_NFT_CONTRACT_ADDRESS key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (8)
lib/utils/services/switch_chain_utils.dart (1)
25-58: LGTM on removing unnecessary toList().
Spreading an Iterable is fine here and slightly lighter on allocs.lib/widgets/profile_widgets/profile_section_widget.dart (1)
37-47: Drop out-of-range index warning; actualData[9] is valid
getUserProfile returns a 10-element tuple (indices 0–9) as defined in the ABI, with careTokens at index 9 (etherscan.io). No change needed.Likely an incorrect or invalid review comment.
lib/models/tree_details.dart (1)
5-6: Confirm coordinate scaling and types.Model stores
latitude/longitudeasint; UI map widgets expectdouble. If contract returns scaled ints (e.g., E6), either:
- store as
doublein the model, or- rename to
latitudeE6/longitudeE6and provide helpers to convert.lib/pages/mint_nft/mint_nft_coordinates.dart (1)
9-9: Import path fix looks good.lib/widgets/nft_display_utils/user_nfts_widget.dart (2)
107-111: Constructor cleanup looks good.Switching to
super.keyis consistent and idiomatic.
341-378: Potential unbounded height withExpandedinside a top-levelColumn.If this widget is placed in an unconstrained parent (e.g., another
ColumnorSingleChildScrollView), this can throw a RenderFlex error.Please verify the parent provides a bounded height. If not, wrap this widget in
Expandedat the call site, or replace thisExpandedwith a sized container (e.g.,SizedBox(height: ...)).lib/pages/tree_details_page.dart (2)
64-71: Verifier eligibility logic likely incomplete.Owners might be allowed/forbidden to verify their own trees depending on business rules. Today only “already verified” is checked.
Confirm the rule. If owners should not verify, add
canVerify &= owner.toLowerCase() != loggedInUser?.toLowerCase();
98-103: Map coordinates correctly decoded.Using
(value/1e6) - offsetmatches mint encoding. Good.
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Modernize action version and fix minor YAML lint issues.
- Use
actions/checkout@v4. - Trim trailing spaces (lines 17, 18, 23).
- Single space after
run:(line 35).
- uses: actions/checkout@v3
+ uses: actions/checkout@v4- - name: Setup environment file from template
- run: cp .env.stencil .env
+ - name: Setup environment file from template
+ run: cp .env.stencil .env- channel: 'stable'
+ channel: 'stable'- run: flutter build apk --release --no-tree-shake-icons
+ run: flutter build apk --release --no-tree-shake-iconsAlso applies to: 17-18, 23-23, 35-35
🧰 Tools
🪛 actionlint (1.7.7)
15-15: the runner of "actions/checkout@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🤖 Prompt for AI Agents
In .github/workflows/flutter.yaml around lines 15, 17-18, 23 and 35 update the
actions/checkout usage to actions/checkout@v4, remove any trailing spaces on
lines 17, 18 and 23, and ensure there is exactly one space after the run: key on
line 35; keep YAML indentation and quoting unchanged while making these minimal
edits.
| run: dart format --output=none --set-exit-if-changed . | ||
|
|
||
| - name: Flutter build (apk) | ||
| run: flutter build apk --release --no-tree-shake-icons | grep -v "deprecated" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build failures may be masked by piping to grep.
flutter build ... | grep -v "deprecated" returns grep's exit code; without pipefail, a failing build can pass. Remove the pipe (or set set -o pipefail).
- - name: Flutter build (apk)
- run: flutter build apk --release --no-tree-shake-icons | grep -v "deprecated"
+ - name: Flutter build (apk)
+ run: flutter build apk --release --no-tree-shake-icons📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| run: flutter build apk --release --no-tree-shake-icons | grep -v "deprecated" | |
| - name: Flutter build (apk) | |
| run: flutter build apk --release --no-tree-shake-icons |
🧰 Tools
🪛 YAMLlint (1.37.1)
[warning] 35-35: too many spaces after colon
(colons)
🤖 Prompt for AI Agents
In .github/workflows/flutter.yaml around line 35, the build command pipes
flutter build to grep which can mask failures because pipe exit status isn't
checked; either remove the pipe entirely and let the action surface build
errors, or ensure the shell runs with pipefail (use bash and set -o pipefail
before the command) so the workflow fails when flutter build fails while still
filtering out deprecated lines.
| try { | ||
| return Tree( | ||
| id: _toInt(userData[0]), | ||
| latitude: _toInt(userData[1]), | ||
| longitude: _toInt(userData[2]), | ||
| planting: _toInt(userData[3]), | ||
| death: _toInt(userData[4]), | ||
| species: userData[5]?.toString() ?? '', | ||
| imageUri: userData[6]?.toString() ?? '', | ||
| qrIpfsHash: userData[7]?.toString() ?? '', | ||
| metadata: userData[8]?.toString() ?? '', | ||
| photos: userData[9] is List | ||
| ? List<String>.from(userData[9].map((p) => p.toString())) | ||
| : [], | ||
| geoHash: userData[10]?.toString() ?? '', | ||
| ancestors: userData[11] is List | ||
| ? List<String>.from(userData[11].map((a) => a.toString())) | ||
| : [], | ||
| lastCareTimestamp: _toInt(userData[12]), | ||
| careCount: _toInt(userData[13]), | ||
| verifiers: List<String>.from(verifiers.map((a) => a.toString())), | ||
| owner: owner, | ||
| ); | ||
| } catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Validate contract payload shape before indexing.
Direct indexing assumes ≥14 items; add a length guard to avoid RangeError and return a safe fallback with logging.
try {
+ if (userData.length < 14) {
+ logger.e('Tree.fromContractData: insufficient userData length=${userData.length}');
+ return Tree(
+ id: 0,
+ latitude: 0,
+ longitude: 0,
+ planting: 0,
+ death: 0,
+ species: 'Unknown',
+ imageUri: '',
+ qrIpfsHash: '',
+ metadata: '',
+ photos: const [],
+ geoHash: '',
+ ancestors: const [],
+ lastCareTimestamp: 0,
+ careCount: 0,
+ verifiers: const [],
+ owner: '',
+ );
+ }
return Tree(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| return Tree( | |
| id: _toInt(userData[0]), | |
| latitude: _toInt(userData[1]), | |
| longitude: _toInt(userData[2]), | |
| planting: _toInt(userData[3]), | |
| death: _toInt(userData[4]), | |
| species: userData[5]?.toString() ?? '', | |
| imageUri: userData[6]?.toString() ?? '', | |
| qrIpfsHash: userData[7]?.toString() ?? '', | |
| metadata: userData[8]?.toString() ?? '', | |
| photos: userData[9] is List | |
| ? List<String>.from(userData[9].map((p) => p.toString())) | |
| : [], | |
| geoHash: userData[10]?.toString() ?? '', | |
| ancestors: userData[11] is List | |
| ? List<String>.from(userData[11].map((a) => a.toString())) | |
| : [], | |
| lastCareTimestamp: _toInt(userData[12]), | |
| careCount: _toInt(userData[13]), | |
| verifiers: List<String>.from(verifiers.map((a) => a.toString())), | |
| owner: owner, | |
| ); | |
| } catch (e) { | |
| try { | |
| if (userData.length < 14) { | |
| logger.e('Tree.fromContractData: insufficient userData length=${userData.length}'); | |
| return Tree( | |
| id: 0, | |
| latitude: 0, | |
| longitude: 0, | |
| planting: 0, | |
| death: 0, | |
| species: 'Unknown', | |
| imageUri: '', | |
| qrIpfsHash: '', | |
| metadata: '', | |
| photos: const [], | |
| geoHash: '', | |
| ancestors: const [], | |
| lastCareTimestamp: 0, | |
| careCount: 0, | |
| verifiers: const [], | |
| owner: '', | |
| ); | |
| } | |
| return Tree( | |
| id: _toInt(userData[0]), | |
| latitude: _toInt(userData[1]), | |
| longitude: _toInt(userData[2]), | |
| planting: _toInt(userData[3]), | |
| death: _toInt(userData[4]), | |
| species: userData[5]?.toString() ?? '', | |
| imageUri: userData[6]?.toString() ?? '', | |
| qrIpfsHash: userData[7]?.toString() ?? '', | |
| metadata: userData[8]?.toString() ?? '', | |
| photos: userData[9] is List | |
| ? List<String>.from(userData[9].map((p) => p.toString())) | |
| : [], | |
| geoHash: userData[10]?.toString() ?? '', | |
| ancestors: userData[11] is List | |
| ? List<String>.from(userData[11].map((a) => a.toString())) | |
| : [], | |
| lastCareTimestamp: _toInt(userData[12]), | |
| careCount: _toInt(userData[13]), | |
| verifiers: List<String>.from(verifiers.map((a) => a.toString())), | |
| owner: owner, | |
| ); | |
| } catch (e) { |
| // ignore: use_build_context_synchronously | ||
| final provider = Provider.of<MintNftProvider>(context, listen: false); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using BuildContext after await; capture provider before the async call
Move the provider lookup above the first await and drop the lint suppression.
Apply:
Future<void> _pickAndUploadImages() async {
try {
- final List<XFile> images = await _picker.pickMultiImage();
+ final provider = Provider.of<MintNftProvider>(context, listen: false);
+ final List<XFile> images = await _picker.pickMultiImage();
if (images.isEmpty) return;
logger.d('Selected ${images.length} images for upload');
- // ignore: use_build_context_synchronously
- final provider = Provider.of<MintNftProvider>(context, listen: false);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ignore: use_build_context_synchronously | |
| final provider = Provider.of<MintNftProvider>(context, listen: false); | |
| Future<void> _pickAndUploadImages() async { | |
| try { | |
| // Capture the provider before any await to avoid use_build_context_synchronously issues | |
| final provider = Provider.of<MintNftProvider>(context, listen: false); | |
| final List<XFile> images = await _picker.pickMultiImage(); | |
| if (images.isEmpty) return; | |
| logger.d('Selected ${images.length} images for upload'); | |
| // ...rest of the upload logic, using `provider` as needed... | |
| } catch (e) { | |
| // error handling... | |
| } | |
| } |
🤖 Prompt for AI Agents
In lib/pages/mint_nft/mint_nft_images.dart around lines 46 to 48, the code
obtains Provider.of<MintNftProvider>(context, listen: false) after an await and
suppresses the lint; move the provider lookup to before the first await so you
capture the provider synchronously, remove the "// ignore:
use_build_context_synchronously" suppression, and then use the captured provider
variable afterwards (avoid calling Provider.of with the context after any
await).
| Future<void> loadTreeDetails() async { | ||
| final walletProvider = Provider.of<WalletProvider>(context, listen: false); | ||
| loggedInUser = walletProvider.currentAddress.toString(); | ||
| setState(() { | ||
| _isLoading = true; | ||
| }); | ||
| final result = await ContractReadFunctions.getTreeNFTInfo( | ||
| walletProvider: walletProvider, | ||
| id: _toInt(widget.treeId), | ||
| offset: TREE_VERIFIERS_OFFSET, | ||
| limit: TREE_VERIFIERS_LIMIT); | ||
| if (result.success && result.data != null) { | ||
| final List<dynamic> treesData = result.data['details'] ?? []; | ||
| final List<dynamic> verifiersData = result.data['verifiers'] ?? []; | ||
| final String owner = result.data['owner'].toString(); | ||
| treeDetails = Tree.fromContractData(treesData, verifiersData, owner); | ||
| logger.d("Verifiers data: $verifiersData"); | ||
| canVerify = true; | ||
| for (var verifier in verifiersData) { | ||
| if (verifier[0].toString().toLowerCase() == | ||
| loggedInUser?.toLowerCase()) { | ||
| canVerify = false; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| logger.d("Tree Details hot: ${treeDetails?.verifiers}"); | ||
| setState(() { | ||
| _isLoading = false; | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null deref risk: treeDetails! used even on read failure.
If the contract read fails, _isLoading becomes false but treeDetails may remain null; _buildMapSection and others force-unwrap it.
Guard in build and/or set an error state. Example change in build:
- body: _isLoading
- ? const Center(child: CircularProgressIndicator())
- : SingleChildScrollView(
+ body: _isLoading
+ ? const Center(child: CircularProgressIndicator())
+ : (treeDetails == null
+ ? Center(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Text(
+ 'Failed to load tree details.',
+ style: TextStyle(color: Colors.red.shade700),
+ ),
+ ),
+ )
+ : SingleChildScrollView(
...
- ));
+ )));Also consider surfacing result.errorMessage to the user.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In lib/pages/tree_details_page.dart around lines 47 to 77, the contract read
failure path leaves treeDetails null while the UI later force-unwraps it,
risking a null dereference; update loadTreeDetails to handle errors by checking
result.success and, on failure, set an explicit error state (e.g., an
_errorMessage) and ensure treeDetails remains null or gets a safe default, then
call setState to update _isLoading and the error flag; additionally, surface
result.errorMessage to the user (via a Snackbar or showing the error text in
build) and modify build to guard all uses of treeDetails (return a loading/error
placeholder when null) instead of force-unwrapping.
| Widget treeVerifiersSection(String? loggedInUser, Tree? treeDetails, | ||
| Function loadTreeDetails, BuildContext context) { | ||
| logger.d("=== treeVerifiersSection ==="); | ||
| logger.d("treeDetails: $treeDetails"); | ||
| logger.d("treeDetails?.verifiers: ${treeDetails?.verifiers}"); | ||
| logger.d("verifiers length: ${treeDetails?.verifiers.length}"); | ||
|
|
||
| final themeColors = _getThemeColors(context); | ||
|
|
||
| if (treeDetails?.verifiers == null || treeDetails!.verifiers.isEmpty) { | ||
| logger.d("No verifiers found, showing empty state"); | ||
| return Container( | ||
| width: double.infinity, | ||
| margin: const EdgeInsets.symmetric(vertical: 16.0), | ||
| padding: const EdgeInsets.all(16.0), | ||
| decoration: BoxDecoration( | ||
| color: Colors.grey.shade50, | ||
| borderRadius: BorderRadius.circular(12.0), | ||
| border: Border.all(color: Colors.grey.shade200), | ||
| ), | ||
| child: Column( | ||
| children: [ | ||
| Icon( | ||
| Icons.group_off, | ||
| color: Colors.grey.shade400, | ||
| size: 32, | ||
| ), | ||
| const SizedBox(height: 8), | ||
| Text( | ||
| "No Verifiers Yet", | ||
| style: TextStyle( | ||
| color: Colors.grey.shade600, | ||
| fontSize: 16, | ||
| fontWeight: FontWeight.w500, | ||
| ), | ||
| ), | ||
| const SizedBox(height: 4), | ||
| Text( | ||
| "This tree hasn't been verified by anyone", | ||
| style: TextStyle( | ||
| color: Colors.grey.shade500, | ||
| fontSize: 14, | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| logger.d( | ||
| "Rendering verifiers section with ${treeDetails.verifiers.length} verifiers"); | ||
| final isOwner = treeDetails.owner == loggedInUser; | ||
|
|
||
| return Container( | ||
| width: double.infinity, | ||
| margin: const EdgeInsets.symmetric(vertical: 16.0), | ||
| padding: const EdgeInsets.all(16.0), | ||
| decoration: BoxDecoration( | ||
| color: themeColors['primaryLight'], | ||
| borderRadius: BorderRadius.circular(12.0), | ||
| border: Border.all(color: themeColors['primaryBorder']!), | ||
| ), | ||
| child: Column( | ||
| crossAxisAlignment: CrossAxisAlignment.start, | ||
| children: [ | ||
| Row( | ||
| children: [ | ||
| Icon( | ||
| Icons.verified_user, | ||
| color: themeColors['primary'], | ||
| size: 24, | ||
| ), | ||
| const SizedBox(width: 8), | ||
| Text( | ||
| "Tree Verifiers", | ||
| style: TextStyle( | ||
| color: themeColors['primary'], | ||
| fontSize: 18, | ||
| fontWeight: FontWeight.bold, | ||
| ), | ||
| ), | ||
| const Spacer(), | ||
| Container( | ||
| padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||
| decoration: BoxDecoration( | ||
| color: themeColors['primary']!, | ||
| borderRadius: BorderRadius.circular(12), | ||
| ), | ||
| child: Text( | ||
| "${treeDetails.verifiers.length}", | ||
| style: TextStyle( | ||
| color: themeColors['primary'], | ||
| fontWeight: FontWeight.bold, | ||
| ), | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| const SizedBox(height: 12), | ||
| Text( | ||
| isOwner | ||
| ? "Tap any verifier to view details • Tap ✕ to remove (owner only)" | ||
| : "Tap any verifier to view verification details", | ||
| style: TextStyle( | ||
| color: themeColors['primary']!, | ||
| fontSize: 12, | ||
| ), | ||
| ), | ||
| const SizedBox(height: 16), | ||
| ...treeDetails.verifiers.asMap().entries.map((entry) { | ||
| final index = entry.key; | ||
| final verifier = entry.value; | ||
| return _buildVerifierCard( | ||
| verifier, index, isOwner, loadTreeDetails, treeDetails, context); | ||
| }), | ||
| ], | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Model duplication; centralize Tree and Verifier.
This file defines full models; another Tree exists in user_nfts_widget.dart, and PR mentions lib/models/tree_details.dart. Divergence risk is high.
Move both models to lib/models/ and import here and in other widgets. Keep a single fromContractData path.
| padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||
| decoration: BoxDecoration( | ||
| color: themeColors['primary']!, | ||
| borderRadius: BorderRadius.circular(12), | ||
| ), | ||
| child: Text( | ||
| "${treeDetails.verifiers.length}", | ||
| style: TextStyle( | ||
| color: themeColors['primary'], | ||
| fontWeight: FontWeight.bold, | ||
| ), | ||
| ), | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unreadable count chip: text color matches background.
Chip background and text both use themeColors['primary'], making the text invisible.
- child: Text(
- "${treeDetails.verifiers.length}",
- style: TextStyle(
- color: themeColors['primary'],
- fontWeight: FontWeight.bold,
- ),
- ),
+ child: Text(
+ "${treeDetails.verifiers.length}",
+ style: const TextStyle(
+ color: Colors.white,
+ fontWeight: FontWeight.bold,
+ ),
+ ),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: themeColors['primary']!, | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| child: Text( | |
| "${treeDetails.verifiers.length}", | |
| style: TextStyle( | |
| color: themeColors['primary'], | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), | |
| padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: themeColors['primary']!, | |
| borderRadius: BorderRadius.circular(12), | |
| ), | |
| child: Text( | |
| "${treeDetails.verifiers.length}", | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), |
🤖 Prompt for AI Agents
In lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart around
lines 421 to 433, the count chip sets both background and text color to
themeColors['primary'], making the text unreadable; change the Text style color
to a contrasting value (for example themeColors['onPrimary'] or Colors.white) or
compute a readable foreground from the background, and ensure the chosen key
exists (fall back to a safe default) so the count remains visible against the
primary-colored background.
| Container( | ||
| padding: const EdgeInsets.symmetric( | ||
| horizontal: 6, vertical: 2), | ||
| decoration: BoxDecoration( | ||
| color: themeColors['primary']!, | ||
| borderRadius: BorderRadius.circular(8), | ||
| ), | ||
| child: Row( | ||
| mainAxisSize: MainAxisSize.min, | ||
| children: [ | ||
| Icon( | ||
| Icons.photo_library, | ||
| size: 10, | ||
| color: themeColors['primary'], | ||
| ), | ||
| const SizedBox(width: 2), | ||
| Text( | ||
| "${verifier.proofHashes.length}", | ||
| style: TextStyle( | ||
| fontSize: 10, | ||
| color: themeColors['primary'], | ||
| fontWeight: FontWeight.w600, | ||
| ), | ||
| ), | ||
| ], | ||
| ), | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Proof badge text/icon also match background.
Same issue: foreground equals background.
- decoration: BoxDecoration(
- color: themeColors['primary']!,
+ decoration: BoxDecoration(
+ color: themeColors['primary']!,
borderRadius: BorderRadius.circular(8),
),
@@
- Icon(
+ Icon(
Icons.photo_library,
- size: 10,
- color: themeColors['primary']!,
+ size: 10,
+ color: Colors.white,
),
@@
- Text(
+ Text(
"${verifier.proofHashes.length}",
- style: TextStyle(
- fontSize: 10,
- color: themeColors['primary'],
- fontWeight: FontWeight.w600,
- ),
+ style: const TextStyle(
+ fontSize: 10,
+ color: Colors.white,
+ fontWeight: FontWeight.w600,
+ ),Also applies to: 560-572
🤖 Prompt for AI Agents
In lib/widgets/nft_display_utils/tree_nft_details_verifiers_widget.dart around
lines 549-575 (and similarly 560-572), the badge's icon and text are using the
same color as the badge background so they aren't visible; update the Icon and
Text style to use a contrasting foreground color (for example
themeColors['onPrimary'] or Colors.white) instead of themeColors['primary'], and
apply the same fix to the other occurrence so both icon and number are readable
against the badge background.
| // ignore: unused_field | ||
| int _currentPage = 0; | ||
| // ignore: unused_field | ||
| final int _itemsPerPage = 10; | ||
| int _totalCount = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Unused pagination fields; wire them in or remove.
_itemsPerPage is never used (suppressed via ignore). Either use it for the contract call or drop both the field and ignores.
Apply this diff to prepare for real pagination:
- // ignore: unused_field
- int _currentPage = 0;
- // ignore: unused_field
- final int _itemsPerPage = 10;
+ int _currentPage = 0;
+ final int _itemsPerPage = 10;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // ignore: unused_field | |
| int _currentPage = 0; | |
| // ignore: unused_field | |
| final int _itemsPerPage = 10; | |
| int _totalCount = 0; | |
| int _currentPage = 0; | |
| final int _itemsPerPage = 10; | |
| int _totalCount = 0; |
🤖 Prompt for AI Agents
In lib/widgets/nft_display_utils/user_nfts_widget.dart around lines 121-125, the
pagination fields are declared but _itemsPerPage is ignored and unused; remove
the ignore comments and wire _itemsPerPage and _currentPage into the NFT fetch
logic so the contract call/request uses limit=_itemsPerPage and
offset=_currentPage * _itemsPerPage (or equivalent), update _totalCount based on
the returned total from the call, and remove any unused-field suppressions;
alternatively if pagination is not yet supported, delete both _currentPage and
_itemsPerPage and their ignore comments and adjust code to not reference them.
| onPressed: () { | ||
| final walletProvider = | ||
| Provider.of<WalletProvider>(context, listen: false); | ||
| walletProvider.connectWallet(); | ||
| }, | ||
| child: const Text('Connect Wallet'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Button doesn’t await or open a wallet; users won’t see any prompt.
connectWallet() returns a WC URI that must be deep-linked into a chosen wallet. Hook it up and handle errors.
- ElevatedButton(
- onPressed: () {
- final walletProvider =
- Provider.of<WalletProvider>(context, listen: false);
- walletProvider.connectWallet();
- },
+ ElevatedButton(
+ onPressed: () async {
+ final walletProvider =
+ Provider.of<WalletProvider>(context, listen: false);
+ final uri = await walletProvider.connectWallet();
+ if (uri != null && walletProvider.walletOptions.isNotEmpty) {
+ // Simple default: open first known wallet; or present a chooser UI.
+ await walletProvider.openWallet(walletProvider.walletOptions.first, uri);
+ } else {
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Unable to start wallet connection')),
+ );
+ }
+ }
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onPressed: () { | |
| final walletProvider = | |
| Provider.of<WalletProvider>(context, listen: false); | |
| walletProvider.connectWallet(); | |
| }, | |
| child: const Text('Connect Wallet'), | |
| ElevatedButton( | |
| onPressed: () async { | |
| final walletProvider = | |
| Provider.of<WalletProvider>(context, listen: false); | |
| final uri = await walletProvider.connectWallet(); | |
| if (uri != null && walletProvider.walletOptions.isNotEmpty) { | |
| // Simple default: open first known wallet; or present a chooser UI. | |
| await walletProvider.openWallet( | |
| walletProvider.walletOptions.first, | |
| uri, | |
| ); | |
| } else { | |
| if (context.mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar( | |
| content: Text('Unable to start wallet connection'), | |
| ), | |
| ); | |
| } | |
| } | |
| }, | |
| child: const Text('Connect Wallet'), | |
| ), |
🤖 Prompt for AI Agents
In lib/widgets/wallet_not_connected_widget.dart around lines 26 to 31, the
button callback calls walletProvider.connectWallet() but doesn't await the
returned WalletConnect (WC) URI or deep-link it into a wallet app, so no wallet
prompt appears; update the onPressed to await connectWallet(), capture the
returned WC URI, then open it with the platform URL launcher (or pass it to the
platform-specific deep-link handler) to start the wallet app, and wrap this in
try/catch to handle and log errors and show user feedback (SnackBar/dialog) and
provide a fallback like copying the URI to clipboard if launching fails.
✨ New Features
User NFT Portfolio Page - Complete dashboard displaying owned NFTs with status indicators
Verification Management - NFT owners can add/remove verifiers through intuitive modals
Interactive Plantation Map - Static map displaying NFT plantation locations
Enhanced Modals - Streamlined "Add Verification" and "View Verifications" interfaces
🔧 Key Improvements
Responsive card-based layout for NFT display
Modal-driven UI patterns for consistent UX
Owner-controlled verification system
Geographic visualization of agricultural assets
UI:
Summary by CodeRabbit