diff --git a/assets/bc-logos/Archethic.svg b/assets/bc-logos/Archethic.svg new file mode 100644 index 000000000..04180bb22 --- /dev/null +++ b/assets/bc-logos/Archethic.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/assets/bc-logos/BNB.svg b/assets/bc-logos/BNB.svg new file mode 100644 index 000000000..d39bfa1da --- /dev/null +++ b/assets/bc-logos/BNB.svg @@ -0,0 +1,14 @@ + + + BSC + + + + + + + + + + + \ No newline at end of file diff --git a/assets/bc-logos/EURe.svg b/assets/bc-logos/EURe.svg new file mode 100644 index 000000000..5f51e5b0f --- /dev/null +++ b/assets/bc-logos/EURe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/bc-logos/Ethereum.svg b/assets/bc-logos/Ethereum.svg new file mode 100644 index 000000000..171c0b605 --- /dev/null +++ b/assets/bc-logos/Ethereum.svg @@ -0,0 +1,19 @@ + + + Ethereum + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/bc-logos/Matic.svg b/assets/bc-logos/Matic.svg new file mode 100644 index 000000000..9cd801399 --- /dev/null +++ b/assets/bc-logos/Matic.svg @@ -0,0 +1,21 @@ + + + Polygon + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/application/market_price.dart b/lib/application/market_price.dart index 37129e2a6..95213aaf4 100644 --- a/lib/application/market_price.dart +++ b/lib/application/market_price.dart @@ -1,4 +1,3 @@ -import 'package:aewallet/application/oracle/provider.dart'; import 'package:aewallet/application/settings/settings.dart'; import 'package:aewallet/domain/models/core/result.dart'; import 'package:aewallet/domain/models/market_price.dart'; @@ -8,6 +7,8 @@ import 'package:aewallet/infrastructure/repositories/market/archethic_oracle_uco import 'package:aewallet/infrastructure/repositories/market/coingecko_uco_market.dart'; import 'package:aewallet/infrastructure/repositories/market/local_uco_market.dart'; import 'package:aewallet/model/available_currency.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -43,7 +44,7 @@ Future _selectedCurrencyMarketPrice(Ref ref) async { if (currency.name == 'usd') { final archethicOracleUCO = - ref.watch(ArchethicOracleUCOProviders.archethicOracleUCO); + ref.watch(aedappfm.ArchethicOracleUCOProviders.archethicOracleUCO); return MarketPrice( amount: archethicOracleUCO.usd, lastLoading: archethicOracleUCO.timestamp, @@ -52,7 +53,7 @@ Future _selectedCurrencyMarketPrice(Ref ref) async { } else { if (currency.name == 'eur') { final archethicOracleUCO = - ref.watch(ArchethicOracleUCOProviders.archethicOracleUCO); + ref.watch(aedappfm.ArchethicOracleUCOProviders.archethicOracleUCO); return MarketPrice( amount: archethicOracleUCO.eur, lastLoading: archethicOracleUCO.timestamp, diff --git a/lib/application/market_price.g.dart b/lib/application/market_price.g.dart index ff361fc16..5eca77a65 100644 --- a/lib/application/market_price.g.dart +++ b/lib/application/market_price.g.dart @@ -191,7 +191,7 @@ class _CurrencyMarketPriceProviderElement } String _$selectedCurrencyMarketPriceHash() => - r'6b813030eaf80361c53bed7ad5e9fe9b4447ce88'; + r'916fbf25a847d2d714b9c5bfe2bf5a6eeb277e08'; /// See also [_selectedCurrencyMarketPrice]. @ProviderFor(_selectedCurrencyMarketPrice) diff --git a/lib/application/nft/nft.dart b/lib/application/nft/nft.dart index 23259bb53..2755a9ec7 100644 --- a/lib/application/nft/nft.dart +++ b/lib/application/nft/nft.dart @@ -1,20 +1,14 @@ -import 'dart:convert'; -import 'dart:typed_data'; - +import 'package:aewallet/infrastructure/repositories/nft/nft.repository.dart'; import 'package:aewallet/model/blockchain/keychain_secured_infos.dart'; import 'package:aewallet/model/blockchain/token_information.dart'; import 'package:aewallet/model/data/account_token.dart'; import 'package:aewallet/model/keychain_service_keypair.dart'; -import 'package:aewallet/service/app_service.dart'; -import 'package:aewallet/util/get_it_instance.dart'; -import 'package:archethic_lib_dart/archethic_lib_dart.dart'; -import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'nft.g.dart'; @riverpod -NFTRepository _nftRepository(_NftRepositoryRef ref) => NFTRepository(); +NFTRepositoryImpl _nftRepository(_NftRepositoryRef ref) => NFTRepositoryImpl(); @riverpod Future _getNFTInfo( @@ -51,223 +45,6 @@ Future<(List, List)> _getNFTList( .getNFTList(address, nameAccount, keychainSecuredInfos); } -class NFTRepository { - static final _logger = Logger('NFTRepository'); - - Future isAccountOwner( - String accountAddress, - String tokenAddress, - String tokenId, - ) async { - final accountLastAddressMap = - await sl.get().getLastTransaction([accountAddress]); - var accounLastAddress = ''; - if (accountLastAddressMap[accountAddress] == null || - accountLastAddressMap[accountAddress]!.address == null || - accountLastAddressMap[accountAddress]!.address!.address == null) { - accounLastAddress = accountAddress; - } else { - accounLastAddress = - accountLastAddressMap[accountAddress]!.address!.address!; - } - - final accountLastInputsMap = - await sl.get().getTransactionInputs( - [accounLastAddress], - request: 'amount, from, tokenAddress, spent, timestamp, type, tokenId', - ); - - if (accountLastInputsMap[accounLastAddress] == null) { - return false; - } - - final _tokenId = int.tryParse(tokenId); - - final accountLastInputs = accountLastInputsMap[accounLastAddress]; - for (final input in accountLastInputs!) { - if (_tokenId == null) { - if (input.tokenAddress == tokenAddress) { - return true; - } - } else { - if (input.tokenAddress == tokenAddress && input.tokenId == _tokenId) { - return true; - } - } - } - - return false; - } - - Future getNFTInfo( - String address, - KeychainServiceKeyPair keychainServiceKeyPair, - ) async { - final tokenMap = await sl.get().getToken( - [address], - ); - - if (tokenMap.isEmpty || - tokenMap[address] == null || - tokenMap[address]!.type != 'non-fungible') { - return null; - } - - final token = tokenMap[address]!; - - final tokenProperties = {...token.properties}; - - if (token.ownerships != null && token.ownerships!.isNotEmpty) { - tokenProperties.addAll( - _tokenPropertiesDecryptedSecret( - keypair: keychainServiceKeyPair, - ownerships: token.ownerships!, - ), - ); - } - final tokenInformation = TokenInformation( - address: address, - name: token.name, - id: token.id, - type: token.type, - decimals: token.decimals, - supply: fromBigInt(token.supply).toDouble(), - symbol: token.symbol, - tokenProperties: tokenProperties, - tokenCollection: token.collection, - ); - return tokenInformation; - } - - Future<(List, List)> getNFTList( - String address, - String nameAccount, - KeychainSecuredInfos keychainSecuredInfos, - ) async { - final balanceMap = await sl.get().fetchBalance([address]); - final balance = balanceMap[address]; - final nftList = []; - final nftCollectionList = []; - - final tokenAddressList = []; - if (balance == null) { - return (nftList, nftCollectionList); - } - - for (final tokenBalance in balance.token) { - if (tokenBalance.address != null) { - tokenAddressList.add(tokenBalance.address!); - } - } - - final tokenMap = await sl.get().getToken( - tokenAddressList.toSet().toList(), - ); - - // TODO(reddwarf03): temporaly section -> need https://github.com/archethic-foundation/archethic-node/issues/714 - - final secretMap = await sl.get().getTransaction( - tokenAddressList.toSet().toList(), - request: - 'data { ownerships { authorizedPublicKeys { encryptedSecretKey, publicKey } secret } }', - ); - - for (final tokenBalance in balance.token) { - final token = tokenMap[tokenBalance.address]; - - if (token == null || token.type != 'non-fungible') { - continue; - } - - final newProperties = {...token.properties}; - - if (secretMap[tokenBalance.address] != null && - secretMap[tokenBalance.address]!.data != null && - secretMap[tokenBalance.address]!.data!.ownerships.isNotEmpty) { - newProperties.addAll( - _tokenPropertiesDecryptedSecret( - keypair: keychainSecuredInfos.services[nameAccount]!.keyPair!, - ownerships: secretMap[tokenBalance.address]!.data!.ownerships, - ), - ); - } - - final collectionWithTokenId = >[]; - var numTokenId = 1; - for (final collection in token.collection) { - final nftCollection = balance.token - .where((element) => element.address == tokenBalance.address); - if (nftCollection.any((element) => element.tokenId == numTokenId)) { - collection['id'] = numTokenId.toString(); - collectionWithTokenId.add(collection); - } - - numTokenId++; - } - - final tokenInformation = TokenInformation( - address: tokenBalance.address, - name: token.name, - id: token.id, - aeip: token.aeip, - type: token.type, - supply: fromBigInt(token.supply).toDouble(), - symbol: token.symbol, - decimals: token.decimals, - tokenCollection: collectionWithTokenId, - tokenProperties: newProperties, - ); - - final accountToken = AccountToken( - tokenInformation: tokenInformation, - amount: fromBigInt(tokenBalance.amount).toDouble(), - ); - - if (tokenInformation.tokenCollection != null && - tokenInformation.tokenCollection!.isNotEmpty) { - nftCollectionList.add(accountToken); - } else { - nftList.add(accountToken); - } - } - nftList.sort( - (a, b) => a.tokenInformation!.name!.compareTo(b.tokenInformation!.name!), - ); - nftCollectionList.sort( - (a, b) => a.tokenInformation!.name!.compareTo(b.tokenInformation!.name!), - ); - return (nftList, nftCollectionList); - } - - Map _tokenPropertiesDecryptedSecret({ - required KeychainServiceKeyPair keypair, - required List ownerships, - }) { - final propertiesDecrypted = {}; - for (final ownership in ownerships) { - final authorizedPublicKey = ownership.authorizedPublicKeys.firstWhere( - (AuthorizedKey authKey) => - authKey.publicKey!.toUpperCase() == - uint8ListToHex(Uint8List.fromList(keypair.publicKey)).toUpperCase(), - orElse: AuthorizedKey.new, - ); - if (authorizedPublicKey.encryptedSecretKey != null) { - final aesKey = ecDecrypt( - authorizedPublicKey.encryptedSecretKey, - Uint8List.fromList(keypair.privateKey), - ); - final decryptedSecret = aesDecrypt(ownership.secret, aesKey); - try { - propertiesDecrypted.addAll(json.decode(utf8.decode(decryptedSecret))); - } catch (e, stack) { - _logger.severe('Decryption error', e, stack); - } - } - } - return propertiesDecrypted; - } -} - abstract class NFTProviders { static const getNFTInfo = _getNFTInfoProvider; static const getNFTList = _getNFTListProvider; diff --git a/lib/application/nft/nft.g.dart b/lib/application/nft/nft.g.dart index ad5042349..fce691a12 100644 --- a/lib/application/nft/nft.g.dart +++ b/lib/application/nft/nft.g.dart @@ -6,11 +6,11 @@ part of 'nft.dart'; // RiverpodGenerator // ************************************************************************** -String _$nftRepositoryHash() => r'c16cc640004fbaf88bbe6c50e643bbe5b8c577ed'; +String _$nftRepositoryHash() => r'32d486b353f1e969e4c4163e61aeabc4bba32263'; /// See also [_nftRepository]. @ProviderFor(_nftRepository) -final _nftRepositoryProvider = AutoDisposeProvider.internal( +final _nftRepositoryProvider = AutoDisposeProvider.internal( _nftRepository, name: r'_nftRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -20,7 +20,7 @@ final _nftRepositoryProvider = AutoDisposeProvider.internal( allTransitiveDependencies: null, ); -typedef _NftRepositoryRef = AutoDisposeProviderRef; +typedef _NftRepositoryRef = AutoDisposeProviderRef; String _$getNFTInfoHash() => r'a59335fcff6d092a022ed269a6c906de7cd90cf5'; /// Copied from Dart SDK diff --git a/lib/application/oracle/provider.dart b/lib/application/oracle/provider.dart deleted file mode 100644 index ef134154b..000000000 --- a/lib/application/oracle/provider.dart +++ /dev/null @@ -1,62 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0-or-later - -import 'dart:async'; - -import 'package:aewallet/application/oracle/state.dart'; -import 'package:aewallet/util/get_it_instance.dart'; -import 'package:archethic_lib_dart/archethic_lib_dart.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'provider.g.dart'; - -@Riverpod(keepAlive: true) -class _ArchethicOracleUCONotifier extends Notifier { - ArchethicOracle? archethicOracle; - - static final _logger = Logger('ArchethicOracleUCONotifier'); - - @override - ArchethicOracleUCO build() { - ref.onDispose(() { - _logger.info('dispose ArchethicOracleUCONotifier'); - if (archethicOracle != null) { - sl - .get() - .closeOracleUpdatesSubscription(archethicOracle!); - } - }); - return const ArchethicOracleUCO(); - } - - Future init() async { - await _getValue(); - await _subscribe(); - } - - Future _getValue() async { - final oracleUcoPrice = await sl.get().getOracleData(); - _fillInfo(oracleUcoPrice); - } - - Future _subscribe() async { - archethicOracle = await sl - .get() - .subscribeToOracleUpdates((oracleUcoPrice) { - _fillInfo(oracleUcoPrice!); - }); - } - - void _fillInfo(OracleUcoPrice oracleUcoPrice) { - _logger.info('Oracle: ${oracleUcoPrice.timestamp}, ${oracleUcoPrice.uco}'); - state = state.copyWith( - timestamp: oracleUcoPrice.timestamp ?? 0, - eur: oracleUcoPrice.uco!.eur ?? 0, - usd: oracleUcoPrice.uco!.usd ?? 0, - ); - } -} - -abstract class ArchethicOracleUCOProviders { - static final archethicOracleUCO = _archethicOracleUCONotifierProvider; -} diff --git a/lib/application/oracle/provider.g.dart b/lib/application/oracle/provider.g.dart deleted file mode 100644 index 8febb3b45..000000000 --- a/lib/application/oracle/provider.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$archethicOracleUCONotifierHash() => - r'acb06155b79f88c81e5a358fa0a8f0878c774991'; - -/// See also [_ArchethicOracleUCONotifier]. -@ProviderFor(_ArchethicOracleUCONotifier) -final _archethicOracleUCONotifierProvider = - NotifierProvider<_ArchethicOracleUCONotifier, ArchethicOracleUCO>.internal( - _ArchethicOracleUCONotifier.new, - name: r'_archethicOracleUCONotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$archethicOracleUCONotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$ArchethicOracleUCONotifier = Notifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/application/oracle/state.dart b/lib/application/oracle/state.dart deleted file mode 100644 index 369cf0664..000000000 --- a/lib/application/oracle/state.dart +++ /dev/null @@ -1,31 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0-or-later - -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'state.freezed.dart'; -part 'state.g.dart'; - -class ArchethicOracleUCOJsonConverter - extends JsonConverter> { - const ArchethicOracleUCOJsonConverter(); - - @override - ArchethicOracleUCO fromJson(Map json) { - return ArchethicOracleUCO.fromJson(json); - } - - @override - Map toJson(ArchethicOracleUCO object) => object.toJson(); -} - -@freezed -class ArchethicOracleUCO with _$ArchethicOracleUCO { - const factory ArchethicOracleUCO({ - @Default(0) int timestamp, - @Default(0) double eur, - @Default(0) double usd, - }) = _ArchethicOracleUCO; - - factory ArchethicOracleUCO.fromJson(Map json) => - _$ArchethicOracleUCOFromJson(json); -} diff --git a/lib/application/oracle/state.freezed.dart b/lib/application/oracle/state.freezed.dart deleted file mode 100644 index 9d4a3fa3b..000000000 --- a/lib/application/oracle/state.freezed.dart +++ /dev/null @@ -1,192 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -ArchethicOracleUCO _$ArchethicOracleUCOFromJson(Map json) { - return _ArchethicOracleUCO.fromJson(json); -} - -/// @nodoc -mixin _$ArchethicOracleUCO { - int get timestamp => throw _privateConstructorUsedError; - double get eur => throw _privateConstructorUsedError; - double get usd => throw _privateConstructorUsedError; - - Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) - $ArchethicOracleUCOCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ArchethicOracleUCOCopyWith<$Res> { - factory $ArchethicOracleUCOCopyWith( - ArchethicOracleUCO value, $Res Function(ArchethicOracleUCO) then) = - _$ArchethicOracleUCOCopyWithImpl<$Res, ArchethicOracleUCO>; - @useResult - $Res call({int timestamp, double eur, double usd}); -} - -/// @nodoc -class _$ArchethicOracleUCOCopyWithImpl<$Res, $Val extends ArchethicOracleUCO> - implements $ArchethicOracleUCOCopyWith<$Res> { - _$ArchethicOracleUCOCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? timestamp = null, - Object? eur = null, - Object? usd = null, - }) { - return _then(_value.copyWith( - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable - as int, - eur: null == eur - ? _value.eur - : eur // ignore: cast_nullable_to_non_nullable - as double, - usd: null == usd - ? _value.usd - : usd // ignore: cast_nullable_to_non_nullable - as double, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ArchethicOracleUCOImplCopyWith<$Res> - implements $ArchethicOracleUCOCopyWith<$Res> { - factory _$$ArchethicOracleUCOImplCopyWith(_$ArchethicOracleUCOImpl value, - $Res Function(_$ArchethicOracleUCOImpl) then) = - __$$ArchethicOracleUCOImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({int timestamp, double eur, double usd}); -} - -/// @nodoc -class __$$ArchethicOracleUCOImplCopyWithImpl<$Res> - extends _$ArchethicOracleUCOCopyWithImpl<$Res, _$ArchethicOracleUCOImpl> - implements _$$ArchethicOracleUCOImplCopyWith<$Res> { - __$$ArchethicOracleUCOImplCopyWithImpl(_$ArchethicOracleUCOImpl _value, - $Res Function(_$ArchethicOracleUCOImpl) _then) - : super(_value, _then); - - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? timestamp = null, - Object? eur = null, - Object? usd = null, - }) { - return _then(_$ArchethicOracleUCOImpl( - timestamp: null == timestamp - ? _value.timestamp - : timestamp // ignore: cast_nullable_to_non_nullable - as int, - eur: null == eur - ? _value.eur - : eur // ignore: cast_nullable_to_non_nullable - as double, - usd: null == usd - ? _value.usd - : usd // ignore: cast_nullable_to_non_nullable - as double, - )); - } -} - -/// @nodoc -@JsonSerializable() -class _$ArchethicOracleUCOImpl implements _ArchethicOracleUCO { - const _$ArchethicOracleUCOImpl( - {this.timestamp = 0, this.eur = 0, this.usd = 0}); - - factory _$ArchethicOracleUCOImpl.fromJson(Map json) => - _$$ArchethicOracleUCOImplFromJson(json); - - @override - @JsonKey() - final int timestamp; - @override - @JsonKey() - final double eur; - @override - @JsonKey() - final double usd; - - @override - String toString() { - return 'ArchethicOracleUCO(timestamp: $timestamp, eur: $eur, usd: $usd)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ArchethicOracleUCOImpl && - (identical(other.timestamp, timestamp) || - other.timestamp == timestamp) && - (identical(other.eur, eur) || other.eur == eur) && - (identical(other.usd, usd) || other.usd == usd)); - } - - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash(runtimeType, timestamp, eur, usd); - - @JsonKey(ignore: true) - @override - @pragma('vm:prefer-inline') - _$$ArchethicOracleUCOImplCopyWith<_$ArchethicOracleUCOImpl> get copyWith => - __$$ArchethicOracleUCOImplCopyWithImpl<_$ArchethicOracleUCOImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$ArchethicOracleUCOImplToJson( - this, - ); - } -} - -abstract class _ArchethicOracleUCO implements ArchethicOracleUCO { - const factory _ArchethicOracleUCO( - {final int timestamp, - final double eur, - final double usd}) = _$ArchethicOracleUCOImpl; - - factory _ArchethicOracleUCO.fromJson(Map json) = - _$ArchethicOracleUCOImpl.fromJson; - - @override - int get timestamp; - @override - double get eur; - @override - double get usd; - @override - @JsonKey(ignore: true) - _$$ArchethicOracleUCOImplCopyWith<_$ArchethicOracleUCOImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/application/oracle/state.g.dart b/lib/application/oracle/state.g.dart deleted file mode 100644 index d3cc4a2b0..000000000 --- a/lib/application/oracle/state.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'state.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_$ArchethicOracleUCOImpl _$$ArchethicOracleUCOImplFromJson( - Map json) => - _$ArchethicOracleUCOImpl( - timestamp: (json['timestamp'] as num?)?.toInt() ?? 0, - eur: (json['eur'] as num?)?.toDouble() ?? 0, - usd: (json['usd'] as num?)?.toDouble() ?? 0, - ); - -Map _$$ArchethicOracleUCOImplToJson( - _$ArchethicOracleUCOImpl instance) => - { - 'timestamp': instance.timestamp, - 'eur': instance.eur, - 'usd': instance.usd, - }; diff --git a/lib/application/price_history/providers.dart b/lib/application/price_history/providers.dart index 3a8ae6daa..5ea57ddde 100644 --- a/lib/application/price_history/providers.dart +++ b/lib/application/price_history/providers.dart @@ -22,6 +22,7 @@ MarketPriceHistoryInterval _intervalOption(Ref ref) => ref.watch( Future> _priceHistory( Ref ref, { required MarketPriceHistoryInterval scaleOption, + required String coinId, }) async { final selectedCurrency = ref.watch( SettingsProviders.settings.select((settings) => settings.currency), @@ -31,30 +32,12 @@ Future> _priceHistory( .getWithInterval( vsCurrency: selectedCurrency, interval: scaleOption, - ) - .valueOrThrow; -} - -@Riverpod(keepAlive: true) -Future _priceEvolution( - Ref ref, { - required MarketPriceHistoryInterval scaleOption, -}) async { - final priceHistory = await ref.watch( - _priceHistoryProvider(scaleOption: scaleOption).future, - ); - - return ref - .watch(_repositoryProvider) - .getPriceEvolution( - priceHistory: priceHistory, - interval: scaleOption, + coinId: coinId, ) .valueOrThrow; } abstract class PriceHistoryProviders { static final scaleOption = _intervalOptionProvider; - static const chartData = _priceHistoryProvider; - static const priceEvolution = _priceEvolutionProvider; + static const priceHistory = _priceHistoryProvider; } diff --git a/lib/application/price_history/providers.g.dart b/lib/application/price_history/providers.g.dart index 669756331..e28c9016a 100644 --- a/lib/application/price_history/providers.g.dart +++ b/lib/application/price_history/providers.g.dart @@ -35,7 +35,7 @@ final _intervalOptionProvider = Provider.internal( ); typedef _IntervalOptionRef = ProviderRef; -String _$priceHistoryHash() => r'53a8fb4fa9f1089577cc2150bd23d7ff77972298'; +String _$priceHistoryHash() => r'54c97861c17ceb12b1d1dc6432d9f5cdfb2a6748'; /// Copied from Dart SDK class _SystemHash { @@ -70,9 +70,11 @@ class _PriceHistoryFamily extends Family>> { /// See also [_priceHistory]. _PriceHistoryProvider call({ required MarketPriceHistoryInterval scaleOption, + required String coinId, }) { return _PriceHistoryProvider( scaleOption: scaleOption, + coinId: coinId, ); } @@ -82,6 +84,7 @@ class _PriceHistoryFamily extends Family>> { ) { return call( scaleOption: provider.scaleOption, + coinId: provider.coinId, ); } @@ -105,10 +108,12 @@ class _PriceHistoryProvider extends FutureProvider> { /// See also [_priceHistory]. _PriceHistoryProvider({ required MarketPriceHistoryInterval scaleOption, + required String coinId, }) : this._internal( (ref) => _priceHistory( ref as _PriceHistoryRef, scaleOption: scaleOption, + coinId: coinId, ), from: _priceHistoryProvider, name: r'_priceHistoryProvider', @@ -120,6 +125,7 @@ class _PriceHistoryProvider extends FutureProvider> { allTransitiveDependencies: _PriceHistoryFamily._allTransitiveDependencies, scaleOption: scaleOption, + coinId: coinId, ); _PriceHistoryProvider._internal( @@ -130,9 +136,11 @@ class _PriceHistoryProvider extends FutureProvider> { required super.debugGetCreateSourceHash, required super.from, required this.scaleOption, + required this.coinId, }) : super.internal(); final MarketPriceHistoryInterval scaleOption; + final String coinId; @override Override overrideWith( @@ -149,6 +157,7 @@ class _PriceHistoryProvider extends FutureProvider> { allTransitiveDependencies: null, debugGetCreateSourceHash: null, scaleOption: scaleOption, + coinId: coinId, ), ); } @@ -160,13 +169,16 @@ class _PriceHistoryProvider extends FutureProvider> { @override bool operator ==(Object other) { - return other is _PriceHistoryProvider && other.scaleOption == scaleOption; + return other is _PriceHistoryProvider && + other.scaleOption == scaleOption && + other.coinId == coinId; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, scaleOption.hashCode); + hash = _SystemHash.combine(hash, coinId.hashCode); return _SystemHash.finish(hash); } @@ -175,6 +187,9 @@ class _PriceHistoryProvider extends FutureProvider> { mixin _PriceHistoryRef on FutureProviderRef> { /// The parameter `scaleOption` of this provider. MarketPriceHistoryInterval get scaleOption; + + /// The parameter `coinId` of this provider. + String get coinId; } class _PriceHistoryProviderElement @@ -185,135 +200,8 @@ class _PriceHistoryProviderElement @override MarketPriceHistoryInterval get scaleOption => (origin as _PriceHistoryProvider).scaleOption; -} - -String _$priceEvolutionHash() => r'bcc3b22660c82056f10a536b6f8285b00e202ec6'; - -/// See also [_priceEvolution]. -@ProviderFor(_priceEvolution) -const _priceEvolutionProvider = _PriceEvolutionFamily(); - -/// See also [_priceEvolution]. -class _PriceEvolutionFamily extends Family> { - /// See also [_priceEvolution]. - const _PriceEvolutionFamily(); - - /// See also [_priceEvolution]. - _PriceEvolutionProvider call({ - required MarketPriceHistoryInterval scaleOption, - }) { - return _PriceEvolutionProvider( - scaleOption: scaleOption, - ); - } - - @override - _PriceEvolutionProvider getProviderOverride( - covariant _PriceEvolutionProvider provider, - ) { - return call( - scaleOption: provider.scaleOption, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'_priceEvolutionProvider'; -} - -/// See also [_priceEvolution]. -class _PriceEvolutionProvider extends FutureProvider { - /// See also [_priceEvolution]. - _PriceEvolutionProvider({ - required MarketPriceHistoryInterval scaleOption, - }) : this._internal( - (ref) => _priceEvolution( - ref as _PriceEvolutionRef, - scaleOption: scaleOption, - ), - from: _priceEvolutionProvider, - name: r'_priceEvolutionProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$priceEvolutionHash, - dependencies: _PriceEvolutionFamily._dependencies, - allTransitiveDependencies: - _PriceEvolutionFamily._allTransitiveDependencies, - scaleOption: scaleOption, - ); - - _PriceEvolutionProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.scaleOption, - }) : super.internal(); - - final MarketPriceHistoryInterval scaleOption; - - @override - Override overrideWith( - FutureOr Function(_PriceEvolutionRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: _PriceEvolutionProvider._internal( - (ref) => create(ref as _PriceEvolutionRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - scaleOption: scaleOption, - ), - ); - } - - @override - FutureProviderElement createElement() { - return _PriceEvolutionProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is _PriceEvolutionProvider && other.scaleOption == scaleOption; - } - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, scaleOption.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin _PriceEvolutionRef on FutureProviderRef { - /// The parameter `scaleOption` of this provider. - MarketPriceHistoryInterval get scaleOption; -} - -class _PriceEvolutionProviderElement extends FutureProviderElement - with _PriceEvolutionRef { - _PriceEvolutionProviderElement(super.provider); - - @override - MarketPriceHistoryInterval get scaleOption => - (origin as _PriceEvolutionProvider).scaleOption; + String get coinId => (origin as _PriceHistoryProvider).coinId; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/application/tokens/tokens.dart b/lib/application/tokens/tokens.dart new file mode 100644 index 000000000..5357e61f2 --- /dev/null +++ b/lib/application/tokens/tokens.dart @@ -0,0 +1,26 @@ +import 'package:aewallet/infrastructure/repositories/tokens/tokens.repository.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; +import 'package:archethic_lib_dart/archethic_lib_dart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'tokens.g.dart'; + +@riverpod +TokensRepositoryImpl _tokensRepository(_TokensRepositoryRef ref) => + TokensRepositoryImpl(); + +@riverpod +Future> _getTokensList( + _GetTokensListRef ref, + String userGenesisAddress, +) async { + final apiService = sl.get(); + + return ref + .watch(_tokensRepositoryProvider) + .getTokensList(userGenesisAddress, apiService); +} + +abstract class TokensProviders { + static const getTokensList = _getTokensListProvider; +} diff --git a/lib/application/tokens/tokens.g.dart b/lib/application/tokens/tokens.g.dart new file mode 100644 index 000000000..5fa89a87e --- /dev/null +++ b/lib/application/tokens/tokens.g.dart @@ -0,0 +1,177 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tokens.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$tokensRepositoryHash() => r'd30111b6cdaaca643925a532add6475526500372'; + +/// See also [_tokensRepository]. +@ProviderFor(_tokensRepository) +final _tokensRepositoryProvider = + AutoDisposeProvider.internal( + _tokensRepository, + name: r'_tokensRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$tokensRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _TokensRepositoryRef = AutoDisposeProviderRef; +String _$getTokensListHash() => r'6828404cac5119af9b100b2ed92133a397020eb8'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_getTokensList]. +@ProviderFor(_getTokensList) +const _getTokensListProvider = _GetTokensListFamily(); + +/// See also [_getTokensList]. +class _GetTokensListFamily extends Family>> { + /// See also [_getTokensList]. + const _GetTokensListFamily(); + + /// See also [_getTokensList]. + _GetTokensListProvider call( + String userGenesisAddress, + ) { + return _GetTokensListProvider( + userGenesisAddress, + ); + } + + @override + _GetTokensListProvider getProviderOverride( + covariant _GetTokensListProvider provider, + ) { + return call( + provider.userGenesisAddress, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_getTokensListProvider'; +} + +/// See also [_getTokensList]. +class _GetTokensListProvider extends AutoDisposeFutureProvider> { + /// See also [_getTokensList]. + _GetTokensListProvider( + String userGenesisAddress, + ) : this._internal( + (ref) => _getTokensList( + ref as _GetTokensListRef, + userGenesisAddress, + ), + from: _getTokensListProvider, + name: r'_getTokensListProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getTokensListHash, + dependencies: _GetTokensListFamily._dependencies, + allTransitiveDependencies: + _GetTokensListFamily._allTransitiveDependencies, + userGenesisAddress: userGenesisAddress, + ); + + _GetTokensListProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.userGenesisAddress, + }) : super.internal(); + + final String userGenesisAddress; + + @override + Override overrideWith( + FutureOr> Function(_GetTokensListRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: _GetTokensListProvider._internal( + (ref) => create(ref as _GetTokensListRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + userGenesisAddress: userGenesisAddress, + ), + ); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _GetTokensListProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is _GetTokensListProvider && + other.userGenesisAddress == userGenesisAddress; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, userGenesisAddress.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin _GetTokensListRef on AutoDisposeFutureProviderRef> { + /// The parameter `userGenesisAddress` of this provider. + String get userGenesisAddress; +} + +class _GetTokensListProviderElement + extends AutoDisposeFutureProviderElement> + with _GetTokensListRef { + _GetTokensListProviderElement(super.provider); + + @override + String get userGenesisAddress => + (origin as _GetTokensListProvider).userGenesisAddress; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/domain/models/market_price_history.dart b/lib/domain/models/market_price_history.dart index 15df7017f..92a148750 100644 --- a/lib/domain/models/market_price_history.dart +++ b/lib/domain/models/market_price_history.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/localizations.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'market_price_history.freezed.dart'; +part 'market_price_history.g.dart'; enum MarketPriceHistoryInterval { hour, @@ -36,12 +40,26 @@ extension PriceHistoryIntervalToString on MarketPriceHistoryInterval { } } -@immutable -class PriceHistoryValue { - const PriceHistoryValue({ - required this.price, - required this.time, - }); - final num price; - final DateTime time; +class PriceHistoryValueConverter + implements JsonConverter> { + const PriceHistoryValueConverter(); + + @override + PriceHistoryValue fromJson(Map json) { + return PriceHistoryValue.fromJson(json); + } + + @override + Map toJson(PriceHistoryValue object) => object.toJson(); +} + +@freezed +class PriceHistoryValue with _$PriceHistoryValue { + const factory PriceHistoryValue({ + required num price, + required DateTime time, + }) = _PriceHistoryValue; + + factory PriceHistoryValue.fromJson(Map json) => + _$PriceHistoryValueFromJson(json); } diff --git a/lib/domain/models/market_price_history.freezed.dart b/lib/domain/models/market_price_history.freezed.dart new file mode 100644 index 000000000..bba89f005 --- /dev/null +++ b/lib/domain/models/market_price_history.freezed.dart @@ -0,0 +1,170 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'market_price_history.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +PriceHistoryValue _$PriceHistoryValueFromJson(Map json) { + return _PriceHistoryValue.fromJson(json); +} + +/// @nodoc +mixin _$PriceHistoryValue { + num get price => throw _privateConstructorUsedError; + DateTime get time => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PriceHistoryValueCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PriceHistoryValueCopyWith<$Res> { + factory $PriceHistoryValueCopyWith( + PriceHistoryValue value, $Res Function(PriceHistoryValue) then) = + _$PriceHistoryValueCopyWithImpl<$Res, PriceHistoryValue>; + @useResult + $Res call({num price, DateTime time}); +} + +/// @nodoc +class _$PriceHistoryValueCopyWithImpl<$Res, $Val extends PriceHistoryValue> + implements $PriceHistoryValueCopyWith<$Res> { + _$PriceHistoryValueCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? price = null, + Object? time = null, + }) { + return _then(_value.copyWith( + price: null == price + ? _value.price + : price // ignore: cast_nullable_to_non_nullable + as num, + time: null == time + ? _value.time + : time // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$PriceHistoryValueImplCopyWith<$Res> + implements $PriceHistoryValueCopyWith<$Res> { + factory _$$PriceHistoryValueImplCopyWith(_$PriceHistoryValueImpl value, + $Res Function(_$PriceHistoryValueImpl) then) = + __$$PriceHistoryValueImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({num price, DateTime time}); +} + +/// @nodoc +class __$$PriceHistoryValueImplCopyWithImpl<$Res> + extends _$PriceHistoryValueCopyWithImpl<$Res, _$PriceHistoryValueImpl> + implements _$$PriceHistoryValueImplCopyWith<$Res> { + __$$PriceHistoryValueImplCopyWithImpl(_$PriceHistoryValueImpl _value, + $Res Function(_$PriceHistoryValueImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? price = null, + Object? time = null, + }) { + return _then(_$PriceHistoryValueImpl( + price: null == price + ? _value.price + : price // ignore: cast_nullable_to_non_nullable + as num, + time: null == time + ? _value.time + : time // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PriceHistoryValueImpl implements _PriceHistoryValue { + const _$PriceHistoryValueImpl({required this.price, required this.time}); + + factory _$PriceHistoryValueImpl.fromJson(Map json) => + _$$PriceHistoryValueImplFromJson(json); + + @override + final num price; + @override + final DateTime time; + + @override + String toString() { + return 'PriceHistoryValue(price: $price, time: $time)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PriceHistoryValueImpl && + (identical(other.price, price) || other.price == price) && + (identical(other.time, time) || other.time == time)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, price, time); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PriceHistoryValueImplCopyWith<_$PriceHistoryValueImpl> get copyWith => + __$$PriceHistoryValueImplCopyWithImpl<_$PriceHistoryValueImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$PriceHistoryValueImplToJson( + this, + ); + } +} + +abstract class _PriceHistoryValue implements PriceHistoryValue { + const factory _PriceHistoryValue( + {required final num price, + required final DateTime time}) = _$PriceHistoryValueImpl; + + factory _PriceHistoryValue.fromJson(Map json) = + _$PriceHistoryValueImpl.fromJson; + + @override + num get price; + @override + DateTime get time; + @override + @JsonKey(ignore: true) + _$$PriceHistoryValueImplCopyWith<_$PriceHistoryValueImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/domain/models/market_price_history.g.dart b/lib/domain/models/market_price_history.g.dart new file mode 100644 index 000000000..4b9020bd3 --- /dev/null +++ b/lib/domain/models/market_price_history.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'market_price_history.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$PriceHistoryValueImpl _$$PriceHistoryValueImplFromJson( + Map json) => + _$PriceHistoryValueImpl( + price: json['price'] as num, + time: DateTime.parse(json['time'] as String), + ); + +Map _$$PriceHistoryValueImplToJson( + _$PriceHistoryValueImpl instance) => + { + 'price': instance.price, + 'time': instance.time.toIso8601String(), + }; diff --git a/lib/domain/repositories/market/price_history.dart b/lib/domain/repositories/market/price_history.dart index 8e3b24278..be2acee94 100644 --- a/lib/domain/repositories/market/price_history.dart +++ b/lib/domain/repositories/market/price_history.dart @@ -7,10 +7,12 @@ abstract class PriceHistoryRepositoryInterface { Future, Failure>> getWithInterval({ required AvailableCurrencyEnum vsCurrency, required MarketPriceHistoryInterval interval, + required String coinId, }); Future> getPriceEvolution({ required List priceHistory, required MarketPriceHistoryInterval interval, + required String coinId, }); } diff --git a/lib/domain/repositories/nft/nft.repository.dart b/lib/domain/repositories/nft/nft.repository.dart new file mode 100644 index 000000000..6c8b8f3b6 --- /dev/null +++ b/lib/domain/repositories/nft/nft.repository.dart @@ -0,0 +1,23 @@ +import 'package:aewallet/model/blockchain/keychain_secured_infos.dart'; +import 'package:aewallet/model/blockchain/token_information.dart'; +import 'package:aewallet/model/data/account_token.dart'; +import 'package:aewallet/model/keychain_service_keypair.dart'; + +abstract class NFTRepository { + Future isAccountOwner( + String accountAddress, + String tokenAddress, + String tokenId, + ); + + Future getNFTInfo( + String address, + KeychainServiceKeyPair keychainServiceKeyPair, + ); + + Future<(List, List)> getNFTList( + String address, + String nameAccount, + KeychainSecuredInfos keychainSecuredInfos, + ); +} diff --git a/lib/domain/repositories/tokens/tokens.repository.dart b/lib/domain/repositories/tokens/tokens.repository.dart new file mode 100644 index 000000000..69053db5d --- /dev/null +++ b/lib/domain/repositories/tokens/tokens.repository.dart @@ -0,0 +1,14 @@ +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; +import 'package:archethic_lib_dart/archethic_lib_dart.dart' as archethic; + +abstract class TokensRepository { + Future> getToken( + List addresses, + archethic.ApiService apiService, + ); + + Future> getTokensList( + String userGenesisAddress, + archethic.ApiService apiService, + ); +} diff --git a/lib/infrastructure/repositories/market/coingecko_price_history_repository.dart b/lib/infrastructure/repositories/market/coingecko_price_history_repository.dart index 90551a127..118033826 100644 --- a/lib/infrastructure/repositories/market/coingecko_price_history_repository.dart +++ b/lib/infrastructure/repositories/market/coingecko_price_history_repository.dart @@ -12,17 +12,16 @@ class CoinGeckoPriceHistoryRepository CoinGeckoApi? _coinGeckoApi; CoinGeckoApi get coinGeckoApi => _coinGeckoApi ??= sl.get(); - static const archethicId = 'archethic'; - @override Future> getPriceEvolution({ required List priceHistory, required MarketPriceHistoryInterval interval, + required String coinId, }) => Result.guard( () async { final coinResult = await coinGeckoApi.coins.getCoinData( - id: archethicId, + id: coinId, // ignore: avoid_redundant_argument_values marketData: true, communityData: false, @@ -46,13 +45,14 @@ class CoinGeckoPriceHistoryRepository Future, Failure>> getWithInterval({ required AvailableCurrencyEnum vsCurrency, required MarketPriceHistoryInterval interval, + required String coinId, }) => Result.guard( () async { final now = DateTime.now(); final coinGeckoResponse = await coinGeckoApi.coins.getCoinMarketChartRanged( - id: archethicId, + id: coinId, vsCurrency: vsCurrency.name, from: now.subtract( interval.duration, diff --git a/lib/infrastructure/repositories/nft/nft.repository.dart b/lib/infrastructure/repositories/nft/nft.repository.dart new file mode 100644 index 000000000..68e9e970d --- /dev/null +++ b/lib/infrastructure/repositories/nft/nft.repository.dart @@ -0,0 +1,232 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:aewallet/domain/repositories/nft/nft.repository.dart'; +import 'package:aewallet/model/blockchain/keychain_secured_infos.dart'; +import 'package:aewallet/model/blockchain/token_information.dart'; +import 'package:aewallet/model/data/account_token.dart'; +import 'package:aewallet/model/keychain_service_keypair.dart'; +import 'package:aewallet/service/app_service.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:archethic_lib_dart/archethic_lib_dart.dart'; +import 'package:logging/logging.dart'; + +class NFTRepositoryImpl implements NFTRepository { + static final _logger = Logger('NFTRepository'); + + @override + Future isAccountOwner( + String accountAddress, + String tokenAddress, + String tokenId, + ) async { + final accountLastAddressMap = + await sl.get().getLastTransaction([accountAddress]); + var accounLastAddress = ''; + if (accountLastAddressMap[accountAddress] == null || + accountLastAddressMap[accountAddress]!.address == null || + accountLastAddressMap[accountAddress]!.address!.address == null) { + accounLastAddress = accountAddress; + } else { + accounLastAddress = + accountLastAddressMap[accountAddress]!.address!.address!; + } + + final accountLastInputsMap = + await sl.get().getTransactionInputs( + [accounLastAddress], + request: 'amount, from, tokenAddress, spent, timestamp, type, tokenId', + ); + + if (accountLastInputsMap[accounLastAddress] == null) { + return false; + } + + final _tokenId = int.tryParse(tokenId); + + final accountLastInputs = accountLastInputsMap[accounLastAddress]; + for (final input in accountLastInputs!) { + if (_tokenId == null) { + if (input.tokenAddress == tokenAddress) { + return true; + } + } else { + if (input.tokenAddress == tokenAddress && input.tokenId == _tokenId) { + return true; + } + } + } + + return false; + } + + @override + Future getNFTInfo( + String address, + KeychainServiceKeyPair keychainServiceKeyPair, + ) async { + final tokenMap = await sl.get().getToken( + [address], + ); + + if (tokenMap.isEmpty || + tokenMap[address] == null || + tokenMap[address]!.type != 'non-fungible') { + return null; + } + + final token = tokenMap[address]!; + + final tokenProperties = {...token.properties}; + + if (token.ownerships != null && token.ownerships!.isNotEmpty) { + tokenProperties.addAll( + _tokenPropertiesDecryptedSecret( + keypair: keychainServiceKeyPair, + ownerships: token.ownerships!, + ), + ); + } + final tokenInformation = TokenInformation( + address: address, + name: token.name, + id: token.id, + type: token.type, + decimals: token.decimals, + supply: fromBigInt(token.supply).toDouble(), + symbol: token.symbol, + tokenProperties: tokenProperties, + tokenCollection: token.collection, + ); + return tokenInformation; + } + + @override + Future<(List, List)> getNFTList( + String address, + String nameAccount, + KeychainSecuredInfos keychainSecuredInfos, + ) async { + final balanceMap = await sl.get().fetchBalance([address]); + final balance = balanceMap[address]; + final nftList = []; + final nftCollectionList = []; + + final tokenAddressList = []; + if (balance == null) { + return (nftList, nftCollectionList); + } + + for (final tokenBalance in balance.token) { + if (tokenBalance.address != null) { + tokenAddressList.add(tokenBalance.address!); + } + } + + final tokenMap = await sl.get().getToken( + tokenAddressList.toSet().toList(), + ); + + // TODO(reddwarf03): temporaly section -> need https://github.com/archethic-foundation/archethic-node/issues/714 + + final secretMap = await sl.get().getTransaction( + tokenAddressList.toSet().toList(), + request: + 'data { ownerships { authorizedPublicKeys { encryptedSecretKey, publicKey } secret } }', + ); + + for (final tokenBalance in balance.token) { + final token = tokenMap[tokenBalance.address]; + + if (token == null || token.type != 'non-fungible') { + continue; + } + + final newProperties = {...token.properties}; + + if (secretMap[tokenBalance.address] != null && + secretMap[tokenBalance.address]!.data != null && + secretMap[tokenBalance.address]!.data!.ownerships.isNotEmpty) { + newProperties.addAll( + _tokenPropertiesDecryptedSecret( + keypair: keychainSecuredInfos.services[nameAccount]!.keyPair!, + ownerships: secretMap[tokenBalance.address]!.data!.ownerships, + ), + ); + } + + final collectionWithTokenId = >[]; + var numTokenId = 1; + for (final collection in token.collection) { + final nftCollection = balance.token + .where((element) => element.address == tokenBalance.address); + if (nftCollection.any((element) => element.tokenId == numTokenId)) { + collection['id'] = numTokenId.toString(); + collectionWithTokenId.add(collection); + } + + numTokenId++; + } + + final tokenInformation = TokenInformation( + address: tokenBalance.address, + name: token.name, + id: token.id, + aeip: token.aeip, + type: token.type, + supply: fromBigInt(token.supply).toDouble(), + symbol: token.symbol, + decimals: token.decimals, + tokenCollection: collectionWithTokenId, + tokenProperties: newProperties, + ); + + final accountToken = AccountToken( + tokenInformation: tokenInformation, + amount: fromBigInt(tokenBalance.amount).toDouble(), + ); + + if (tokenInformation.tokenCollection != null && + tokenInformation.tokenCollection!.isNotEmpty) { + nftCollectionList.add(accountToken); + } else { + nftList.add(accountToken); + } + } + nftList.sort( + (a, b) => a.tokenInformation!.name!.compareTo(b.tokenInformation!.name!), + ); + nftCollectionList.sort( + (a, b) => a.tokenInformation!.name!.compareTo(b.tokenInformation!.name!), + ); + return (nftList, nftCollectionList); + } + + Map _tokenPropertiesDecryptedSecret({ + required KeychainServiceKeyPair keypair, + required List ownerships, + }) { + final propertiesDecrypted = {}; + for (final ownership in ownerships) { + final authorizedPublicKey = ownership.authorizedPublicKeys.firstWhere( + (AuthorizedKey authKey) => + authKey.publicKey!.toUpperCase() == + uint8ListToHex(Uint8List.fromList(keypair.publicKey)).toUpperCase(), + orElse: AuthorizedKey.new, + ); + if (authorizedPublicKey.encryptedSecretKey != null) { + final aesKey = ecDecrypt( + authorizedPublicKey.encryptedSecretKey, + Uint8List.fromList(keypair.privateKey), + ); + final decryptedSecret = aesDecrypt(ownership.secret, aesKey); + try { + propertiesDecrypted.addAll(json.decode(utf8.decode(decryptedSecret))); + } catch (e, stack) { + _logger.severe('Decryption error', e, stack); + } + } + } + return propertiesDecrypted; + } +} diff --git a/lib/infrastructure/repositories/tokens/tokens.repository.dart b/lib/infrastructure/repositories/tokens/tokens.repository.dart new file mode 100644 index 000000000..b99c2a7d3 --- /dev/null +++ b/lib/infrastructure/repositories/tokens/tokens.repository.dart @@ -0,0 +1,169 @@ +import 'package:aewallet/domain/repositories/tokens/tokens.repository.dart'; +import 'package:aewallet/infrastructure/datasources/preferences.hive.dart'; +import 'package:aewallet/infrastructure/datasources/tokens_list.hive.dart'; +import 'package:aewallet/infrastructure/datasources/wallet_token_dto.hive.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; +import 'package:archethic_lib_dart/archethic_lib_dart.dart' as archethic; + +class TokensRepositoryImpl implements TokensRepository { + @override + Future> getToken( + List addresses, + archethic.ApiService apiService, + ) async { + final tokenMap = {}; + + final addressesOutCache = []; + final tokensListDatasource = await TokensListHiveDatasource.getInstance(); + + for (final address in addresses.toSet()) { + final token = tokensListDatasource.getToken(address); + if (token != null) { + tokenMap[address] = token.toModel(); + } else { + addressesOutCache.add(address); + } + } + + var antiSpam = 0; + final futures = []; + for (final address in addressesOutCache) { + // Delay the API call if we have made more than 10 requests + if (antiSpam > 0 && antiSpam % 10 == 0) { + await Future.delayed(const Duration(seconds: 1)); + } + + // Make the API call and update the antiSpam counter + futures.add( + apiService.getToken( + [address], + ), + ); + antiSpam++; + } + + final getTokens = await Future.wait(futures); + for (final Map getToken in getTokens) { + tokenMap.addAll(getToken); + + getToken.forEach((key, value) async { + value = value.copyWith(address: key); + await tokensListDatasource.setToken(value.toHive()); + }); + } + + return tokenMap; + } + + @override + Future> getTokensList( + String userGenesisAddress, + archethic.ApiService apiService, + ) async { + final tokensList = []; + final balanceMap = await apiService.fetchBalance([userGenesisAddress]); + if (balanceMap[userGenesisAddress] == null) { + return tokensList; + } + + final defUCOToken = + await aedappfm.DefTokensRepositoryImpl().getDefToken('UCO'); + tokensList.add( + ucoToken.copyWith( + name: defUCOToken?.name ?? '', + isVerified: true, + icon: defUCOToken?.icon, + coingeckoCoinId: defUCOToken?.coingeckoCoinId, + balance: archethic + .fromBigInt(balanceMap[userGenesisAddress]!.uco) + .toDouble(), + ), + ); + + if (balanceMap[userGenesisAddress]!.token.isNotEmpty) { + final tokenAddressList = []; + for (final tokenBalance in balanceMap[userGenesisAddress]!.token) { + if (tokenBalance.address != null) { + tokenAddressList.add(tokenBalance.address!); + } + } + + // Search token Information + final tokenMap = await getToken( + tokenAddressList.toSet().toList(), + apiService, + ); + + final preferences = await PreferencesHiveDatasource.getInstance(); + final network = preferences.getNetwork().getNetworkLabel(); + final verifiedTokens = await aedappfm.VerifiedTokensRepositoryImpl() + .getVerifiedTokensFromNetwork(network); + + for (final tokenBalance in balanceMap[userGenesisAddress]!.token) { + String? pairSymbolToken1; + String? pairSymbolToken2; + final token = tokenMap[tokenBalance.address]; + if (token != null && token.type == 'fungible') { + final tokenSymbolSearch = []; + if (token.properties.isNotEmpty && + token.properties['token1_address'] != null && + token.properties['token2_address'] != null) { + if (token.properties['token1_address'] != 'UCO') { + tokenSymbolSearch.add(token.properties['token1_address']); + } + if (token.properties['token2_address'] != 'UCO') { + tokenSymbolSearch.add(token.properties['token2_address']); + } + final tokensSymbolMap = await getToken( + tokenSymbolSearch, + apiService, + ); + pairSymbolToken1 = token.properties['token1_address'] != 'UCO' + ? tokensSymbolMap[token.properties['token1_address']] != null + ? tokensSymbolMap[token.properties['token1_address']]! + .symbol! + : '' + : 'UCO'; + pairSymbolToken2 = token.properties['token2_address'] != 'UCO' + ? tokensSymbolMap[token.properties['token2_address']] != null + ? tokensSymbolMap[token.properties['token2_address']]! + .symbol! + : '' + : 'UCO'; + } + + final defToken = await aedappfm.DefTokensRepositoryImpl() + .getDefToken(token.address!.toUpperCase()); + final aeToken = AEToken( + name: defToken?.name ?? '', + address: token.address!.toUpperCase(), + balance: archethic.fromBigInt(tokenBalance.amount).toDouble(), + icon: defToken?.icon, + coingeckoCoinId: defToken?.coingeckoCoinId ?? '', + supply: archethic.fromBigInt(token.supply).toDouble(), + isLpToken: pairSymbolToken1 != null && pairSymbolToken2 != null, + symbol: pairSymbolToken1 != null && pairSymbolToken2 != null + ? 'LP Token' + : token.symbol!, + lpTokenPair: pairSymbolToken1 != null && pairSymbolToken2 != null + ? aedappfm.AETokenPair( + token1: AEToken( + symbol: pairSymbolToken1, + ), + token2: AEToken( + symbol: pairSymbolToken2, + ), + ) + : null, + isVerified: verifiedTokens.contains(token.address!.toUpperCase()), + ); + tokensList.add(aeToken); + } + } + } + + return tokensList; + } +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c48ab2ccb..f62a28e5d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -256,7 +256,6 @@ "introNewWalletGetFirstInfosNameRequest": "Let's start by naming your first account, which will be stored on your decentralized keychain", "introNewWalletGetFirstInfosNameInfos": "It will allow you to distinguish this account from other accounts that you can, if you want, create later.\n\nWARNING : This name will be added to your decentralized keychain and cannot be modified.", "introNewWalletGetFirstInfosNameBlank": "Please, enter the name of the new account", - "priceChartHeader": "Price Chart", "blogHeader": "Archethic Blog", "contactExistsName": "You already have a contact with this name", "contactExistsAddress": "You already have a contact with this address", @@ -290,7 +289,6 @@ "archethicDoesntKeepCopy": "As a reminder, Archethic doesn't keep any copy.", "amountZero": "Your amount should be > 0", "maxSendRecipientMissing": "Please, enter the recipient to define the max amount.", - "fungiblesTokensListNoTokenYet": "No token yet", "createFungibleToken": "Create a token", "tokenInitialSupplyTooHigh": "The initial supply is too high", "tokenSymbolMissing": "Choose a Symbol for the Token", @@ -536,5 +534,6 @@ "transactionRawSmartContractCalls": "Smart contract calls", "transactionRawSmartContractCallAction": "Action", "transactionRawSmartContractCallAddress": "Contract address", - "transactionRawSmartContractCallArguments": "Arguments" + "transactionRawSmartContractCallArguments": "Arguments", + "price": "Price" } \ No newline at end of file diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index e83204899..621246c77 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -127,7 +127,6 @@ "updateAvailableTitle": "Mise à jour de l'application", "updateAvailableDesc": "Une nouvelle version (%1) est disponible.\n\nVeuillez mettre à jour l'application.", "createToken": "Créer un jeton", - "fungiblesTokensListNoTokenYet": "Aucun jeton pour le moment", "addTokenConfirmationMessage": "Confirmez-vous la création du jeton suivant ?", "addAccountConfirmationMessage": "Confirmez-vous la création du compte suivant ?", "searchNFTHint": "Rechercher un NFT\nà partir d'une adresse.", @@ -249,7 +248,6 @@ "introNewWalletGetFirstInfosNameRequest": "Commençons par donner un nom à votre premier compte, qui sera stocké sur votre porte-clés décentralisé.", "introNewWalletGetFirstInfosNameInfos": "Il vous permettra de distinguer ce compte avec les autres comptes que vous pourrez, si vous le souhaitez, créer par la suite.\n\nATTENTION : Ce nom sera rattaché à votre porte-clés décentralisé et ne pourra plus être modifié.", "introNewWalletGetFirstInfosNameBlank": "Veuillez préciser le nom du nouveau compte", - "priceChartHeader": "Graphique du cours", "blogHeader": "Le blog d'Archethic", "contactExistsName": "Vous possédez déjà un contact avec ce nom", "contactExistsAddress": "Vous possédez déjà un contact avec cette adresse", @@ -516,5 +514,6 @@ "transactionRawSmartContractCalls": "Appels smart contract", "transactionRawSmartContractCallAction": "Action", "transactionRawSmartContractCallAddress": "Adresse du contrat", - "transactionRawSmartContractCallArguments": "Arguments" + "transactionRawSmartContractCallArguments": "Arguments", + "price": "Prix" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index cade688d9..7161fca9d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import 'dart:io'; import 'package:aewallet/application/authentication/authentication.dart'; import 'package:aewallet/application/migrations/migration_manager.dart'; import 'package:aewallet/application/notification/providers.dart'; -import 'package:aewallet/application/oracle/provider.dart'; import 'package:aewallet/application/session/session.dart'; import 'package:aewallet/application/settings/language.dart'; import 'package:aewallet/application/settings/settings.dart'; @@ -27,6 +26,8 @@ import 'package:aewallet/ui/widgets/components/window_size.dart'; import 'package:aewallet/util/security_manager.dart'; import 'package:aewallet/util/service_locator.dart'; import 'package:aewallet/util/universal_platform.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -141,9 +142,9 @@ class AppState extends ConsumerState with WidgetsBindingObserver { Future didChangeAppLifecycleStateAsync(AppLifecycleState state) async { _logger.info('Lifecycle State : $state'); var isDeviceSecured = false; - ref.invalidate(ArchethicOracleUCOProviders.archethicOracleUCO); + ref.invalidate(aedappfm.ArchethicOracleUCOProviders.archethicOracleUCO); await ref - .read(ArchethicOracleUCOProviders.archethicOracleUCO.notifier) + .read(aedappfm.ArchethicOracleUCOProviders.archethicOracleUCO.notifier) .init(); // Account for user changing locale when leaving the app switch (state) { @@ -294,7 +295,9 @@ class SplashState extends ConsumerState with WidgetsBindingObserver { return; } await ref - .read(ArchethicOracleUCOProviders.archethicOracleUCO.notifier) + .read( + aedappfm.ArchethicOracleUCOProviders.archethicOracleUCO.notifier, + ) .init(); context.go(HomePage.routerPage); diff --git a/lib/model/available_networks.dart b/lib/model/available_networks.dart index 9bdaf8c6d..8f506d41c 100644 --- a/lib/model/available_networks.dart +++ b/lib/model/available_networks.dart @@ -84,6 +84,17 @@ class NetworksSetting extends SettingSelectionItem { } } + String getNetworkLabel() { + switch (network) { + case AvailableNetworks.archethicMainNet: + return 'mainnet'; + case AvailableNetworks.archethicTestNet: + return 'testnet'; + case AvailableNetworks.archethicDevNet: + return 'devnet'; + } + } + // For saving to shared prefs int getIndex() { return network.index; diff --git a/lib/model/blockchain/token_information.dart b/lib/model/blockchain/token_information.dart index a831e17bb..0013885ed 100644 --- a/lib/model/blockchain/token_information.dart +++ b/lib/model/blockchain/token_information.dart @@ -25,6 +25,7 @@ class TokenInformationConverter aeip: json['aeip'] as List?, decimals: json['decimals'] as int?, isLPToken: json['isLPToken'] as bool?, + isVerified: json['isVerified'] as bool?, ); } @@ -42,11 +43,12 @@ class TokenInformationConverter 'aeip': tokenInformation.aeip, 'decimals': tokenInformation.decimals, 'isLPToken': tokenInformation.isLPToken, + 'isVerified': tokenInformation.isVerified, }; } } -/// Next field available : 17 +/// Next field available : 18 @HiveType(typeId: HiveTypeIds.tokenInformation) class TokenInformation extends HiveObject { TokenInformation({ @@ -61,6 +63,7 @@ class TokenInformation extends HiveObject { this.aeip, this.decimals, this.isLPToken, + this.isVerified, }); /// Address of token @@ -106,4 +109,8 @@ class TokenInformation extends HiveObject { /// LP Token ? @HiveField(16) bool? isLPToken; + + /// Verified token ? + @HiveField(17) + bool? isVerified; } diff --git a/lib/model/blockchain/token_information.g.dart b/lib/model/blockchain/token_information.g.dart index 84863e46b..7042fa6df 100644 --- a/lib/model/blockchain/token_information.g.dart +++ b/lib/model/blockchain/token_information.g.dart @@ -30,13 +30,14 @@ class TokenInformationAdapter extends TypeAdapter { aeip: (fields[13] as List?)?.cast(), decimals: fields[15] as int?, isLPToken: fields[16] as bool?, + isVerified: fields[17] as bool?, ); } @override void write(BinaryWriter writer, TokenInformation obj) { writer - ..writeByte(11) + ..writeByte(12) ..writeByte(0) ..write(obj.address) ..writeByte(1) @@ -58,7 +59,9 @@ class TokenInformationAdapter extends TypeAdapter { ..writeByte(15) ..write(obj.decimals) ..writeByte(16) - ..write(obj.isLPToken); + ..write(obj.isLPToken) + ..writeByte(17) + ..write(obj.isVerified); } @override diff --git a/lib/model/data/account.dart b/lib/model/data/account.dart index de73376ae..a58e7e7f9 100644 --- a/lib/model/data/account.dart +++ b/lib/model/data/account.dart @@ -1,7 +1,10 @@ /// SPDX-License-Identifier: AGPL-3.0-or-later +import 'dart:developer'; + import 'package:aewallet/infrastructure/datasources/account.hive.dart'; import 'package:aewallet/infrastructure/datasources/appdb.hive.dart'; +import 'package:aewallet/infrastructure/datasources/preferences.hive.dart'; import 'package:aewallet/model/blockchain/keychain_secured_infos.dart'; import 'package:aewallet/model/blockchain/recent_transaction.dart'; import 'package:aewallet/model/data/account_balance.dart'; @@ -9,7 +12,10 @@ import 'package:aewallet/model/data/account_token.dart'; import 'package:aewallet/model/data/nft_infos_off_chain.dart'; import 'package:aewallet/service/app_service.dart'; import 'package:aewallet/util/get_it_instance.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:archethic_lib_dart/archethic_lib_dart.dart'; +import 'package:decimal/decimal.dart'; import 'package:hive/hive.dart'; part 'account.g.dart'; @@ -189,28 +195,64 @@ class Account extends HiveObject with KeychainServiceMixin { } Future updateBalance() async { + const _logName = 'updateBalance'; + var totalUSD = 0.0; final balanceGetResponseMap = await sl.get().getBalanceGetResponse([lastAddress!]); if (balanceGetResponseMap[lastAddress] == null) { return; } + final preferences = await PreferencesHiveDatasource.getInstance(); + final network = preferences.getNetwork().getNetworkLabel(); + final ucidsTokens = await aedappfm.UcidsTokensRepositoryImpl() + .getUcidsTokensFromNetwork(network); + log('ucidsTokens $ucidsTokens', name: _logName); + + final cryptoPrice = await aedappfm.CoinPriceRepositoryImpl().fetchPrices(); + log('cryptoPrice $cryptoPrice', name: _logName); + final balanceGetResponse = balanceGetResponseMap[lastAddress]!; + final ucoAmount = fromBigInt(balanceGetResponse.uco).toDouble(); final accountBalance = AccountBalance( nativeTokenName: AccountBalance.cryptoCurrencyLabel, - nativeTokenValue: fromBigInt(balanceGetResponse.uco).toDouble(), + nativeTokenValue: ucoAmount, ); + if (balanceGetResponse.uco > 0) { + final oracleUcoPrice = await sl.get().getOracleData(); + totalUSD = (Decimal.parse(totalUSD.toString()) + + Decimal.parse(ucoAmount.toString()) * + Decimal.parse(oracleUcoPrice.uco!.usd!.toString())) + .toDouble(); + log('totalUSD UCO $totalUSD', name: _logName); + } + for (final token in balanceGetResponse.token) { if (token.tokenId != null) { if (token.tokenId == 0) { accountBalance.tokensFungiblesNb++; + + final ucidsToken = ucidsTokens[token.address]; + if (ucidsToken != null && cryptoPrice != null) { + final amountTokenUSD = + (Decimal.parse(fromBigInt(token.amount).toString()) * + Decimal.parse( + aedappfm.CoinPriceRepositoryImpl() + .getPriceFromUcid(ucidsToken, cryptoPrice) + .toString(), + )) + .toDouble(); + log('totalUSD ${token.address} $amountTokenUSD', name: _logName); + totalUSD = totalUSD + amountTokenUSD; + } } else { accountBalance.nftNb++; } } } - + log('totalUSD $totalUSD', name: _logName); + accountBalance.totalUSD = totalUSD; balance = accountBalance; await updateAccount(); } diff --git a/lib/model/data/account_balance.dart b/lib/model/data/account_balance.dart index a49491acf..d4b700ebf 100644 --- a/lib/model/data/account_balance.dart +++ b/lib/model/data/account_balance.dart @@ -33,7 +33,7 @@ class AccountBalanceConverter } } -/// Next field available : 7 +/// Next field available : 8 @HiveType(typeId: HiveTypeIds.accountBalance) class AccountBalance extends HiveObject { AccountBalance({ @@ -41,6 +41,7 @@ class AccountBalance extends HiveObject { required this.nativeTokenName, this.tokensFungiblesNb = 0, this.nftNb = 0, + this.totalUSD = 0, }); static const String cryptoCurrencyLabel = 'UCO'; @@ -61,6 +62,10 @@ class AccountBalance extends HiveObject { @HiveField(6, defaultValue: 0) int nftNb; + /// Token Price + @HiveField(7, defaultValue: 0) + double totalUSD; + String nativeTokenValueToString(String locale, {int? digits}) { if (nativeTokenValue > 1000000) { return NumberUtil.formatThousands(nativeTokenValue.round()); diff --git a/lib/model/data/account_balance.g.dart b/lib/model/data/account_balance.g.dart index 62f9f521b..f4128b240 100644 --- a/lib/model/data/account_balance.g.dart +++ b/lib/model/data/account_balance.g.dart @@ -21,13 +21,14 @@ class AccountBalanceAdapter extends TypeAdapter { nativeTokenName: fields[1] as String, tokensFungiblesNb: fields[5] == null ? 0 : fields[5] as int, nftNb: fields[6] == null ? 0 : fields[6] as int, + totalUSD: fields[7] == null ? 0 : fields[7] as double, ); } @override void write(BinaryWriter writer, AccountBalance obj) { writer - ..writeByte(4) + ..writeByte(5) ..writeByte(0) ..write(obj.nativeTokenValue) ..writeByte(1) @@ -35,7 +36,9 @@ class AccountBalanceAdapter extends TypeAdapter { ..writeByte(5) ..write(obj.tokensFungiblesNb) ..writeByte(6) - ..write(obj.nftNb); + ..write(obj.nftNb) + ..writeByte(7) + ..write(obj.totalUSD); } @override diff --git a/lib/router/router.authenticated.dart b/lib/router/router.authenticated.dart index 2aea945fe..c80689b18 100644 --- a/lib/router/router.authenticated.dart +++ b/lib/router/router.authenticated.dart @@ -251,15 +251,35 @@ final _authenticatedRoutes = [ ), ), GoRoute( - path: ChartSheet.routerPage, - pageBuilder: (context, state) => CustomTransitionPage( - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - key: state.pageKey, - child: const ChartSheet(), - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child), - ), + path: TokenDetailSheet.routerPage, + pageBuilder: (context, state) { + final aeToken = const aedappfm.AETokenJsonConverter().fromJson( + (state.extra! as Map)['aeToken'], + ); + + final chartInfosJson = + (state.extra! as Map)['chartInfos']; + final chartInfos = chartInfosJson != null + ? (chartInfosJson as List) + .map( + (item) => + PriceHistoryValue.fromJson(item as Map), + ) + .toList() + : null; + + return CustomTransitionPage( + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + key: state.pageKey, + child: TokenDetailSheet( + aeToken: aeToken, + chartInfos: chartInfos, + ), + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ); + }, ), GoRoute( path: AppSeedBackupSheet.routerPage, @@ -301,33 +321,40 @@ final _authenticatedRoutes = [ ), GoRoute( path: TransferSheet.routerPage, - pageBuilder: (context, state) => CustomTransitionPage( - transitionDuration: Duration.zero, - reverseTransitionDuration: Duration.zero, - key: state.pageKey, - child: TransferSheet( - transferType: TransferType.values.byName( - (state.extra! as Map)['transferType']! as String, + pageBuilder: (context, state) { + return CustomTransitionPage( + transitionDuration: Duration.zero, + reverseTransitionDuration: Duration.zero, + key: state.pageKey, + child: TransferSheet( + transferType: TransferType.values.byName( + (state.extra! as Map)['transferType']! as String, + ), + recipient: TransferRecipient.fromJson( + (state.extra! as Map)['recipient'], + ), + actionButtonTitle: (state.extra! + as Map)['actionButtonTitle'] as String?, + aeToken: (state.extra! as Map)['aeToken'] == null + ? null + : const aedappfm.AETokenJsonConverter().fromJson( + (state.extra! as Map)['aeToken'], + ), + accountToken: + (state.extra! as Map)['accountToken'] == null + ? null + : const AccountTokenConverter().fromJson( + (state.extra! as Map)['accountToken'], + ), + tokenId: (state.extra! as Map)['tokenId'] as String?, ), - recipient: TransferRecipient.fromJson( - (state.extra! as Map)['recipient'], + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition( + opacity: animation, + child: child, ), - actionButtonTitle: (state.extra! - as Map)['actionButtonTitle'] as String?, - accountToken: - (state.extra! as Map)['accountToken'] == null - ? null - : const AccountTokenConverter().fromJson( - (state.extra! as Map)['accountToken'], - ), - tokenId: (state.extra! as Map)['tokenId'] as String?, - ), - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition( - opacity: animation, - child: child, - ), - ), + ); + }, ), GoRoute( path: NFTCreationProcessImportTabAEWebForm.routerPage, diff --git a/lib/router/router.dart b/lib/router/router.dart index bdb438564..7d8317573 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:aewallet/domain/models/market_price_history.dart'; import 'package:aewallet/infrastructure/rpc/deeplink_server.dart'; import 'package:aewallet/main.dart'; import 'package:aewallet/model/available_networks.dart'; @@ -45,9 +46,9 @@ import 'package:aewallet/ui/views/settings/backupseed_sheet.dart'; import 'package:aewallet/ui/views/settings/set_password.dart'; import 'package:aewallet/ui/views/settings/set_yubikey.dart'; import 'package:aewallet/ui/views/sheets/buy_sheet.dart'; -import 'package:aewallet/ui/views/sheets/chart_sheet.dart'; import 'package:aewallet/ui/views/sheets/connectivity_warning.dart'; import 'package:aewallet/ui/views/sheets/dex_sheet.dart'; +import 'package:aewallet/ui/views/tokens_detail/layouts/token_detail_sheet.dart'; import 'package:aewallet/ui/views/tokens_fungibles/layouts/add_token_sheet.dart'; import 'package:aewallet/ui/views/transactions/transaction_infos_sheet.dart'; import 'package:aewallet/ui/views/transfer/bloc/state.dart'; @@ -56,6 +57,8 @@ import 'package:aewallet/ui/widgets/components/sheet_skeleton.dart'; import 'package:aewallet/ui/widgets/components/show_sending_animation.dart'; import 'package:aewallet/ui/widgets/dialogs/network_dialog.dart'; import 'package:aewallet/util/get_it_instance.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/service/app_service.dart b/lib/service/app_service.dart index 03f2b7e6a..aa0a98178 100644 --- a/lib/service/app_service.dart +++ b/lib/service/app_service.dart @@ -39,6 +39,7 @@ class AppService { return transactionChainMap; } + // TODO(reddwarf03): doublons with TokenRepositoryImpl Future> getToken( List addresses, ) async { @@ -699,6 +700,7 @@ class AppService { return recentTransaction; } + // TODO(reddwarf03): USE PROVIDER Future> getFungiblesTokensList(String address) async { _logger.info( '>> START getFungiblesTokensList : ${DateTime.now()}', diff --git a/lib/ui/themes/styles.dart b/lib/ui/themes/styles.dart index 0614a7e88..890b4dd75 100644 --- a/lib/ui/themes/styles.dart +++ b/lib/ui/themes/styles.dart @@ -154,7 +154,7 @@ class ArchethicThemeStyles { static TextStyle get textStyleSize12W100PositiveValue { return TextStyle( fontSize: AppFontSizes.size12, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w100, color: ArchethicTheme.positiveValue, ); } @@ -162,7 +162,7 @@ class ArchethicThemeStyles { static TextStyle get textStyleSize12W100NegativeValue { return TextStyle( fontSize: AppFontSizes.size12, - fontWeight: FontWeight.w800, + fontWeight: FontWeight.w100, color: ArchethicTheme.negativeValue, ); } diff --git a/lib/ui/views/authenticate/auto_lock_guard.dart b/lib/ui/views/authenticate/auto_lock_guard.dart index bfba51854..b0baef98b 100644 --- a/lib/ui/views/authenticate/auto_lock_guard.dart +++ b/lib/ui/views/authenticate/auto_lock_guard.dart @@ -117,14 +117,14 @@ class _AutoLockGuardState extends ConsumerState ref.read(AuthenticationProviders.authenticationGuard.notifier).unlock(); break; - case AppLifecycleState.hidden: + case AppLifecycleState.inactive: ref .read(AuthenticationProviders.authenticationGuard.notifier) .scheduleNextStartupAutolock(); break; case AppLifecycleState.paused: case AppLifecycleState.detached: - case AppLifecycleState.inactive: + case AppLifecycleState.hidden: break; } super.didChangeAppLifecycleState(state); diff --git a/lib/ui/views/main/account_tab.dart b/lib/ui/views/main/account_tab.dart index 272b0e4cf..22b375d35 100644 --- a/lib/ui/views/main/account_tab.dart +++ b/lib/ui/views/main/account_tab.dart @@ -9,7 +9,7 @@ import 'package:aewallet/ui/views/blog/last_articles_list.dart'; import 'package:aewallet/ui/views/main/components/app_update_button.dart'; import 'package:aewallet/ui/views/main/components/menu_widget_wallet.dart'; import 'package:aewallet/ui/views/main/home_page.dart'; -import 'package:aewallet/ui/views/tokens_fungibles/layouts/fungibles_tokens_list.dart'; +import 'package:aewallet/ui/views/tokens_list/layouts/tokens_list_sheet.dart'; import 'package:aewallet/ui/views/transactions/transaction_recent_list.dart'; import 'package:aewallet/ui/widgets/balance/balance_infos.dart'; import 'package:aewallet/ui/widgets/components/refresh_indicator.dart'; @@ -82,30 +82,15 @@ class AccountTab extends ConsumerWidget { children: [ /// BALANCE const BalanceInfos(), - const SizedBox( - height: 10, - ), - - /// PRICE CHART - if (preferences.showPriceChart && - connectivityStatusProvider == - ConnectivityStatus.isConnected) - const BalanceInfosChart(), - - /// KPI - if (preferences.showPriceChart && - connectivityStatusProvider == - ConnectivityStatus.isConnected) - const BalanceInfosKpi(), const SizedBox( - height: 30, + height: 10, ), const MenuWidgetWallet(), const ExpandablePageView( children: [ TxList(), - FungiblesTokensListWidget(), + TokensListSheet(), ], ), diff --git a/lib/ui/views/main/components/menu_widget_wallet.dart b/lib/ui/views/main/components/menu_widget_wallet.dart index e1e83bd7f..ed335fac6 100644 --- a/lib/ui/views/main/components/menu_widget_wallet.dart +++ b/lib/ui/views/main/components/menu_widget_wallet.dart @@ -9,12 +9,11 @@ import 'package:aewallet/application/market_price.dart'; import 'package:aewallet/application/refresh_in_progress.dart'; import 'package:aewallet/application/settings/settings.dart'; import 'package:aewallet/application/verified_tokens.dart'; -import 'package:aewallet/ui/themes/archethic_theme.dart'; -import 'package:aewallet/ui/themes/styles.dart'; import 'package:aewallet/ui/views/contacts/layouts/contact_detail.dart'; import 'package:aewallet/ui/views/sheets/buy_sheet.dart'; import 'package:aewallet/ui/views/transfer/bloc/state.dart'; import 'package:aewallet/ui/views/transfer/layouts/transfer_sheet.dart'; +import 'package:aewallet/ui/widgets/components/action_button.dart'; import 'package:aewallet/util/get_it_instance.dart'; import 'package:aewallet/util/haptic_util.dart'; import 'package:archethic_lib_dart/archethic_lib_dart.dart'; @@ -58,7 +57,7 @@ class MenuWidgetWallet extends ConsumerWidget { children: [ if (accountSelected.balance!.isNativeTokenValuePositive() && connectivityStatusProvider == ConnectivityStatus.isConnected) - _ActionButton( + ActionButton( key: const Key('sendUCObutton'), text: localizations.send, icon: Symbols.call_made, @@ -83,7 +82,7 @@ class MenuWidgetWallet extends ConsumerWidget { .fade(duration: const Duration(milliseconds: 200)) .scale(duration: const Duration(milliseconds: 200)) else - _ActionButton( + ActionButton( text: localizations.send, icon: Symbols.call_made, enabled: false, @@ -92,7 +91,7 @@ class MenuWidgetWallet extends ConsumerWidget { .fade(duration: const Duration(milliseconds: 200)) .scale(duration: const Duration(milliseconds: 200)), if (contact != null) - _ActionButton( + ActionButton( key: const Key('receiveUCObutton'), text: localizations.receive, icon: Symbols.call_received, @@ -115,7 +114,7 @@ class MenuWidgetWallet extends ConsumerWidget { .fade(duration: const Duration(milliseconds: 250)) .scale(duration: const Duration(milliseconds: 250)) else - _ActionButton( + ActionButton( text: localizations.receive, icon: Symbols.call_received, enabled: false, @@ -124,7 +123,7 @@ class MenuWidgetWallet extends ConsumerWidget { .fade(duration: const Duration(milliseconds: 250)) .scale(duration: const Duration(milliseconds: 250)), if (connectivityStatusProvider == ConnectivityStatus.isConnected) - _ActionButton( + ActionButton( text: localizations.buy, icon: Symbols.add, onTap: () { @@ -139,7 +138,7 @@ class MenuWidgetWallet extends ConsumerWidget { .fade(duration: const Duration(milliseconds: 300)) .scale(duration: const Duration(milliseconds: 300)) else - _ActionButton( + ActionButton( text: localizations.buy, icon: Symbols.add, enabled: false, @@ -148,7 +147,7 @@ class MenuWidgetWallet extends ConsumerWidget { .fade(duration: const Duration(milliseconds: 300)) .scale(duration: const Duration(milliseconds: 300)), if (refreshInProgress == false) - _ActionButton( + ActionButton( text: localizations.refresh, icon: Symbols.refresh, onTap: () async { @@ -203,7 +202,7 @@ class MenuWidgetWallet extends ConsumerWidget { ), ), ), - _ActionButton( + ActionButton( text: localizations.refresh, icon: Symbols.refresh, enabled: false, @@ -219,98 +218,3 @@ class MenuWidgetWallet extends ConsumerWidget { ); } } - -class _ActionButton extends ConsumerWidget { - const _ActionButton({ - this.onTap, - required this.text, - required this.icon, - this.enabled = true, - super.key, - }); - - final VoidCallback? onTap; - final String text; - final IconData icon; - final bool enabled; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: onTap != null - ? InkWell( - onTap: onTap, - child: Column( - children: [ - ShaderMask( - child: SizedBox( - width: 40, - height: 40, - child: Icon( - icon, - weight: 800, - opticalSize: IconSize.opticalSizeM, - grade: IconSize.gradeM, - color: enabled - ? Colors.white - : ArchethicTheme.text.withOpacity(0.3), - size: 38, - ), - ), - shaderCallback: (Rect bounds) { - const rect = Rect.fromLTRB(0, 0, 40, 40); - return ArchethicTheme.gradient.createShader(rect); - }, - ), - const SizedBox(height: 5), - if (enabled) - Text( - text, - style: ArchethicThemeStyles.textStyleSize14W600Primary, - ) - else - Text( - text, - style: ArchethicThemeStyles - .textStyleSize14W600PrimaryDisabled, - ), - ], - ), - ) - : Column( - children: [ - ShaderMask( - child: SizedBox( - width: 40, - height: 40, - child: Icon( - icon, - color: enabled - ? Colors.white - : ArchethicTheme.text.withOpacity(0.3), - size: 38, - ), - ), - shaderCallback: (Rect bounds) { - const rect = Rect.fromLTRB(0, 0, 40, 40); - return ArchethicTheme.gradient.createShader(rect); - }, - ), - const SizedBox(height: 5), - if (enabled) - Text( - text, - style: ArchethicThemeStyles.textStyleSize14W600Primary, - ) - else - Text( - text, - style: - ArchethicThemeStyles.textStyleSize14W600PrimaryDisabled, - ), - ], - ), - ); - } -} diff --git a/lib/ui/views/main/components/sheet_appbar.dart b/lib/ui/views/main/components/sheet_appbar.dart index 3d1d423a0..f62d197c3 100644 --- a/lib/ui/views/main/components/sheet_appbar.dart +++ b/lib/ui/views/main/components/sheet_appbar.dart @@ -56,11 +56,13 @@ class SheetAppBar extends ConsumerWidget implements PreferredSizeWidget { fit: BoxFit.fitWidth, child: Column( children: [ - AutoSizeText( - title, - style: - styleTitle ?? ArchethicThemeStyles.textStyleSize24W700Primary, - ), + if (widgetBeforeTitle != null) widgetBeforeTitle!, + if (title.isNotEmpty) + AutoSizeText( + title, + style: styleTitle ?? + ArchethicThemeStyles.textStyleSize24W700Primary, + ), if (widgetAfterTitle != null) widgetAfterTitle!, ], ), diff --git a/lib/ui/views/main/home_page.dart b/lib/ui/views/main/home_page.dart index f698942d0..fb0f6bfab 100755 --- a/lib/ui/views/main/home_page.dart +++ b/lib/ui/views/main/home_page.dart @@ -24,20 +24,16 @@ import 'package:aewallet/ui/views/main/keychain_tab.dart'; import 'package:aewallet/ui/views/main/nft_tab.dart'; import 'package:aewallet/ui/views/messenger/bloc/providers.dart'; import 'package:aewallet/ui/views/messenger/layouts/messenger_tab.dart'; -import 'package:aewallet/ui/views/tokens_fungibles/layouts/add_token_sheet.dart'; import 'package:aewallet/ui/views/transactions/incoming_transactions_notifier.dart'; import 'package:aewallet/ui/widgets/components/sheet_skeleton.dart'; import 'package:aewallet/ui/widgets/components/sheet_skeleton_interface.dart'; import 'package:aewallet/ui/widgets/tab_item.dart'; import 'package:aewallet/util/get_it_instance.dart'; -import 'package:aewallet/util/haptic_util.dart'; import 'package:aewallet/util/notifications_util.dart'; import 'package:archethic_lib_dart/archethic_lib_dart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; class HomePage extends ConsumerStatefulWidget { @@ -434,13 +430,7 @@ class _ExpandablePageViewState extends ConsumerState final localizations = AppLocalizations.of(context)!; final session = ref.watch(SessionProviders.session).loggedIn; - final accountSelected = ref - .watch( - AccountProviders.selectedAccount, - ) - .valueOrNull; if (session == null) return const SizedBox(); - final preferences = ref.watch(SettingsProviders.settings); return DefaultTabController( length: 2, @@ -475,39 +465,6 @@ class _ExpandablePageViewState extends ConsumerState style: ArchethicThemeStyles.textStyleSize14W600Primary, textAlign: TextAlign.center, ), - const SizedBox( - width: 10, - ), - if (accountSelected?.balance - ?.isNativeTokenValuePositive() == - true) - InkWell( - onTap: () { - sl.get().feedback( - FeedbackType.light, - preferences.activeVibrations, - ); - context.go(AddTokenSheet.routerPage); - }, - child: ShaderMask( - child: SizedBox( - width: 30, - height: 30, - child: Icon( - Icons.add_circle_outline, - opticalSize: IconSize.opticalSizeM, - grade: IconSize.gradeM, - weight: 800, - color: ArchethicTheme.text, - size: 28, - ), - ), - shaderCallback: (Rect bounds) { - const rect = Rect.fromLTRB(0, 0, 40, 40); - return ArchethicTheme.gradient.createShader(rect); - }, - ), - ), ], ), ], diff --git a/lib/ui/views/sheets/chart_sheet.dart b/lib/ui/views/sheets/chart_sheet.dart deleted file mode 100755 index 1effce582..000000000 --- a/lib/ui/views/sheets/chart_sheet.dart +++ /dev/null @@ -1,213 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0-or-later -import 'package:aewallet/application/price_history/providers.dart'; -import 'package:aewallet/application/settings/settings.dart'; -import 'package:aewallet/domain/models/market_price_history.dart'; -import 'package:aewallet/ui/themes/archethic_theme.dart'; -import 'package:aewallet/ui/themes/styles.dart'; -import 'package:aewallet/ui/views/main/components/sheet_appbar.dart'; -import 'package:aewallet/ui/views/main/home_page.dart'; -import 'package:aewallet/ui/widgets/balance/balance_infos.dart'; -import 'package:aewallet/ui/widgets/components/history_chart.dart'; -import 'package:aewallet/ui/widgets/components/sheet_skeleton.dart'; -import 'package:aewallet/ui/widgets/components/sheet_skeleton_interface.dart'; -import 'package:aewallet/util/get_it_instance.dart'; -import 'package:aewallet/util/haptic_util.dart'; -import 'package:animate_do/animate_do.dart'; -import 'package:bottom_bar/bottom_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:go_router/go_router.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -class ChartSheet extends ConsumerWidget implements SheetSkeletonInterface { - const ChartSheet({ - super.key, - }); - - static const String routerPage = '/chart'; - - static const List _chartIntervalOptions = [ - MarketPriceHistoryInterval.hour, - MarketPriceHistoryInterval.day, - MarketPriceHistoryInterval.week, - MarketPriceHistoryInterval.twoWeeks, - MarketPriceHistoryInterval.month, - MarketPriceHistoryInterval.twoMonths, - MarketPriceHistoryInterval.year, - MarketPriceHistoryInterval.all, - ]; - - int _intervalOptionIndex(MarketPriceHistoryInterval interval) => - _chartIntervalOptions.indexWhere((element) => element == interval); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SheetSkeleton( - appBar: getAppBar(context, ref), - floatingActionButton: getFloatingActionButton(context, ref), - sheetContent: getSheetContent(context, ref), - ); - } - - @override - Widget getFloatingActionButton(BuildContext context, WidgetRef ref) { - return const SizedBox.shrink(); - } - - @override - PreferredSizeWidget getAppBar(BuildContext context, WidgetRef ref) { - return SheetAppBar( - title: AppLocalizations.of(context)!.chart, - widgetLeft: BackButton( - key: const Key('back'), - color: ArchethicTheme.text, - onPressed: () { - context.go(HomePage.routerPage); - }, - ), - ); - } - - @override - Widget getSheetContent(BuildContext context, WidgetRef ref) { - final currency = ref.watch( - SettingsProviders.settings.select((settings) => settings.currency), - ); - final selectedInterval = ref.watch(PriceHistoryProviders.scaleOption); - final asyncChartInfos = ref.watch( - PriceHistoryProviders.chartData( - scaleOption: selectedInterval, - ), - ); - - return Column( - children: [ - FadeIn( - duration: const Duration(milliseconds: 1000), - child: Container( - height: MediaQuery.of(context).size.height * 0.45, - padding: const EdgeInsets.only(top: 20), - child: Padding( - padding: const EdgeInsets.only(right: 5, left: 5), - child: asyncChartInfos.when( - data: (chartInfos) => HistoryChart( - intervals: chartInfos, - gradientColors: LinearGradient( - colors: [ - ArchethicTheme.text20, - ArchethicTheme.text, - ], - ), - gradientColorsBar: LinearGradient( - colors: [ - ArchethicTheme.text.withOpacity(0.9), - ArchethicTheme.text.withOpacity(0), - ], - begin: Alignment.center, - end: Alignment.bottomCenter, - ), - tooltipBg: ArchethicTheme.backgroundDark, - tooltipText: ArchethicThemeStyles.textStyleSize12W100Primary, - axisTextStyle: - ArchethicThemeStyles.textStyleSize12W100Primary, - optionChartSelected: selectedInterval, - currency: currency.name, - completeChart: true, - ), - error: (_, __) { - if (asyncChartInfos.isLoading) { - return const _ChartLoading(); - } - return const _ChartLoadFailed(); - }, - loading: () => const _ChartLoading(), - ), - ), - ), - ), - const SizedBox( - height: 30, - ), - Wrap( - children: [ - BottomBar( - selectedIndex: _intervalOptionIndex(selectedInterval), - curve: Curves.easeIn, - duration: const Duration(milliseconds: 500), - itemPadding: const EdgeInsets.all(10), - padding: const EdgeInsets.only(right: 10, left: 10), - onTap: (int index) async { - final settings = ref.read(SettingsProviders.settings); - - sl.get().feedback( - FeedbackType.light, - settings.activeVibrations, - ); - await ref - .read(SettingsProviders.settings.notifier) - .setPriceChartInterval(_chartIntervalOptions[index]); - }, - items: _chartIntervalOptions.map((optionChart) { - return BottomBarItem( - icon: Text( - optionChart.getChartOptionLabel(context), - style: ArchethicThemeStyles.textStyleSize12W100Primary, - ), - backgroundColorOpacity: - ArchethicTheme.bottomBarBackgroundColorOpacity, - activeIconColor: ArchethicTheme.bottomBarActiveIconColor, - activeTitleColor: ArchethicTheme.bottomBarActiveTitleColor, - activeColor: ArchethicTheme.bottomBarActiveColor, - inactiveColor: ArchethicTheme.bottomBarInactiveIcon, - ); - }).toList(), - ), - ], - ), - if (asyncChartInfos.valueOrNull != null) const BalanceInfosKpi(), - ], - ); - } -} - -class _ChartLoading extends ConsumerWidget { - const _ChartLoading(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Center( - child: SizedBox( - height: 78, - child: Center( - child: CircularProgressIndicator( - color: ArchethicTheme.text, - strokeWidth: 1, - ), - ), - ), - ); - } -} - -class _ChartLoadFailed extends ConsumerWidget { - const _ChartLoadFailed(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final priceChartInterval = ref.watch(PriceHistoryProviders.scaleOption); - return Center( - child: TextButton( - child: Icon(Symbols.replay, color: ArchethicTheme.text, size: 30), - onPressed: () { - ref.invalidate( - PriceHistoryProviders.chartData( - scaleOption: priceChartInterval, - ), - ); - }, - ), - ); - } -} diff --git a/lib/ui/views/tokens_detail/bloc/provider.dart b/lib/ui/views/tokens_detail/bloc/provider.dart new file mode 100644 index 000000000..2e55bdfd4 --- /dev/null +++ b/lib/ui/views/tokens_detail/bloc/provider.dart @@ -0,0 +1,21 @@ +import 'package:aewallet/ui/views/tokens_detail/bloc/state.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +final _tokenDetailFormProvider = + AutoDisposeNotifierProvider( + TokenDetailFormNotifier.new, +); + +class TokenDetailFormNotifier + extends AutoDisposeNotifier { + TokenDetailFormNotifier(); + + @override + TokenDetailFormState build() { + return const TokenDetailFormState(); + } +} + +class TokenDetailFormProvider { + static final tokenDetailForm = _tokenDetailFormProvider; +} diff --git a/lib/ui/views/tokens_detail/bloc/state.dart b/lib/ui/views/tokens_detail/bloc/state.dart new file mode 100644 index 000000000..d3142cdc4 --- /dev/null +++ b/lib/ui/views/tokens_detail/bloc/state.dart @@ -0,0 +1,10 @@ +/// SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'state.freezed.dart'; + +@freezed +class TokenDetailFormState with _$TokenDetailFormState { + const factory TokenDetailFormState() = _TokenDetailFormState; + const TokenDetailFormState._(); +} diff --git a/lib/ui/views/tokens_detail/bloc/state.freezed.dart b/lib/ui/views/tokens_detail/bloc/state.freezed.dart new file mode 100644 index 000000000..4d376ed09 --- /dev/null +++ b/lib/ui/views/tokens_detail/bloc/state.freezed.dart @@ -0,0 +1,79 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$TokenDetailFormState {} + +/// @nodoc +abstract class $TokenDetailFormStateCopyWith<$Res> { + factory $TokenDetailFormStateCopyWith(TokenDetailFormState value, + $Res Function(TokenDetailFormState) then) = + _$TokenDetailFormStateCopyWithImpl<$Res, TokenDetailFormState>; +} + +/// @nodoc +class _$TokenDetailFormStateCopyWithImpl<$Res, + $Val extends TokenDetailFormState> + implements $TokenDetailFormStateCopyWith<$Res> { + _$TokenDetailFormStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$TokenDetailFormStateImplCopyWith<$Res> { + factory _$$TokenDetailFormStateImplCopyWith(_$TokenDetailFormStateImpl value, + $Res Function(_$TokenDetailFormStateImpl) then) = + __$$TokenDetailFormStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$TokenDetailFormStateImplCopyWithImpl<$Res> + extends _$TokenDetailFormStateCopyWithImpl<$Res, _$TokenDetailFormStateImpl> + implements _$$TokenDetailFormStateImplCopyWith<$Res> { + __$$TokenDetailFormStateImplCopyWithImpl(_$TokenDetailFormStateImpl _value, + $Res Function(_$TokenDetailFormStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$TokenDetailFormStateImpl extends _TokenDetailFormState { + const _$TokenDetailFormStateImpl() : super._(); + + @override + String toString() { + return 'TokenDetailFormState()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TokenDetailFormStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; +} + +abstract class _TokenDetailFormState extends TokenDetailFormState { + const factory _TokenDetailFormState() = _$TokenDetailFormStateImpl; + const _TokenDetailFormState._() : super._(); +} diff --git a/lib/ui/views/tokens_detail/layouts/components/token_detail_chart.dart b/lib/ui/views/tokens_detail/layouts/components/token_detail_chart.dart new file mode 100644 index 000000000..c10ecb8d9 --- /dev/null +++ b/lib/ui/views/tokens_detail/layouts/components/token_detail_chart.dart @@ -0,0 +1,59 @@ +import 'package:aewallet/application/price_history/providers.dart'; +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/domain/models/market_price_history.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:aewallet/ui/widgets/components/history_chart.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TokenDetailChart extends ConsumerWidget { + const TokenDetailChart({ + super.key, + this.chartInfos, + }); + + final List? chartInfos; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currency = ref.watch( + SettingsProviders.settings.select((settings) => settings.currency), + ); + final selectedInterval = ref.watch(PriceHistoryProviders.scaleOption); + if (chartInfos == null) { + return const SizedBox.shrink(); + } + return aedappfm.BlockInfo( + paddingEdgeInsetsInfo: const EdgeInsets.only(top: 15, right: 15), + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.35, + info: HistoryChart( + intervals: chartInfos!, + gradientColors: LinearGradient( + colors: [ + ArchethicTheme.text20, + ArchethicTheme.text, + ], + ), + gradientColorsBar: LinearGradient( + colors: [ + ArchethicTheme.text.withOpacity(0.9), + ArchethicTheme.text.withOpacity(0.1), + ], + begin: Alignment.center, + end: Alignment.bottomCenter, + ), + tooltipBg: ArchethicTheme.backgroundDark, + tooltipText: ArchethicThemeStyles.textStyleSize12W100Primary, + axisTextStyle: ArchethicThemeStyles.textStyleSize12W100Primary, + optionChartSelected: selectedInterval, + currency: currency.name, + completeChart: true, + lineTouchEnabled: true, + ), + ); + } +} diff --git a/lib/ui/views/tokens_detail/layouts/components/token_detail_chart_interval.dart b/lib/ui/views/tokens_detail/layouts/components/token_detail_chart_interval.dart new file mode 100644 index 000000000..0105a65fb --- /dev/null +++ b/lib/ui/views/tokens_detail/layouts/components/token_detail_chart_interval.dart @@ -0,0 +1,81 @@ +/// SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:aewallet/application/price_history/providers.dart'; +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/domain/models/market_price_history.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:aewallet/util/haptic_util.dart'; +import 'package:bottom_bar/bottom_bar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; + +class TokenDetailChartInterval extends ConsumerWidget { + const TokenDetailChartInterval({ + super.key, + this.chartInfos, + }); + + final List? chartInfos; + + static const List _chartIntervalOptions = [ + MarketPriceHistoryInterval.hour, + MarketPriceHistoryInterval.day, + MarketPriceHistoryInterval.week, + MarketPriceHistoryInterval.twoWeeks, + MarketPriceHistoryInterval.month, + MarketPriceHistoryInterval.twoMonths, + MarketPriceHistoryInterval.year, + MarketPriceHistoryInterval.all, + ]; + + int _intervalOptionIndex(MarketPriceHistoryInterval interval) => + _chartIntervalOptions.indexWhere((element) => element == interval); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedInterval = ref.watch(PriceHistoryProviders.scaleOption); + + if (chartInfos == null) { + return const SizedBox.shrink(); + } + + return Wrap( + children: [ + BottomBar( + selectedIndex: _intervalOptionIndex(selectedInterval), + curve: Curves.easeIn, + duration: const Duration(milliseconds: 500), + itemPadding: const EdgeInsets.all(10), + padding: const EdgeInsets.only(right: 10, left: 10), + onTap: (int index) async { + final settings = ref.read(SettingsProviders.settings); + + sl.get().feedback( + FeedbackType.light, + settings.activeVibrations, + ); + await ref + .read(SettingsProviders.settings.notifier) + .setPriceChartInterval(_chartIntervalOptions[index]); + }, + items: _chartIntervalOptions.map((optionChart) { + return BottomBarItem( + icon: Text( + optionChart.getChartOptionLabel(context), + style: ArchethicThemeStyles.textStyleSize12W100Primary, + ), + backgroundColorOpacity: + ArchethicTheme.bottomBarBackgroundColorOpacity, + activeIconColor: ArchethicTheme.bottomBarActiveIconColor, + activeTitleColor: ArchethicTheme.bottomBarActiveTitleColor, + activeColor: ArchethicTheme.bottomBarActiveColor, + inactiveColor: ArchethicTheme.bottomBarInactiveIcon, + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/ui/views/tokens_detail/layouts/components/token_detail_info.dart b/lib/ui/views/tokens_detail/layouts/components/token_detail_info.dart new file mode 100644 index 000000000..cfea8f4bb --- /dev/null +++ b/lib/ui/views/tokens_detail/layouts/components/token_detail_info.dart @@ -0,0 +1,84 @@ +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class TokenDetailInfo extends ConsumerWidget { + const TokenDetailInfo({ + super.key, + required this.aeToken, + }); + + final AEToken aeToken; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(SettingsProviders.settings); + final price = ref.watch( + aedappfm.AETokensProviders.estimateTokenInFiat( + aeToken, + ), + ); + return Column( + children: [ + if (aeToken.icon != null && aeToken.icon!.isNotEmpty) + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.2), + ), + ), + SvgPicture.asset( + 'assets/bc-logos/${aeToken.icon}', + width: 20, + height: 20, + ), + ], + ), + const SizedBox( + height: 10, + ), + if (settings.showBalances == true) + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${aeToken.balance.formatNumber(precision: 8)} ${aeToken.symbol}', + style: ArchethicThemeStyles.textStyleSize16W600Primary, + ), + const SizedBox(width: 5), + AutoSizeText( + '\$${(aeToken.balance * price).formatNumber(precision: 2)}', + style: ArchethicThemeStyles.textStyleSize12W100Primary, + ), + ], + ) + else + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '···········', + style: ArchethicThemeStyles.textStyleSize16W400Primary60, + ), + const SizedBox(width: 5), + AutoSizeText( + '···········', + style: ArchethicThemeStyles.textStyleSize12W100Primary60, + ), + ], + ), + ], + ); + } +} diff --git a/lib/ui/views/tokens_detail/layouts/token_detail_sheet.dart b/lib/ui/views/tokens_detail/layouts/token_detail_sheet.dart new file mode 100644 index 000000000..112bdee71 --- /dev/null +++ b/lib/ui/views/tokens_detail/layouts/token_detail_sheet.dart @@ -0,0 +1,218 @@ +import 'package:aewallet/application/account/providers.dart'; +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/domain/models/market_price_history.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:aewallet/ui/util/address_formatters.dart'; +import 'package:aewallet/ui/util/dimens.dart'; +import 'package:aewallet/ui/util/ui_util.dart'; +import 'package:aewallet/ui/views/main/components/sheet_appbar.dart'; +import 'package:aewallet/ui/views/tokens_detail/layouts/components/token_detail_chart.dart'; +import 'package:aewallet/ui/views/tokens_detail/layouts/components/token_detail_chart_interval.dart'; +import 'package:aewallet/ui/views/tokens_detail/layouts/components/token_detail_info.dart'; +import 'package:aewallet/ui/views/transfer/bloc/state.dart'; +import 'package:aewallet/ui/views/transfer/layouts/transfer_sheet.dart'; +import 'package:aewallet/ui/widgets/balance/balance_infos.dart'; +import 'package:aewallet/ui/widgets/components/app_button_tiny.dart'; +import 'package:aewallet/ui/widgets/components/sheet_skeleton.dart'; +import 'package:aewallet/ui/widgets/components/sheet_skeleton_interface.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:aewallet/util/haptic_util.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:archethic_lib_dart/archethic_lib_dart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class TokenDetailSheet extends ConsumerWidget + implements SheetSkeletonInterface { + const TokenDetailSheet({ + super.key, + required this.aeToken, + this.chartInfos, + }); + + final aedappfm.AEToken aeToken; + final List? chartInfos; + + static const String routerPage = '/tokenDetail'; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SheetSkeleton( + appBar: getAppBar(context, ref), + floatingActionButton: getFloatingActionButton(context, ref), + sheetContent: getSheetContent(context, ref), + ); + } + + @override + Widget getFloatingActionButton(BuildContext context, WidgetRef ref) { + final localizations = AppLocalizations.of(context)!; + final preferences = ref.watch(SettingsProviders.settings); + final accountSelected = ref + .watch( + AccountProviders.selectedAccount, + ) + .valueOrNull; + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + AppButtonTinyConnectivity( + localizations.viewExplorer, + Dimens.buttonTopDimens, + key: const Key('viewExplorer'), + onPressed: () async { + UIUtil.showWebview( + context, + '${ref.read(SettingsProviders.settings).network.getLink()}/explorer/transaction/${aeToken.address}', + '', + ); + }, + ), + ], + ), + Row( + children: [ + AppButtonTinyConnectivity( + localizations.send, + Dimens.buttonBottomDimens, + key: const Key('addAccount'), + onPressed: () async { + sl.get().feedback( + FeedbackType.light, + preferences.activeVibrations, + ); + + await TransferSheet( + transferType: + aeToken.isUCO ? TransferType.uco : TransferType.token, + recipient: const TransferRecipient.address( + address: Address(address: ''), + ), + aeToken: aeToken, + ).show( + context: context, + ref: ref, + ); + }, + disabled: !accountSelected!.balance!.isNativeTokenValuePositive(), + ), + ], + ), + ], + ); + } + + @override + PreferredSizeWidget getAppBar(BuildContext context, WidgetRef ref) { + final localizations = AppLocalizations.of(context)!; + final preferences = ref.watch(SettingsProviders.settings); + return SheetAppBar( + title: '', + widgetBeforeTitle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + aeToken.symbol, + style: ArchethicThemeStyles.textStyleSize24W700Primary, + ), + if (aeToken.isVerified) + Padding( + padding: const EdgeInsets.only( + left: 5, + bottom: 1, + ), + child: Icon( + Symbols.verified, + color: ArchethicTheme.activeColorSwitch, + size: 15, + ), + ), + ], + ), + widgetAfterTitle: aeToken.address != null && aeToken.address!.isNotEmpty + ? InkWell( + onTap: () { + sl.get().feedback( + FeedbackType.light, + preferences.activeVibrations, + ); + Clipboard.setData( + ClipboardData( + text: aeToken.address ?? '', + ), + ); + UIUtil.showSnackbar( + '${localizations.addressCopied}\n${aeToken.address!.toLowerCase()}', + context, + ref, + ArchethicTheme.text, + ArchethicTheme.snackBarShadow, + icon: Symbols.info, + ); + }, + child: Row( + children: [ + Text( + AddressFormatters( + aeToken.address ?? '', + ).getShortString4().toLowerCase(), + style: ArchethicThemeStyles.textStyleSize14W600Primary, + ), + const SizedBox( + width: 5, + ), + const Icon( + Symbols.content_copy, + weight: IconSize.weightM, + opticalSize: IconSize.opticalSizeM, + grade: IconSize.gradeM, + size: 16, + ), + ], + ), + ) + : const SizedBox.shrink(), + widgetLeft: BackButton( + key: const Key('back'), + color: ArchethicTheme.text, + onPressed: () { + context.pop(); + }, + ), + ); + } + + @override + Widget getSheetContent(BuildContext context, WidgetRef ref) { + return Column( + children: [ + TokenDetailInfo( + aeToken: aeToken, + ), + TokenDetailChart(chartInfos: chartInfos), + SizedBox( + width: MediaQuery.of(context).size.width, + height: 20, + ), + TokenDetailChartInterval(chartInfos: chartInfos), + if (chartInfos != null) + Padding( + padding: const EdgeInsets.only(top: 20, left: 10), + child: BalanceInfosKpi( + chartInfos: chartInfos, + aeToken: aeToken, + ), + ), + ], + ); + } +} diff --git a/lib/ui/views/tokens_fungibles/layouts/components/add_token_confirm_sheet.dart b/lib/ui/views/tokens_fungibles/layouts/components/add_token_confirm_sheet.dart index 5e7ada2e3..53529cc24 100755 --- a/lib/ui/views/tokens_fungibles/layouts/components/add_token_confirm_sheet.dart +++ b/lib/ui/views/tokens_fungibles/layouts/components/add_token_confirm_sheet.dart @@ -9,7 +9,6 @@ import 'package:aewallet/ui/themes/styles.dart'; import 'package:aewallet/ui/util/dimens.dart'; import 'package:aewallet/ui/util/ui_util.dart'; import 'package:aewallet/ui/views/main/components/sheet_appbar.dart'; -import 'package:aewallet/ui/views/main/home_page.dart'; import 'package:aewallet/ui/views/tokens_fungibles/bloc/provider.dart'; import 'package:aewallet/ui/views/tokens_fungibles/bloc/state.dart'; import 'package:aewallet/ui/views/tokens_fungibles/layouts/components/add_token_detail.dart'; @@ -100,8 +99,7 @@ class _AddTokenConfirmState extends ConsumerState .read(AccountProviders.selectedAccount.notifier) .refreshFungibleTokens(), ); - - context.go(HomePage.routerPage); + context.pop(); } void _showSendFailed( diff --git a/lib/ui/views/tokens_fungibles/layouts/components/add_token_form_sheet.dart b/lib/ui/views/tokens_fungibles/layouts/components/add_token_form_sheet.dart index 7916dae7b..b4e56ae56 100755 --- a/lib/ui/views/tokens_fungibles/layouts/components/add_token_form_sheet.dart +++ b/lib/ui/views/tokens_fungibles/layouts/components/add_token_form_sheet.dart @@ -6,7 +6,6 @@ import 'package:aewallet/ui/themes/styles.dart'; import 'package:aewallet/ui/util/dimens.dart'; import 'package:aewallet/ui/util/formatters.dart'; import 'package:aewallet/ui/views/main/components/sheet_appbar.dart'; -import 'package:aewallet/ui/views/main/home_page.dart'; import 'package:aewallet/ui/views/tokens_fungibles/bloc/provider.dart'; import 'package:aewallet/ui/views/tokens_fungibles/bloc/state.dart'; import 'package:aewallet/ui/widgets/balance/balance_indicator.dart'; @@ -79,7 +78,7 @@ class AddTokenFormSheet extends ConsumerWidget key: const Key('back'), color: ArchethicTheme.text, onPressed: () { - context.go(HomePage.routerPage); + context.pop(); }, ), widgetBeforeTitle: const NetworkIndicator(), diff --git a/lib/ui/views/tokens_fungibles/layouts/fungibles_tokens_list.dart b/lib/ui/views/tokens_fungibles/layouts/fungibles_tokens_list.dart deleted file mode 100644 index b66a49c53..000000000 --- a/lib/ui/views/tokens_fungibles/layouts/fungibles_tokens_list.dart +++ /dev/null @@ -1,332 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0-or-later - -import 'dart:ui'; - -import 'package:aewallet/application/account/providers.dart'; -import 'package:aewallet/application/settings/settings.dart'; -import 'package:aewallet/domain/models/token.dart'; -import 'package:aewallet/model/data/account_token.dart'; -import 'package:aewallet/ui/themes/archethic_theme.dart'; -import 'package:aewallet/ui/themes/styles.dart'; -import 'package:aewallet/ui/util/address_formatters.dart'; -import 'package:aewallet/ui/util/ui_util.dart'; -import 'package:aewallet/ui/views/transfer/bloc/state.dart'; -import 'package:aewallet/ui/views/transfer/layouts/transfer_sheet.dart'; -import 'package:aewallet/ui/widgets/tokens/verified_token_icon.dart'; -import 'package:aewallet/util/get_it_instance.dart'; -import 'package:aewallet/util/haptic_util.dart'; -import 'package:aewallet/util/number_util.dart'; -import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' - as aedappfm; -import 'package:archethic_lib_dart/archethic_lib_dart.dart'; -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_gen/gen_l10n/localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -class FungiblesTokensListWidget extends ConsumerWidget { - const FungiblesTokensListWidget({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final localizations = AppLocalizations.of(context)!; - final fungibleTokensAsyncValue = ref.watch( - AccountProviders.selectedAccount - .select((value) => value.valueOrNull?.accountTokens), - ) ?? - []; - - var index = 0; - if (fungibleTokensAsyncValue.isEmpty == true) { - return Container( - alignment: Alignment.center, - color: Colors.transparent, - width: MediaQuery.of(context).size.width, - child: Padding( - padding: const EdgeInsets.only(top: 6), - child: Card( - shape: RoundedRectangleBorder( - side: BorderSide( - color: ArchethicTheme.backgroundFungiblesTokensListCard, - ), - borderRadius: BorderRadius.circular(10), - ), - elevation: 0, - color: ArchethicTheme.backgroundFungiblesTokensListCard, - child: Container( - padding: const EdgeInsets.all(9.5), - width: MediaQuery.of(context).size.width, - alignment: Alignment.center, - child: Row( - children: [ - const Icon( - Symbols.info, - size: 18, - ), - const SizedBox(width: 8), - Text( - localizations.fungiblesTokensListNoTokenYet, - style: ArchethicThemeStyles.textStyleSize12W100Primary, - ), - ], - ), - ), - ), - ), - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: fungibleTokensAsyncValue.map((accountToken) { - index++; - return _FungiblesTokensLine( - accountToken: accountToken, - ) - .animate(delay: (100 * index).ms) - .fadeIn(duration: 400.ms, delay: 200.ms) - .move( - begin: const Offset(-16, 0), - curve: Curves.easeOutQuad, - ); - }).toList(), - ); - } -} - -class _FungiblesTokensLine extends StatelessWidget { - const _FungiblesTokensLine({ - required this.accountToken, - }); - - final AccountToken accountToken; - - @override - Widget build(BuildContext context) { - return Container( - color: Colors.transparent, - width: MediaQuery.of(context).size.width, - child: Padding( - padding: const EdgeInsets.only(top: 15), - child: _FungiblesTokensDetailTransfer( - accountFungibleToken: accountToken, - ), - ), - ); - } -} - -class _FungiblesTokensDetailTransfer extends ConsumerWidget { - const _FungiblesTokensDetailTransfer({ - required this.accountFungibleToken, - }); - - final AccountToken accountFungibleToken; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final preferences = ref.watch(SettingsProviders.settings); - final localizations = AppLocalizations.of(context)!; - return Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: DecoratedBox( - decoration: BoxDecoration( - color: aedappfm.AppThemeBase.sheetBackground, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: aedappfm.AppThemeBase.sheetBorder, - ), - ), - child: Container( - padding: const EdgeInsets.all(9.5), - width: MediaQuery.of(context).size.width, - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Row( - children: [ - Container( - alignment: Alignment.center, - height: 40, - width: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: ArchethicTheme.backgroundDark - .withOpacity(0.3), - border: Border.all( - color: ArchethicTheme.backgroundDarkest - .withOpacity(0.2), - width: 2, - ), - ), - child: IconButton( - icon: Icon( - Symbols.arrow_circle_up, - color: ArchethicTheme.backgroundDarkest, - size: 21, - ), - onPressed: () async { - sl.get().feedback( - FeedbackType.light, - preferences.activeVibrations, - ); - await TransferSheet( - transferType: TransferType.token, - accountToken: accountFungibleToken, - recipient: - const TransferRecipient.address( - address: Address(address: ''), - ), - ).show( - context: context, - ref: ref, - ); - }, - ), - ), - const SizedBox( - width: 10, - ), - Expanded( - child: InkWell( - key: const Key('viewExplorer'), - onTap: () { - UIUtil.showWebview( - context, - '${ref.read(SettingsProviders.settings).network.getLink()}/explorer/transaction/${accountFungibleToken.tokenInformation!.address!}', - '', - ); - }, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: AutoSizeText( - accountFungibleToken - .tokenInformation!.name!, - style: ArchethicThemeStyles - .textStyleSize12W100Primary, - ), - ), - ], - ), - Row( - children: [ - AutoSizeText( - AddressFormatters( - accountFungibleToken - .tokenInformation!.address!, - ).getShortString4(), - style: ArchethicThemeStyles - .textStyleSize12W100Primary, - ), - const SizedBox(width: 5), - const Icon( - Symbols.open_in_new, - size: 11, - ), - ], - ), - if (preferences.showBalances == true) - Row( - children: [ - Text( - '${NumberUtil.formatThousands(accountFungibleToken.amount!)} ${accountFungibleToken.tokenInformation!.symbol!}', - style: ArchethicThemeStyles - .textStyleSize12W100Primary, - ), - const SizedBox(width: 5), - VerifiedTokenIcon( - address: accountFungibleToken - .tokenInformation!.address!, - ), - ], - ) - else - Row( - children: [ - Text( - '···········', - style: ArchethicThemeStyles - .textStyleSize12W100Primary60, - ), - const SizedBox(width: 5), - Row( - children: [ - Text( - accountFungibleToken - .tokenInformation! - .symbol!, - style: ArchethicThemeStyles - .textStyleSize12W100Primary, - ), - const SizedBox( - width: 5, - ), - VerifiedTokenIcon( - address: accountFungibleToken - .tokenInformation! - .address!, - ), - ], - ), - ], - ), - if (kTokenFordiddenName.contains( - accountFungibleToken - .tokenInformation!.name! - .toUpperCase(), - ) || - kTokenFordiddenName.contains( - accountFungibleToken - .tokenInformation!.symbol! - .toUpperCase(), - )) - Row( - children: [ - const Icon( - Symbols.warning, - size: 10, - ), - const SizedBox(width: 5), - Text( - localizations - .notOfficialUCOWarning, - style: ArchethicThemeStyles - .textStyleSize10W100Primary, - textAlign: TextAlign.end, - ), - ], - ), - ], - ), - ), - ), - ], - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/ui/views/tokens_list/bloc/provider.dart b/lib/ui/views/tokens_list/bloc/provider.dart new file mode 100644 index 000000000..d323726c2 --- /dev/null +++ b/lib/ui/views/tokens_list/bloc/provider.dart @@ -0,0 +1,110 @@ +import 'package:aewallet/application/account/providers.dart'; +import 'package:aewallet/application/price_history/providers.dart'; +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/application/tokens/tokens.dart'; +import 'package:aewallet/domain/models/market_price_history.dart'; +import 'package:aewallet/ui/views/tokens_list/bloc/state.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +final _tokensListFormProvider = + AutoDisposeNotifierProvider( + TokensListFormNotifier.new, +); + +class TokensListFormNotifier extends AutoDisposeNotifier { + TokensListFormNotifier(); + + @override + TokensListFormState build() { + return const TokensListFormState( + tokensToDisplay: AsyncValue.loading(), + ); + } + + void reset() { + state = state.copyWith( + tokensToDisplay: const AsyncValue.data([]), + ); + } + + Future setSearchCriteria(String searchCriteria) async { + state = state.copyWith(searchCriteria: searchCriteria); + await getTokensList( + cancelToken: UniqueKey().toString(), + ); + } + + Future getTokensList({ + required String cancelToken, + }) async { + state = state.copyWith( + tokensToDisplay: const AsyncValue.loading(), + cancelToken: cancelToken, + ); + + final selectedAccount = await ref.read( + AccountProviders.selectedAccount.future, + ); + + final tokensList = await ref.read( + TokensProviders.getTokensList(selectedAccount!.genesisAddress).future, + ); + + final sortedTokens = tokensList.toList() + ..sort((a, b) { + if (a.address == null && b.address != null) return -1; + if (a.address != null && b.address == null) return 1; + + if (a.isVerified && !b.isVerified) return -1; + if (!a.isVerified && b.isVerified) return 1; + + if (!a.isLpToken && b.isLpToken) return -1; + if (a.isLpToken && !b.isLpToken) return 1; + + final symbolComparison = a.symbol.compareTo(b.symbol); + if (symbolComparison != 0) return symbolComparison; + + return 0; + }); + + if (state.searchCriteria.isNotEmpty) { + sortedTokens.removeWhere( + (element) => + element.symbol.toUpperCase().contains(state.searchCriteria) == + false && + element.address?.toUpperCase().contains(state.searchCriteria) == + false, + ); + } + + if (state.cancelToken == cancelToken) { + state = state.copyWith( + tokensToDisplay: AsyncValue.data(sortedTokens), + ); + } + } + + Future?> getPriceHistoryValues( + String? coingeckoCoinId, + ) async { + List? chartInfos; + + if (coingeckoCoinId != null && coingeckoCoinId.isNotEmpty) { + final settings = ref.watch(SettingsProviders.settings); + + chartInfos = await ref.read( + PriceHistoryProviders.priceHistory( + scaleOption: settings.priceChartIntervalOption, + coinId: coingeckoCoinId, + ).future, + ); + } + return chartInfos; + } +} + +class TokensListFormProvider { + static final tokensListForm = _tokensListFormProvider; +} diff --git a/lib/ui/views/tokens_list/bloc/state.dart b/lib/ui/views/tokens_list/bloc/state.dart new file mode 100644 index 000000000..aa353585d --- /dev/null +++ b/lib/ui/views/tokens_list/bloc/state.dart @@ -0,0 +1,18 @@ +/// SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'state.freezed.dart'; + +enum AddTokenProcessStep { form, confirmation } + +@freezed +class TokensListFormState with _$TokensListFormState { + const factory TokensListFormState({ + required AsyncValue?> tokensToDisplay, + String? cancelToken, + @Default('') String searchCriteria, + }) = _TokensListFormState; + const TokensListFormState._(); +} diff --git a/lib/ui/views/tokens_list/bloc/state.freezed.dart b/lib/ui/views/tokens_list/bloc/state.freezed.dart new file mode 100644 index 000000000..c28816148 --- /dev/null +++ b/lib/ui/views/tokens_list/bloc/state.freezed.dart @@ -0,0 +1,185 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$TokensListFormState { + AsyncValue?> get tokensToDisplay => + throw _privateConstructorUsedError; + String? get cancelToken => throw _privateConstructorUsedError; + String get searchCriteria => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $TokensListFormStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TokensListFormStateCopyWith<$Res> { + factory $TokensListFormStateCopyWith( + TokensListFormState value, $Res Function(TokensListFormState) then) = + _$TokensListFormStateCopyWithImpl<$Res, TokensListFormState>; + @useResult + $Res call( + {AsyncValue?> tokensToDisplay, + String? cancelToken, + String searchCriteria}); +} + +/// @nodoc +class _$TokensListFormStateCopyWithImpl<$Res, $Val extends TokensListFormState> + implements $TokensListFormStateCopyWith<$Res> { + _$TokensListFormStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tokensToDisplay = null, + Object? cancelToken = freezed, + Object? searchCriteria = null, + }) { + return _then(_value.copyWith( + tokensToDisplay: null == tokensToDisplay + ? _value.tokensToDisplay + : tokensToDisplay // ignore: cast_nullable_to_non_nullable + as AsyncValue?>, + cancelToken: freezed == cancelToken + ? _value.cancelToken + : cancelToken // ignore: cast_nullable_to_non_nullable + as String?, + searchCriteria: null == searchCriteria + ? _value.searchCriteria + : searchCriteria // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$TokensListFormStateImplCopyWith<$Res> + implements $TokensListFormStateCopyWith<$Res> { + factory _$$TokensListFormStateImplCopyWith(_$TokensListFormStateImpl value, + $Res Function(_$TokensListFormStateImpl) then) = + __$$TokensListFormStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {AsyncValue?> tokensToDisplay, + String? cancelToken, + String searchCriteria}); +} + +/// @nodoc +class __$$TokensListFormStateImplCopyWithImpl<$Res> + extends _$TokensListFormStateCopyWithImpl<$Res, _$TokensListFormStateImpl> + implements _$$TokensListFormStateImplCopyWith<$Res> { + __$$TokensListFormStateImplCopyWithImpl(_$TokensListFormStateImpl _value, + $Res Function(_$TokensListFormStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tokensToDisplay = null, + Object? cancelToken = freezed, + Object? searchCriteria = null, + }) { + return _then(_$TokensListFormStateImpl( + tokensToDisplay: null == tokensToDisplay + ? _value.tokensToDisplay + : tokensToDisplay // ignore: cast_nullable_to_non_nullable + as AsyncValue?>, + cancelToken: freezed == cancelToken + ? _value.cancelToken + : cancelToken // ignore: cast_nullable_to_non_nullable + as String?, + searchCriteria: null == searchCriteria + ? _value.searchCriteria + : searchCriteria // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$TokensListFormStateImpl extends _TokensListFormState { + const _$TokensListFormStateImpl( + {required this.tokensToDisplay, + this.cancelToken, + this.searchCriteria = ''}) + : super._(); + + @override + final AsyncValue?> tokensToDisplay; + @override + final String? cancelToken; + @override + @JsonKey() + final String searchCriteria; + + @override + String toString() { + return 'TokensListFormState(tokensToDisplay: $tokensToDisplay, cancelToken: $cancelToken, searchCriteria: $searchCriteria)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TokensListFormStateImpl && + (identical(other.tokensToDisplay, tokensToDisplay) || + other.tokensToDisplay == tokensToDisplay) && + (identical(other.cancelToken, cancelToken) || + other.cancelToken == cancelToken) && + (identical(other.searchCriteria, searchCriteria) || + other.searchCriteria == searchCriteria)); + } + + @override + int get hashCode => + Object.hash(runtimeType, tokensToDisplay, cancelToken, searchCriteria); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$TokensListFormStateImplCopyWith<_$TokensListFormStateImpl> get copyWith => + __$$TokensListFormStateImplCopyWithImpl<_$TokensListFormStateImpl>( + this, _$identity); +} + +abstract class _TokensListFormState extends TokensListFormState { + const factory _TokensListFormState( + {required final AsyncValue?> tokensToDisplay, + final String? cancelToken, + final String searchCriteria}) = _$TokensListFormStateImpl; + const _TokensListFormState._() : super._(); + + @override + AsyncValue?> get tokensToDisplay; + @override + String? get cancelToken; + @override + String get searchCriteria; + @override + @JsonKey(ignore: true) + _$$TokensListFormStateImplCopyWith<_$TokensListFormStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/views/tokens_list/layouts/components/token_add_btn.dart b/lib/ui/views/tokens_list/layouts/components/token_add_btn.dart new file mode 100644 index 000000000..8479455b3 --- /dev/null +++ b/lib/ui/views/tokens_list/layouts/components/token_add_btn.dart @@ -0,0 +1,63 @@ +/// SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:aewallet/application/account/providers.dart'; +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/ui/views/tokens_fungibles/layouts/add_token_sheet.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:aewallet/util/haptic_util.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class TokenAddBtn extends ConsumerWidget { + const TokenAddBtn({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final preferences = ref.watch(SettingsProviders.settings); + final accountSelected = ref + .watch( + AccountProviders.selectedAccount, + ) + .valueOrNull; + if (accountSelected?.balance?.isNativeTokenValuePositive() == false) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.only(top: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + child: Container( + height: 30, + width: 30, + alignment: Alignment.center, + decoration: BoxDecoration( + gradient: aedappfm.AppThemeBase.gradientBtn, + shape: BoxShape.circle, + ), + child: const Icon( + Symbols.add, + size: 15, + ), + ), + onTap: () { + sl.get().feedback( + FeedbackType.light, + preferences.activeVibrations, + ); + context.push(AddTokenSheet.routerPage); + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/tokens_list/layouts/components/token_detail.dart b/lib/ui/views/tokens_list/layouts/components/token_detail.dart new file mode 100644 index 000000000..cf3adb491 --- /dev/null +++ b/lib/ui/views/tokens_list/layouts/components/token_detail.dart @@ -0,0 +1,259 @@ +import 'package:aewallet/application/connectivity_status.dart'; +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/domain/models/market_price_history.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:aewallet/ui/util/address_formatters.dart'; +import 'package:aewallet/ui/views/tokens_detail/layouts/token_detail_sheet.dart'; +import 'package:aewallet/ui/views/tokens_list/bloc/provider.dart'; +import 'package:aewallet/ui/widgets/balance/balance_infos.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:aewallet/util/haptic_util.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class TokenDetail extends ConsumerStatefulWidget { + const TokenDetail({ + super.key, + required this.aeToken, + }); + + final aedappfm.AEToken aeToken; + + @override + ConsumerState createState() => _TokenDetailState(); +} + +class _TokenDetailState extends ConsumerState { + @override + Widget build(BuildContext context) { + final settings = ref.watch(SettingsProviders.settings); + final connectivityStatusProvider = ref.watch(connectivityStatusProviders); + final price = widget.aeToken.isVerified + ? ref.watch( + aedappfm.AETokensProviders.estimateTokenInFiat( + widget.aeToken, + ), + ) + : 0.0; + + return FutureBuilder?>( + future: ref + .read(TokensListFormProvider.tokensListForm.notifier) + .getPriceHistoryValues(widget.aeToken.coingeckoCoinId), + builder: (context, snapshot) { + return InkWell( + onTap: () async { + sl.get().feedback( + FeedbackType.light, + settings.activeVibrations, + ); + + await context.push( + TokenDetailSheet.routerPage, + extra: { + 'aeToken': widget.aeToken.toJson(), + 'chartInfos': + snapshot.data?.map((item) => item.toJson()).toList(), + }, + ); + }, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + aedappfm.BlockInfo( + width: MediaQuery.of(context).size.width, + height: 80, + info: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + if (widget.aeToken.icon != null && + widget.aeToken.icon!.isNotEmpty) + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(0.2), + ), + ), + SvgPicture.asset( + 'assets/bc-logos/${widget.aeToken.icon}', + width: 20, + height: 20, + ), + ], + ) + else + const SizedBox( + width: 30, + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + AutoSizeText( + minFontSize: 5, + wrapWords: false, + widget.aeToken.symbol, + style: ArchethicThemeStyles + .textStyleSize18W600MainButtonLabel, + ), + if (widget.aeToken.isVerified) + Padding( + padding: const EdgeInsets.only( + left: 5, + bottom: 1, + ), + child: Icon( + Symbols.verified, + color: + ArchethicTheme.activeColorSwitch, + size: 15, + ), + ), + ], + ), + + if (settings.showBalances == true) + Row( + children: [ + if (widget.aeToken.isLpToken) + AutoSizeText( + minFontSize: 5, + wrapWords: false, + '${widget.aeToken.balance.formatNumber(precision: 8)} ${widget.aeToken.lpTokenPair!.token1.symbol.reduceSymbol()}/${widget.aeToken.lpTokenPair!.token2.symbol.reduceSymbol()}', + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ) + else + AutoSizeText( + minFontSize: 5, + wrapWords: false, + '${widget.aeToken.balance.formatNumber(precision: 8)} ${widget.aeToken.symbol.reduceSymbol(lengthMax: 10)}', + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + const SizedBox(width: 5), + if (price > 0) + AutoSizeText( + minFontSize: 5, + wrapWords: false, + '\$${(widget.aeToken.balance * price).formatNumber(precision: 2)}', + textAlign: TextAlign.center, + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + ], + ) + else + Row( + children: [ + Text( + '···········', + style: ArchethicThemeStyles + .textStyleSize12W100Primary60, + ), + const SizedBox(width: 5), + ], + ), + if (widget.aeToken.isVerified == false) + AutoSizeText( + AddressFormatters( + widget.aeToken.address ?? '', + ).getShortString4(), + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + + /// KPI + if (settings.showPriceChart && + snapshot.hasData && + widget.aeToken.isVerified && + widget.aeToken.coingeckoCoinId != null && + widget + .aeToken.coingeckoCoinId!.isNotEmpty && + connectivityStatusProvider == + ConnectivityStatus.isConnected) + BalanceInfosKpi( + chartInfos: snapshot.data, + aeToken: widget.aeToken, + ), + ], + ), + ), + ], + ), + ), + ], + ), + blockInfoColor: widget.aeToken.isUCO + ? aedappfm.BlockInfoColor.purple + : widget.aeToken.isLpToken + ? aedappfm.BlockInfoColor.neutral + : aedappfm.BlockInfoColor.blue, + ), + + const Positioned( + right: 5, + top: 30, + child: Icon( + Symbols.keyboard_arrow_right, + weight: IconSize.weightM, + opticalSize: IconSize.opticalSizeM, + grade: IconSize.gradeM, + size: 16, + ), + ), + + /// PRICE CHART + if (settings.showPriceChart && + snapshot.hasData && + widget.aeToken.isVerified && + widget.aeToken.coingeckoCoinId != null && + widget.aeToken.coingeckoCoinId!.isNotEmpty && + connectivityStatusProvider == ConnectivityStatus.isConnected) + Positioned( + child: Column( + children: [ + BalanceInfosChart( + chartInfos: snapshot.data, + ), + Container( + height: 10, + decoration: BoxDecoration( + color: ArchethicTheme.text.withOpacity(0.05), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/views/tokens_list/layouts/components/token_detail_old.dart b/lib/ui/views/tokens_list/layouts/components/token_detail_old.dart new file mode 100644 index 000000000..3cad4d343 --- /dev/null +++ b/lib/ui/views/tokens_list/layouts/components/token_detail_old.dart @@ -0,0 +1,200 @@ +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/domain/models/token.dart'; +import 'package:aewallet/model/data/account_token.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:aewallet/ui/util/address_formatters.dart'; +import 'package:aewallet/ui/util/ui_util.dart'; +import 'package:aewallet/ui/views/transfer/bloc/state.dart'; +import 'package:aewallet/ui/views/transfer/layouts/transfer_sheet.dart'; +import 'package:aewallet/ui/widgets/tokens/verified_token_icon.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:aewallet/util/haptic_util.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:archethic_lib_dart/archethic_lib_dart.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class TokenDetailOld extends ConsumerWidget { + const TokenDetailOld({ + super.key, + required this.accountFungibleToken, + }); + + final AccountToken accountFungibleToken; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final preferences = ref.watch(SettingsProviders.settings); + final localizations = AppLocalizations.of(context)!; + return aedappfm.BlockInfo( + width: MediaQuery.of(context).size.width, + height: 70, + info: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + Container( + alignment: Alignment.center, + height: 40, + width: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: ArchethicTheme.backgroundDark.withOpacity(0.3), + border: Border.all( + color: ArchethicTheme.backgroundDarkest.withOpacity(0.2), + width: 2, + ), + ), + child: IconButton( + icon: Icon( + Symbols.arrow_circle_up, + color: ArchethicTheme.backgroundDarkest, + size: 21, + ), + onPressed: () async { + sl.get().feedback( + FeedbackType.light, + preferences.activeVibrations, + ); + await TransferSheet( + transferType: TransferType.token, + accountToken: accountFungibleToken, + recipient: const TransferRecipient.address( + address: Address(address: ''), + ), + ).show( + context: context, + ref: ref, + ); + }, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: InkWell( + key: const Key('viewExplorer'), + onTap: () { + UIUtil.showWebview( + context, + '${ref.read(SettingsProviders.settings).network.getLink()}/explorer/transaction/${accountFungibleToken.tokenInformation!.address!}', + '', + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: AutoSizeText( + accountFungibleToken.tokenInformation!.name!, + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + ), + ], + ), + Row( + children: [ + AutoSizeText( + AddressFormatters( + accountFungibleToken.tokenInformation!.address!, + ).getShortString4(), + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + const SizedBox(width: 5), + const Icon( + Symbols.open_in_new, + size: 11, + ), + ], + ), + if (preferences.showBalances == true) + Row( + children: [ + Text( + '${accountFungibleToken.amount!.formatNumber(precision: 8)} ${accountFungibleToken.tokenInformation!.symbol!}', + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + const SizedBox(width: 5), + VerifiedTokenIcon( + address: accountFungibleToken + .tokenInformation!.address!, + ), + ], + ) + else + Row( + children: [ + Text( + '···········', + style: ArchethicThemeStyles + .textStyleSize12W100Primary60, + ), + const SizedBox(width: 5), + Row( + children: [ + Text( + accountFungibleToken + .tokenInformation!.symbol!, + style: ArchethicThemeStyles + .textStyleSize12W100Primary, + ), + const SizedBox( + width: 5, + ), + VerifiedTokenIcon( + address: accountFungibleToken + .tokenInformation!.address!, + ), + ], + ), + ], + ), + if (kTokenFordiddenName.contains( + accountFungibleToken.tokenInformation!.name! + .toUpperCase(), + ) || + kTokenFordiddenName.contains( + accountFungibleToken.tokenInformation!.symbol! + .toUpperCase(), + )) + Row( + children: [ + const Icon( + Symbols.warning, + size: 10, + ), + const SizedBox(width: 5), + Text( + localizations.notOfficialUCOWarning, + style: ArchethicThemeStyles + .textStyleSize10W100Primary, + textAlign: TextAlign.end, + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/ui/views/tokens_list/layouts/tokens_list_sheet.dart b/lib/ui/views/tokens_list/layouts/tokens_list_sheet.dart new file mode 100644 index 000000000..4273d551b --- /dev/null +++ b/lib/ui/views/tokens_list/layouts/tokens_list_sheet.dart @@ -0,0 +1,175 @@ +/// SPDX-License-Identifier: AGPL-3.0-or-later +import 'package:aewallet/application/settings/settings.dart'; +import 'package:aewallet/infrastructure/datasources/preferences.hive.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:aewallet/ui/util/formatters.dart'; +import 'package:aewallet/ui/views/tokens_detail/layouts/token_detail_sheet.dart'; +import 'package:aewallet/ui/views/tokens_list/bloc/provider.dart'; +import 'package:aewallet/ui/views/tokens_list/layouts/components/token_add_btn.dart'; +import 'package:aewallet/ui/views/tokens_list/layouts/components/token_detail.dart'; +import 'package:aewallet/util/get_it_instance.dart'; +import 'package:aewallet/util/haptic_util.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_gen/gen_l10n/localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class TokensListSheet extends ConsumerStatefulWidget { + const TokensListSheet({super.key}); + + @override + ConsumerState createState() => _TokensListSheetState(); +} + +class _TokensListSheetState extends ConsumerState + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + final searchCriteriaController = TextEditingController(); + + @override + void didChangeDependencies() { + Future.delayed(Duration.zero, () async { + // TODO(reddwarf03): See https://github.com/archethic-foundation/archethic-wallet/pull/988 + final ucidsTokens = ref.read(aedappfm.UcidsTokensProviders.ucidsTokens); + if (ucidsTokens.isEmpty) { + final preferences = await PreferencesHiveDatasource.getInstance(); + final network = preferences.getNetwork().getNetworkLabel(); + await ref + .read(aedappfm.UcidsTokensProviders.ucidsTokens.notifier) + .init(network); + } + await ref.read(aedappfm.CoinPriceProviders.coinPrice.notifier).init(); + + await ref + .read(TokensListFormProvider.tokensListForm.notifier) + .getTokensList( + cancelToken: UniqueKey().toString(), + ); + }); + + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + + final asyncTokens = + ref.watch(TokensListFormProvider.tokensListForm).tokensToDisplay; + final localizations = AppLocalizations.of(context)!; + final settings = ref.watch(SettingsProviders.settings); + return Column( + children: [ + Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.only( + right: 15, + ), + child: TextFormField( + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + prefixIcon: Icon( + Symbols.search, + color: ArchethicTheme.text, + size: 18, + weight: IconSize.weightM, + opticalSize: IconSize.opticalSizeM, + grade: IconSize.gradeM, + ), + suffixIcon: const SizedBox( + width: 26, + ), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all( + Radius.circular(90), + ), + borderSide: BorderSide.none, + ), + hintStyle: + ArchethicThemeStyles.textStyleSize12W100Primary, + filled: true, + //fillColor: ArchethicTheme.text30, + hintText: localizations.searchField, + ), + style: ArchethicThemeStyles.textStyleSize12W100Primary, + textAlign: TextAlign.left, + controller: searchCriteriaController, + autocorrect: false, + cursorColor: ArchethicTheme.text, + inputFormatters: [ + UpperCaseTextFormatter(), + ], + onChanged: (text) async { + await ref + .read(TokensListFormProvider.tokensListForm.notifier) + .setSearchCriteria(text.toUpperCase()); + }, + ), + ), + ), + ), + const TokenAddBtn(), + ], + ), + asyncTokens.when( + error: (error, stackTrace) => aedappfm.ErrorMessage( + failure: aedappfm.Failure.fromError(error), + failureMessage: 'Error loading', + ), + loading: () { + return const Stack( + alignment: Alignment.centerLeft, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 10, + ), + ], + ), + ], + ); + }, + data: (tokens) { + return Column( + children: tokens!.map((aeToken) { + return InkWell( + onTap: () async { + sl.get().feedback( + FeedbackType.light, + settings.activeVibrations, + ); + + await context.push( + TokenDetailSheet.routerPage, + extra: { + 'aeToken': aeToken.toJson(), + 'chartInfos': null, + }, + ); + }, + child: TokenDetail( + aeToken: aeToken, + ), + ); + }).toList(), + ); + }, + ), + ], + ); + } +} diff --git a/lib/ui/views/transfer/bloc/state.dart b/lib/ui/views/transfer/bloc/state.dart index 4238f1b3f..9b074383b 100644 --- a/lib/ui/views/transfer/bloc/state.dart +++ b/lib/ui/views/transfer/bloc/state.dart @@ -2,6 +2,7 @@ import 'package:aewallet/model/data/account_balance.dart'; import 'package:aewallet/model/data/account_token.dart'; import 'package:aewallet/model/data/contact.dart'; import 'package:aewallet/model/primary_currency.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart'; import 'package:archethic_lib_dart/archethic_lib_dart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -30,6 +31,7 @@ class TransferFormState with _$TransferFormState { @Default(0.0) double amountConverted, required AccountBalance accountBalance, required TransferRecipient recipient, + AEToken? aeToken, AccountToken? accountToken, @Default('') String tokenId, @Default('') String message, @@ -57,7 +59,7 @@ class TransferFormState with _$TransferFormState { return (amount + fees) < accountBalance.nativeTokenValue; } case TransferType.token: - return amount != accountToken!.amount!; + return amount != aeToken!.balance; case TransferType.nft: return false; } @@ -67,9 +69,9 @@ class TransferFormState with _$TransferFormState { String symbol(BuildContext context) => transferType == TransferType.uco ? AccountBalance.cryptoCurrencyLabel - : accountToken!.tokenInformation!.isLPToken == true + : aeToken!.isLpToken == true ? 'LP Token' - : accountToken!.tokenInformation!.symbol!; + : aeToken!.symbol; String symbolFees(BuildContext context) => AccountBalance.cryptoCurrencyLabel; } diff --git a/lib/ui/views/transfer/bloc/state.freezed.dart b/lib/ui/views/transfer/bloc/state.freezed.dart index 905ee4953..34c252a0c 100644 --- a/lib/ui/views/transfer/bloc/state.freezed.dart +++ b/lib/ui/views/transfer/bloc/state.freezed.dart @@ -28,6 +28,7 @@ mixin _$TransferFormState { double get amountConverted => throw _privateConstructorUsedError; AccountBalance get accountBalance => throw _privateConstructorUsedError; TransferRecipient get recipient => throw _privateConstructorUsedError; + AEToken? get aeToken => throw _privateConstructorUsedError; AccountToken? get accountToken => throw _privateConstructorUsedError; String get tokenId => throw _privateConstructorUsedError; String get message => throw _privateConstructorUsedError; @@ -55,6 +56,7 @@ abstract class $TransferFormStateCopyWith<$Res> { double amountConverted, AccountBalance accountBalance, TransferRecipient recipient, + AEToken? aeToken, AccountToken? accountToken, String tokenId, String message, @@ -63,6 +65,7 @@ abstract class $TransferFormStateCopyWith<$Res> { String errorMessageText}); $TransferRecipientCopyWith<$Res> get recipient; + $AETokenCopyWith<$Res>? get aeToken; } /// @nodoc @@ -86,6 +89,7 @@ class _$TransferFormStateCopyWithImpl<$Res, $Val extends TransferFormState> Object? amountConverted = null, Object? accountBalance = null, Object? recipient = null, + Object? aeToken = freezed, Object? accountToken = freezed, Object? tokenId = null, Object? message = null, @@ -126,6 +130,10 @@ class _$TransferFormStateCopyWithImpl<$Res, $Val extends TransferFormState> ? _value.recipient : recipient // ignore: cast_nullable_to_non_nullable as TransferRecipient, + aeToken: freezed == aeToken + ? _value.aeToken + : aeToken // ignore: cast_nullable_to_non_nullable + as AEToken?, accountToken: freezed == accountToken ? _value.accountToken : accountToken // ignore: cast_nullable_to_non_nullable @@ -160,6 +168,18 @@ class _$TransferFormStateCopyWithImpl<$Res, $Val extends TransferFormState> return _then(_value.copyWith(recipient: value) as $Val); }); } + + @override + @pragma('vm:prefer-inline') + $AETokenCopyWith<$Res>? get aeToken { + if (_value.aeToken == null) { + return null; + } + + return $AETokenCopyWith<$Res>(_value.aeToken!, (value) { + return _then(_value.copyWith(aeToken: value) as $Val); + }); + } } /// @nodoc @@ -179,6 +199,7 @@ abstract class _$$TransferFormStateImplCopyWith<$Res> double amountConverted, AccountBalance accountBalance, TransferRecipient recipient, + AEToken? aeToken, AccountToken? accountToken, String tokenId, String message, @@ -188,6 +209,8 @@ abstract class _$$TransferFormStateImplCopyWith<$Res> @override $TransferRecipientCopyWith<$Res> get recipient; + @override + $AETokenCopyWith<$Res>? get aeToken; } /// @nodoc @@ -209,6 +232,7 @@ class __$$TransferFormStateImplCopyWithImpl<$Res> Object? amountConverted = null, Object? accountBalance = null, Object? recipient = null, + Object? aeToken = freezed, Object? accountToken = freezed, Object? tokenId = null, Object? message = null, @@ -249,6 +273,10 @@ class __$$TransferFormStateImplCopyWithImpl<$Res> ? _value.recipient : recipient // ignore: cast_nullable_to_non_nullable as TransferRecipient, + aeToken: freezed == aeToken + ? _value.aeToken + : aeToken // ignore: cast_nullable_to_non_nullable + as AEToken?, accountToken: freezed == accountToken ? _value.accountToken : accountToken // ignore: cast_nullable_to_non_nullable @@ -289,6 +317,7 @@ class _$TransferFormStateImpl extends _TransferFormState { this.amountConverted = 0.0, required this.accountBalance, required this.recipient, + this.aeToken, this.accountToken, this.tokenId = '', this.message = '', @@ -323,6 +352,8 @@ class _$TransferFormStateImpl extends _TransferFormState { @override final TransferRecipient recipient; @override + final AEToken? aeToken; + @override final AccountToken? accountToken; @override @JsonKey() @@ -342,7 +373,7 @@ class _$TransferFormStateImpl extends _TransferFormState { @override String toString() { - return 'TransferFormState(transferType: $transferType, transferProcessStep: $transferProcessStep, feeEstimation: $feeEstimation, defineMaxAmountInProgress: $defineMaxAmountInProgress, amount: $amount, amountConverted: $amountConverted, accountBalance: $accountBalance, recipient: $recipient, accountToken: $accountToken, tokenId: $tokenId, message: $message, errorAddressText: $errorAddressText, errorAmountText: $errorAmountText, errorMessageText: $errorMessageText)'; + return 'TransferFormState(transferType: $transferType, transferProcessStep: $transferProcessStep, feeEstimation: $feeEstimation, defineMaxAmountInProgress: $defineMaxAmountInProgress, amount: $amount, amountConverted: $amountConverted, accountBalance: $accountBalance, recipient: $recipient, aeToken: $aeToken, accountToken: $accountToken, tokenId: $tokenId, message: $message, errorAddressText: $errorAddressText, errorAmountText: $errorAmountText, errorMessageText: $errorMessageText)'; } @override @@ -366,6 +397,7 @@ class _$TransferFormStateImpl extends _TransferFormState { other.accountBalance == accountBalance) && (identical(other.recipient, recipient) || other.recipient == recipient) && + (identical(other.aeToken, aeToken) || other.aeToken == aeToken) && (identical(other.accountToken, accountToken) || other.accountToken == accountToken) && (identical(other.tokenId, tokenId) || other.tokenId == tokenId) && @@ -389,6 +421,7 @@ class _$TransferFormStateImpl extends _TransferFormState { amountConverted, accountBalance, recipient, + aeToken, accountToken, tokenId, message, @@ -414,6 +447,7 @@ abstract class _TransferFormState extends TransferFormState { final double amountConverted, required final AccountBalance accountBalance, required final TransferRecipient recipient, + final AEToken? aeToken, final AccountToken? accountToken, final String tokenId, final String message, @@ -442,6 +476,8 @@ abstract class _TransferFormState extends TransferFormState { @override TransferRecipient get recipient; @override + AEToken? get aeToken; + @override AccountToken? get accountToken; @override String get tokenId; diff --git a/lib/ui/views/transfer/layouts/components/transfer_textfield_amount.dart b/lib/ui/views/transfer/layouts/components/transfer_textfield_amount.dart index 6c4d2b33a..f0906ed55 100644 --- a/lib/ui/views/transfer/layouts/components/transfer_textfield_amount.dart +++ b/lib/ui/views/transfer/layouts/components/transfer_textfield_amount.dart @@ -208,26 +208,19 @@ class _TransferTextFieldAmountState Row( children: [ AutoSizeText( - '${NumberUtil.formatThousands(transfer.accountToken!.amount!)} ${transfer.accountToken!.tokenInformation!.isLPToken == true ? transfer.accountToken!.amount! > 1 ? 'LP Tokens' : 'LP Token ' : transfer.accountToken!.tokenInformation!.symbol}', + '${NumberUtil.formatThousands(transfer.aeToken!.balance)} ${transfer.aeToken!.isLpToken == true ? '${transfer.aeToken!.lpTokenPair!.token1.symbol}/${transfer.aeToken!.lpTokenPair!.token2.symbol}' : transfer.aeToken!.symbol}', style: ArchethicThemeStyles .textStyleSize14W200Primary, ), - if (transfer.accountToken != null && - transfer.accountToken!.tokenInformation != - null && - transfer.accountToken!.tokenInformation!.type == - 'fungible' && - transfer.accountToken!.tokenInformation! - .address != - null) + if (transfer.aeToken != null && + transfer.aeToken!.isVerified) Row( children: [ const SizedBox( width: 5, ), VerifiedTokenIcon( - address: transfer.accountToken! - .tokenInformation!.address!, + address: transfer.aeToken!.address!, ), ], ), diff --git a/lib/ui/views/transfer/layouts/transfer_sheet.dart b/lib/ui/views/transfer/layouts/transfer_sheet.dart index fff2627cf..03bdc5d25 100755 --- a/lib/ui/views/transfer/layouts/transfer_sheet.dart +++ b/lib/ui/views/transfer/layouts/transfer_sheet.dart @@ -27,6 +27,8 @@ import 'package:aewallet/util/get_it_instance.dart'; import 'package:aewallet/util/haptic_util.dart'; import 'package:aewallet/util/number_util.dart'; import 'package:aewallet/util/user_data_util.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:archethic_lib_dart/archethic_lib_dart.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; @@ -49,6 +51,7 @@ class TransferSheet extends ConsumerWidget { required this.recipient, this.actionButtonTitle, this.accountToken, + this.aeToken, this.tokenId, super.key, }); @@ -58,6 +61,7 @@ class TransferSheet extends ConsumerWidget { final TransferRecipient recipient; final String? actionButtonTitle; final AccountToken? accountToken; + final aedappfm.AEToken? aeToken; final TransferType transferType; final String? tokenId; @@ -75,6 +79,9 @@ class TransferSheet extends ConsumerWidget { 'accountToken': accountToken == null ? null : const AccountTokenConverter().toJson(accountToken!), + 'aeToken': aeToken == null + ? null + : const aedappfm.AETokenJsonConverter().toJson(aeToken!), 'tokenId': tokenId, }, ); @@ -97,6 +104,7 @@ class TransferSheet extends ConsumerWidget { feeEstimation: const AsyncValue.data(0), transferType: transferType, accountToken: accountToken, + aeToken: aeToken, recipient: recipient, accountBalance: selectedAccount.balance!, amount: transferType == TransferType.nft ? 1 : 0, @@ -170,9 +178,9 @@ class TransferSheetBody extends ConsumerWidget { case TransferType.token: return localizations.transferTokens.replaceAll( '%1', - transfer.accountToken!.tokenInformation!.isLPToken! == true + transfer.aeToken!.isLpToken == true ? 'LP Tokens' - : transfer.accountToken!.tokenInformation!.symbol!, + : transfer.aeToken!.symbol, ); case TransferType.nft: return localizations.transferNFT; diff --git a/lib/ui/widgets/balance/balance_infos.dart b/lib/ui/widgets/balance/balance_infos.dart index 7a846e31a..a1c6ce720 100644 --- a/lib/ui/widgets/balance/balance_infos.dart +++ b/lib/ui/widgets/balance/balance_infos.dart @@ -1,33 +1,20 @@ /// SPDX-License-Identifier: AGPL-3.0-or-later import 'package:aewallet/application/account/providers.dart'; -import 'package:aewallet/application/market_price.dart'; import 'package:aewallet/application/price_history/providers.dart'; -import 'package:aewallet/application/settings/language.dart'; -import 'package:aewallet/application/settings/primary_currency.dart'; import 'package:aewallet/application/settings/settings.dart'; import 'package:aewallet/domain/models/market_price_history.dart'; -import 'package:aewallet/model/available_language.dart'; import 'package:aewallet/model/data/account_balance.dart'; -import 'package:aewallet/model/primary_currency.dart'; import 'package:aewallet/ui/themes/archethic_theme.dart'; import 'package:aewallet/ui/themes/styles.dart'; -import 'package:aewallet/ui/views/sheets/chart_sheet.dart'; -import 'package:aewallet/ui/widgets/balance/components/balance_infos_popup.dart'; -import 'package:aewallet/ui/widgets/components/dialog.dart'; +import 'package:aewallet/ui/widgets/balance/components/price_evolution_indicator.dart'; import 'package:aewallet/ui/widgets/components/history_chart.dart'; -import 'package:aewallet/ui/widgets/components/icon_widget.dart'; -import 'package:aewallet/util/currency_util.dart'; -import 'package:aewallet/util/get_it_instance.dart'; -import 'package:aewallet/util/haptic_util.dart'; -import 'package:animate_do/animate_do.dart'; +import 'package:archethic_dapp_framework_flutter/archethic_dapp_framework_flutter.dart' + as aedappfm; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_vibrate/flutter_vibrate.dart'; -import 'package:go_router/go_router.dart'; -import 'package:material_symbols_icons/symbols.dart'; part 'components/balance_infos_build_chart.dart'; part 'components/balance_infos_build_kpi.dart'; @@ -41,180 +28,62 @@ class BalanceInfos extends ConsumerWidget { AccountProviders.selectedAccount .select((value) => value.valueOrNull?.balance), ); - final settings = ref.watch(SettingsProviders.settings); - final primaryCurrency = - ref.watch(PrimaryCurrencyProviders.selectedPrimaryCurrency); if (accountSelectedBalance == null) return const SizedBox(); - return GestureDetector( - child: SizedBox( - height: 60, - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: primaryCurrency.primaryCurrency == - AvailablePrimaryCurrencyEnum.native - ? Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AutoSizeText( - AccountBalance.cryptoCurrencyLabel, - style: ArchethicThemeStyles.textStyleSize35W900Primary, - ), - if (settings.showBalances) - _BalanceInfosNativeShowed( - accountSelectedBalance: accountSelectedBalance, - ) - else - const _BalanceInfosNotShowed(), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - AutoSizeText( - settings.currency.name, - style: ArchethicThemeStyles.textStyleSize35W900Primary, - ), - if (settings.showBalances) - _BalanceInfosFiatShowed( - accountSelectedBalance: accountSelectedBalance, - ) - else - const _BalanceInfosNotShowed(), - ], - ), - ), + final preferences = ref.watch(SettingsProviders.settings); + return SizedBox( + height: 60, + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + width: MediaQuery.of(context).size.width, + child: preferences.showBalances == true + ? _BalanceTotalUSDShowed( + accountSelectedBalance: accountSelectedBalance, + ) + : const _BalanceTotalUSDNotShowed(), ), ), - onTapDown: (details) { - BalanceInfosPopup.getPopup( - context, - ref, - details, - accountSelectedBalance, - ); - }, ); } } -class _BalanceInfosNativeShowed extends ConsumerWidget { - const _BalanceInfosNativeShowed({ +class _BalanceTotalUSDShowed extends ConsumerWidget { + const _BalanceTotalUSDShowed({ required this.accountSelectedBalance, }); final AccountBalance accountSelectedBalance; @override Widget build(BuildContext context, WidgetRef ref) { - final currency = ref.watch( - SettingsProviders.settings.select((settings) => settings.currency), - ); - final language = ref.watch( - LanguageProviders.selectedLanguage, - ); - final fiatValue = ref - .watch( - MarketPriceProviders.convertedToSelectedCurrency( - nativeAmount: accountSelectedBalance.nativeTokenValue, - ), - ) - .valueOrNull; - if (fiatValue == null) { - return const SizedBox(); - } return Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ AutoSizeText( - accountSelectedBalance.nativeTokenValueToString( - language.getLocaleStringWithoutDefault(), - digits: accountSelectedBalance.nativeTokenValue < 1 ? 8 : 2, - ), - style: ArchethicThemeStyles.textStyleSize25W900Primary, - ), - AutoSizeText( - CurrencyUtil.format( - currency.name, - fiatValue, - ), + '\$${accountSelectedBalance.totalUSD.formatNumber(precision: 2)}', textAlign: TextAlign.center, - style: ArchethicThemeStyles.textStyleSize12W600Primary, + style: ArchethicThemeStyles.textStyleSize35W900Primary, ), ], ); } } -class _BalanceInfosFiatShowed extends ConsumerWidget { - const _BalanceInfosFiatShowed({ - required this.accountSelectedBalance, - }); - final AccountBalance accountSelectedBalance; +class _BalanceTotalUSDNotShowed extends ConsumerWidget { + const _BalanceTotalUSDNotShowed(); @override Widget build(BuildContext context, WidgetRef ref) { - final currency = ref.watch( - SettingsProviders.settings.select((settings) => settings.currency), - ); - final language = ref.watch( - LanguageProviders.selectedLanguage, - ); - final fiatValue = ref - .watch( - MarketPriceProviders.convertedToSelectedCurrency( - nativeAmount: accountSelectedBalance.nativeTokenValue, - ), - ) - .valueOrNull; - if (fiatValue == null) { - return const SizedBox(); - } - return Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - AutoSizeText( - CurrencyUtil.format( - currency.name, - fiatValue, - ), - textAlign: TextAlign.center, - style: ArchethicThemeStyles.textStyleSize25W900Primary, - ), - AutoSizeText( - '${accountSelectedBalance.nativeTokenValueToString( - language.getLocaleStringWithoutDefault(), - digits: accountSelectedBalance.nativeTokenValue < 1 ? 8 : 2, - )} ${accountSelectedBalance.nativeTokenName}', - style: ArchethicThemeStyles.textStyleSize12W600Primary, - ), - ], - ); - } -} - -class _BalanceInfosNotShowed extends ConsumerWidget { - const _BalanceInfosNotShowed(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - AutoSizeText( - '···········', - style: ArchethicThemeStyles.textStyleSize25W900Primary60, - ), AutoSizeText( '···········', textAlign: TextAlign.center, - style: ArchethicThemeStyles.textStyleSize12W600Primary60, + style: ArchethicThemeStyles.textStyleSize35W900Primary, ), ], ); diff --git a/lib/ui/widgets/balance/components/balance_infos_build_chart.dart b/lib/ui/widgets/balance/components/balance_infos_build_chart.dart index 571b3311a..472632721 100644 --- a/lib/ui/widgets/balance/components/balance_infos_build_chart.dart +++ b/lib/ui/widgets/balance/components/balance_infos_build_chart.dart @@ -3,83 +3,43 @@ part of '../balance_infos.dart'; class BalanceInfosChart extends ConsumerWidget { - const BalanceInfosChart({super.key}); + const BalanceInfosChart({required this.chartInfos, super.key}); + + final List? chartInfos; @override Widget build(BuildContext context, WidgetRef ref) { - final localizations = AppLocalizations.of(context)!; - final settings = ref.watch(SettingsProviders.settings); - final chartInfos = ref - .watch( - PriceHistoryProviders.chartData( - scaleOption: settings.priceChartIntervalOption, - ), - ) - .valueOrNull; - - return InkWell( - onTap: () { - sl.get().feedback( - FeedbackType.light, - settings.activeVibrations, - ); - context.go(ChartSheet.routerPage); - }, - child: Ink( - child: SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height * 0.08, - child: Stack( - children: [ - FadeIn( - duration: const Duration(milliseconds: 1000), - child: chartInfos != null - ? HistoryChart( - intervals: chartInfos, - gradientColors: LinearGradient( - colors: [ - ArchethicTheme.text20, - ArchethicTheme.text, - ], - ), - gradientColorsBar: LinearGradient( - colors: [ - ArchethicTheme.text.withOpacity(0.9), - ArchethicTheme.text.withOpacity(0), - ], - begin: Alignment.center, - end: Alignment.bottomCenter, - ), - tooltipBg: ArchethicTheme.backgroundDark, - tooltipText: - ArchethicThemeStyles.textStyleSize12W100Primary, - axisTextStyle: - ArchethicThemeStyles.textStyleSize12W100Primary, - optionChartSelected: settings.priceChartIntervalOption, - currency: settings.currency.name, - completeChart: false, - ) - : const SizedBox(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - localizations.priceChartHeader, - style: ArchethicThemeStyles.textStyleSize14W600Primary, - ), - const IconDataWidget( - icon: Symbols.show_chart, - width: AppFontSizes.size20, - height: AppFontSizes.size20, - ), - ], - ), - ], - ), + if (chartInfos == null) { + return const SizedBox.shrink(); + } + return SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height * 0.05, + child: HistoryChart( + intervals: chartInfos!, + gradientColors: LinearGradient( + colors: [ + ArchethicTheme.text.withOpacity(0.05), + ArchethicTheme.text.withOpacity(0), + ], + ), + gradientColorsBar: LinearGradient( + colors: [ + ArchethicTheme.text.withOpacity(0.1), + ArchethicTheme.text.withOpacity(0.05), + ], + begin: Alignment.center, + end: Alignment.bottomCenter, ), + tooltipBg: ArchethicTheme.backgroundDark, + tooltipText: ArchethicThemeStyles.textStyleSize12W100Primary, + axisTextStyle: ArchethicThemeStyles.textStyleSize12W100Primary, + optionChartSelected: settings.priceChartIntervalOption, + currency: settings.currency.name, + completeChart: false, + lineTouchEnabled: false, ), ); } diff --git a/lib/ui/widgets/balance/components/balance_infos_build_kpi.dart b/lib/ui/widgets/balance/components/balance_infos_build_kpi.dart index 479503330..77cfefeef 100644 --- a/lib/ui/widgets/balance/components/balance_infos_build_kpi.dart +++ b/lib/ui/widgets/balance/components/balance_infos_build_kpi.dart @@ -3,147 +3,50 @@ part of '../balance_infos.dart'; class BalanceInfosKpi extends ConsumerWidget { - const BalanceInfosKpi({super.key}); + const BalanceInfosKpi({ + required this.aeToken, + this.chartInfos, + super.key, + }); + + final aedappfm.AEToken aeToken; + final List? chartInfos; @override Widget build(BuildContext context, WidgetRef ref) { final localizations = AppLocalizations.of(context)!; - - final preferences = ref.watch(SettingsProviders.settings); - - final chartInfos = ref - .watch( - PriceHistoryProviders.chartData( - scaleOption: preferences.priceChartIntervalOption, - ), - ) - .valueOrNull; - if (chartInfos == null) { - return const SizedBox( - height: 30, - ); - } - - final currencyMarketPrice = ref - .watch( - MarketPriceProviders.selectedCurrencyMarketPrice, - ) - .valueOrNull; - final selectedCurrency = ref.watch( - SettingsProviders.settings.select((settings) => settings.currency), - ); - final accountSelectedBalance = ref.watch( - AccountProviders.selectedAccount.select( - (value) => value.valueOrNull?.balance, + final price = ref.watch( + aedappfm.AETokensProviders.estimateTokenInFiat( + aeToken, ), ); - if (accountSelectedBalance == null || currencyMarketPrice == null) { - return const SizedBox(); + + if (chartInfos == null) { + return const SizedBox.shrink(); } final selectedPriceHistoryInterval = ref.watch(PriceHistoryProviders.scaleOption); - return FadeIn( - duration: const Duration(milliseconds: 1000), - child: Container( - height: 30, - width: MediaQuery.of(context).size.width, - alignment: Alignment.bottomLeft, - child: Row( - children: [ - AutoSizeText( - '1 ${accountSelectedBalance.nativeTokenName} = ${CurrencyUtil.getAmountPlusSymbol(selectedCurrency.name, currencyMarketPrice.amount)}', - style: ArchethicThemeStyles.textStyleSize12W100Primary, - ), - const SizedBox( - width: 10, - ), - const _PriceEvolutionIndicator(), - const SizedBox( - width: 10, - ), - AutoSizeText( - selectedPriceHistoryInterval.getChartOptionLabel( - context, - ), - style: ArchethicThemeStyles.textStyleSize12W100Primary, - ), - const SizedBox( - width: 10, - ), - if (currencyMarketPrice.useOracle) - InkWell( - onTap: () { - sl.get().feedback( - FeedbackType.light, - preferences.activeVibrations, - ); - AppDialogs.showInfoDialog( - context, - ref, - localizations.information, - localizations.currencyOracleInfo, - ); - }, - child: Icon( - Symbols.info, - color: ArchethicTheme.text, - size: 15, - ), - ) - else - const SizedBox(), - ], + return Row( + children: [ + AutoSizeText( + '${localizations.price}: \$${price.formatNumber(precision: price < 1 ? 5 : 2)}', + style: ArchethicThemeStyles.textStyleSize12W100Primary, ), - ), - ); - } -} - -class _PriceEvolutionIndicator extends ConsumerWidget { - const _PriceEvolutionIndicator(); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final preferences = ref.watch(SettingsProviders.settings); - final asyncPriceEvolution = ref.watch( - PriceHistoryProviders.priceEvolution( - scaleOption: preferences.priceChartIntervalOption, - ), - ); - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 100), - child: asyncPriceEvolution.maybeWhen( - data: (priceEvolution) { - return Row( - children: [ - AutoSizeText( - '${priceEvolution.toStringAsFixed(2)}%', - style: priceEvolution >= 0 - ? ArchethicThemeStyles.textStyleSize12W100PositiveValue - : ArchethicThemeStyles.textStyleSize12W100NegativeValue, - ), - const SizedBox(width: 5), - if (priceEvolution >= 0) - Icon( - Symbols.arrow_upward, - color: ArchethicTheme.positiveValue, - size: 14, - ) - else - Icon( - Symbols.arrow_downward, - color: ArchethicTheme.negativeValue, - size: 14, - ), - ], - ); - }, - orElse: () { - return const SizedBox(); - }, - ), + const SizedBox( + width: 10, + ), + PriceEvolutionIndicator(chartInfos), + const SizedBox( + width: 10, + ), + AutoSizeText( + selectedPriceHistoryInterval.getChartOptionLabel( + context, + ), + style: ArchethicThemeStyles.textStyleSize12W100Primary, + ), + ], ); } } diff --git a/lib/ui/widgets/balance/components/balance_infos_popup.dart b/lib/ui/widgets/balance/components/balance_infos_popup.dart deleted file mode 100644 index 89c1d75a8..000000000 --- a/lib/ui/widgets/balance/components/balance_infos_popup.dart +++ /dev/null @@ -1,163 +0,0 @@ -/// SPDX-License-Identifier: AGPL-3.0-or-later - -import 'package:aewallet/application/market_price.dart'; -import 'package:aewallet/application/settings/language.dart'; -import 'package:aewallet/application/settings/primary_currency.dart'; -import 'package:aewallet/application/settings/settings.dart'; -import 'package:aewallet/model/available_language.dart'; -import 'package:aewallet/model/data/account_balance.dart'; -import 'package:aewallet/model/primary_currency.dart'; -import 'package:aewallet/ui/themes/archethic_theme.dart'; -import 'package:aewallet/ui/themes/styles.dart'; -import 'package:aewallet/ui/util/ui_util.dart'; -import 'package:aewallet/util/currency_util.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -class BalanceInfosPopup { - static Future getPopup( - BuildContext context, - WidgetRef ref, - TapDownDetails details, - AccountBalance accountSelectedBalance, - ) async { - final primaryCurrency = - ref.watch(PrimaryCurrencyProviders.selectedPrimaryCurrency); - final language = ref.watch( - LanguageProviders.selectedLanguage, - ); - final fiatBalance = ref - .watch( - MarketPriceProviders.convertedToSelectedCurrency( - nativeAmount: accountSelectedBalance.nativeTokenValue, - ), - ) - .valueOrNull; - final currency = ref.watch( - SettingsProviders.settings.select((settings) => settings.currency), - ); - if (fiatBalance == null) return const SizedBox(); - - return showMenu( - color: ArchethicTheme.backgroundDark, - elevation: 5, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20).copyWith(topLeft: Radius.zero), - side: BorderSide( - color: ArchethicTheme.text60, - ), - ), - context: context, - position: RelativeRect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx, - details.globalPosition.dy, - ), - items: - primaryCurrency.primaryCurrency == AvailablePrimaryCurrencyEnum.native - ? [ - _popupMenuItem( - context, - ref, - '1', - accountSelectedBalance.nativeTokenValueToString( - language.getLocaleStringWithoutDefault(), - ), - ' ${accountSelectedBalance.nativeTokenName}', - ), - _popupMenuItem( - context, - ref, - '2', - CurrencyUtil.format( - currency.name, - fiatBalance, - ), - '', - ), - ] - : [ - _popupMenuItem( - context, - ref, - '2', - CurrencyUtil.format( - currency.name, - fiatBalance, - ), - '', - ), - _popupMenuItem( - context, - ref, - '1', - accountSelectedBalance.nativeTokenValueToString( - language.getLocaleStringWithoutDefault(), - ), - ' ${accountSelectedBalance.nativeTokenName}', - ), - ], - ); - } - - static PopupMenuItem _popupMenuItem( - BuildContext context, - WidgetRef ref, - String id, - String value, - String suffixe, - ) { - return PopupMenuItem( - value: id, - onTap: () { - _copyAmount( - context, - ref, - value, - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Symbols.content_copy, - size: 20, - color: ArchethicTheme.text, - weight: IconSize.weightM, - opticalSize: IconSize.opticalSizeM, - grade: IconSize.gradeM, - ), - const SizedBox( - width: 5, - ), - Text( - value + suffixe, - style: ArchethicThemeStyles.textStyleSize12W100Primary, - ), - ], - ), - ], - ), - ); - } - - static void _copyAmount(BuildContext context, WidgetRef ref, String amount) { - Clipboard.setData(ClipboardData(text: amount)); - final localizations = AppLocalizations.of(context)!; - - UIUtil.showSnackbar( - localizations.amountCopied, - context, - ref, - ArchethicTheme.text, - ArchethicTheme.snackBarShadow, - icon: Symbols.info, - ); - } -} diff --git a/lib/ui/widgets/balance/components/price_evolution_indicator.dart b/lib/ui/widgets/balance/components/price_evolution_indicator.dart new file mode 100644 index 000000000..48ad5f7eb --- /dev/null +++ b/lib/ui/widgets/balance/components/price_evolution_indicator.dart @@ -0,0 +1,44 @@ +import 'package:aewallet/domain/models/market_price_history.dart'; +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class PriceEvolutionIndicator extends ConsumerWidget { + const PriceEvolutionIndicator(this.chartInfos, {super.key}); + + final List? chartInfos; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final firstPrice = chartInfos!.first.price.toDouble(); + final lastPrice = chartInfos!.last.price.toDouble(); + final priceEvolution = + firstPrice > 0 ? ((lastPrice - firstPrice) / firstPrice) * 100 : 0; + return Row( + children: [ + AutoSizeText( + '${priceEvolution.toStringAsFixed(2)}%', + style: priceEvolution >= 0 + ? ArchethicThemeStyles.textStyleSize12W100PositiveValue + : ArchethicThemeStyles.textStyleSize12W100NegativeValue, + ), + const SizedBox(width: 5), + if (priceEvolution >= 0) + Icon( + Symbols.arrow_upward, + color: ArchethicTheme.positiveValue, + size: 14, + ) + else + Icon( + Symbols.arrow_downward, + color: ArchethicTheme.negativeValue, + size: 14, + ), + ], + ); + } +} diff --git a/lib/ui/widgets/components/action_button.dart b/lib/ui/widgets/components/action_button.dart new file mode 100644 index 000000000..9583153f9 --- /dev/null +++ b/lib/ui/widgets/components/action_button.dart @@ -0,0 +1,99 @@ +import 'package:aewallet/ui/themes/archethic_theme.dart'; +import 'package:aewallet/ui/themes/styles.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class ActionButton extends ConsumerWidget { + const ActionButton({ + this.onTap, + required this.text, + required this.icon, + this.enabled = true, + super.key, + }); + + final VoidCallback? onTap; + final String text; + final IconData icon; + final bool enabled; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: onTap != null + ? InkWell( + onTap: onTap, + child: Column( + children: [ + ShaderMask( + child: SizedBox( + width: 40, + height: 40, + child: Icon( + icon, + weight: 800, + opticalSize: IconSize.opticalSizeM, + grade: IconSize.gradeM, + color: enabled + ? Colors.white + : ArchethicTheme.text.withOpacity(0.3), + size: 38, + ), + ), + shaderCallback: (Rect bounds) { + const rect = Rect.fromLTRB(0, 0, 40, 40); + return ArchethicTheme.gradient.createShader(rect); + }, + ), + const SizedBox(height: 5), + if (enabled) + Text( + text, + style: ArchethicThemeStyles.textStyleSize14W600Primary, + ) + else + Text( + text, + style: ArchethicThemeStyles + .textStyleSize14W600PrimaryDisabled, + ), + ], + ), + ) + : Column( + children: [ + ShaderMask( + child: SizedBox( + width: 40, + height: 40, + child: Icon( + icon, + color: enabled + ? Colors.white + : ArchethicTheme.text.withOpacity(0.3), + size: 38, + ), + ), + shaderCallback: (Rect bounds) { + const rect = Rect.fromLTRB(0, 0, 40, 40); + return ArchethicTheme.gradient.createShader(rect); + }, + ), + const SizedBox(height: 5), + if (enabled) + Text( + text, + style: ArchethicThemeStyles.textStyleSize14W600Primary, + ) + else + Text( + text, + style: + ArchethicThemeStyles.textStyleSize14W600PrimaryDisabled, + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/components/history_chart.dart b/lib/ui/widgets/components/history_chart.dart index 6dec74da0..20b71c717 100644 --- a/lib/ui/widgets/components/history_chart.dart +++ b/lib/ui/widgets/components/history_chart.dart @@ -19,6 +19,7 @@ class HistoryChart extends StatelessWidget { required this.optionChartSelected, required this.currency, required this.completeChart, + required this.lineTouchEnabled, }); final List intervals; @@ -30,6 +31,7 @@ class HistoryChart extends StatelessWidget { final MarketPriceHistoryInterval optionChartSelected; final String currency; final bool completeChart; + final bool lineTouchEnabled; double get maxY { final max = intervals.fold( @@ -62,12 +64,13 @@ class HistoryChart extends StatelessWidget { isStrokeCapRound: true, belowBarData: completeChart ? BarAreaData(show: true, gradient: gradientColorsBar) - : BarAreaData(), + : BarAreaData(show: true, gradient: gradientColorsBar), dotData: const FlDotData(show: false), ); return LineChartData( lineTouchData: LineTouchData( + enabled: lineTouchEnabled, touchTooltipData: LineTouchTooltipData( fitInsideHorizontally: true, fitInsideVertically: true, diff --git a/pubspec.lock b/pubspec.lock index 7e4908c80..699015bd5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,15 +69,23 @@ packages: archethic_dapp_framework_flutter: dependency: "direct main" description: +<<<<<<< HEAD name: archethic_dapp_framework_flutter sha256: "1d1574db56534c2167da3a376eedfff0dcaee0e3d9b01d575d6f88f8022ede32" url: "https://pub.dev" source: hosted version: "1.2.3" +======= + path: "../archethic-dapp-framework-flutter" + relative: true + source: path + version: "1.2.0" +>>>>>>> 870f7567 (🚧 UI Adjustments) archethic_lib_dart: dependency: "direct main" description: name: archethic_lib_dart +<<<<<<< HEAD sha256: "421f3ffe4b2f3816424fbb81ef572344a46a67590e4a237cc6251f8101582335" url: "https://pub.dev" source: hosted @@ -90,6 +98,19 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.17" +======= + sha256: ecdf2e5619725f4f5ca4f2ce6a35b3ac99fa8ca46ec2bac4b2ed6990b350cbfe + url: "https://pub.dev" + source: hosted + version: "3.4.0" + archethic_messaging_lib_dart: + dependency: "direct main" + description: + path: "../messaging_dart_sdk" + relative: true + source: path + version: "0.0.13" +>>>>>>> 870f7567 (🚧 UI Adjustments) archethic_wallet_client: dependency: "direct main" description: @@ -999,6 +1020,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + gradient_borders: + dependency: transitive + description: + name: gradient_borders + sha256: b1cd969552c83f458ff755aa68e13a0327d09f06c3f42f471b423b01427f21f8 + url: "https://pub.dev" + source: hosted + version: "1.0.1" graphql: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87fc86adb..b9f9562c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,13 +20,13 @@ dependencies: url: https://github.com/redDwarf03/app_version_update.git ref: main - archethic_dapp_framework_flutter: ^1.2.3 + #archethic_dapp_framework_flutter: ^1.2.3 #archethic_dapp_framework_flutter: # git: # url: https://github.com/archethic-foundation/archethic-dapp-framework-flutter.git # ref: web_extension_support - #archethic_dapp_framework_flutter: - # path: ../archethic-dapp-framework-flutter + archethic_dapp_framework_flutter: + path: ../archethic-dapp-framework-flutter # Archethic dart library for Flutter based on Official Archethic Javascript library for Node and Browser archethic_lib_dart: ^3.4.0 @@ -38,13 +38,13 @@ dependencies: # path: ../libdart-2 # Archethic Messaging Dart SDK - archethic_messaging_lib_dart: ^0.0.17 + #archethic_messaging_lib_dart: ^0.0.17 #archethic_messaging_lib_dart: # git: # url: https://github.com/archethic-foundation/messaging_dart_sdk.git # ref: main - #archethic_messaging_lib_dart: - # path: ../messaging_dart_sdk + archethic_messaging_lib_dart: + path: ../messaging_dart_sdk # RPC datastructures. archethic_wallet_client: ^2.0.2 @@ -357,6 +357,7 @@ flutter: assets: - assets/ssl/isrg-root-x1.pem - assets/ssl/r3.pem + - assets/bc-logos/ - assets/icons/ - assets/icons/currency/ - assets/icons/languages/