diff --git a/lib/pages/home.dart b/lib/pages/home.dart index c8f4ada..6e96618 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -30,6 +30,7 @@ class _HomeScreenState extends State { String? _balance; SolanaClient? client; String imageProfile = ''; + bool loading = true; final storage = const FlutterSecureStorage(); @@ -37,7 +38,6 @@ class _HomeScreenState extends State { void initState() { super.initState(); _readPk(); - getInitialInfo(); } @override @@ -56,7 +56,9 @@ class _HomeScreenState extends State { SizedBox(height: statusBarHeight), GestureDetector( onTap: () { - GoRouter.of(context).replace('/profile'); + if (!loading && _publicKey != null) { + GoRouter.of(context).replace('/profile'); + } }, child: Card( color: Colors.white, @@ -97,7 +99,7 @@ class _HomeScreenState extends State { ), const SizedBox(height: 3), Text( - _publicKey == null + loading || _publicKey == null ? 'Loading...' : '${_publicKey!.substring(0, 6)}...${_publicKey!.substring(_publicKey!.length - 6, _publicKey!.length)}', maxLines: 1, @@ -186,16 +188,23 @@ class _HomeScreenState extends State { ), child: TabBarView( physics: const NeverScrollableScrollPhysics(), - children: [ - const MiniGamesScreen(), - const RankingScreen(), - MydinogrowScreen( - address: _publicKey ?? '', getBalance: () => _getBalance()), - WalletScreen( - address: _publicKey ?? '', - balance: _balance, - getBalance: () => _getBalance()), - ], + children: loading + ? List.filled( + 4, + const Center( + child: CircularProgressIndicator(), + )) + : [ + const MiniGamesScreen(), + const RankingScreen(), + MydinogrowScreen( + address: _publicKey ?? '', + getBalance: () => _getBalance()), + WalletScreen( + address: _publicKey ?? '', + balance: _balance, + getBalance: () => _getBalance()), + ], ), ), ), @@ -214,25 +223,35 @@ class _HomeScreenState extends State { } void _initializeClient() async { - await dotenv.load(fileName: ".env"); + try { + setState(() { + loading = true; + }); + } finally { + await dotenv.load(fileName: ".env"); - client = SolanaClient( - rpcUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_URL'].toString()), - websocketUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_WSS'].toString()), - ); - _getBalance(); + client = SolanaClient( + rpcUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_URL'].toString()), + websocketUrl: Uri.parse(dotenv.env['QUICKNODE_RPC_WSS'].toString()), + ); + _getBalance(); + } } void _getBalance() async { - setState(() { - _balance = null; - }); - final getBalance = await client?.rpcClient - .getBalance(_publicKey!, commitment: Commitment.confirmed); - final balance = (getBalance!.value) / lamportsPerSol; - setState(() { - _balance = balance.toString(); - }); + try { + setState(() { + _balance = null; + }); + final getBalance = await client?.rpcClient + .getBalance(_publicKey!, commitment: Commitment.confirmed); + final balance = (getBalance!.value) / lamportsPerSol; + setState(() { + _balance = balance.toString(); + }); + } finally { + getInitialInfo(); + } } Future getInitialInfo() async { @@ -260,6 +279,10 @@ class _HomeScreenState extends State { ..backgroundColor = Colors.red; SnakAlertWidget().show(alert); + } finally { + setState(() { + loading = false; + }); } } diff --git a/lib/pages/login.dart b/lib/pages/login.dart index 480d722..1bcd4ec 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -153,15 +153,12 @@ class _LoginScreenState extends State { } GoRouter.of(context).pushReplacement("/home"); } else { - const snackBar = SnackBar( - content: Text( - 'Error: Invalid Password', - style: TextStyle(color: Colors.white), - ), - backgroundColor: Colors.red, - ); + ShowProps alert = ShowProps() + ..text = 'Error: Invalid Password' + ..context = context + ..backgroundColor = Colors.red; - ScaffoldMessenger.of(context).showSnackBar(snackBar); + SnakAlertWidget().show(alert); setState(() { _loading = false; diff --git a/lib/pages/profile.dart b/lib/pages/profile.dart index 49b79b2..3241e22 100644 --- a/lib/pages/profile.dart +++ b/lib/pages/profile.dart @@ -1,8 +1,10 @@ import 'dart:typed_data'; +import 'package:flutter/services.dart'; import 'package:solana/dto.dart'; import 'package:dinogrow/pages/upload_to_ipfs.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:image_picker/image_picker.dart'; import 'package:go_router/go_router.dart'; import 'dart:async'; @@ -12,6 +14,7 @@ import 'package:solana_web3/solana_web3.dart' as web3; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:solana/solana.dart'; import 'package:solana/solana.dart' as solana; +import 'package:url_launcher/url_launcher.dart'; import '../ui/widgets/widgets.dart'; import 'package:solana/anchor.dart' as solana_anchor; import 'package:solana/encoder.dart' as solana_encoder; @@ -26,6 +29,8 @@ class ProfileScreen extends StatefulWidget { } class _ProfileScreenState extends State { + final GlobalKey scaffoldMessengerKey = + GlobalKey(); final _formKey = GlobalKey(); final nicknameController = TextEditingController(); final bioController = TextEditingController(); @@ -62,121 +67,128 @@ class _ProfileScreenState extends State { } return WillPopScope( onWillPop: () async => false, - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: AppBar( - shadowColor: Colors.transparent, - backgroundColor: Colors.transparent, - leading: IconButton( - icon: const Icon( - Icons.chevron_left, - color: Colors.white, - size: 30, + child: ScaffoldMessenger( + key: scaffoldMessengerKey, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + shadowColor: Colors.transparent, + backgroundColor: Colors.transparent, + leading: IconButton( + icon: const Icon( + Icons.chevron_left, + color: Colors.white, + size: 30, + ), + onPressed: () { + GoRouter.of(context).replace('/home'); + }, ), - onPressed: () { - GoRouter.of(context).replace('/home'); - }, ), - ), - body: SingleChildScrollView( - child: Container( - height: MediaQuery.of(context).size.height, - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage("assets/images/ui/intro_jungle_bg.png"), - fit: BoxFit.cover, + body: SingleChildScrollView( + child: Container( + height: MediaQuery.of(context).size.height, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage("assets/images/ui/intro_jungle_bg.png"), + fit: BoxFit.cover, + ), ), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Expanded(child: SizedBox()), - GestureDetector( - onTap: pickImage, - child: Container( - width: 150, - height: 150, - decoration: BoxDecoration( - color: Colors.black, - image: DecorationImage( - scale: 3, - fit: localImgUrl.isNotEmpty - ? BoxFit.cover - : BoxFit.scaleDown, - image: localImgUrl.isNotEmpty - ? (imageProfile.path.isNotEmpty - ? Image.file( - imageProfile, - fit: BoxFit.cover, - ).image - : Image.network(beforeImageProfile).image) - : const AssetImage( - 'assets/images/icons/add_image.png'), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Expanded(child: SizedBox()), + GestureDetector( + onTap: pickImage, + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + color: Colors.black, + image: DecorationImage( + scale: 3, + fit: localImgUrl.isNotEmpty + ? BoxFit.cover + : BoxFit.scaleDown, + image: localImgUrl.isNotEmpty + ? (imageProfile.path.isNotEmpty + ? Image.file( + imageProfile, + fit: BoxFit.cover, + ).image + : (beforeImageProfile.isNotEmpty + ? Image.network(beforeImageProfile) + .image + : const AssetImage( + 'assets/images/icons/add_image.png'))) + : const AssetImage( + 'assets/images/icons/add_image.png'), + ), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 6), ), - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 6), ), ), - ), - const Expanded(child: SizedBox()), - const TextBoxWidget( - text: - 'Edit your profile to share it to our community ^.^'), - const Expanded(child: SizedBox()), - Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextFormField( - controller: nicknameController, - decoration: InputDecoration( - labelText: 'Nickname', - filled: true, - fillColor: Colors.black, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + const Expanded(child: SizedBox()), + const TextBoxWidget( + text: + 'Edit your profile to share it to our community ^.^'), + const Expanded(child: SizedBox()), + Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: nicknameController, + decoration: InputDecoration( + labelText: 'Nickname', + filled: true, + fillColor: Colors.black, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), - ), - const SizedBox(height: 12), - TextFormField( - controller: bioController, - decoration: InputDecoration( - labelText: 'Bio', - filled: true, - fillColor: Colors.black, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + const SizedBox(height: 12), + TextFormField( + controller: bioController, + decoration: InputDecoration( + labelText: 'Bio', + filled: true, + fillColor: Colors.black, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), - ), - const SizedBox(height: 12), - TextFormField( - controller: statusController, - decoration: InputDecoration( - labelText: 'Status', - filled: true, - fillColor: Colors.black, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), + const SizedBox(height: 12), + TextFormField( + controller: statusController, + decoration: InputDecoration( + labelText: 'Status', + filled: true, + fillColor: Colors.black, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), ), ), - ), - const SizedBox(height: 30), - IntroButtonWidget( - text: 'Save', - onPressed: () => onWant2Save(context), - ), - ], + const SizedBox(height: 30), + IntroButtonWidget( + text: 'Save', + onPressed: () => onWant2Save(context), + ), + ], + ), ), - ), - const Expanded(child: SizedBox()), - ]), + const Expanded(child: SizedBox()), + ]), + ), ), ), ), @@ -219,6 +231,7 @@ class _ProfileScreenState extends State { ShowProps alert = ShowProps() ..text = 'Error get profile data ($e)' ..context = context + ..scaffoldMessengerKey = scaffoldMessengerKey ..backgroundColor = Colors.red; SnakAlertWidget().show(alert); @@ -236,11 +249,13 @@ class _ProfileScreenState extends State { final imageTemp = File(image.path); setState(() { imageProfile = imageTemp; + beforeImageProfile = ''; }); } catch (e) { ShowProps alert = ShowProps() ..text = 'Failed to pick image: $e' ..context = context + ..scaffoldMessengerKey = scaffoldMessengerKey ..backgroundColor = Colors.red; SnakAlertWidget().show(alert); @@ -290,6 +305,7 @@ class _ProfileScreenState extends State { ShowProps alert = ShowProps() ..text = 'Error: Please fill all fields to continue' ..context = context + ..scaffoldMessengerKey = scaffoldMessengerKey ..backgroundColor = Colors.red; SnakAlertWidget().show(alert); @@ -304,13 +320,24 @@ class _ProfileScreenState extends State { String? cid = ''; - if (imageProfile.uri.toString() != beforeImageProfile) { + if (!imageProfile.existsSync()) { + var bytes = await rootBundle.load('assets/images/icons/no_user.png'); + String tempPath = (await getTemporaryDirectory()).path; + File file = File('$tempPath/profile.png'); + final localImage = await file.writeAsBytes( + bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes)); + + setState(() { + imageProfile = localImage; + }); + } + + if (beforeImageProfile.isEmpty) { cid = await uploadToIPFS(imageProfile); } else { cid = beforeImageProfile.replaceAll( RegExp('https://quicknode.myfilebase.com/ipfs/'), ''); } - //save profile await dotenv.load(fileName: ".env"); @@ -384,24 +411,23 @@ class _ProfileScreenState extends State { commitment: solana.Commitment.confirmed, ); - print('Tx successful with hash: $signature'); + showResultMessage(signature); ShowProps alert = ShowProps() ..text = 'Profile updated' ..context = context + ..scaffoldMessengerKey = scaffoldMessengerKey ..backgroundColor = Colors.green; SnakAlertWidget().show(alert); } catch (e) { - final snackBar = SnackBar( - content: Text( - 'Failed to save data: $e', - style: const TextStyle(color: Colors.white), - ), - backgroundColor: Colors.red, - ); + ShowProps alert = ShowProps() + ..text = 'Failed to save data: $e' + ..context = context + ..backgroundColor = Colors.red + ..scaffoldMessengerKey = scaffoldMessengerKey; - ScaffoldMessenger.of(context).showSnackBar(snackBar); + SnakAlertWidget().show(alert); } finally { setState(() { _loading = false; @@ -453,4 +479,41 @@ class _ProfileScreenState extends State { return null; } } + + showResultMessage(String transaction) { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Profile updated'), + content: Text( + "You can review information on blockchain with this transaction reference: \n\n$transaction"), + actions: [ + TextButton( + onPressed: () { + _launchUrl(transaction); + }, + child: const Text('View transaction'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text("Ok"), + ), + ], + ), + ); + } +} + +Future _launchUrl(String transaction) async { + Uri url = Uri( + scheme: 'https', + host: 'explorer.solana.com', + path: '/tx/$transaction', + queryParameters: {'cluster': 'devnet'}); + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } } diff --git a/lib/pages/ranking/ranking.dart b/lib/pages/ranking/ranking.dart index c919ae0..cdce944 100644 --- a/lib/pages/ranking/ranking.dart +++ b/lib/pages/ranking/ranking.dart @@ -160,83 +160,79 @@ class _RankingScreenState extends State { ); // Recorre las cuentas y muestra los datos - for (var account - in (accounts.length <= 15 ? accounts : accounts.sublist(0, 15))) { - final bytes = account.account.data as BinaryAccountData; + bool rankingCompleted = false; - //Get all data - final decodeAllData = - anchor_types_dino_score.GetScoreArguments.fromBorsh( - bytes.data as Uint8List); + for (var account in accounts) { + try { + if (!rankingCompleted) { + final bytes = account.account.data as BinaryAccountData; - // //Get Score - // final decoderDataScore = anchor_types_dino.DinoScoreArguments.fromBorsh( - // bytes.data as Uint8List); + //Get all data + final decodeAllData = + anchor_types_dino_score.GetScoreArguments.fromBorsh( + bytes.data as Uint8List); - // //Get Game Data - // final decoderDataGame = - // anchor_types_dino_game.DinoGameArguments.fromBorsh( - // bytes.data as Uint8List); + String? localImgUrl = + await storage.read(key: '${decodeAllData.dinokey}'); - String? localImgUrl = - await storage.read(key: '${decodeAllData.dinokey}'); + final findprofileb = + await findprofile('${decodeAllData.playerkey}'); + String nickName = ''; - final findprofileb = await findprofile('${decodeAllData.playerkey}'); - String nickName = ''; + if (findprofileb != null) { + nickName = findprofileb.nickname; + } - if (findprofileb != null) { - nickName = findprofileb.nickname; - } - - if (localImgUrl == null) { - final response = await http.post( - Uri.parse(dotenv.env['QUICKNODE_RPC_URL'].toString()), - headers: { - 'Content-Type': 'application/json', - "x-qn-api-version": '1' - }, - body: jsonEncode({ - "method": "qn_fetchNFTs", - "params": { - "wallet": '${decodeAllData.playerkey}', - "page": 1, - "perPage": 10 - } - })); - - final dataResponse = jsonDecode(response.body); - - if (dataResponse.isNotEmpty) { - final arrayAssets = dataResponse['result']['assets']; - final indexNft = arrayAssets.indexWhere( - (item) => item["tokenAddress"] == '${decodeAllData.dinokey}'); - String imgurl = indexNft > -1 - ? arrayAssets[int.parse('$indexNft')]['imageUrl'] - : ''; - - await storage.write(key: '${decodeAllData.dinokey}', value: imgurl); - - items2save.add(ItemsProps( - nickName: nickName, - imageUrl: imgurl, - dinoPubkey: '${decodeAllData.dinokey}', + ItemsProps data2save = ItemsProps( + playerPubkey: '${decodeAllData.playerkey}', gamescore: '${decodeAllData.score}', - playerPubkey: '${decodeAllData.playerkey}')); - } else { - items2save.add(ItemsProps( - nickName: nickName, imageUrl: '', dinoPubkey: '${decodeAllData.dinokey}', - gamescore: '${decodeAllData.score}', - playerPubkey: '${decodeAllData.playerkey}')); + nickName: nickName); + + if (localImgUrl == null) { + final response = await http.post( + Uri.parse(dotenv.env['QUICKNODE_RPC_URL'].toString()), + headers: { + 'Content-Type': 'application/json', + "x-qn-api-version": '1' + }, + body: jsonEncode({ + "method": "qn_fetchNFTs", + "params": { + "wallet": '${decodeAllData.playerkey}', + "page": 1, + "perPage": 10 + } + })); + + final dataResponse = jsonDecode(response.body); + + if (dataResponse.isNotEmpty) { + final arrayAssets = dataResponse['result']['assets']; + final indexNft = arrayAssets.indexWhere((item) => + item["tokenAddress"] == '${decodeAllData.dinokey}'); + String imgurl = indexNft > -1 + ? arrayAssets[int.parse('$indexNft')]['imageUrl'] + : ''; + + await storage.write( + key: '${decodeAllData.dinokey}', value: imgurl); + + data2save.imageUrl = imgurl; + } + } else { + data2save.imageUrl = localImgUrl; + } + + items2save.add(data2save); + + if (items2save.length >= 15) { + rankingCompleted = true; + } } - } else { - items2save.add(ItemsProps( - nickName: nickName, - imageUrl: localImgUrl, - dinoPubkey: '${decodeAllData.dinokey}', - gamescore: '${decodeAllData.score}', - playerPubkey: '${decodeAllData.playerkey}')); + } catch (e) { + print('User error: $e'); } } diff --git a/lib/ui/widgets/SnakAlert/snak_alert.dart b/lib/ui/widgets/SnakAlert/snak_alert.dart index 3d558e1..ffe7136 100644 --- a/lib/ui/widgets/SnakAlert/snak_alert.dart +++ b/lib/ui/widgets/SnakAlert/snak_alert.dart @@ -4,6 +4,7 @@ class ShowProps { late BuildContext context; late String text; late Color backgroundColor; + late GlobalKey? scaffoldMessengerKey; } class SnakAlertWidget { @@ -15,6 +16,11 @@ class SnakAlertWidget { ), backgroundColor: props.backgroundColor, ); - ScaffoldMessenger.of(props.context).showSnackBar(snackBar); + + if (props.scaffoldMessengerKey != null) { + props.scaffoldMessengerKey?.currentState?.showSnackBar(snackBar); + } else { + ScaffoldMessenger.of(props.context).showSnackBar(snackBar); + } } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c1b6dae..3ef8816 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,11 +8,13 @@ import Foundation import connectivity_plus import file_selector_macos import flutter_secure_storage_macos +import path_provider_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index abe4bb5..be54ae7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -688,6 +688,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: @@ -704,6 +752,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" plugin_platform_interface: dependency: transitive description: @@ -997,6 +1053,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + url: "https://pub.dev" + source: hosted + version: "5.0.9" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9915bce..52f2ee4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dinogrow description: 2D blockchain game about growing dinosaurs. -version: 1.0.3+6 +version: 1.0.5+8 environment: sdk: ">=3.0.2 <4.0.0" @@ -25,6 +25,7 @@ dependencies: solana_web3: ^0.0.8 solana_common: ^0.0.5 image_picker: ^1.0.4 + path_provider: ^2.1.1 dev_dependencies: flutter_test: