diff --git a/assets/translations/en.json b/assets/translations/en.json index cbecb40..3ee0948 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -165,6 +165,7 @@ "choiceOfSourceWallet": "Choose a source wallet", "extrinsicInProgress": "{} in progress", "extrinsicValidated": "{} validated !", + "extrinsicFinalized": "{} finalized !", "fromMinus": "from", "toMinus": "to", "deleteThisWallet": "Delete this wallet", @@ -229,5 +230,10 @@ "gotit": "Got it", "moreInfo": "More information", "keepThisPaperSafe": "Keep this sheet safe from prying lizards.\nIt will allow you to restore all your wallets at any time.", - "fundsUnavailable": "Insufficient funds" + "fundsUnavailable": "Insufficient funds", + "addressNotBelongToMnemonic": "The address you provided does not belong to this recovery sentence", + "enterYourNewMnemonic": "Enter your new recovery sentence", + "enterYourNewAddress": "Enter your new address {}", + "youCanMigrateThisIdentity": "You can migrate this identity !", + "identityMigrated": "Identity migrated" } \ No newline at end of file diff --git a/assets/translations/es.json b/assets/translations/es.json index 466a36a..e1ca23e 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -166,6 +166,7 @@ "choiceOfSourceWallet": "Elige un monedero de origen", "extrinsicInProgress": "{} en progreso", "extrinsicValidated": "¡ {} validado !", + "extrinsicFinalized": "{} finalized !", "fromMinus": "de", "toMinus": "a", "deleteThisWallet": "Borrar este monedero", @@ -230,5 +231,10 @@ "gotit": "Got it", "moreInfo": "More information", "keepThisPaperSafe": "Keep this sheet safe from prying lizards.\nIt will allow you to restore all your wallets at any time.", - "fundsUnavailable": "Insufficient funds" + "fundsUnavailable": "Insufficient funds", + "addressNotBelongToMnemonic": "The address you provided does not belong to this recovery sentence", + "enterYourNewMnemonic": "Enter your new recovery sentence", + "enterYourNewAddress": "Enter your new address {}", + "youCanMigrateThisIdentity": "You can migrate this identity !", + "identityMigrated": "Identity migrated" } \ No newline at end of file diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 3286cce..ffa277e 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -165,6 +165,7 @@ "choiceOfSourceWallet": "Choix du portefeuille source", "extrinsicInProgress": "{} en cours", "extrinsicValidated": "{} validée !", + "extrinsicFinalized": "{} finalisé !", "fromMinus": "de", "toMinus": "vers", "deleteThisWallet": "Supprimer ce portefeuille", @@ -229,5 +230,10 @@ "gotit": "J'ai compris", "moreInfo": "Plus d'info", "keepThisPaperSafe": "Gardez cette feuille précieusement, à l’abri des lézards indiscrets.\nElle vous permettra de restaurer tous vos portefeuilles à tout moment.", - "fundsUnavailable": "Fonds insuffisants" + "fundsUnavailable": "Fonds insuffisants", + "addressNotBelongToMnemonic": "L'adresse que vous avez fournit n'appartient pas à cette phrase de restauration", + "enterYourNewMnemonic": "Entrez votre nouvelle phrase de restauration", + "enterYourNewAddress": "Entrez votre nouvelle adresse {}", + "youCanMigrateThisIdentity": "Vous pouvez migrer vers cette identité !", + "identityMigrated": "Identité migré" } \ No newline at end of file diff --git a/config/indexer_endpoints.json b/config/indexer_endpoints.json index b5e967e..66f30c3 100644 --- a/config/indexer_endpoints.json +++ b/config/indexer_endpoints.json @@ -1,3 +1,5 @@ [ - "https://hasura.gdev.coinduf.eu" + "gdev-indexer.p2p.legal", + "gdev-hasura.cgeek.fr", + "hasura-gdev.pini.fr" ] diff --git a/integration_test/duniter/data/gecko_data.json b/integration_test/duniter/data/gecko_data.json index a034777..088753c 100644 --- a/integration_test/duniter/data/gecko_data.json +++ b/integration_test/duniter/data/gecko_data.json @@ -1,5 +1,5 @@ { - "initial_monetary_mass": 50100, + "initial_monetary_mass": 60100, "identities": { "test1": { "index": 0, @@ -49,7 +49,7 @@ }, "owner_pubkey": "5LqbvutJtRTHvnforyndwPbkC4Kf5cJtdRQaDcHoMi8S" }, - "testCesium1": { + "test5": { "index": 4, "balance": 10000, "membership_expire_on": 1705509948, @@ -59,6 +59,18 @@ "test2": 1727758466, "test3": 1727758466 }, + "owner_pubkey": "6FgzG8NwatTWHo7rM7sPP6P4Q95R2ZQNqYiHCs38RT21" + }, + "testCesium1": { + "index": 5, + "balance": 10000, + "membership_expire_on": 1705509948, + "next_cert_issuable_on": 1668347505, + "certs_received": { + "test1": 1727758466, + "test2": 1727758466, + "test3": 1727758466 + }, "owner_pubkey": "DCovzCEnQm9GUWe6mr8u42JR1JAuoj3HbQUGdCkfTzSr" } } diff --git a/lib/globals.dart b/lib/globals.dart index 7621851..3ca2db8 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -29,7 +29,7 @@ const cesiumPod = "https://g1.data.le-sou.org"; // String cesiumPod = "https://g1.data.presles.fr"; // String cesiumPod = "https://g1.data.e-is.pro"; -const datapodEndpoint = 'https://gdev-datapod.p2p.legal'; +const datapodEndpoint = 'gdev-datapod.p2p.legal'; // const v2sDatapod = 'http://10.0.2.2:8080'; // Contexts diff --git a/lib/models/migrate_wallet_checks.dart b/lib/models/migrate_wallet_checks.dart index 4b491fc..c9f070a 100644 --- a/lib/models/migrate_wallet_checks.dart +++ b/lib/models/migrate_wallet_checks.dart @@ -3,14 +3,30 @@ import 'package:gecko/models/wallet_data.dart'; class MigrateWalletChecks { final Map balance; final IdtyStatus idtyStatus; - final bool isSmith; final String validationStatus; final bool canValidate; - const MigrateWalletChecks( - {required this.balance, - required this.idtyStatus, - required this.isSmith, - required this.validationStatus, - required this.canValidate}); + const MigrateWalletChecks({ + required this.balance, + required this.idtyStatus, + required this.validationStatus, + required this.canValidate, + }); + + const MigrateWalletChecks.defaultValues({ + this.balance = const {'transferableBalance': 0}, + this.idtyStatus = IdtyStatus.none, + this.validationStatus = '', + this.canValidate = false, + }); + + @override + String toString() { + return { + 'balance': balance, + 'idtyStatus': idtyStatus, + 'validationStatus': validationStatus, + 'canValidate': canValidate, + }.toString(); + } } diff --git a/lib/models/queries_indexer.dart b/lib/models/queries_indexer.dart index e1987fe..9d76b12 100644 --- a/lib/models/queries_indexer.dart +++ b/lib/models/queries_indexer.dart @@ -18,7 +18,7 @@ query ($name: String!) { } '''; -const String getHistoryByAddressQ = r''' +const String getHistoryByAddressRelayQ = r''' query ($address: String!, $number: Int!, $cursor: String) { transaction_connection(where: {_or: [ @@ -56,9 +56,36 @@ query ($address: String!, $number: Int!, $cursor: String) { } '''; +const String getHistoryByAddressQ = r''' +query ($address: String!, $number: Int!, $offset: Int!) { + transaction_aggregate(where: {_or: [{issuer_pubkey: {_eq: $address}}, {receiver_pubkey: {_eq: $address}}]}) { + aggregate { + count + } + } + transaction(where: {_or: [{issuer_pubkey: {_eq: $address}}, {receiver_pubkey: {_eq: $address}}]}, order_by: {created_at: desc}, limit: $number, offset: $offset) { + amount + comment + created_at + issuer { + pubkey + identity { + name + } + } + receiver { + pubkey + identity { + name + } + } + } +} +'''; + const String getCertsReceived = r''' query ($address: String!) { - certification(where: {receiver: {pubkey: {_eq: $address}}}) { + certification(where: {receiver: {pubkey: {_eq: $address}}}, order_by: {created_at: desc}) { issuer { pubkey name @@ -70,7 +97,7 @@ query ($address: String!) { const String getCertsSent = r''' query ($address: String!) { - certification(where: {issuer: {pubkey: {_eq: $address}}}) { + certification(where: {issuer: {pubkey: {_eq: $address}}}, order_by: {created_at: desc}) { receiver { pubkey name @@ -96,3 +123,15 @@ query { } } '''; + +const String subscribeHistoryIssuedQ = r''' +subscription ($address: String!) { + account_by_pk(pubkey: $address) { + transactions_issued(limit: 1, order_by: {created_at: desc}) { + receiver_pubkey + amount + created_at + } + } +} +'''; diff --git a/lib/models/transaction_content.dart b/lib/models/transaction_content.dart new file mode 100644 index 0000000..10dadb6 --- /dev/null +++ b/lib/models/transaction_content.dart @@ -0,0 +1,19 @@ +import 'package:gecko/widgets/transaction_status.dart'; + +class TransactionContent { + final String transactionId; + TransactionStatus status; + final String from; + final String to; + final double amount; + String? error; + + TransactionContent({ + required this.transactionId, + required this.status, + required this.from, + required this.to, + required this.amount, + this.error, + }); +} diff --git a/lib/models/wallet_data.dart b/lib/models/wallet_data.dart index 431b807..725e773 100644 --- a/lib/models/wallet_data.dart +++ b/lib/models/wallet_data.dart @@ -70,7 +70,7 @@ class WalletData extends HiveObject { // creates the ':'-separated string from the WalletData String inLine() { - return "$chest:$number:$name:$derivation:$imageDefaultPath"; + return "$chest:$number:$name:$derivation:$imageDefaultPath:$imageCustomPath:$identityStatus"; } bool hasIdentity() { @@ -112,7 +112,7 @@ class WalletData extends HiveObject { final datapod = Provider.of(homeContext, listen: false); final avatarUuid = const Uuid().v4(); - await datapod.getAvatar(address, saveOnDisk: true, uuid: avatarUuid); + await datapod.getRemoteAvatar(address, uuid: avatarUuid); final avatarPath = '${avatarsDirectory.path}/$address-$avatarUuid'; if (!await File(avatarPath).exists()) return; diff --git a/lib/providers/duniter_indexer.dart b/lib/providers/duniter_indexer.dart index bd1f98d..8fff32f 100644 --- a/lib/providers/duniter_indexer.dart +++ b/lib/providers/duniter_indexer.dart @@ -11,11 +11,12 @@ import 'package:graphql_flutter/graphql_flutter.dart'; class DuniterIndexer with ChangeNotifier { Map walletNameIndexer = {}; - String? fetchMoreCursor; - Map? pageInfo; List? transBC; List listIndexerEndpoints = []; bool isLoadingIndexer = false; + bool hasNextPage = false; + Future?> Function()? refetch; + late GraphQLClient indexerClient; void reload() { notifyListeners(); @@ -27,10 +28,11 @@ class DuniterIndexer with ChangeNotifier { final client = HttpClient(); client.connectionTimeout = const Duration(milliseconds: 4000); try { - final request = await client.postUrl(Uri.parse('$endpoint/v1/graphql')); + final request = + await client.postUrl(Uri.parse('https://$endpoint/v1/graphql')); final response = await request.close(); if (response.statusCode != 200) { - log.w('INDEXER IS OFFLINE'); + log.w('Indexer $endpoint is offline'); indexerEndpoint = ''; isLoadingIndexer = false; notifyListeners(); @@ -46,7 +48,7 @@ class DuniterIndexer with ChangeNotifier { return true; } } catch (e) { - log.w('INDEXER IS OFFLINE'); + log.w('Indexer $endpoint is offline'); indexerEndpoint = ''; isLoadingIndexer = false; notifyListeners(); @@ -96,7 +98,7 @@ class DuniterIndexer with ChangeNotifier { } try { - final endpointPath = '${listIndexerEndpoints[i]}/v1/graphql'; + final endpointPath = 'https://${listIndexerEndpoints[i]}/v1/graphql'; final request = await client.postUrl(Uri.parse(endpointPath)); final response = await request.close(); @@ -128,14 +130,13 @@ class DuniterIndexer with ChangeNotifier { return indexerEndpoint; } - List parseHistory(blockchainTX, pubkey) { + List parseHistory(List blockchainTX, String address) { List transBC = []; int i = 0; - for (final trans in blockchainTX) { - final transaction = trans['node']; + for (final transaction in blockchainTX) { final direction = - transaction['issuer_pubkey'] != pubkey ? 'RECEIVED' : 'SENT'; + transaction['issuer']['pubkey'] != address ? 'RECEIVED' : 'SENT'; transBC.add(i); transBC[i] = []; @@ -143,10 +144,10 @@ class DuniterIndexer with ChangeNotifier { final amountBrut = transaction['amount']; final amount = removeDecimalZero(amountBrut / 100); if (direction == "RECEIVED") { - transBC[i].add(transaction['issuer_pubkey']); + transBC[i].add(transaction['issuer']['pubkey']); transBC[i].add(transaction['issuer']['identity']?['name'] ?? ''); } else if (direction == "SENT") { - transBC[i].add(transaction['receiver_pubkey']); + transBC[i].add(transaction['receiver']['pubkey']); transBC[i].add(transaction['receiver']['identity']?['name'] ?? ''); } transBC[i].add(amount); @@ -157,38 +158,35 @@ class DuniterIndexer with ChangeNotifier { return transBC; } - FetchMoreOptions? mergeQueryResult(result, opts, pubkey, nRepositories) { - final List? blockchainTX = - (result.data['transaction_connection']['edges'] as List?); + FetchMoreOptions? mergeQueryResult( + {required List transactions, + required FetchMoreOptions? opts, + required String address, + required int nRepositories, + required int offset}) { + // pageInfo = result.data!['transaction_connection']['pageInfo']; + // fetchMoreCursor = pageInfo!['endCursor']; + // final hasNextPage = pageInfo!['hasNextPage']; + // final hasPreviousPage = pageInfo!['hasPreviousPage']; + // log.d('endCursor: $fetchMoreCursor $hasNextPage $hasPreviousPage'); - pageInfo = result.data['transaction_connection']['pageInfo']; - fetchMoreCursor = pageInfo!['endCursor']; - final hasNextPage = pageInfo!['hasNextPage']; - final hasPreviousPage = pageInfo!['hasPreviousPage']; - log.d('endCursor: $fetchMoreCursor $hasNextPage $hasPreviousPage'); + // if (fetchMoreCursor != null) { + opts = FetchMoreOptions( + variables: {'offset': offset, 'number': nRepositories}, + updateQuery: (previousResultData, fetchMoreResultData) { + final List repos = [ + ...previousResultData!['transaction'] as List, + ...fetchMoreResultData!['transaction'] as List + ]; - if (fetchMoreCursor != null) { - opts = FetchMoreOptions( - variables: {'cursor': fetchMoreCursor, 'number': nRepositories}, - updateQuery: (previousResultData, fetchMoreResultData) { - final List repos = [ - ...previousResultData!['transaction_connection']['edges'] - as List, - ...fetchMoreResultData!['transaction_connection']['edges'] - as List - ]; - - fetchMoreResultData['transaction_connection']['edges'] = repos; - return fetchMoreResultData; - }, - ); - } - - if (fetchMoreCursor != null) { - transBC = parseHistory(blockchainTX, pubkey); - } else { - log.d("Activity start of $pubkey"); - } + fetchMoreResultData['transaction'] = repos; + return fetchMoreResultData; + }, + ); + transBC = parseHistory(transactions, address); + // } else { + // log.d("Activity start of $address"); + // } return opts; } @@ -197,113 +195,119 @@ class DuniterIndexer with ChangeNotifier { String result = n.toStringAsFixed(n.truncateToDouble() == n ? 0 : 2); return double.parse(result); } -} //// Manuals queries -Future isIdtyExist(String name) async { - final variables = { - 'name': name, - }; - final result = await _execQuery(isIdtyExistQ, variables); - return result.data!['identity']?.isNotEmpty ?? false; -} - -Future getBlockStart() async { - final result = await _execQuery(getBlockchainStartQ, {}); - if (!result.hasException) { - startBlockchainTime = - DateTime.parse(result.data!['block'][0]['created_at']); - startBlockchainInitialized = true; - return startBlockchainTime; - } - return DateTime(0, 0, 0, 0, 0); -} - -Future _execQuery( - String query, Map variables) async { - final httpLink = HttpLink( - '$indexerEndpoint/v1/graphql', - ); - - final GraphQLClient client = GraphQLClient( - cache: GraphQLCache(), - link: httpLink, - ); - - final QueryOptions options = - QueryOptions(document: gql(query), variables: variables); - - return await client.query(options); -} - -Map computeHistoryView(repository, String address) { - final bool isUdUnit = configBox.get('isUdUnit') ?? false; - late double amount; - late String finalAmount; - final DateTime date = repository[0]; - - final dateForm = "${date.day} ${monthsInYear[date.month]!.substring(0, { - 1, - 2, - 7, - 9 - }.contains(date.month) ? 4 : 3)}"; - - DateTime normalizeDate(DateTime inputDate) { - return DateTime(inputDate.year, inputDate.month, inputDate.day); + Future isIdtyExist(String name) async { + final variables = { + 'name': name, + }; + final result = await _execQuery(isIdtyExistQ, variables); + return result.data!['identity']?.isNotEmpty ?? false; } - String getDateDelimiter() { - DateTime now = DateTime.now(); - final transactionDate = normalizeDate(date.toLocal()); - final todayDate = normalizeDate(now); - final yesterdayDate = normalizeDate(now.subtract(const Duration(days: 1))); - final isSameWeek = weekNumber(transactionDate) == weekNumber(now) && - transactionDate.year == now.year; - final isTodayOrYesterday = - transactionDate == todayDate || transactionDate == yesterdayDate; - - if (transactionDate == todayDate) { - return "today".tr(); - } else if (transactionDate == yesterdayDate) { - return "yesterday".tr(); - } else if (isSameWeek && !isTodayOrYesterday) { - return "thisWeek".tr(); - } else if (!isSameWeek && !isTodayOrYesterday) { - if (transactionDate.year == now.year) { - return monthsInYear[transactionDate.month]!; - } else { - return "${monthsInYear[transactionDate.month]} ${transactionDate.year}"; - } - } else { - return ''; + Future getBlockStart() async { + final result = await _execQuery(getBlockchainStartQ, {}); + if (!result.hasException) { + startBlockchainTime = + DateTime.parse(result.data!['block'][0]['created_at']); + startBlockchainInitialized = true; + return startBlockchainTime; } + return DateTime(0, 0, 0, 0, 0); } - final dateDelimiter = getDateDelimiter(); + Future _execQuery( + String query, Map variables) async { + final options = QueryOptions(document: gql(query), variables: variables); - amount = repository[4] == 'RECEIVED' ? repository[3] : repository[3] * -1; + // 5GMyvKsTNk9wDBy9jwKaX6mhSzmFFtpdK9KNnmrLoSTSuJHv - if (isUdUnit) { - amount = round(amount / balanceRatio); - finalAmount = 'ud'.tr(args: ['$amount ']); - } else { - finalAmount = '$amount $currencyName'; + return await indexerClient.query(options); } - bool isMigrationTime = - startBlockchainInitialized && date.compareTo(startBlockchainTime) < 0; + Stream subscribeHistoryIssued(String address) { + final variables = { + 'address': address, + }; - return { - 'finalAmount': finalAmount, - 'isMigrationTime': isMigrationTime, - 'dateDelimiter': dateDelimiter, - 'dateForm': dateForm, - }; -} + final options = SubscriptionOptions( + document: gql(subscribeHistoryIssuedQ), + variables: variables, + ); -int weekNumber(DateTime date) { - int dayOfYear = int.parse(DateFormat("D").format(date)); - return ((dayOfYear - date.weekday + 10) / 7).floor(); + return indexerClient.subscribe(options); + } + + Map computeHistoryView(repository, String address) { + final bool isUdUnit = configBox.get('isUdUnit') ?? false; + late double amount; + late String finalAmount; + final DateTime date = repository[0]; + + final dateForm = "${date.day} ${monthsInYear[date.month]!.substring(0, { + 1, + 2, + 7, + 9 + }.contains(date.month) ? 4 : 3)}"; + + DateTime normalizeDate(DateTime inputDate) { + return DateTime(inputDate.year, inputDate.month, inputDate.day); + } + + String getDateDelimiter() { + DateTime now = DateTime.now(); + final transactionDate = normalizeDate(date.toLocal()); + final todayDate = normalizeDate(now); + final yesterdayDate = + normalizeDate(now.subtract(const Duration(days: 1))); + final isSameWeek = weekNumber(transactionDate) == weekNumber(now) && + transactionDate.year == now.year; + final isTodayOrYesterday = + transactionDate == todayDate || transactionDate == yesterdayDate; + + if (transactionDate == todayDate) { + return "today".tr(); + } else if (transactionDate == yesterdayDate) { + return "yesterday".tr(); + } else if (isSameWeek && !isTodayOrYesterday) { + return "thisWeek".tr(); + } else if (!isSameWeek && !isTodayOrYesterday) { + if (transactionDate.year == now.year) { + return monthsInYear[transactionDate.month]!; + } else { + return "${monthsInYear[transactionDate.month]} ${transactionDate.year}"; + } + } else { + return ''; + } + } + + final dateDelimiter = getDateDelimiter(); + + amount = repository[4] == 'RECEIVED' ? repository[3] : repository[3] * -1; + + if (isUdUnit) { + amount = round(amount / balanceRatio); + finalAmount = 'ud'.tr(args: ['$amount ']); + } else { + finalAmount = '$amount $currencyName'; + } + + bool isMigrationTime = + startBlockchainInitialized && date.compareTo(startBlockchainTime) < 0; + + return { + 'finalAmount': finalAmount, + 'isMigrationTime': isMigrationTime, + 'dateDelimiter': dateDelimiter, + 'dateForm': dateForm, + }; + } + + int weekNumber(DateTime date) { + int dayOfYear = int.parse(DateFormat("D").format(date)); + return ((dayOfYear - date.weekday + 10) / 7).floor(); + } } diff --git a/lib/providers/my_wallets.dart b/lib/providers/my_wallets.dart index 28469e0..367f80f 100644 --- a/lib/providers/my_wallets.dart +++ b/lib/providers/my_wallets.dart @@ -119,6 +119,7 @@ class MyWalletsProvider with ChangeNotifier { final avatarFolder = Directory('${directory.path}/avatars/'); if (await avatarFolder.exists()) { await avatarFolder.delete(recursive: true); + await avatarFolder.create(); } myWalletProvider.pinCode = ''; diff --git a/lib/providers/search.dart b/lib/providers/search.dart index 7f914dc..38360b4 100644 --- a/lib/providers/search.dart +++ b/lib/providers/search.dart @@ -12,7 +12,7 @@ class SearchProvider with ChangeNotifier { } Future> searchAddress() async { - if (isAddress(searchController.text)) { + if (await isAddress(searchController.text)) { G1WalletsList wallet = G1WalletsList(address: searchController.text); return [wallet]; } else { diff --git a/lib/providers/substrate_sdk.dart b/lib/providers/substrate_sdk.dart index 2caa502..03e72fe 100644 --- a/lib/providers/substrate_sdk.dart +++ b/lib/providers/substrate_sdk.dart @@ -6,12 +6,15 @@ import 'package:flutter/material.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/models/chest_data.dart'; import 'package:gecko/models/migrate_wallet_checks.dart'; +import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/models/transaction_content.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/home.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/wallet_options.dart'; import 'package:gecko/providers/wallets_profiles.dart'; +import 'package:gecko/widgets/transaction_status.dart'; import 'package:pinenacl/ed25519.dart'; import 'package:polkawallet_sdk/api/apiKeyring.dart'; import 'package:polkawallet_sdk/api/types/networkParams.dart'; @@ -36,8 +39,8 @@ class SubstrateSdk with ChangeNotifier { bool importIsLoading = false; int blocNumber = 0; bool isLoadingEndpoint = false; - Map transactionStatus = {}; - static const int initSs58 = 42; + Map transactionStatus = {}; + final int initSs58 = 42; Map currencyParameters = {}; final csSalt = TextEditingController(); final csPassword = TextEditingController(); @@ -53,21 +56,35 @@ class SubstrateSdk with ChangeNotifier { ////////// 1: API METHODS /////////// ///////////////////////////////////// - Future _executeCall(String currentTransactionId, TxInfoData txInfo, + Map statusMap = { + 'sending': TransactionStatus.sending, + 'Ready': TransactionStatus.propagation, + 'Broadcast': TransactionStatus.validating, + 'Finalized': TransactionStatus.finalized + }; + + Future _executeCall(TransactionContent transcationContent, TxInfoData txInfo, txOptions, String password, [String? rawParams]) async { final walletOptions = Provider.of(homeContext, listen: false); final walletProfiles = Provider.of(homeContext, listen: false); - transactionStatus.putIfAbsent(currentTransactionId, () => 'sending'); + final currentTransactionId = transcationContent.transactionId; + transactionStatus.putIfAbsent( + currentTransactionId, () => transcationContent); notifyListeners(); try { final hash = await sdk.api.tx.signAndSend(txInfo, txOptions, password, - rawParam: rawParams, onStatusChange: (p0) { - transactionStatus.update(currentTransactionId, (_) => p0, - ifAbsent: () => p0); + rawParam: rawParams, onStatusChange: (newStatus) { + transactionStatus.update(currentTransactionId, (trans) { + trans.status = statusMap[newStatus]!; + return trans; + }, ifAbsent: () { + transcationContent.status = statusMap[newStatus]!; + return transcationContent; + }); notifyListeners(); }).timeout( const Duration(seconds: 18), @@ -76,25 +93,47 @@ class SubstrateSdk with ChangeNotifier { log.d(hash); if (hash.isEmpty) { transactionStatus.update( - currentTransactionId, (_) => 'Exception: timeout'); + currentTransactionId, + (trans) { + trans.status = TransactionStatus.timeout; + return trans; + }, + ifAbsent: () { + transcationContent.status = TransactionStatus.timeout; + return transcationContent; + }, + ); notifyListeners(); - - return 'Exception: timeout'; } else { // Success ! - transactionStatus.update(currentTransactionId, (_) => hash.toString(), - ifAbsent: () => hash.toString()); + transactionStatus.update(currentTransactionId, (trans) { + trans.status = TransactionStatus.success; + return trans; + }, ifAbsent: () { + transcationContent.status = TransactionStatus.success; + return transcationContent; + }); notifyListeners(); walletOptions.reload(); walletProfiles.reload(); - return hash.toString(); } } catch (e) { - transactionStatus.update(currentTransactionId, (_) => e.toString(), - ifAbsent: () => e.toString()); + transactionStatus.update( + currentTransactionId, + (trans) { + trans.status = TransactionStatus.failed; + trans.error = e.toString(); + return trans; + }, + ifAbsent: () { + transcationContent.status = TransactionStatus.failed; + transcationContent.error = e.toString(); + return transcationContent; + }, + ); notifyListeners(); - return e.toString(); } + transactionStatus.remove(currentTransactionId); } Future _getStorage(String call) async { @@ -603,6 +642,8 @@ class SubstrateSdk with ChangeNotifier { final homeProvider = Provider.of(homeContext, listen: false); final myWalletProvider = Provider.of(homeContext, listen: false); + final duniterIndexer = + Provider.of(homeContext, listen: false); homeProvider.changeMessage("connectionPending".tr(), 0); @@ -647,7 +688,7 @@ class SubstrateSdk with ChangeNotifier { await initCurrencyParameters(); // Indexer Blockchain start - getBlockStart(); + duniterIndexer.getBlockStart(); notifyListeners(); homeProvider.changeMessage( @@ -889,7 +930,6 @@ class SubstrateSdk with ChangeNotifier { Future getBalanceAndIdtyStatus( String fromAddress, String toAddress) async { - final sub = Provider.of(homeContext, listen: false); bool canValidate = false; String validationStatus = ''; @@ -907,7 +947,9 @@ class SubstrateSdk with ChangeNotifier { final isSmithData = await isSmith(fromAddress); // Check conditions to set 'canValidate' and 'validationStatus' - if (transferableBalance != 0 && !fromHasConsumer) { + if (transferableBalance != 0 && + !fromHasConsumer && + await isAddress(toAddress)) { canValidate = true; } else if (toIdtyStatus != IdtyStatus.none && fromIdtyStatus != IdtyStatus.none) { @@ -920,14 +962,9 @@ class SubstrateSdk with ChangeNotifier { validationStatus = 'thisAccountIsEmpty'.tr(); } - if (sub.g1V1NewAddress == '') { - validationStatus = ''; - } - return MigrateWalletChecks( balance: fromBalance, idtyStatus: toIdtyStatus, - isSmith: isSmithData, validationStatus: validationStatus, canValidate: canValidate, ); @@ -988,7 +1025,14 @@ class SubstrateSdk with ChangeNotifier { } final transactionId = const Uuid().v4(); - _executeCall(transactionId, txInfo, txOptions, password, rawParams); + final transactionContent = TransactionContent( + transactionId: transactionId, + status: TransactionStatus.sending, + from: fromAddress, + to: destAddress, + amount: amount, + ); + _executeCall(transactionContent, txInfo, txOptions, password, rawParams); return transactionId; } @@ -1056,7 +1100,14 @@ class SubstrateSdk with ChangeNotifier { log.d('Cert action: ${txInfo.call!}'); final transactionId = const Uuid().v4(); - _executeCall(transactionId, txInfo, txOptions, password, rawParams); + final transactionContent = TransactionContent( + transactionId: const Uuid().v4(), + status: TransactionStatus.sending, + from: fromAddress, + to: destAddress, + amount: -1, + ); + _executeCall(transactionContent, txInfo, txOptions, password, rawParams); return transactionId; } @@ -1072,8 +1123,14 @@ class SubstrateSdk with ChangeNotifier { final txOptions = [name]; final transactionId = const Uuid().v4(); - - _executeCall(transactionId, txInfo, txOptions, password); + final transactionContent = TransactionContent( + transactionId: const Uuid().v4(), + status: TransactionStatus.sending, + from: fromAddress, + to: fromAddress, + amount: -1, + ); + _executeCall(transactionContent, txInfo, txOptions, password); return transactionId; } @@ -1143,7 +1200,15 @@ newKeySig: $newKeySigType"""); } final transactionId = const Uuid().v4(); - _executeCall(transactionId, txInfo, txOptions, fromPassword, rawParams); + final transactionContent = TransactionContent( + transactionId: const Uuid().v4(), + status: TransactionStatus.sending, + from: fromAddress, + to: fromAddress, + amount: -1, + ); + _executeCall( + transactionContent, txInfo, txOptions, fromPassword, rawParams); return transactionId; } @@ -1169,7 +1234,14 @@ newKeySig: $newKeySigType"""); final txOptions = [idtyIndex, address, revocationSigTyped]; final transactionId = const Uuid().v4(); - _executeCall(transactionId, txInfo, txOptions, password); + final transactionContent = TransactionContent( + transactionId: const Uuid().v4(), + status: TransactionStatus.sending, + from: address, + to: address, + amount: -1, + ); + _executeCall(transactionContent, txInfo, txOptions, password); return transactionId; } @@ -1244,10 +1316,6 @@ newKeySig: $newKeySigType"""); void reload() { notifyListeners(); } - - void resetTransactionStatus() { - transactionStatus.clear(); - } } //////////////////////////////////////////// @@ -1267,7 +1335,7 @@ void snackNode(bool isConnected) { final snackBar = SnackBar( backgroundColor: Colors.grey[900], padding: const EdgeInsets.all(20), - content: Text(message, style: const TextStyle(fontSize: 16)), + content: Text(message, style: scaledTextStyle(fontSize: 14)), duration: const Duration(seconds: 4)); ScaffoldMessenger.of(homeContext).showSnackBar(snackBar); } diff --git a/lib/providers/v2s_datapod.dart b/lib/providers/v2s_datapod.dart index bb1ef6a..5b1d906 100644 --- a/lib/providers/v2s_datapod.dart +++ b/lib/providers/v2s_datapod.dart @@ -5,26 +5,20 @@ import 'package:flutter/material.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/models/queries_datapod.dart'; import 'package:gecko/models/scale_functions.dart'; -import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; class V2sDatapodProvider with ChangeNotifier { + late GraphQLClient datapodClient; + Future _execQuery( String query, Map variables) async { - final httpLink = HttpLink('$datapodEndpoint/v1/graphql'); - - final GraphQLClient client = GraphQLClient( - cache: GraphQLCache(), - link: httpLink, - ); - final QueryOptions options = QueryOptions(document: gql(query), variables: variables); - return await client.query(options); + return await datapodClient.query(options); } Future updateProfile( @@ -36,9 +30,6 @@ class V2sDatapodProvider with ChangeNotifier { List>? socials, Map? geoloc}) async { final sub = Provider.of(homeContext, listen: false); - final myWallets = - Provider.of(homeContext, listen: false); - final walletData = myWallets.getWalletDataByAddress(address); final messageToSign = jsonEncode({ 'address': address, @@ -70,9 +61,6 @@ class V2sDatapodProvider with ChangeNotifier { return false; } log.d(result.data!['updateProfile']['message']); - walletData!.profileUpdatedTime = DateTime.now(); - walletBox.put(address, walletData); - return true; } @@ -145,8 +133,8 @@ class V2sDatapodProvider with ChangeNotifier { return profileDate; } - Future getAvatar(String address, - {double size = 20, bool saveOnDisk = false, String? uuid}) async { + Future getRemoteAvatar(String address, + {double size = 20, String? uuid}) async { final variables = { 'address': address, }; @@ -164,12 +152,8 @@ class V2sDatapodProvider with ChangeNotifier { final sanitizedAvatar64 = avatar64.replaceAll('\n', '').replaceAll('\r', '').replaceAll(' ', ''); - if (saveOnDisk) { - log.d('We save avatar for $address'); - await saveAvatar(address, sanitizedAvatar64, uuid); - } else { - await cacheAvatar(address, sanitizedAvatar64); - } + log.d('We save avatar for $address'); + await saveAvatar(address, sanitizedAvatar64, uuid); return Image.memory( base64.decode(sanitizedAvatar64), @@ -185,16 +169,26 @@ class V2sDatapodProvider with ChangeNotifier { } Future cacheAvatar(String address, String data) async { - final file = File('${avatarsCacheDirectory.path}/$address'); - return await file.writeAsBytes(base64.decode(data)); + final uuid = const Uuid().v4(); + final tempFile = File('${avatarsCacheDirectory.path}/$uuid$address'); + final targetFile = File('${avatarsCacheDirectory.path}/$address'); + + try { + // Write to a temporary file first to prevent data race + await tempFile.writeAsBytes(base64.decode(data)); + log.d('Caching avatar of $address'); + return await tempFile.rename(targetFile.path); + } catch (e) { + log.e("An error occurred while caching avatar: $e"); + rethrow; + } } - Image getAvatarLocal(String address, double size) { + Image getAvatarLocal(String address) { final avatarFile = File('${avatarsCacheDirectory.path}/$address'); return Image.file( avatarFile, - height: size, - fit: BoxFit.fitWidth, + fit: BoxFit.cover, ); } @@ -207,13 +201,12 @@ class V2sDatapodProvider with ChangeNotifier { } } - Future deleteAvatarsDirectory() async { + Future deleteAvatarsDirectory() async { if (await avatarsDirectory.exists()) { await avatarsDirectory.delete(recursive: true); } } - reload() { notifyListeners(); } diff --git a/lib/providers/wallet_options.dart b/lib/providers/wallet_options.dart index fd1f4cf..de84886 100644 --- a/lib/providers/wallet_options.dart +++ b/lib/providers/wallet_options.dart @@ -18,6 +18,7 @@ import 'package:gecko/screens/transaction_in_progress.dart'; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import 'package:image_cropper/image_cropper.dart'; +import 'package:uuid/uuid.dart'; class WalletOptionsProvider with ChangeNotifier { final address = TextEditingController(); @@ -116,15 +117,31 @@ class WalletOptionsProvider with ChangeNotifier { ], ); - final newPath = "${avatarsDirectory.path}/${address.text}"; + final avatarUuid = const Uuid().v4(); + final newPath = "${avatarsDirectory.path}/${address.text}-$avatarUuid"; - if (croppedFile != null) { - await File(croppedFile.path).rename(newPath); - } else { + if (croppedFile == null) { log.w('No image selected.'); return ''; } + + await File(croppedFile.path).rename(newPath); + + final walletData = + MyWalletsProvider().getWalletDataByAddress(address.text); + + if (walletData!.imageCustomPath != null) { + final avatarFile = File(walletData.imageCustomPath!); + await avatarFile.delete(); + } + + walletData.profileUpdatedTime = DateTime.now(); + walletData.imageCustomPath = newPath; + + await walletBox.put(address.text, walletData); + notifyListeners(); datapod.setAvatar(address.text, newPath); + return newPath; } else { log.w('No image selected.'); @@ -139,6 +156,7 @@ class WalletOptionsProvider with ChangeNotifier { Provider.of(context, listen: false); final myWalletProvider = Provider.of(context, listen: false); + final duniterIndexer = Provider.of(context, listen: false); bool canValidate = false; bool idtyExist = false; @@ -151,7 +169,7 @@ class WalletOptionsProvider with ChangeNotifier { title: Text( 'confirmYourIdentity'.tr(), textAlign: TextAlign.center, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500), ), content: SizedBox( height: 100, @@ -160,9 +178,9 @@ class WalletOptionsProvider with ChangeNotifier { TextField( key: keyEnterIdentityUsername, onChanged: (_) async { - idtyExist = await isIdtyExist(idtyName.text); + idtyExist = await duniterIndexer.isIdtyExist(idtyName.text); canValidate = !idtyExist && - !await isIdtyExist(idtyName.text) && + !await duniterIndexer.isIdtyExist(idtyName.text) && idtyName.text.length >= 2 && idtyName.text.length <= 32; @@ -176,7 +194,7 @@ class WalletOptionsProvider with ChangeNotifier { textAlign: TextAlign.center, autofocus: true, controller: idtyName, - style: const TextStyle(fontSize: 19), + style: const TextStyle(fontSize: 17), ), const SizedBox(height: 10), Consumer(builder: (context, wOptions, _) { diff --git a/lib/providers/wallets_profiles.dart b/lib/providers/wallets_profiles.dart index b69771e..a26c789 100644 --- a/lib/providers/wallets_profiles.dart +++ b/lib/providers/wallets_profiles.dart @@ -6,10 +6,12 @@ import 'package:flutter/material.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/models/g1_wallets_list.dart'; import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/screens/wallet_view.dart'; import 'package:jdenticon_dart/jdenticon_dart.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:barcode_scan2/barcode_scan2.dart'; +import 'package:provider/provider.dart'; class WalletsProfilesProvider with ChangeNotifier { WalletsProfilesProvider(this.address); @@ -35,7 +37,7 @@ class WalletsProfilesProvider with ChangeNotifier { log.e("BarcodeScanner ERR: $e"); return 'false'; } - if (isAddress(barcode.rawContent)) { + if (await isAddress(barcode.rawContent)) { address = barcode.rawContent; Navigator.popUntil( context, @@ -93,24 +95,30 @@ class WalletsProfilesProvider with ChangeNotifier { } } -bool isAddress(address) { - final RegExp regExp = RegExp( - r'^[a-zA-Z0-9]+$', - caseSensitive: false, - multiLine: false, - ); +// bool isAddress(address) { +// final RegExp regExp = RegExp( +// r'^[a-zA-Z0-9]+$', +// caseSensitive: false, +// multiLine: false, +// ); - if (regExp.hasMatch(address) == true && - address.length > 45 && - address.length < 52) { - return true; - } else { - return false; - } +// if (regExp.hasMatch(address) == true && +// address.length > 45 && +// address.length < 52) { +// return true; +// } else { +// return false; +// } +// } + +Future isAddress(String address) async { + final sub = Provider.of(homeContext, listen: false); + return await sub.sdk.api.account.checkAddressFormat(address, sub.initSs58) ?? + false; } snackMessage(context, - {required String message, int duration = 2, double fontSize = 16}) { + {required String message, int duration = 2, double fontSize = 14}) { final snackBar = SnackBar( backgroundColor: Colors.grey[900], padding: EdgeInsets.all(scaleSize(19)), @@ -124,7 +132,7 @@ snackCopyKey(context) { backgroundColor: Colors.grey[900], padding: EdgeInsets.all(scaleSize(19)), content: Text("thisAddressHasBeenCopiedToClipboard".tr(), - style: scaledTextStyle(fontSize: 16)), + style: scaledTextStyle(fontSize: 14)), duration: const Duration(seconds: 2)); ScaffoldMessenger.of(context).showSnackBar(snackBar); } @@ -134,7 +142,7 @@ snackCopySeed(context) { backgroundColor: Colors.grey[900], padding: EdgeInsets.all(scaleSize(19)), content: Text("thisMnemonicHasBeenCopiedToClipboard".tr(), - style: scaledTextStyle(fontSize: 16)), + style: scaledTextStyle(fontSize: 14)), duration: const Duration(seconds: 4)); ScaffoldMessenger.of(context).showSnackBar(snackBar); } diff --git a/lib/screens/activity.dart b/lib/screens/activity.dart index ba62888..bc40dc4 100644 --- a/lib/screens/activity.dart +++ b/lib/screens/activity.dart @@ -36,8 +36,7 @@ class _ActivityScreenState extends State { return PopScope( onPopInvoked: (_) { - duniterIndexer.fetchMoreCursor = - duniterIndexer.pageInfo = duniterIndexer.transBC = null; + duniterIndexer.refetch = duniterIndexer.transBC = null; }, child: Scaffold( appBar: AppBar( @@ -45,7 +44,7 @@ class _ActivityScreenState extends State { toolbarHeight: scaleSize(57), title: Text( 'accountActivity'.tr(), - style: scaledTextStyle(fontSize: 20), + style: scaledTextStyle(fontSize: 18), ), ), bottomNavigationBar: const GeckoBottomAppBar(), diff --git a/lib/screens/certifications.dart b/lib/screens/certifications.dart index 278d61d..ff05daa 100644 --- a/lib/screens/certifications.dart +++ b/lib/screens/certifications.dart @@ -51,7 +51,7 @@ class CertificationsScreen extends StatelessWidget { CertsCounter(address: address) ]), content: CertsList( - address: address, direction: CertDirection.sent), + address: address, direction: CertDirection.received), contentHorizontalPadding: 0, contentBorderWidth: 1, ), diff --git a/lib/screens/debug_screen.dart b/lib/screens/debug_screen.dart index 413ca9b..84e9b41 100644 --- a/lib/screens/debug_screen.dart +++ b/lib/screens/debug_screen.dart @@ -35,8 +35,8 @@ class DebugScreen extends StatelessWidget { ), const SizedBox(height: 20), SizedBox( - height: 60, - width: 250, + height: 50, + width: 210, child: ElevatedButton( style: ElevatedButton.styleFrom( foregroundColor: Colors.white, @@ -47,7 +47,7 @@ class DebugScreen extends StatelessWidget { child: const Text( 'Spawn a bloc', style: TextStyle( - fontSize: 20, fontWeight: FontWeight.w600), + fontSize: 17, fontWeight: FontWeight.w600), ), ), ), diff --git a/lib/screens/home.dart b/lib/screens/home.dart index acbd6ba..318be44 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -20,6 +20,7 @@ import 'package:gecko/screens/myWallets/restore_chest.dart'; import 'package:gecko/screens/onBoarding/1.dart'; import 'package:gecko/widgets/drawer.dart'; import 'package:gecko/widgets/buttons/home_buttons.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:provider/provider.dart'; @@ -76,7 +77,25 @@ class _HomeScreenState extends State { homeProvider.isWalletBoxInit = true; myWalletProvider.reload(); - duniterIndexer.getValidIndexerEndpoint(); + duniterIndexer.getValidIndexerEndpoint().then((validIndexerEndpoint) { + final wsLinkIndexer = WebSocketLink( + 'wss://$validIndexerEndpoint/v1/graphql', + ); + + final wsLinkDatapod = WebSocketLink( + 'wss://$datapodEndpoint/v1/graphql', + ); + + duniterIndexer.indexerClient = GraphQLClient( + cache: GraphQLCache(), + link: wsLinkIndexer, + ); + + datapod.datapodClient = GraphQLClient( + cache: GraphQLCache(), + link: wsLinkDatapod, + ); + }); await homeProvider.getValidEndpoints(); if (configBox.get('isCacheChecked') == null) { @@ -101,7 +120,6 @@ class _HomeScreenState extends State { } }); } - // _duniterIndexer.checkIndexerEndpointBackground(); }); super.initState(); } diff --git a/lib/screens/myWallets/change_pin.dart b/lib/screens/myWallets/change_pin.dart index e30dcdf..349f5a5 100644 --- a/lib/screens/myWallets/change_pin.dart +++ b/lib/screens/myWallets/change_pin.dart @@ -4,11 +4,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:durt/durt.dart'; import 'package:gecko/globals.dart'; -import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; +import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:provider/provider.dart'; class ChangePinScreen extends StatefulWidget with ChangeNotifier { @@ -45,11 +45,7 @@ class _ChangePinScreenState extends State { }, child: Scaffold( resizeToAvoidBottomInset: false, - appBar: AppBar( - elevation: 1, - toolbarHeight: scaleSize(57), - title: Text(widget.walletName!), - ), + appBar: GeckoAppBar(widget.walletName!), body: Center( child: SafeArea( child: Column(children: [ diff --git a/lib/screens/myWallets/import_g1_v1.dart b/lib/screens/myWallets/import_g1_v1.dart index f6b5f0a..80ed6c1 100644 --- a/lib/screens/myWallets/import_g1_v1.dart +++ b/lib/screens/myWallets/import_g1_v1.dart @@ -91,9 +91,10 @@ class ImportG1v1 extends StatelessWidget { keyboardType: TextInputType.text, controller: sub.csSalt, obscureText: !sub.isCesiumIDVisible, - style: scaledTextStyle(fontSize: 16), + style: scaledTextStyle(fontSize: 14), decoration: InputDecoration( hintText: 'enterCesiumId'.tr(), + hintStyle: scaledTextStyle(fontSize: 14), suffixIcon: IconButton( key: keyCesiumIdVisible, icon: Icon( @@ -132,9 +133,10 @@ class ImportG1v1 extends StatelessWidget { keyboardType: TextInputType.text, controller: sub.csPassword, obscureText: !sub.isCesiumIDVisible, - style: scaledTextStyle(fontSize: 16), + style: scaledTextStyle(fontSize: 14), decoration: InputDecoration( hintText: 'enterCesiumPassword'.tr(), + hintStyle: scaledTextStyle(fontSize: 14), suffixIcon: IconButton( icon: Icon( sub.isCesiumIDVisible @@ -167,7 +169,7 @@ class ImportG1v1 extends StatelessWidget { child: Text( 'v1: ${getShortPubkey(sub.g1V1OldPubkey)}', style: scaledTextStyle( - fontSize: 17, + fontSize: 16, fontWeight: FontWeight.w600, fontFamily: 'Monospace'), ), @@ -183,7 +185,7 @@ class ImportG1v1 extends StatelessWidget { child: Text( 'v2: ${getShortPubkey(sub.g1V1NewAddress)}', style: scaledTextStyle( - fontSize: 17, + fontSize: 16, fontWeight: FontWeight.w600, fontFamily: 'Monospace'), ), @@ -212,7 +214,7 @@ class ImportG1v1 extends StatelessWidget { ScaledSizedBox(height: 20), Text( 'migrateToThisWallet'.tr(), - style: scaledTextStyle(fontSize: 17), + style: scaledTextStyle(fontSize: 16), ), ScaledSizedBox(height: 5), DropdownButtonHideUnderline( @@ -226,7 +228,7 @@ class ImportG1v1 extends StatelessWidget { value: wallet, child: Text( wallet.name!, - style: scaledTextStyle(fontSize: 17), + style: scaledTextStyle(fontSize: 16), ), ); }).toList(), @@ -292,7 +294,7 @@ class ImportG1v1 extends StatelessWidget { child: Text( 'migrateAccount'.tr(), style: scaledTextStyle( - fontSize: 20, fontWeight: FontWeight.w600), + fontSize: 19, fontWeight: FontWeight.w600), ), ), ), @@ -301,7 +303,7 @@ class ImportG1v1 extends StatelessWidget { statusData.validationStatus, textAlign: TextAlign.center, style: scaledTextStyle( - fontSize: 14, color: Colors.grey[600]), + fontSize: 12, color: Colors.grey[600]), ) ]); }); diff --git a/lib/screens/myWallets/migrate_identity.dart b/lib/screens/myWallets/migrate_identity.dart index ced07f1..dc44cc3 100644 --- a/lib/screens/myWallets/migrate_identity.dart +++ b/lib/screens/myWallets/migrate_identity.dart @@ -9,12 +9,15 @@ import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/duniter_indexer.dart'; +import 'package:gecko/providers/generate_wallets.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/wallet_options.dart'; +import 'package:gecko/providers/wallets_profiles.dart'; import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; import 'package:gecko/widgets/commons/top_appbar.dart'; +import 'package:polkawallet_sdk/api/apiKeyring.dart'; import 'package:provider/provider.dart'; class MigrateIdentityScreen extends StatelessWidget { @@ -22,190 +25,251 @@ class MigrateIdentityScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // final _homeProvider = Provider.of(context); final walletOptions = Provider.of(context, listen: false); final myWalletProvider = Provider.of(context, listen: false); + final generatedWalletsProvider = + Provider.of(context, listen: false); final duniterIndexer = Provider.of(context, listen: false); + final sub = Provider.of(context, listen: false); final fromAddress = walletOptions.address.text; - final defaultWallet = myWalletProvider.getDefaultWallet(); - final walletsList = myWalletProvider.listWallets.toList(); - late WalletData selectedWallet; - - if (fromAddress == defaultWallet.address) { - selectedWallet = - walletsList[fromAddress == walletsList[0].address ? 1 : 0]; - } else { - selectedWallet = defaultWallet; - } + final newMnemonicSentence = TextEditingController(); + final newWalletAddress = TextEditingController(); final mdStyle = MarkdownStyleSheet( - p: scaledTextStyle(fontSize: 17, color: Colors.black, letterSpacing: 0.3), + p: scaledTextStyle(fontSize: 16, color: Colors.black, letterSpacing: 0.3), textAlign: WrapAlignment.center, ); + final bool isUdUnit = configBox.get('isUdUnit') ?? false; + final unit = isUdUnit ? 'ud'.tr(args: ['']) : currencyName; - if (walletsList.length < 2) { - return Column( - children: [ - ScaledSizedBox(height: 80), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Vous devez avoir au moins 2 portefeuilles\npour effecter cette opération', - style: scaledTextStyle(fontSize: 17), - ) - ], - ) - ], + var statusData = const MigrateWalletChecks.defaultValues(); + var mnemonicIsValid = false; + int? matchDerivationNbr; + String matchInfo = ''; + + Future scanDerivations() async { + if (!await isAddress(newWalletAddress.text) || + !await sub.isMnemonicValid(newMnemonicSentence.text) || + !statusData.canValidate) { + mnemonicIsValid = false; + matchInfo = ''; + walletOptions.reload(); + return; + } + log.d('Scan derivations to find a match'); + + //Scan root wallet + final addressData = await sub.sdk.api.keyring.addressFromMnemonic( + sub.currencyParameters['ss58']!, + cryptoType: CryptoType.sr25519, + mnemonic: newMnemonicSentence.text, ); + + if (addressData.address == newWalletAddress.text) { + matchDerivationNbr = -1; + mnemonicIsValid = true; + walletOptions.reload(); + return; + } + + //Scan derivations + for (int derivationNbr in [ + for (var i = 0; i < generatedWalletsProvider.numberScan; i += 1) i + ]) { + final addressData = await sub.sdk.api.keyring.addressFromMnemonic( + sub.currencyParameters['ss58']!, + cryptoType: CryptoType.sr25519, + mnemonic: newMnemonicSentence.text, + derivePath: '//$derivationNbr'); + + if (addressData.address == newWalletAddress.text) { + matchDerivationNbr = derivationNbr; + mnemonicIsValid = true; + matchInfo = "youCanMigrateThisIdentity".tr(); + break; + } else { + mnemonicIsValid = false; + } + } + + if (!mnemonicIsValid) { + matchInfo = "addressNotBelongToMnemonic".tr(); + } + walletOptions.reload(); } return Scaffold( backgroundColor: backgroundColor, appBar: GeckoAppBar('migrateIdentity'.tr()), body: SafeArea( - child: Consumer(builder: (context, sub, _) { - return FutureBuilder( - future: sub.getBalanceAndIdtyStatus( - fromAddress, selectedWallet.address), - builder: (BuildContext context, - AsyncSnapshot status) { - if (status.data == null) { - return Column(children: [ - ScaledSizedBox(height: 80), - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - ScaledSizedBox( - height: scaleSize(32), - width: scaleSize(32), - child: CircularProgressIndicator( - color: orangeC, - strokeWidth: scaleSize(4), - ), - ), - ]), - ]); + child: Column(children: [ + const Row(children: []), + ScaledSizedBox(height: 18), + ScaledSizedBox( + width: 320, + child: MarkdownBody( + data: 'areYouSureMigrateIdentity'.tr(args: [ + duniterIndexer.walletNameIndexer[fromAddress] ?? '???', + '${walletOptions.balanceCache[fromAddress]} $unit' + ]), + styleSheet: mdStyle), + ), + ScaledSizedBox(height: 55), + Text('migrateToThisWallet'.tr(), + style: scaledTextStyle(fontSize: 16)), + ScaledSizedBox(height: 5), + ScaledSizedBox( + width: 320, + child: TextField( + controller: newMnemonicSentence, + autofocus: true, + minLines: 2, + maxLines: 2, + style: scaledTextStyle(fontSize: 14), + decoration: InputDecoration( + icon: Image.asset( + 'assets/onBoarding/phrase_de_restauration_flou.png', + width: scaleSize(30), + ), + hintText: 'enterYourNewMnemonic'.tr(), + hintStyle: scaledTextStyle(fontSize: 14), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: orangeC), + ), + ), + onChanged: (newMnemonic) async { + await scanDerivations(); + }, + ), + ), + ScaledSizedBox(height: 5), + ScaledSizedBox( + width: 320, + child: TextField( + controller: newWalletAddress, + style: scaledTextStyle(fontSize: 14), + decoration: InputDecoration( + icon: Image.asset( + 'assets/walletOptions/key.png', + height: scaleSize(30), + ), + hintText: 'enterYourNewAddress'.tr(args: [currencyName]), + hintStyle: scaledTextStyle(fontSize: 14), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: orangeC), + ), + ), + onChanged: (newAddress) async { + if (await isAddress(newAddress)) { + statusData = await sub.getBalanceAndIdtyStatus( + fromAddress, newAddress); + await scanDerivations(); + } else { + statusData = const MigrateWalletChecks.defaultValues(); + matchInfo = ''; + walletOptions.reload(); } + }, + ), + ), + const Spacer(flex: 2), + Consumer(builder: (context, _, __) { + return ScaledSizedBox( + width: 320, + height: 55, + child: ElevatedButton( + key: keyConfirm, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + elevation: 4, + backgroundColor: orangeC, + ), + onPressed: statusData.canValidate && mnemonicIsValid + ? () async { + WalletData? defaultWallet = + myWalletProvider.getDefaultWallet(); - final statusData = status.data!; - final walletsList = myWalletProvider.listWallets.toList(); + String? pin; + if (myWalletProvider.pinCode == '') { + pin = await Navigator.push( + context, + MaterialPageRoute( + builder: (homeContext) { + return UnlockingWallet(wallet: defaultWallet); + }, + ), + ); + } + if (myWalletProvider.pinCode == '') return; - walletsList - .removeWhere((element) => element.address == fromAddress); + await sub.importAccount( + mnemonic: newMnemonicSentence.text, + derivePath: matchDerivationNbr == -1 + ? '' + : "//$matchDerivationNbr", + password: 'password'); - final bool isUdUnit = configBox.get('isUdUnit') ?? false; - final unit = isUdUnit ? 'ud'.tr(args: ['']) : currencyName; + final transactionId = await sub.migrateIdentity( + fromAddress: fromAddress, + destAddress: newWalletAddress.text, + fromPassword: pin ?? myWalletProvider.pinCode, + destPassword: 'password', + withBalance: true, + fromBalance: statusData.balance); - return Column(children: [ - const Row(children: []), - ScaledSizedBox(height: 18), - ScaledSizedBox( - width: 320, - child: MarkdownBody( - data: 'areYouSureMigrateIdentity'.tr(args: [ - duniterIndexer.walletNameIndexer[fromAddress] ?? - '???', - '${statusData.balance['transferableBalance']} $unit' - ]), - styleSheet: mdStyle), - ), - ScaledSizedBox(height: 55), - Text('migrateToThisWallet'.tr(), - style: scaledTextStyle(fontSize: 17)), - ScaledSizedBox(height: 5), - DropdownButtonHideUnderline( - key: keySelectWallet, - child: DropdownButton( - value: selectedWallet, - icon: const Icon(Icons.keyboard_arrow_down), - items: walletsList.map((wallet) { - return DropdownMenuItem( - key: keySelectThisWallet(wallet.address), - value: wallet, - child: Text( - wallet.name!, - style: scaledTextStyle(fontSize: 17), - ), + sub.deleteAccounts([newWalletAddress.text]); + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return TransactionInProgress( + transactionId: transactionId, + transType: 'identityMigration', + fromAddress: getShortPubkey(fromAddress), + toAddress: + getShortPubkey(newWalletAddress.text)); + }), ); - }).toList(), - onChanged: (WalletData? newSelectedWallet) { - selectedWallet = newSelectedWallet!; - sub.reload(); - }, - ), - ), - const Spacer(flex: 2), - ScaledSizedBox( - width: 320, - height: 55, - child: ElevatedButton( - key: keyConfirm, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - elevation: 4, - backgroundColor: orangeC, - ), - onPressed: statusData.canValidate - ? () async { - WalletData? defaultWallet = - myWalletProvider.getDefaultWallet(); - - String? pin; - if (myWalletProvider.pinCode == '') { - pin = await Navigator.push( - context, - MaterialPageRoute( - builder: (homeContext) { - return UnlockingWallet( - wallet: defaultWallet); - }, - ), - ); - } - - if (myWalletProvider.pinCode == '') return; - final transactionId = await sub.migrateIdentity( - fromAddress: fromAddress, - destAddress: selectedWallet.address, - fromPassword: pin ?? myWalletProvider.pinCode, - destPassword: pin ?? myWalletProvider.pinCode, - withBalance: true, - fromBalance: statusData.balance); - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return TransactionInProgress( - transactionId: transactionId, - transType: 'identityMigration', - fromAddress: getShortPubkey(fromAddress), - toAddress: getShortPubkey( - selectedWallet.address)); - }), - ); - } - : null, - child: Text( - 'migrateIdentity'.tr(), - style: scaledTextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: Colors.white), - ), - ), - ), + } + : null, + child: Text( + 'migrateIdentity'.tr(), + style: scaledTextStyle( + fontSize: 19, + fontWeight: FontWeight.w600, + color: Colors.white), + ), + ), + ); + }), + Consumer(builder: (context, _, __) { + return ScaledSizedBox( + width: 320, + child: Column( + children: [ ScaledSizedBox(height: 10), Text( statusData.validationStatus, textAlign: TextAlign.center, style: - scaledTextStyle(fontSize: 15, color: Colors.grey[600]), + scaledTextStyle(fontSize: 12, color: Colors.grey[600]), ), - const Spacer(), - ]); - }); - }), + ScaledSizedBox(height: 5), + Text( + matchInfo, + textAlign: TextAlign.center, + style: + scaledTextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + ); + }), + const Spacer(), + ]), ), ); } diff --git a/lib/screens/myWallets/restore_chest.dart b/lib/screens/myWallets/restore_chest.dart index 83974f2..3f5740f 100644 --- a/lib/screens/myWallets/restore_chest.dart +++ b/lib/screens/myWallets/restore_chest.dart @@ -108,7 +108,7 @@ class RestoreChest extends StatelessWidget { child: Text( 'restoreThisChest'.tr(), style: scaledTextStyle( - fontSize: 20, + fontSize: 19, fontWeight: FontWeight.w600, color: Colors.white), ), diff --git a/lib/screens/myWallets/wallet_options.dart b/lib/screens/myWallets/wallet_options.dart index e3a3a98..08ac398 100644 --- a/lib/screens/myWallets/wallet_options.dart +++ b/lib/screens/myWallets/wallet_options.dart @@ -10,7 +10,6 @@ import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/substrate_sdk.dart'; -import 'package:gecko/providers/v2s_datapod.dart'; import 'package:gecko/providers/wallet_options.dart'; import 'package:gecko/providers/wallets_profiles.dart'; import 'package:gecko/screens/certifications.dart'; @@ -50,6 +49,24 @@ class WalletOptions extends StatelessWidget { final isWalletNameIndexed = duniterIndexer.walletNameIndexer[walletOptions.address.text] != null; + // StreamSubscription? subscription; + // final stream = duniterIndexer.subscribeHistoryIssued(wallet.address); + + // subscription = stream.listen((result) { + // if (result.hasException) { + // log.e(result.exception); + // } else { + // final Map transData = + // result.data?['account_by_pk']['transactions_issued'].first; + // final String receiver = transData['receiver_pubkey']; + // final double amount = transData['amount'] / 100; + // final createdAt = DateTime.parse(transData['created_at']); + // log.d('$receiver --- $amount --- $createdAt'); + + // subscription?.cancel(); + // } + // }); + return PopScope( onPopInvoked: (_) { walletOptions.isEditing = false; @@ -69,7 +86,7 @@ class WalletOptions extends StatelessWidget { ? duniterIndexer .walletNameIndexer[walletOptions.address.text]! : wallet.name!, - style: scaledTextStyle(fontSize: 20), + style: scaledTextStyle(fontSize: 19), ); }), actions: [ @@ -259,58 +276,48 @@ class WalletOptions extends StatelessWidget { } Widget avatar(WalletOptionsProvider walletProvider) { - return Consumer(builder: (context, datapod, _) { - return Stack( - children: [ - InkWell( + return Stack( + children: [ + InkWell( + onTap: () async { + await (walletProvider.changeAvatar()); + }, + child: wallet.imageCustomPath == null || wallet.imageCustomPath == '' + ? Image.asset( + 'assets/avatars/${wallet.imageDefaultPath}', + width: scaleSize(122), + ) + : Container( + width: scaleSize(122), + height: scaleSize(122), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent, + image: DecorationImage( + fit: BoxFit.cover, + image: FileImage( + File(wallet.imageCustomPath!), + ), + ), + ), + ), + ), + Positioned( + right: 0, + top: 0, + child: InkWell( onTap: () async { - final newPath = await (walletProvider.changeAvatar()); - if (newPath != '') { - wallet.imageCustomPath = newPath; - walletBox.put(wallet.key, wallet); - // Uncomment to enable Cs+ avatar storage - // CesiumPlusProvider().setAvatar(wallet.address, newPath); - } + wallet.imageCustomPath = await (walletProvider.changeAvatar()); walletProvider.reload(); }, - child: - wallet.imageCustomPath == null || wallet.imageCustomPath == '' - ? Image.asset( - 'assets/avatars/${wallet.imageDefaultPath}', - width: scaleSize(122), - ) - : Container( - width: scaleSize(122), - height: scaleSize(122), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent, - image: DecorationImage( - fit: BoxFit.cover, - image: FileImage( - File(wallet.imageCustomPath!), - ), - ), - ), - ), - ), - Positioned( - right: 0, - top: 0, - child: InkWell( - onTap: () async { - wallet.imageCustomPath = await (walletProvider.changeAvatar()); - walletProvider.reload(); - }, - child: Image.asset( - 'assets/walletOptions/camera.png', - height: scaleSize(38), - ), + child: Image.asset( + 'assets/walletOptions/camera.png', + height: scaleSize(38), ), ), - ], - ); - }); + ), + ], + ); } Widget confirmIdentityButton(WalletOptionsProvider walletProvider) { diff --git a/lib/screens/myWallets/wallets_home.dart b/lib/screens/myWallets/wallets_home.dart index d5b3bd5..09e59c5 100644 --- a/lib/screens/myWallets/wallets_home.dart +++ b/lib/screens/myWallets/wallets_home.dart @@ -44,7 +44,7 @@ class _WalletsHomeState extends State { return Scaffold( backgroundColor: backgroundColor, appBar: AppBar( - elevation: 1, + backgroundColor: yellowC, toolbarHeight: scaleSize(57), title: Row( children: [ @@ -62,7 +62,6 @@ class _WalletsHomeState extends State { ), ], ), - backgroundColor: const Color(0xffFFD58D), ), bottomNavigationBar: Consumer(builder: (context, _, __) { @@ -161,7 +160,7 @@ class _WalletsHomeState extends State { 'explainDraggableWallet'.tr(), textAlign: TextAlign.center, style: scaledTextStyle( - fontSize: 20, fontWeight: FontWeight.w500), + fontSize: 17, fontWeight: FontWeight.w500), ), ], )) diff --git a/lib/screens/onBoarding/10.dart b/lib/screens/onBoarding/10.dart index 75724c7..18925b1 100644 --- a/lib/screens/onBoarding/10.dart +++ b/lib/screens/onBoarding/10.dart @@ -83,7 +83,7 @@ class OnboardingStepTen extends StatelessWidget { Text( "youHaveToBeConnectedToValidateChest".tr(), style: scaledTextStyle( - fontSize: 20, + fontSize: 17, color: Colors.redAccent, fontWeight: FontWeight.w500, ), diff --git a/lib/screens/qrcode_fullscreen.dart b/lib/screens/qrcode_fullscreen.dart index 1d4cda0..d4f1c74 100644 --- a/lib/screens/qrcode_fullscreen.dart +++ b/lib/screens/qrcode_fullscreen.dart @@ -63,7 +63,7 @@ class _QrCodeFullscreenState extends State { }), title: Text( 'QR Code de ${getShortPubkey(widget.address)}', - style: scaledTextStyle(color: orangeC, fontSize: 20), + style: scaledTextStyle(color: orangeC, fontSize: 18), )), body: SafeArea( child: SizedBox.expand( diff --git a/lib/screens/search.dart b/lib/screens/search.dart index cc984c0..4bf41f7 100644 --- a/lib/screens/search.dart +++ b/lib/screens/search.dart @@ -32,7 +32,7 @@ class _SearchScreenState extends State { final searchProvider = Provider.of(context, listen: false); final clipboard = await Clipboard.getData('text/plain'); pastedAddress = clipboard?.text ?? ''; - canPasteAddress = isAddress(pastedAddress); + canPasteAddress = await isAddress(pastedAddress); searchProvider.reload(); } diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index 0afd48f..691b064 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -9,6 +9,7 @@ import 'package:gecko/providers/settings_provider.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/widgets/commons/loading.dart'; +import 'package:gecko/widgets/commons/top_appbar.dart'; import 'package:polkawallet_sdk/api/types/networkParams.dart'; import 'package:provider/provider.dart'; @@ -21,17 +22,12 @@ class SettingsScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: backgroundColor, - appBar: AppBar( - toolbarHeight: scaleSize(57), - title: Text( - 'parameters'.tr(), - style: scaledTextStyle(fontSize: 21), - )), + appBar: GeckoAppBar('parameters'.tr()), body: Column(children: [ ScaledSizedBox(height: 30), Text( 'networkSettings'.tr(), - style: scaledTextStyle(color: Colors.grey[500]!, fontSize: 20), + style: scaledTextStyle(color: Colors.grey[500]!, fontSize: 19), ), ScaledSizedBox(height: 20), duniterEndpointSelection(context), @@ -40,7 +36,7 @@ class SettingsScreen extends StatelessWidget { ScaledSizedBox(height: 35), Text( 'displaySettings'.tr(), - style: scaledTextStyle(color: Colors.grey[500]!, fontSize: 20), + style: scaledTextStyle(color: Colors.grey[500]!, fontSize: 19), ), ScaledSizedBox(height: 20), chooseCurrencyUnit(context), @@ -60,7 +56,7 @@ class SettingsScreen extends StatelessWidget { child: Text( 'forgetAllMyChests'.tr(), style: scaledTextStyle( - fontSize: 18, + fontSize: 17, color: const Color(0xffD80000), fontWeight: FontWeight.w600, ), @@ -87,14 +83,14 @@ class SettingsScreen extends StatelessWidget { child: Row( children: [ ScaledSizedBox(width: 12), - Text('showUdAmounts'.tr(), style: scaledTextStyle(fontSize: 16)), + Text('showUdAmounts'.tr(), style: scaledTextStyle(fontSize: 15)), const Spacer(), Consumer(builder: (context, homeProvider, _) { final bool isUdUnit = configBox.get('isUdUnit') ?? false; return Icon( isUdUnit ? Icons.check_box : Icons.check_box_outline_blank, color: orangeC, - size: scaleSize(30), + size: scaleSize(27), ); }), ScaledSizedBox(width: 30), @@ -145,7 +141,7 @@ class SettingsScreen extends StatelessWidget { width: 55, child: Text( 'currencyNode'.tr(), - style: scaledTextStyle(fontSize: 16), + style: scaledTextStyle(fontSize: 15), ), ), const Spacer(), @@ -165,7 +161,7 @@ class SettingsScreen extends StatelessWidget { return DropdownButtonHideUnderline( key: keySelectDuniterNodeDropDown, child: DropdownButton( - style: scaledTextStyle(fontSize: 16, color: Colors.black), + style: scaledTextStyle(fontSize: 15, color: Colors.black), value: selectedDuniterEndpoint, icon: const Icon(Icons.keyboard_arrow_down), items: duniterBootstrapNodes @@ -233,7 +229,7 @@ class SettingsScreen extends StatelessWidget { key: keyCustomDuniterEndpoint, controller: endpointController, autocorrect: false, - style: scaledTextStyle(fontSize: 16), + style: scaledTextStyle(fontSize: 15), ), ), ); @@ -297,7 +293,7 @@ class SettingsScreen extends StatelessWidget { ScaledSizedBox(width: 5), ScaledSizedBox( width: 55, - child: Text('Indexer', style: scaledTextStyle(fontSize: 16)), + child: Text('Indexer', style: scaledTextStyle(fontSize: 15)), ), const Spacer(), Icon(indexerEndpoint != '' ? Icons.check : Icons.close), @@ -307,7 +303,7 @@ class SettingsScreen extends StatelessWidget { child: Consumer(builder: (context, set, _) { return DropdownButtonHideUnderline( child: DropdownButton( - style: scaledTextStyle(fontSize: 16, color: Colors.black), + style: scaledTextStyle(fontSize: 15, color: Colors.black), value: selectedIndexerEndpoint, icon: const Icon(Icons.keyboard_arrow_down), items: @@ -370,7 +366,7 @@ class SettingsScreen extends StatelessWidget { child: TextField( controller: indexerEndpointController, autocorrect: false, - style: scaledTextStyle(fontSize: 16), + style: scaledTextStyle(fontSize: 15), ), ), ); diff --git a/lib/screens/template_screen.dart b/lib/screens/template_screen.dart index b00aac4..26300f7 100644 --- a/lib/screens/template_screen.dart +++ b/lib/screens/template_screen.dart @@ -1,6 +1,7 @@ import 'package:gecko/globals.dart'; import 'package:flutter/material.dart'; import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/widgets/commons/top_appbar.dart'; class TemplateScreen extends StatelessWidget { const TemplateScreen({Key? key}) : super(key: key); @@ -11,13 +12,12 @@ class TemplateScreen extends StatelessWidget { return Scaffold( backgroundColor: backgroundColor, - appBar: AppBar( - toolbarHeight: scaleSize(57), title: const Text('Template screen')), - body: const SafeArea( + appBar: const GeckoAppBar('Template screen'), + body: SafeArea( child: Column(children: [ - SizedBox(height: 20), - Text('data'), - SizedBox(height: 20), + ScaledSizedBox(height: 20), + const Text('data'), + ScaledSizedBox(height: 20), ]), )); } diff --git a/lib/screens/transaction_in_progress.dart b/lib/screens/transaction_in_progress.dart index aa77a72..4a4ce88 100644 --- a/lib/screens/transaction_in_progress.dart +++ b/lib/screens/transaction_in_progress.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; import 'package:gecko/globals.dart'; import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/models/transaction_content.dart'; import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/wallets_profiles.dart'; +import 'package:gecko/widgets/transaction_status.dart'; +import 'package:gecko/widgets/transaction_status_icon.dart'; import 'package:provider/provider.dart'; import 'package:easy_localization/easy_localization.dart'; -class TransactionInProgress extends StatelessWidget { +class TransactionInProgress extends StatefulWidget { final String transactionId; final String transType; final String? fromAddress, toAddress, toUsername; @@ -22,56 +25,69 @@ class TransactionInProgress extends StatelessWidget { this.toUsername, }) : super(key: key); + @override + State createState() => _TransactionInProgressState(); +} + +class _TransactionInProgressState extends State { + String resultText = ''; + late String fromAddressFormat; + late String toAddressFormat; + late String toUsernameFormat; + late String amount; + late bool isUdUnit; + late TransactionContent txContent; + + @override + void initState() { + final walletProfiles = + Provider.of(homeContext, listen: false); + final myWalletProvider = + Provider.of(homeContext, listen: false); + + String defaultWalletAddress = myWalletProvider.getDefaultWallet().address; + String defaultWalletName = myWalletProvider.getDefaultWallet().name!; + String? walletDataName = + myWalletProvider.getWalletDataByAddress(widget.toAddress ?? '')?.name; + + fromAddressFormat = widget.fromAddress ?? + g1WalletsBox.get(defaultWalletAddress)?.username ?? + defaultWalletName; + toAddressFormat = widget.toAddress ?? walletProfiles.address; + toUsernameFormat = + widget.toUsername ?? walletDataName ?? getShortPubkey(toAddressFormat); + + amount = walletProfiles.payAmount.text; + isUdUnit = configBox.get('isUdUnit') ?? false; + super.initState(); + } + @override Widget build(BuildContext context) { final sub = Provider.of(context, listen: true); - final transactionDetails = TransactionDetails( - transactionId: transactionId, - fromAddress: fromAddress, - toAddress: toAddress, - toUsername: toUsername, - sub: sub, - transType: transType, - ); - - Widget getTransactionStatusIcon(TransactionDetails details) { - switch (details.txStatus) { - case TransactionStatus.loading: - return ScaledSizedBox( - height: 17, - width: 17, - child: const CircularProgressIndicator( - color: orangeC, - strokeWidth: 2, - ), - ); - case TransactionStatus.success: - return Icon( - Icons.done_all, - size: scaleSize(32), - color: Colors.greenAccent, - ); - case TransactionStatus.failed: - return Icon( - Icons.close, - size: scaleSize(32), - color: Colors.redAccent, - ); - case TransactionStatus.none: - default: - return const SizedBox.shrink(); - } + if (sub.transactionStatus.containsKey(widget.transactionId)) { + txContent = sub.transactionStatus[widget.transactionId]!; } - Widget buildTransactionStatus(TransactionDetails details) { + if (txContent.status == TransactionStatus.success) { + resultText = 'extrinsicValidated' + .tr(args: [actionMap[widget.transType] ?? 'strangeTransaction'.tr()]); + } else if (txContent.status == TransactionStatus.failed) { + resultText = errorTransactionMap[txContent.error] ?? txContent.error!; + } else { + resultText = statusStatusMap[txContent.status] ?? + 'Unknown status: ${txContent.status}'; + } + + Widget buildTransactionStatus() { return Column( children: [ - getTransactionStatusIcon(details), + TransactionStatusIcon(txContent.status), ScaledSizedBox(height: 7), - if (details.txStatus != TransactionStatus.none) + if (txContent.status != TransactionStatus.none) Text( - transactionDetails.resultText, + resultText, textAlign: TextAlign.center, style: scaledTextStyle(fontSize: 17), ) @@ -80,9 +96,6 @@ class TransactionInProgress extends StatelessWidget { } return PopScope( - onPopInvoked: (_) { - sub.resetTransactionStatus(); - }, child: Scaffold( backgroundColor: backgroundColor, appBar: AppBar( @@ -94,8 +107,7 @@ class TransactionInProgress extends StatelessWidget { children: [ Text( 'extrinsicInProgress'.tr(args: [ - transactionDetails.actionMap[transType] ?? - 'strangeTransaction'.tr() + actionMap[widget.transType] ?? 'strangeTransaction'.tr() ]), style: scaledTextStyle(fontSize: 20), ) @@ -117,30 +129,29 @@ class TransactionInProgress extends StatelessWidget { )), child: Column(children: [ ScaledSizedBox(height: 10), - if (transType == 'pay') + if (widget.transType == 'pay') Text( - transactionDetails.isUdUnit - ? 'ud'.tr(args: ['${transactionDetails.amount} ']) - : '${transactionDetails.amount} $currencyName', + isUdUnit + ? 'ud'.tr(args: ['$amount ']) + : '$amount $currencyName', textAlign: TextAlign.center, style: scaledTextStyle( fontSize: 17, fontWeight: FontWeight.w500), ), - if (transType == 'pay') ScaledSizedBox(height: 10), + if (widget.transType == 'pay') ScaledSizedBox(height: 10), Text( 'fromMinus'.tr(), textAlign: TextAlign.center, style: scaledTextStyle(fontSize: 16), ), Text( - transactionDetails.fromAddress!, + fromAddressFormat, textAlign: TextAlign.center, style: scaledTextStyle( fontSize: 17, fontWeight: FontWeight.w500), ), Visibility( - visible: transactionDetails.fromAddress != - transactionDetails.toAddress, + visible: fromAddressFormat != toAddressFormat, child: Column( children: [ ScaledSizedBox(height: 10), @@ -150,7 +161,7 @@ class TransactionInProgress extends StatelessWidget { style: scaledTextStyle(fontSize: 16), ), Text( - transactionDetails.toUsername!, + toUsernameFormat, textAlign: TextAlign.center, style: scaledTextStyle( fontSize: 17, fontWeight: FontWeight.w500), @@ -162,7 +173,7 @@ class TransactionInProgress extends StatelessWidget { ]), ), const Spacer(), - buildTransactionStatus(transactionDetails), + buildTransactionStatus(), const Spacer(), Expanded( child: Align( @@ -178,13 +189,12 @@ class TransactionInProgress extends StatelessWidget { backgroundColor: orangeC, ), onPressed: () { - sub.resetTransactionStatus(); Navigator.pop(context); }, child: Text( 'close'.tr(), style: scaledTextStyle( - fontSize: 20, fontWeight: FontWeight.w600), + fontSize: 19, fontWeight: FontWeight.w600), ), ), ), @@ -198,84 +208,3 @@ class TransactionInProgress extends StatelessWidget { ); } } - -enum TransactionStatus { loading, failed, success, none } - -class TransactionDetails { - String? fromAddress, toAddress, toUsername, amount; - bool isUdUnit = false; - String resultText = ''; - TransactionStatus txStatus = TransactionStatus.none; - - TransactionDetails({ - required transactionId, - required this.fromAddress, - required this.toAddress, - required this.toUsername, - required SubstrateSdk sub, - required String transType, - }) { - final walletProfiles = - Provider.of(homeContext, listen: false); - final myWalletProvider = - Provider.of(homeContext, listen: false); - String defaultWalletAddress = myWalletProvider.getDefaultWallet().address; - String defaultWalletName = myWalletProvider.getDefaultWallet().name!; - String? walletDataName = - myWalletProvider.getWalletDataByAddress(toAddress ?? '')?.name; - - fromAddress = fromAddress ?? - g1WalletsBox.get(defaultWalletAddress)?.username ?? - defaultWalletName; - toAddress = toAddress ?? walletProfiles.address; - toUsername = toUsername ?? walletDataName ?? getShortPubkey(toAddress!); - - amount = walletProfiles.payAmount.text; - isUdUnit = configBox.get('isUdUnit') ?? false; - - if (sub.transactionStatus.containsKey(transactionId)) { - calculateTransactionStatus( - sub.transactionStatus[transactionId], transType); - } - } - - void calculateTransactionStatus(String? result, String transType) { - if (result == null) { - txStatus = TransactionStatus.none; - } else if (result.contains('blockHash: ')) { - txStatus = TransactionStatus.success; - resultText = 'extrinsicValidated' - .tr(args: [actionMap[transType] ?? 'strangeTransaction'.tr()]); - } else if (result.contains('Exception: ')) { - txStatus = TransactionStatus.failed; - String exception = result.split('Exception: ')[1]; - resultText = resultMap[exception] ?? exception; - } else { - txStatus = TransactionStatus.loading; - resultText = resultMap[result] ?? 'Unknown status: $result'; - } - } - - Map actionMap = { - 'pay': 'transaction'.tr(), - 'cert': 'certification'.tr(), - 'comfirmIdty': 'identityConfirm'.tr(), - 'revokeIdty': 'revokeAdhesion'.tr(), - 'identityMigration': 'identityMigration'.tr(), - }; - - Map resultMap = { - 'sending': 'sending'.tr(), - 'Ready': 'propagating'.tr(), - 'Broadcast': 'validating'.tr(), - 'cert.NotRespectCertPeriod': '24hbetweenCerts'.tr(), - 'identity.CreatorNotAllowedToCreateIdty': '24hbetweenCerts'.tr(), - 'cert.CannotCertifySelf': 'canNotCertifySelf'.tr(), - 'identity.IdtyNameAlreadyExist': 'nameAlreadyExist'.tr(), - 'balances.KeepAlive': '2GDtoKeepAlive'.tr(), - '1010: Invalid Transaction: Inability to pay some fees , e.g. account balance too low': - 'youHaveToFeedThisAccountBeforeUsing'.tr(), - 'Token.FundsUnavailable': 'fundsUnavailable'.tr(), - 'Exception: timeout': 'execTimeoutOver'.tr(), - }; -} diff --git a/lib/widgets/bottom_app_bar.dart b/lib/widgets/bottom_app_bar.dart index b09eef2..7f8af23 100644 --- a/lib/widgets/bottom_app_bar.dart +++ b/lib/widgets/bottom_app_bar.dart @@ -104,16 +104,15 @@ class GeckoBottomAppBar extends StatelessWidget { }, ), ); - - if (myWalletProvider.pinCode == '') return; - Navigator.popUntil( - context, ModalRoute.withName('/')); - //FIXME: Should not have to wait 300 milliseconds when /mywallets exist in navigator... - sleep(const Duration(milliseconds: 300)); - Navigator.pushNamed(context, '/mywallets'); - // Navigator.pushNamedAndRemoveUntil( - // context, '/mywallets', ModalRoute.withName('/')); } + + if (myWalletProvider.pinCode == '') return; + Navigator.popUntil(context, ModalRoute.withName('/')); + //FIXME: Should not have to wait 300 milliseconds when /mywallets exist in navigator... + sleep(const Duration(milliseconds: 300)); + Navigator.pushNamed(context, '/mywallets'); + // Navigator.pushNamedAndRemoveUntil( + // context, '/mywallets', ModalRoute.withName('/')); }, ), ), diff --git a/lib/widgets/buttons/chest_options_buttons.dart b/lib/widgets/buttons/chest_options_buttons.dart index 4c31bdd..7c9a8c1 100644 --- a/lib/widgets/buttons/chest_options_buttons.dart +++ b/lib/widgets/buttons/chest_options_buttons.dart @@ -79,7 +79,7 @@ class ChestOptionsButtons extends StatelessWidget { ], ), ), - ScaledSizedBox(height: 20), + // ScaledSizedBox(height: 20), // InkWell( // key: keyChangeChest, // onTap: () { diff --git a/lib/widgets/cert_tile.dart b/lib/widgets/cert_tile.dart index 0e7d7ea..59ffbc2 100644 --- a/lib/widgets/cert_tile.dart +++ b/lib/widgets/cert_tile.dart @@ -3,8 +3,8 @@ import 'package:gecko/globals.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/substrate_sdk.dart'; -import 'package:gecko/providers/v2s_datapod.dart'; import 'package:gecko/screens/wallet_view.dart'; +import 'package:gecko/widgets/datapod_avatar.dart'; class CertTile extends StatelessWidget { const CertTile({ @@ -28,9 +28,8 @@ class CertTile extends StatelessWidget { key: keyTransaction(keyID++), contentPadding: EdgeInsets.only( left: 10, right: 0, top: scaleSize(3), bottom: scaleSize(3)), - leading: ClipOval( - child: V2sDatapodProvider().defaultAvatar(avatarSize), - ), + leading: DatapodAvatar( + address: repository['address'], size: avatarSize), title: Padding( padding: const EdgeInsets.only(bottom: 2), child: Text( diff --git a/lib/widgets/certs_list.dart b/lib/widgets/certs_list.dart index 3c06680..40cb4fd 100644 --- a/lib/widgets/certs_list.dart +++ b/lib/widgets/certs_list.dart @@ -4,8 +4,10 @@ import 'package:gecko/globals.dart'; import 'package:gecko/models/queries_indexer.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/widgets_keys.dart'; +import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/widgets/cert_tile.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:provider/provider.dart'; class CertsList extends StatelessWidget { const CertsList( @@ -18,14 +20,11 @@ class CertsList extends StatelessWidget { @override Widget build(BuildContext context) { + final indexerProvider = Provider.of(context, listen: false); final screenHeight = MediaQuery.of(context).size.height; final appBarHeight = AppBar().preferredSize.height; final windowHeight = screenHeight - appBarHeight - (isTall ? 170 : 140); - final httpLink = HttpLink( - '$indexerEndpoint/v1/graphql', - ); - late String gertCertsReq; late String certFrom; @@ -37,14 +36,8 @@ class CertsList extends StatelessWidget { certFrom = 'receiver'; } - final client = ValueNotifier( - GraphQLClient( - cache: GraphQLCache(store: HiveStore()), - link: httpLink, - ), - ); return GraphQLProvider( - client: client, + client: ValueNotifier(indexerProvider.indexerClient), child: Query( options: QueryOptions( document: gql(gertCertsReq), @@ -53,7 +46,7 @@ class CertsList extends StatelessWidget { }, ), builder: (QueryResult result, {fetchMore, refetch}) { - if (result.isLoading && result.data == null) { + if (result.isLoading || result.data == null) { return const Center( child: CircularProgressIndicator(), ); @@ -87,31 +80,39 @@ class CertsList extends StatelessWidget { final date = DateTime.parse(cert['created_at']); final dp = DateTime(date.year, date.month, date.day); final dateForm = '${dp.day}-${dp.month}-${dp.year}'; - listCerts.add({ - 'address': issuerAddress, - 'name': issuerName, - 'date': dateForm - }); + + // Check if we have a more recent certification, we skip + if (!listCerts.any((cert) => cert['address'] == issuerAddress)) { + listCerts.add({ + 'address': issuerAddress, + 'name': issuerName, + 'date': dateForm + }); + } } // Build history list return SizedBox( height: windowHeight, - child: ListView( - key: keyListTransactions, - children: [ - result.data == null - ? Column(children: [ - ScaledSizedBox(height: 50), - Text( - "noTransactionToDisplay".tr(), - style: scaledTextStyle(fontSize: 18), - ) - ]) - : Column(children: [ - CertTile(listCerts: listCerts), - ]) - ], + child: RefreshIndicator( + color: orangeC, + onRefresh: () async => refetch!.call(), + child: ListView( + key: keyListTransactions, + children: [ + result.data == null + ? Column(children: [ + ScaledSizedBox(height: 50), + Text( + "noTransactionToDisplay".tr(), + style: scaledTextStyle(fontSize: 18), + ) + ]) + : Column(children: [ + CertTile(listCerts: listCerts), + ]) + ], + ), ), ); }, diff --git a/lib/widgets/commons/common_elements.dart b/lib/widgets/commons/common_elements.dart index 53164a8..e25d807 100644 --- a/lib/widgets/commons/common_elements.dart +++ b/lib/widgets/commons/common_elements.dart @@ -112,7 +112,7 @@ Future confirmPopupCertification(BuildContext context, String question1, child: Text( "yes".tr(), style: scaledTextStyle( - fontSize: 20, + fontSize: 19, color: const Color(0xffD80000), ), ), @@ -124,7 +124,7 @@ Future confirmPopupCertification(BuildContext context, String question1, TextButton( child: Text( "no".tr(), - style: scaledTextStyle(fontSize: 20), + style: scaledTextStyle(fontSize: 19), ), onPressed: () { Navigator.pop(context, false); diff --git a/lib/widgets/commons/loading.dart b/lib/widgets/commons/loading.dart index b969606..a4354a3 100644 --- a/lib/widgets/commons/loading.dart +++ b/lib/widgets/commons/loading.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:gecko/globals.dart'; +import 'package:gecko/models/scale_functions.dart'; class Loading extends StatelessWidget { const Loading({ @@ -13,7 +14,7 @@ class Loading extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( + return ScaledSizedBox( height: size, width: size, child: CircularProgressIndicator( diff --git a/lib/widgets/contacts_list.dart b/lib/widgets/contacts_list.dart index baaafc5..1896371 100644 --- a/lib/widgets/contacts_list.dart +++ b/lib/widgets/contacts_list.dart @@ -48,7 +48,7 @@ class ContactsList extends StatelessWidget { contentPadding: const EdgeInsets.all(5), dense: !isTall, leading: DatapodAvatar( - address: g1Wallet.address, size: scaleSize(50)), + address: g1Wallet.address, size: 47), title: Row(children: [ Text(getShortPubkey(g1Wallet.address), style: scaledTextStyle( diff --git a/lib/widgets/datapod_avatar.dart b/lib/widgets/datapod_avatar.dart index a87e20b..7bf86fa 100644 --- a/lib/widgets/datapod_avatar.dart +++ b/lib/widgets/datapod_avatar.dart @@ -6,7 +6,6 @@ import 'package:gecko/globals.dart'; import 'package:gecko/models/queries_datapod.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/providers/v2s_datapod.dart'; -import 'package:gecko/widgets/commons/loading.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; @@ -24,27 +23,17 @@ class DatapodAvatar extends StatelessWidget { if (cachedImage.existsSync()) { return ScaledSizedBox( width: size, + height: size, child: ClipOval( - child: datapod.getAvatarLocal(address, size), + child: datapod.getAvatarLocal(address), ), ); } - final httpLink = HttpLink( - '$datapodEndpoint/v1/graphql', - ); - - final client = ValueNotifier( - GraphQLClient( - cache: GraphQLCache(store: HiveStore()), - link: httpLink, - ), - ); - return ScaledSizedBox( width: size, child: GraphQLProvider( - client: client, + client: ValueNotifier(datapod.datapodClient), child: Query( options: QueryOptions( document: gql(getAvatarQ), @@ -53,9 +42,9 @@ class DatapodAvatar extends StatelessWidget { }, ), builder: (QueryResult result, {fetchMore, refetch}) { - if (result.isLoading) { - return const Center( - child: Loading(), + if (result.isLoading || result.data == null) { + return Center( + child: ClipOval(child: datapod.defaultAvatar(size)), ); } final String? avatar64 = @@ -75,8 +64,7 @@ class DatapodAvatar extends StatelessWidget { return ClipOval( child: Image.memory( base64.decode(sanitizedAvatar64), - height: size, - fit: BoxFit.fitWidth, + fit: BoxFit.cover, ), ); }), diff --git a/lib/widgets/header_profile.dart b/lib/widgets/header_profile.dart index 48fa47d..7c532ab 100644 --- a/lib/widgets/header_profile.dart +++ b/lib/widgets/header_profile.dart @@ -28,7 +28,7 @@ class HeaderProfile extends StatelessWidget { @override Widget build(BuildContext context) { - final avatarSize = scaleSize(110); + const double avatarSize = 110; final duniterIndexer = Provider.of(context, listen: false); final walletOptions = Provider.of(context, listen: false); @@ -79,30 +79,37 @@ class HeaderProfile extends StatelessWidget { ]), ScaledSizedBox(height: 15), Balance(address: address, size: 20), - ScaledSizedBox(height: 5), - InkWell( - onTap: () => sub.certsCounterCache[address] != null - ? { - Navigator.push( - context, - PageNoTransit(builder: (context) { - return CertificationsScreen( - address: address, - username: duniterIndexer - .walletNameIndexer[address] ?? - ''); - }), - ), - } - : null, + ScaledSizedBox( + height: 60, child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - IdentityStatus( - address: address, - isOwner: false, - color: Colors.black), - Certifications(address: address, size: 18) + ScaledSizedBox(height: 5), + InkWell( + onTap: () => sub.certsCounterCache[address] != null + ? { + Navigator.push( + context, + PageNoTransit(builder: (context) { + return CertificationsScreen( + address: address, + username: duniterIndexer + .walletNameIndexer[address] ?? + ''); + }), + ), + } + : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IdentityStatus( + address: address, + isOwner: false, + color: Colors.black), + Certifications(address: address, size: 18) + ], + ), + ), ], ), ), diff --git a/lib/widgets/history_query.dart b/lib/widgets/history_query.dart index e13adb0..2b0812c 100644 --- a/lib/widgets/history_query.dart +++ b/lib/widgets/history_query.dart @@ -5,7 +5,9 @@ import 'package:gecko/models/queries_indexer.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/duniter_indexer.dart'; +import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/widgets/history_view.dart'; +import 'package:gecko/widgets/transaction_in_progress_tile.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; @@ -16,11 +18,11 @@ class HistoryQuery extends StatelessWidget { @override Widget build(BuildContext context) { final duniterIndexer = Provider.of(context, listen: false); + final sub = Provider.of(context, listen: false); final ScrollController scrollController = ScrollController(); FetchMoreOptions? opts; - int nPage = 1; int nRepositories = 20; if (indexerEndpoint == '') { @@ -34,19 +36,8 @@ class HistoryQuery extends StatelessWidget { ]); } - final httpLink = HttpLink( - '$indexerEndpoint/v1beta1/relay', - ); - - final client = ValueNotifier( - GraphQLClient( - cache: GraphQLCache(), - link: httpLink, - ), - ); - return GraphQLProvider( - client: client, + client: ValueNotifier(duniterIndexer.indexerClient), child: Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -57,11 +48,12 @@ class HistoryQuery extends StatelessWidget { document: gql(getHistoryByAddressQ), variables: { 'address': address, - 'number': 20, - 'cursor': null + 'number': nRepositories, + 'offset': 0 }, ), builder: (QueryResult result, {fetchMore, refetch}) { + duniterIndexer.refetch = refetch; if (result.isLoading && result.data == null) { return const Center( child: CircularProgressIndicator( @@ -69,6 +61,7 @@ class HistoryQuery extends StatelessWidget { ), ); } + final List transactions = result.data?["transaction"]; if (result.hasException) { log.e('Error Indexer: ${result.exception}'); @@ -80,8 +73,7 @@ class HistoryQuery extends StatelessWidget { style: scaledTextStyle(fontSize: 18), ) ]); - } else if (result - .data?['transaction_connection']?['edges'].isEmpty) { + } else if (transactions.isEmpty) { return Column(children: [ ScaledSizedBox(height: 50), Text( @@ -91,47 +83,57 @@ class HistoryQuery extends StatelessWidget { ]); } - if (result.isNotLoading) { - if (duniterIndexer.fetchMoreCursor == null) nPage = 1; + final int totalTransactions = + result.data!["transaction_aggregate"]["aggregate"]["count"]; + duniterIndexer.hasNextPage = + !(transactions.length == totalTransactions); - if (nPage <= 3) { - nRepositories = 20; - } else if (nPage <= 6) { - nRepositories = 40; - } else if (nPage <= 12) { - nRepositories = 80; - } else { - nRepositories = 120; + opts = duniterIndexer.mergeQueryResult( + transactions: transactions, + opts: opts, + address: address, + nRepositories: nRepositories, + offset: transactions.length, + ); + + // Get transaction in progress if exist + String? transactionId; + for (final entry in sub.transactionStatus.entries) { + if (entry.value.from == address) { + transactionId = entry.key; + break; } - nPage++; - opts = duniterIndexer.mergeQueryResult( - result, opts, address, nRepositories); } // Build history list return NotificationListener( child: Builder( builder: (context) => Expanded( - child: ListView( - key: keyListTransactions, - controller: scrollController, - children: [ - HistoryView( - result: result, - address: address, - ) - ], + child: RefreshIndicator( + color: orangeC, + onRefresh: () async => refetch!.call(), + child: ListView( + key: keyListTransactions, + controller: scrollController, + children: [ + if (transactionId != null) + TransactionInProgressTule( + address: address, + transactionId: transactionId), + HistoryView( + result: result, + address: address, + ) + ], + ), ), ), ), onNotification: (dynamic t) { - if (duniterIndexer.pageInfo == null) { - duniterIndexer.reload(); - } if (t is ScrollEndNotification && scrollController.position.pixels >= scrollController.position.maxScrollExtent * 0.7 && - duniterIndexer.pageInfo!['hasNextPage'] && + duniterIndexer.hasNextPage && result.isNotLoading) { fetchMore!(opts!); } diff --git a/lib/widgets/history_view.dart b/lib/widgets/history_view.dart index 7b8fb6c..6483e34 100644 --- a/lib/widgets/history_view.dart +++ b/lib/widgets/history_view.dart @@ -5,6 +5,7 @@ import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/screens/wallet_view.dart'; +import 'package:gecko/widgets/commons/loading.dart'; import 'package:gecko/widgets/page_route_no_transition.dart'; import 'package:gecko/widgets/transaction_tile.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; @@ -34,13 +35,14 @@ class HistoryView extends StatelessWidget { ScaledSizedBox(height: 50), Text( "noTransactionToDisplay".tr(), - style: scaledTextStyle(fontSize: 18), + style: scaledTextStyle(fontSize: 17), ) ]) : Column(children: [ Column( children: duniterIndexer.transBC!.map((repository) { - final answer = computeHistoryView(repository, address); + final answer = + duniterIndexer.computeHistoryView(repository, address); pastDelimiters.add(answer['dateDelimiter']); bool isMigrationTime = false; @@ -52,23 +54,24 @@ class HistoryView extends StatelessWidget { return Column(children: [ if (isMigrationTime) Padding( - padding: EdgeInsets.symmetric(vertical: scaleSize(23)), + padding: EdgeInsets.only( + top: scaleSize(25), bottom: scaleSize(15)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Image( image: const AssetImage('assets/party.png'), - height: scaleSize(32)), + height: scaleSize(31)), Text( 'blockchainStart'.tr(), style: scaledTextStyle( fontSize: 20, color: Colors.blueAccent, - fontWeight: FontWeight.w500), + fontWeight: FontWeight.w400), ), Image( image: const AssetImage('assets/party.png'), - height: scaleSize(32)), + height: scaleSize(31)), ], ), ), @@ -96,14 +99,14 @@ class HistoryView extends StatelessWidget { context: context), ]); }).toList()), - if (result.isLoading && duniterIndexer.pageInfo!['hasPreviousPage']) + if (result.isLoading && duniterIndexer.hasNextPage) const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - CircularProgressIndicator(), + Loading(size: 30, stroke: 3), ], ), - if (!duniterIndexer.pageInfo!['hasNextPage'] && + if (!duniterIndexer.hasNextPage && sub.oldOwnerKeys[address]?[0] != null) Padding( padding: const EdgeInsets.symmetric(vertical: 30), @@ -127,7 +130,7 @@ class HistoryView extends StatelessWidget { ), Column(children: [ Text( - 'Identité migré:'.tr(), + 'identityMigrated'.tr(), style: scaledTextStyle( fontSize: 20, color: Colors.green[700], @@ -149,14 +152,22 @@ class HistoryView extends StatelessWidget { ), ), ), - if (!duniterIndexer.pageInfo!['hasNextPage']) + if (!duniterIndexer.hasNextPage) Column( children: [ ScaledSizedBox(height: 15), - Text("historyStart".tr(), - textAlign: TextAlign.center, - style: scaledTextStyle(fontSize: 20)), - ScaledSizedBox(height: 15) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon(Icons.blur_on_outlined, size: scaleSize(31)), + Text("historyStart".tr(), + textAlign: TextAlign.center, + style: scaledTextStyle( + fontSize: 20, fontWeight: FontWeight.w300)), + Icon(Icons.blur_on_outlined, size: scaleSize(31)), + ], + ), + ScaledSizedBox(height: 30) ], ) ]); diff --git a/lib/widgets/idty_status.dart b/lib/widgets/idty_status.dart index 65dd65c..48e42d2 100644 --- a/lib/widgets/idty_status.dart +++ b/lib/widgets/idty_status.dart @@ -5,6 +5,7 @@ import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/widgets/commons/animated_text.dart'; +import 'package:gecko/widgets/commons/loading.dart'; import 'package:gecko/widgets/name_by_address.dart'; import 'package:provider/provider.dart'; @@ -28,6 +29,13 @@ class IdentityStatus extends StatelessWidget { future: sub.idtyStatus([address]), initialData: [walletData.identityStatus], builder: (context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Loading(size: 18); + } else if (snapshot.hasError || snapshot.data == null) { + log.e(snapshot.error); + return const Icon(Icons.close, color: Colors.red); + } + final resStatus = snapshot.data!.first; walletData.identityStatus = resStatus; walletBox.put(address, walletData); @@ -51,7 +59,7 @@ class IdentityStatus extends StatelessWidget { } final Map statusText = { - IdtyStatus.none: 'noIdentity'.tr(), + IdtyStatus.none: '', IdtyStatus.created: 'identityCreated'.tr(), IdtyStatus.confirmed: 'identityConfirmed'.tr(), IdtyStatus.validated: 'memberValidated'.tr(), diff --git a/lib/widgets/name_by_address.dart b/lib/widgets/name_by_address.dart index 763c225..2817fe9 100644 --- a/lib/widgets/name_by_address.dart +++ b/lib/widgets/name_by_address.dart @@ -6,6 +6,7 @@ import 'package:gecko/models/queries_indexer.dart'; import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/wallet_data.dart'; import 'package:gecko/providers/duniter_indexer.dart'; +import 'package:gecko/widgets/commons/loading.dart'; import 'package:gecko/widgets/wallet_name.dart'; import 'package:graphql_flutter/graphql_flutter.dart'; import 'package:provider/provider.dart'; @@ -34,22 +35,8 @@ class NameByAddress extends StatelessWidget { return WalletName(wallet: wallet, size: size, color: color); } - // if (g1WalletsBox.get(wallet.address)?.username != null) { - // return Text(g1WalletsBox.get(wallet.address)!.username!); - // } - - final httpLink = HttpLink( - '$indexerEndpoint/v1/graphql', - ); - - final client = ValueNotifier( - GraphQLClient( - cache: GraphQLCache(store: HiveStore()), - link: httpLink, - ), - ); return GraphQLProvider( - client: client, + client: ValueNotifier(duniterIndexer.indexerClient), child: Query( options: QueryOptions( document: gql(getNameByAddressQ), @@ -66,7 +53,7 @@ class NameByAddress extends StatelessWidget { } if (result.isLoading) { - return const Text('Loading'); + return const Loading(); } duniterIndexer.walletNameIndexer[wallet.address] = diff --git a/lib/widgets/payment_popup.dart b/lib/widgets/payment_popup.dart index 3945cac..268d8a9 100644 --- a/lib/widgets/payment_popup.dart +++ b/lib/widgets/payment_popup.dart @@ -11,6 +11,7 @@ import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/wallet_options.dart'; import 'package:gecko/providers/wallets_profiles.dart'; +import 'package:gecko/screens/activity.dart'; import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; import 'package:gecko/widgets/balance.dart'; @@ -54,13 +55,17 @@ void paymentPopup(BuildContext context, String toAddress, String? username) { destAddress: toAddress, amount: double.parse(walletViewProvider.payAmount.text), password: myWalletProvider.pinCode); + + const legacyMonitor = false; Navigator.push( context, MaterialPageRoute(builder: (context) { - return TransactionInProgress( - transactionId: transactionId, - toAddress: toAddress, - toUsername: username); + return legacyMonitor + ? TransactionInProgress( + transactionId: transactionId, + toAddress: toAddress, + toUsername: username) + : ActivityScreen(address: acc.address!); }), ); } @@ -131,7 +136,7 @@ void paymentPopup(BuildContext context, String toAddress, String? username) { Text( 'executeATransfer'.tr(), style: scaledTextStyle( - fontSize: 20, fontWeight: FontWeight.w700), + fontSize: 19, fontWeight: FontWeight.w700), ), IconButton( key: keyPopButton, diff --git a/lib/widgets/search_identity_query.dart b/lib/widgets/search_identity_query.dart index 9403cac..1d912cf 100644 --- a/lib/widgets/search_identity_query.dart +++ b/lib/widgets/search_identity_query.dart @@ -29,19 +29,8 @@ class SearchIdentityQuery extends StatelessWidget { return Text('noResult'.tr()); } - final httpLink = HttpLink( - '$indexerEndpoint/v1/graphql', - ); - - final client = ValueNotifier( - GraphQLClient( - cache: GraphQLCache( - store: HiveStore()), // GraphQLCache(store: HiveStore()) - link: httpLink, - ), - ); return GraphQLProvider( - client: client, + client: ValueNotifier(duniterIndexer.indexerClient), child: Query( options: QueryOptions( document: gql(searchAddressByNameQ), @@ -74,7 +63,7 @@ class SearchIdentityQuery extends StatelessWidget { searchProvider.resultLenght = identities.length; - final avatarSize = scaleSize(45); + const double avatarSize = 45; return Expanded( child: ListView(children: [ for (Map profile in identities) diff --git a/lib/widgets/transaction_in_progress_tile.dart b/lib/widgets/transaction_in_progress_tile.dart new file mode 100644 index 0000000..4331f84 --- /dev/null +++ b/lib/widgets/transaction_in_progress_tile.dart @@ -0,0 +1,160 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gecko/globals.dart'; +import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/models/transaction_content.dart'; +import 'package:gecko/providers/duniter_indexer.dart'; +import 'package:gecko/providers/substrate_sdk.dart'; +import 'package:gecko/widgets/datapod_avatar.dart'; +import 'package:gecko/widgets/transaction_status.dart'; +import 'package:gecko/widgets/transaction_status_icon.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; +import 'package:provider/provider.dart'; +import 'package:fade_and_translate/fade_and_translate.dart'; + +class TransactionInProgressTule extends StatefulWidget { + const TransactionInProgressTule( + {Key? key, required this.address, this.transactionId}) + : super(key: key); + + final String address; + final String? transactionId; + + @override + State createState() => + _TransactionInProgressTuleState(); +} + +class _TransactionInProgressTuleState extends State { + late bool isVisible; + late TransactionContent txContent; + @override + void initState() { + isVisible = true; + StreamSubscription? subscription; + final sub = Provider.of(context, listen: false); + final duniterIndexer = Provider.of(context, listen: false); + final stream = duniterIndexer.subscribeHistoryIssued(widget.address); + txContent = sub.transactionStatus[widget.transactionId]!; + + subscription = stream.listen((result) { + if (result.hasException) { + log.e(result.exception); + isVisible = true; + } else { + final Map transData = + result.data?['account_by_pk']['transactions_issued'].first; + final String receiver = transData['receiver_pubkey']; + final double amount = transData['amount'] / 100; + final createdAt = DateTime.parse(transData['created_at']); + final difference = createdAt.difference(DateTime.now()); + + if (receiver == txContent.to && + amount == txContent.amount && + difference.inSeconds.abs() < 30) { + isVisible = false; + txContent.status = TransactionStatus.finalized; + sub.reload(); + subscription?.cancel(); + } else { + isVisible = true; + } + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final duniterIndexer = Provider.of(context, listen: false); + return Consumer(builder: (context, sub, _) { + final statusIcon = + TransactionStatusIcon(txContent.status, size: 21, stroke: 2); + String humanStatus = ''; + final finalAmount = txContent.amount * -1; + + if (txContent.status == TransactionStatus.success) { + humanStatus = 'extrinsicValidated'.tr(args: [actionMap['pay']!]); + } else if (txContent.status == TransactionStatus.failed) { + humanStatus = errorTransactionMap[txContent.error] ?? txContent.error!; + } else { + humanStatus = statusStatusMap[txContent.status] ?? + 'Unknown status: ${txContent.status}'; + } + + return FadeAndTranslate( + visible: isVisible, + translate: const Offset(0, -40), + delay: const Duration(seconds: 2), + duration: const Duration(milliseconds: 700), + onCompleted: () async => duniterIndexer.refetch?.call(), + child: Padding( + padding: const EdgeInsets.all(8), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: orangeC, + width: 2, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 10), + Text( + 'Transaction en cours', + style: scaledTextStyle( + fontSize: 20, + color: Colors.blueAccent, + fontWeight: FontWeight.w400), + ), + ListTile( + key: const Key('transactionInProgress'), + contentPadding: + const EdgeInsets.symmetric(horizontal: 5, vertical: 15), + leading: DatapodAvatar(address: txContent.to, size: 50), + title: Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Text(getShortPubkey(txContent.to), + style: scaledTextStyle( + fontSize: 17, fontFamily: 'Monospace')), + ), + subtitle: Row( + children: [ + statusIcon, + ScaledSizedBox(width: 10), + ScaledSizedBox( + width: 160, + child: Text( + humanStatus, + style: scaledTextStyle( + fontStyle: FontStyle.italic, + color: Theme.of(context) + .textTheme + .titleLarge! + .color, + fontSize: 14), + ), + ), + ], + ), + trailing: Text("$finalAmount $currencyName", + style: scaledTextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.blue[700]), + textAlign: TextAlign.justify), + dense: !isTall, + isThreeLine: false), + ], + ), + ), + ), + ); + }); + } +} diff --git a/lib/widgets/transaction_status.dart b/lib/widgets/transaction_status.dart new file mode 100644 index 0000000..f5e6a78 --- /dev/null +++ b/lib/widgets/transaction_status.dart @@ -0,0 +1,42 @@ +import 'package:easy_localization/easy_localization.dart'; + +enum TransactionStatus { + sending, + propagation, + validating, + failed, + success, + timeout, + finalized, + none +} + +Map actionMap = { + 'pay': 'transaction'.tr(), + 'cert': 'certification'.tr(), + 'comfirmIdty': 'identityConfirm'.tr(), + 'revokeIdty': 'revokeAdhesion'.tr(), + 'identityMigration': 'identityMigration'.tr(), +}; + +Map statusStatusMap = { + TransactionStatus.none: 'noTransaction'.tr(), + TransactionStatus.sending: 'sending'.tr(), + TransactionStatus.propagation: 'propagating'.tr(), + TransactionStatus.validating: 'validating'.tr(), + TransactionStatus.success: 'extrinsicValidated'.tr(args: [actionMap['pay']!]), + TransactionStatus.finalized: + 'extrinsicFinalized'.tr(args: [actionMap['pay']!]), + TransactionStatus.timeout: 'execTimeoutOver'.tr(), +}; + +Map errorTransactionMap = { + 'cert.NotRespectCertPeriod': '24hbetweenCerts'.tr(), + 'identity.CreatorNotAllowedToCreateIdty': '24hbetweenCerts'.tr(), + 'cert.CannotCertifySelf': 'canNotCertifySelf'.tr(), + 'identity.IdtyNameAlreadyExist': 'nameAlreadyExist'.tr(), + 'balances.KeepAlive': '2GDtoKeepAlive'.tr(), + '1010: Invalid Transaction: Inability to pay some fees , e.g. account balance too low': + 'youHaveToFeedThisAccountBeforeUsing'.tr(), + 'Token.FundsUnavailable': 'fundsUnavailable'.tr(), +}; \ No newline at end of file diff --git a/lib/widgets/transaction_status_icon.dart b/lib/widgets/transaction_status_icon.dart new file mode 100644 index 0000000..86fa69b --- /dev/null +++ b/lib/widgets/transaction_status_icon.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:gecko/models/scale_functions.dart'; +import 'package:gecko/widgets/commons/loading.dart'; +import 'package:gecko/widgets/transaction_status.dart'; + +class TransactionStatusIcon extends StatelessWidget { + const TransactionStatusIcon(this.status, + {Key? key, this.size = 32, this.stroke = 3}) + : super(key: key); + final TransactionStatus status; + final double size; + final double stroke; + + @override + Widget build(BuildContext context) { + switch (status) { + case TransactionStatus.sending: + case TransactionStatus.propagation: + case TransactionStatus.validating: + return Loading(size: size, stroke: stroke); + case TransactionStatus.success: + return Icon( + Icons.done, + size: scaleSize(size), + color: Colors.green, + ); + case TransactionStatus.finalized: + return Icon( + Icons.done_all, + size: scaleSize(size), + color: Colors.green, + ); + case TransactionStatus.failed: + case TransactionStatus.timeout: + return Icon( + Icons.close, + size: scaleSize(size), + color: Colors.red, + ); + case TransactionStatus.none: + default: + return ScaledSizedBox(height: size, width: size); + } + } +} diff --git a/lib/widgets/transaction_tile.dart b/lib/widgets/transaction_tile.dart index 2c1ae91..3e82cdc 100644 --- a/lib/widgets/transaction_tile.dart +++ b/lib/widgets/transaction_tile.dart @@ -4,8 +4,8 @@ import 'package:gecko/models/scale_functions.dart'; import 'package:gecko/models/widgets_keys.dart'; import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/substrate_sdk.dart'; -import 'package:gecko/providers/v2s_datapod.dart'; import 'package:gecko/screens/wallet_view.dart'; +import 'package:gecko/widgets/datapod_avatar.dart'; import 'package:gecko/widgets/page_route_no_transition.dart'; class TransactionTile extends StatelessWidget { @@ -39,9 +39,7 @@ class TransactionTile extends StatelessWidget { key: keyTransaction(newKey), contentPadding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5), - leading: ClipOval( - child: V2sDatapodProvider().defaultAvatar(avatarSize), - ), + leading: DatapodAvatar(address: repository[1], size: avatarSize), title: Padding( padding: const EdgeInsets.only(bottom: 5), child: Text(getShortPubkey(repository[1]), diff --git a/pubspec.lock b/pubspec.lock index 8c5434f..1282d95 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,6 +417,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.1+3" + fade_and_translate: + dependency: "direct main" + description: + name: fade_and_translate + sha256: d5ebb7279e00cbef6da9391e15976f0348d3deceb2aee0ed172c23a736fa5dd4 + url: "https://pub.dev" + source: hosted + version: "0.1.3" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9ed91ae..7479318 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: Pay with G1. # pub.dev using `pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev -version: 0.1.3+63 +version: 0.1.4+66 environment: sdk: ">=2.12.0 <3.0.0" @@ -59,6 +59,7 @@ dependencies: crypto: ^3.0.3 screen_brightness: ^0.2.2+1 uuid: ^3.0.7 + fade_and_translate: ^0.1.3 dev_dependencies: # flutter_launcher_icons: ^0.9.2