diff --git a/assets/translations/en.json b/assets/translations/en.json index ea0a3febd5d2515d8556b8f1d689fff69a750e3c..bb857819061572553a5ad1b4e503adcd164764f9 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -179,5 +179,8 @@ "feedback_draw": "Draw", "feedback_navigate": "Navigate", "bug_report": "Bug Report", - "slidable_tutorial": "Swipe for more actions" + "slidable_tutorial": "Swipe for more actions", + "retry_payment": "Retry Payment", + "cancel_payment": "Cancle Payment", + "payment_canceled": "Payment canceled although we cannot ensure that it has not already been executed" } diff --git a/assets/translations/es.json b/assets/translations/es.json index 2d46a4ecfd996b58482b343af89a1e630107e215..1fd16e2f9f2d2f8cdfb2b4bf8243f89811b6a28a 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -180,5 +180,8 @@ "feedback_draw": "Dibuja", "feedback_navigate": "Navegar", "bug_report": "Reportar un error", - "slidable_tutorial": "Desliza para más acciones" + "slidable_tutorial": "Desliza para más acciones", + "retry_payment": "Reintentar pago", + "cancel_payment": "Cancelar pago", + "payment_canceled": "Pago cancelado aunque no podemos asegurar que no se haya hecho ejecutado ya" } diff --git a/lib/data/models/pending_transaction.dart b/lib/data/models/pending_transaction.dart deleted file mode 100644 index 648cef9a778656ce6595399c1e9ce1543d4263ec..0000000000000000000000000000000000000000 --- a/lib/data/models/pending_transaction.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - -import 'contact.dart'; - -part 'pending_transaction.g.dart'; - -@JsonSerializable() -@CopyWith() -class PendingTransaction extends Equatable { - const PendingTransaction({ - required this.amount, - required this.comment, - required this.time, - required this.from, - required this.to, - }); - - factory PendingTransaction.fromJson(Map<String, dynamic> json) => - _$PendingTransactionFromJson(json); - - final Contact from; - final Contact to; - final double amount; - final String comment; - final DateTime time; - - Map<String, dynamic> toJson() => _$PendingTransactionToJson(this); - - @override - List<Object?> get props => <dynamic>[from, to, amount, comment, time]; -} diff --git a/lib/data/models/pending_transaction.g.dart b/lib/data/models/pending_transaction.g.dart deleted file mode 100644 index 56b161072bf6fb70451a456cde2c4542d70f9811..0000000000000000000000000000000000000000 --- a/lib/data/models/pending_transaction.g.dart +++ /dev/null @@ -1,123 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'pending_transaction.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$PendingTransactionCWProxy { - PendingTransaction amount(double amount); - - PendingTransaction comment(String comment); - - PendingTransaction time(DateTime time); - - PendingTransaction from(Contact from); - - PendingTransaction to(Contact to); - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PendingTransaction(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// PendingTransaction(...).copyWith(id: 12, name: "My name") - /// ```` - PendingTransaction call({ - double? amount, - String? comment, - DateTime? time, - Contact? from, - Contact? to, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfPendingTransaction.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfPendingTransaction.copyWith.fieldName(...)` -class _$PendingTransactionCWProxyImpl implements _$PendingTransactionCWProxy { - const _$PendingTransactionCWProxyImpl(this._value); - - final PendingTransaction _value; - - @override - PendingTransaction amount(double amount) => this(amount: amount); - - @override - PendingTransaction comment(String comment) => this(comment: comment); - - @override - PendingTransaction time(DateTime time) => this(time: time); - - @override - PendingTransaction from(Contact from) => this(from: from); - - @override - PendingTransaction to(Contact to) => this(to: to); - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `PendingTransaction(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// PendingTransaction(...).copyWith(id: 12, name: "My name") - /// ```` - PendingTransaction call({ - Object? amount = const $CopyWithPlaceholder(), - Object? comment = const $CopyWithPlaceholder(), - Object? time = const $CopyWithPlaceholder(), - Object? from = const $CopyWithPlaceholder(), - Object? to = const $CopyWithPlaceholder(), - }) { - return PendingTransaction( - amount: amount == const $CopyWithPlaceholder() || amount == null - ? _value.amount - // ignore: cast_nullable_to_non_nullable - : amount as double, - comment: comment == const $CopyWithPlaceholder() || comment == null - ? _value.comment - // ignore: cast_nullable_to_non_nullable - : comment as String, - time: time == const $CopyWithPlaceholder() || time == null - ? _value.time - // ignore: cast_nullable_to_non_nullable - : time as DateTime, - from: from == const $CopyWithPlaceholder() || from == null - ? _value.from - // ignore: cast_nullable_to_non_nullable - : from as Contact, - to: to == const $CopyWithPlaceholder() || to == null - ? _value.to - // ignore: cast_nullable_to_non_nullable - : to as Contact, - ); - } -} - -extension $PendingTransactionCopyWith on PendingTransaction { - /// Returns a callable class that can be used as follows: `instanceOfPendingTransaction.copyWith(...)` or like so:`instanceOfPendingTransaction.copyWith.fieldName(...)`. - // ignore: library_private_types_in_public_api - _$PendingTransactionCWProxy get copyWith => - _$PendingTransactionCWProxyImpl(this); -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PendingTransaction _$PendingTransactionFromJson(Map<String, dynamic> json) => - PendingTransaction( - amount: (json['amount'] as num).toDouble(), - comment: json['comment'] as String, - time: DateTime.parse(json['time'] as String), - from: Contact.fromJson(json['from'] as Map<String, dynamic>), - to: Contact.fromJson(json['to'] as Map<String, dynamic>), - ); - -Map<String, dynamic> _$PendingTransactionToJson(PendingTransaction instance) => - <String, dynamic>{ - 'from': instance.from, - 'to': instance.to, - 'amount': instance.amount, - 'comment': instance.comment, - 'time': instance.time.toIso8601String(), - }; diff --git a/lib/data/models/transaction.dart b/lib/data/models/transaction.dart index d55881fdd33f75c20fb0bbfee2489c9ad60fd2ca..2bd007839635ba92d37fe4cfe7852603044ba971 100644 --- a/lib/data/models/transaction.dart +++ b/lib/data/models/transaction.dart @@ -30,7 +30,9 @@ class Transaction extends Equatable { final DateTime time; bool get isOutgoing => - type == TransactionType.sending || type == TransactionType.sent; + type == TransactionType.sending || + type == TransactionType.sent || + type == TransactionType.pending; bool get isIncoming => type == TransactionType.receiving || type == TransactionType.received; diff --git a/lib/data/models/transaction_cubit.dart b/lib/data/models/transaction_cubit.dart index f04dde8557794ea4252117309ca1ced13eed595b..c6eb85636036f67fffe427327c83dfcd35a11218 100644 --- a/lib/data/models/transaction_cubit.dart +++ b/lib/data/models/transaction_cubit.dart @@ -11,7 +11,6 @@ import 'contact.dart'; import 'node.dart'; import 'node_list_cubit.dart'; import 'node_type.dart'; -import 'pending_transaction.dart'; import 'transaction.dart'; import 'transaction_state.dart'; import 'transaction_type.dart'; @@ -19,28 +18,28 @@ import 'transaction_type.dart'; class TransactionCubit extends HydratedCubit<TransactionState> { TransactionCubit() : super(TransactionState( - transactions: const <Transaction>[], - pendingTransactions: const <PendingTransaction>[], - balance: 0, - lastChecked: DateTime.now())); + transactions: const <Transaction>[], + pendingTransactions: const <Transaction>[], + balance: 0, + lastChecked: DateTime.now())); @override String get storagePrefix => kIsWeb ? 'TransactionsCubit' : super.storagePrefix; - void addPendingTransaction(PendingTransaction pendingTransaction) { + void addPendingTransaction(Transaction pendingTransaction) { final TransactionState currentState = state; - final List<PendingTransaction> newPendingTransactions = - List<PendingTransaction>.of(currentState.pendingTransactions) - ..add(pendingTransaction); + final List<Transaction> newPendingTransactions = + List<Transaction>.of(currentState.pendingTransactions) + ..add(pendingTransaction); emit(currentState.copyWith(pendingTransactions: newPendingTransactions)); } - void removePendingTransaction(PendingTransaction pendingTransaction) { + void removePendingTransaction(Transaction pendingTransaction) { final TransactionState currentState = state; - final List<PendingTransaction> newPendingTransactions = - List<PendingTransaction>.of(currentState.pendingTransactions) - ..remove(pendingTransaction); + final List<Transaction> newPendingTransactions = + List<Transaction>.of(currentState.pendingTransactions) + ..remove(pendingTransaction); emit(currentState.copyWith(pendingTransactions: newPendingTransactions)); } @@ -66,7 +65,7 @@ class TransactionCubit extends HydratedCubit<TransactionState> { final Map<String, dynamic> txData = txDataResult.item1!; final TransactionState newState = - await transactionsGvaParser(txData, state); + await transactionsGvaParser(txData, state); if (newState.balance < 0) { logger('Warning: Negative balance in node ${txDataResult.item2}'); @@ -76,11 +75,9 @@ class TransactionCubit extends HydratedCubit<TransactionState> { success = true; logger( - 'Last received notification: ${newState.latestReceivedNotification - .toIso8601String()})}'); + 'Last received notification: ${newState.latestReceivedNotification.toIso8601String()})}'); logger( - 'Last sent notification: ${newState.latestSentNotification - .toIso8601String()})}'); + 'Last sent notification: ${newState.latestSentNotification.toIso8601String()})}'); emit(newState); for (final Transaction tx in newState.transactions.reversed) { diff --git a/lib/data/models/transaction_state.dart b/lib/data/models/transaction_state.dart index 7bd1235211e7947bbcb939d7c43afb10c605dbd9..0db66319da7dcaae60ae9fd1bacd1067a0908740 100644 --- a/lib/data/models/transaction_state.dart +++ b/lib/data/models/transaction_state.dart @@ -2,7 +2,6 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; -import 'pending_transaction.dart'; import 'transaction.dart'; part 'transaction_state.g.dart'; @@ -26,7 +25,7 @@ class TransactionState extends Equatable { _$TransactionStateFromJson(json); final List<Transaction> transactions; - final List<PendingTransaction> pendingTransactions; + final List<Transaction> pendingTransactions; final double balance; final DateTime lastChecked; final DateTime latestSentNotification; diff --git a/lib/data/models/transaction_state.g.dart b/lib/data/models/transaction_state.g.dart index 70404c0ef7a0174ac18b26783b780ace32fe2b7d..6867a541d8afbc2c7189d071e10ae2dd69806c7c 100644 --- a/lib/data/models/transaction_state.g.dart +++ b/lib/data/models/transaction_state.g.dart @@ -9,8 +9,7 @@ part of 'transaction_state.dart'; abstract class _$TransactionStateCWProxy { TransactionState transactions(List<Transaction> transactions); - TransactionState pendingTransactions( - List<PendingTransaction> pendingTransactions); + TransactionState pendingTransactions(List<Transaction> pendingTransactions); TransactionState balance(double balance); @@ -31,7 +30,7 @@ abstract class _$TransactionStateCWProxy { /// ```` TransactionState call({ List<Transaction>? transactions, - List<PendingTransaction>? pendingTransactions, + List<Transaction>? pendingTransactions, double? balance, DateTime? lastChecked, DateTime? latestSentNotification, @@ -51,8 +50,7 @@ class _$TransactionStateCWProxyImpl implements _$TransactionStateCWProxy { this(transactions: transactions); @override - TransactionState pendingTransactions( - List<PendingTransaction> pendingTransactions) => + TransactionState pendingTransactions(List<Transaction> pendingTransactions) => this(pendingTransactions: pendingTransactions); @override @@ -102,7 +100,7 @@ class _$TransactionStateCWProxyImpl implements _$TransactionStateCWProxy { pendingTransactions == null ? _value.pendingTransactions // ignore: cast_nullable_to_non_nullable - : pendingTransactions as List<PendingTransaction>, + : pendingTransactions as List<Transaction>, balance: balance == const $CopyWithPlaceholder() || balance == null ? _value.balance // ignore: cast_nullable_to_non_nullable @@ -146,7 +144,7 @@ TransactionState _$TransactionStateFromJson(Map<String, dynamic> json) => .map((e) => Transaction.fromJson(e as Map<String, dynamic>)) .toList(), pendingTransactions: (json['pendingTransactions'] as List<dynamic>) - .map((e) => PendingTransaction.fromJson(e as Map<String, dynamic>)) + .map((e) => Transaction.fromJson(e as Map<String, dynamic>)) .toList(), balance: (json['balance'] as num).toDouble(), lastChecked: DateTime.parse(json['lastChecked'] as String), diff --git a/lib/g1/transaction_parser.dart b/lib/g1/transaction_parser.dart index cffeac04ef0ef868431ca29172878316b90cb5b1..e59d27619aefcd32f0f179f5ba6c9c331a34a9d1 100644 --- a/lib/g1/transaction_parser.dart +++ b/lib/g1/transaction_parser.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import '../data/models/contact.dart'; -import '../data/models/pending_transaction.dart'; import '../data/models/transaction.dart'; import '../data/models/transaction_state.dart'; import '../data/models/transaction_type.dart'; @@ -10,7 +9,7 @@ import '../ui/contacts_cache.dart'; final RegExp exp = RegExp(r'\((.*?)\)'); Future<TransactionState> transactionParser( - String txData, List<PendingTransaction> pendingTransactions) async { + String txData, List<Transaction> pendingTransactions) async { final Map<String, dynamic> parsedTxData = json.decode(txData) as Map<String, dynamic>; final String pubKey = parsedTxData['pubkey'] as String; diff --git a/lib/ui/pay_helper.dart b/lib/ui/pay_helper.dart new file mode 100644 index 0000000000000000000000000000000000000000..4d5035b6ba6dccdbd641213c25f09a77cf19855e --- /dev/null +++ b/lib/ui/pay_helper.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../data/models/contact.dart'; +import '../../../data/models/node_list_cubit.dart'; +import '../../../data/models/node_type.dart'; +import '../../../data/models/payment_cubit.dart'; +import '../../../data/models/transaction.dart'; +import '../../../data/models/transaction_cubit.dart'; +import '../../../data/models/transaction_type.dart'; +import '../../../g1/api.dart'; +import '../../../shared_prefs.dart'; +import 'contacts_cache.dart'; +import 'logger.dart'; +import 'ui_helpers.dart'; + +Future<void> payWithRetry(BuildContext context, Contact to, double amount, + String comment, bool useMempool, + [bool addPending = false]) async { + logger('Trying to pay state with useMempool: $useMempool'); + final TransactionCubit txCubit = context.read<TransactionCubit>(); + final PaymentCubit paymentCubit = context.read<PaymentCubit>(); + paymentCubit.sending(); + final String contactPubKey = to.pubKey; + final bool? confirmed = await _confirmSend( + context, amount.toString(), humanizePubKey(contactPubKey)); + final Contact fromContact = + await ContactsCache().getContact(SharedPreferencesHelper().getPubKey()); + + if (confirmed == null || !confirmed) { + paymentCubit.sentFailed(); + } else { + final String response = + await pay(to: contactPubKey, comment: comment, amount: amount); + if (response == 'success') { + paymentCubit.sent(); + if (!context.mounted) { + return; + } + showTooltip( + context, tr('payment_successful'), tr('payment_successful_desc')); + + // Add here the transaction to the pending list (so we can check it the tx is confirmed) + if (inDevelopment && addPending) { + txCubit.addPendingTransaction(Transaction( + type: TransactionType.pending, + from: fromContact, + to: to, + amount: amount, + comment: comment, + time: DateTime.now())); + } + } else { + /* this retry didn't work + if (!useMempool) { + throw RetryException(); + } */ + if (!context.mounted) { + return; + } + final bool failedWithBalance = + response == 'insufficient balance' && weHaveBalance(context, amount); + showPayError( + context, + failedWithBalance + ? tr('payment_error_retry') + : tr('payment_error_desc', namedArgs: <String, String>{ + // We try to translate the error, like "insufficient balance" + 'error': tr(response) + })); + } + } +} + +bool weHaveBalance(BuildContext context, double amount) { + final double balance = getBalance(context); + final bool weHave = balance >= amount * 100; + return weHave; +} + +double getBalance(BuildContext context) => + context.read<TransactionCubit>().balance; + +Future<bool?> _confirmSend( + BuildContext context, String amount, String to) async { + return showDialog<bool>( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(tr('please_confirm_sent')), + content: Text(tr('please_confirm_sent_desc', + namedArgs: <String, String>{'amount': amount, 'to': to})), + actions: <Widget>[ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(tr('cancel')), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(tr('yes_sent')), + ), + ], + ); + }, + ); +} + +void showPayError(BuildContext context, String desc) { + showTooltip(context, tr('payment_error'), desc); + context.read<PaymentCubit>().sentFailed(); + // Shuffle the nodes so we can retry with other + context.read<NodeListCubit>().shuffle(NodeType.gva, true); +} diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart index 95c037c94c286eb44ffb6a4d2bab9e889cc74e3f..0d25bb47bafd88e553fe2f2b8dd37614d98a4748 100644 --- a/lib/ui/ui_helpers.dart +++ b/lib/ui/ui_helpers.dart @@ -99,14 +99,7 @@ String humanizePubKey(String address) => '\u{1F511} ${simplifyPubKey(address)}'; String simplifyPubKey(String address) => address.length <= 8 ? 'WRONG ADDRESS' : address.substring(0, 8); -/* -Widget humanizePubKeyAsWidget(String pubKey) => Text( - humanizePubKey(pubKey), - style: const TextStyle( - fontSize: 16.0, - ), - ); -*/ + Color tileColor(int index, BuildContext context, [bool inverse = false]) { final ColorScheme colorScheme = Theme.of(context).colorScheme; final Color selectedColor = colorScheme.primary.withOpacity(0.1); @@ -362,3 +355,5 @@ String ginkgoNetIcon = final GlobalKey<ScaffoldMessengerState> globalMessengerKey = GlobalKey<ScaffoldMessengerState>(); + +const Color deleteColor = Color(0xFFFE4A49); diff --git a/lib/ui/widgets/first_screen/pay_form.dart b/lib/ui/widgets/first_screen/pay_form.dart index 3d267f92c64c8127c2936d3e3735f2c2ffbed6c9..506f0f9a9c5fabc597979759689cbf429972c820 100644 --- a/lib/ui/widgets/first_screen/pay_form.dart +++ b/lib/ui/widgets/first_screen/pay_form.dart @@ -1,18 +1,13 @@ -import 'dart:async'; - import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../data/models/node_list_cubit.dart'; -import '../../../data/models/node_type.dart'; import '../../../data/models/payment_cubit.dart'; import '../../../data/models/payment_state.dart'; import '../../../data/models/transaction_cubit.dart'; -import '../../../g1/api.dart'; import '../../logger.dart'; +import '../../pay_helper.dart'; import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import '../connectivity_widget_wrapper_wrapper.dart'; @@ -62,8 +57,8 @@ class _PayFormState extends State<PayForm> { fontSize: 16, ), ); - final Widget payBtnText = Text( - tr('g1_form_pay_send') + (!kReleaseMode ? ' ${state.status}' : '')); + final Widget payBtnText = Text(tr( + 'g1_form_pay_send')); // + (!kReleaseMode ? ' ${state.status}' : '')); return Form( key: _formKey, child: Column( @@ -111,10 +106,12 @@ class _PayFormState extends State<PayForm> { ? null : () async { try { - await payWithRetry(context, state, false); + await payWithRetry(context, state.contact!, + state.amount!, state.comment, false, true); } on RetryException { // Here the transactions can be lost, so we must implement some manual retry use - await payWithRetry(context, state, true); + await payWithRetry(context, state.contact!, + state.amount!, state.comment, true, true); } }, style: payBtnStyle, @@ -185,83 +182,6 @@ class _PayFormState extends State<PayForm> { double getBalance(BuildContext context) => context.read<TransactionCubit>().balance; - - Future<bool?> _confirmSend( - BuildContext context, String amount, String to) async { - return showDialog<bool>( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(tr('please_confirm_sent')), - content: Text(tr('please_confirm_sent_desc', - namedArgs: <String, String>{'amount': amount, 'to': to})), - actions: <Widget>[ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(tr('cancel')), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(tr('yes_sent')), - ), - ], - ); - }, - ); - } - - Future<void> payWithRetry( - BuildContext context, PaymentState state, bool useMempool) async { - logger('Trying to pay state with useMempool: $useMempool'); - // We disable the number, anyway - context.read<PaymentCubit>().sending(); - final String contactPubKey = state.contact!.pubKey; - final bool? confirmed = await _confirmSend( - context, state.amount.toString(), humanizePubKey(contactPubKey)); - if (!mounted) { - return; - } - if (confirmed == null || !confirmed) { - context.read<PaymentCubit>().sentFailed(); - } else { - final String response = await pay( - to: contactPubKey, comment: state.comment, amount: state.amount!); - if (!mounted) { - // Cannot show a tooltip if the widget is not now visible - return; - } - if (response == 'success') { - context.read<PaymentCubit>().sent(); - showTooltip( - context, tr('payment_successful'), tr('payment_successful_desc')); - // ADD here the transaction to the pending list - // context.read<PaymentCubit>().addPendingTransaction( - // state.amount!, contactPubKey, state.comment); - } else { - /* this retry didn't work - if (!useMempool) { - throw RetryException(); - } */ - final bool failedWithBalance = response == 'insufficient balance' && - _weHaveBalance(context, state.amount!); - showPayError( - context, - failedWithBalance - ? tr('payment_error_retry') - : tr('payment_error_desc', namedArgs: <String, String>{ - // We try to translate the error, like "insufficient balance" - 'error': tr(response) - })); - } - } - } - - void showPayError(BuildContext context, String desc) { - showTooltip(context, tr('payment_error'), desc); - context.read<PaymentCubit>().sentFailed(); - // Shuffle the nodes so we can retry with other - context.read<NodeListCubit>().shuffle(NodeType.gva, true); - } } class RetryException implements Exception { diff --git a/lib/ui/widgets/fourth_screen/transaction_chart.dart b/lib/ui/widgets/fourth_screen/transaction_chart.dart index 089ef964706f29b1827f1499053b70bfdebd1ef4..4540ed5761443ec85ba5d174753d693e7cdf2eba 100644 --- a/lib/ui/widgets/fourth_screen/transaction_chart.dart +++ b/lib/ui/widgets/fourth_screen/transaction_chart.dart @@ -6,10 +6,6 @@ import '../../../data/models/transaction.dart'; class TransactionChart extends StatefulWidget { const TransactionChart({super.key, required this.transactions}); - final Color leftBarColor = Colors.yellow; - final Color rightBarColor = Colors.red; - final Color avgColor = Colors.orange; - final List<Transaction> transactions; @override @@ -19,6 +15,10 @@ class TransactionChart extends StatefulWidget { class TransactionChartState extends State<TransactionChart> { final double width = 7; + static const Color leftBarColor = Colors.yellow; + static const Color rightBarColor = Colors.red; + static const Color avgColor = Colors.orange; + late List<BarChartGroupData> rawBarGroups; late List<BarChartGroupData> showingBarGroups; @@ -72,7 +72,7 @@ class TransactionChartState extends State<TransactionChart> { barRods: <BarChartRodData>[ BarChartRodData( toY: total, - color: widget.leftBarColor, + color: leftBarColor, width: width, ), ], @@ -168,8 +168,7 @@ class TransactionChartState extends State<TransactionChart> { barRods: showingBarGroups[touchedGroupIndex] .barRods .map((BarChartRodData rod) { - return rod.copyWith( - toY: avg, color: widget.avgColor); + return rod.copyWith(toY: avg, color: avgColor); }).toList(), ); } @@ -278,12 +277,12 @@ class TransactionChartState extends State<TransactionChart> { barRods: <BarChartRodData>[ BarChartRodData( toY: y1, - color: widget.leftBarColor, + color: leftBarColor, width: width, ), BarChartRodData( toY: y2, - color: widget.rightBarColor, + color: rightBarColor, width: width, ), ], diff --git a/lib/ui/widgets/fourth_screen/transaction_item.dart b/lib/ui/widgets/fourth_screen/transaction_item.dart index 671b25e66263ef2ebe706838b4f4a0f6c3da0eee..7c296ec8cbe3c0315e1a9cbde10aa50466ae2812 100644 --- a/lib/ui/widgets/fourth_screen/transaction_item.dart +++ b/lib/ui/widgets/fourth_screen/transaction_item.dart @@ -11,6 +11,7 @@ import '../../../data/models/transaction_state.dart'; import '../../../data/models/transaction_type.dart'; import '../../../shared_prefs.dart'; import '../../contacts_cache.dart'; +import '../../pay_helper.dart'; import '../../ui_helpers.dart'; import '../third_screen/contact_form.dart'; @@ -39,12 +40,13 @@ class TransactionListItem extends StatelessWidget { IconData? icon; Color? iconColor; String statusText; + final bool isPending = transaction.type == TransactionType.pending; final String amountS = - '${transaction.amount < 0 ? "" : "+"}${formatKAmount(context, transaction.amount)}'; + '${transaction.amount < 0 ? "" : "+"}${formatKAmount(context, isPending ? transaction.amount * 100 : transaction.amount)}'; statusText = tr('transaction_${transaction.type.name}'); switch (transaction.type) { case TransactionType.pending: - icon = Icons.timelapse; + icon = Icons.schedule; iconColor = Colors.grey; break; case TransactionType.sending: @@ -63,40 +65,75 @@ class TransactionListItem extends StatelessWidget { final String myPubKey = SharedPreferencesHelper().getPubKey(); final ContactsCubit contactsCubit = context.read<ContactsCubit>(); + return Slidable( // Specify a key if the Slidable is dismissible. key: ValueKey<int>(index), // The end action pane is the one at the right or the bottom side. - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: <SlidableAction>[ + startActionPane: + ActionPane(motion: const ScrollMotion(), children: <SlidableAction>[ + if (isPending) SlidableAction( onPressed: (BuildContext c) { - final Contact newContact = - transaction.isIncoming ? transaction.from : transaction.to; - contactsCubit.addContact(newContact); + context + .read<TransactionCubit>() + .removePendingTransaction(transaction); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(tr('contact_added')), + content: Text(tr('payment_canceled')), + duration: const Duration(seconds: 3), ), ); - showDialog( - context: context, - builder: (BuildContext context) { - return ContactEditDialog( - contact: newContact, - onSave: (Contact c) { - context.read<ContactsCubit>().updateContact(c); - ContactsCache().saveContact(c); - }); - }, - ); }, - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: deleteColor, foregroundColor: Colors.white, - icon: Icons.contacts, - label: tr('add_contact'), + icon: Icons.delete, + label: tr('cancel_payment'), ), + ]), + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: <SlidableAction>[ + if (isPending) + SlidableAction( + onPressed: (BuildContext c) async { + await payWithRetry(context, transaction.to, + transaction.amount, transaction.comment, false); + }, + backgroundColor: Theme.of(context).primaryColorDark, + foregroundColor: Colors.white, + icon: Icons.replay, + label: tr('retry_payment'), + ), + if (transaction.type != TransactionType.pending) + SlidableAction( + onPressed: (BuildContext c) { + final Contact newContact = transaction.isIncoming + ? transaction.from + : transaction.to; + contactsCubit.addContact(newContact); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tr('contact_added')), + ), + ); + showDialog( + context: context, + builder: (BuildContext context) { + return ContactEditDialog( + contact: newContact, + onSave: (Contact c) { + context.read<ContactsCubit>().updateContact(c); + ContactsCache().saveContact(c); + }); + }, + ); + }, + backgroundColor: Theme.of(context).primaryColor, + foregroundColor: Colors.white, + icon: Icons.contacts, + label: tr('add_contact'), + ), ], ), child: ListTile( @@ -125,20 +162,6 @@ class TransactionListItem extends StatelessWidget { Text.rich( TextSpan( children: <InlineSpan>[ - /* TextSpan( - text: isIncoming(transaction.type) - ? 'Recibido de ' - : 'Pago a ', - style: const TextStyle( - fontSize: 14.0, - // fontWeight: FontWeight.bold, - ), - ), */ - /* WidgetSpan( - child: avatar != null - ? const SizedBox(width: 8.0) - : const SizedBox.shrink(), - ), */ WidgetSpan( child: Text( tr('transaction_from_to', namedArgs: <String, diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart index 070206d1a834c6a5e79d73d59e1de6db2c5ba616..c760a1e499db07a5fce617cf419b8ddb9e082492 100644 --- a/lib/ui/widgets/fourth_screen/transaction_page.dart +++ b/lib/ui/widgets/fourth_screen/transaction_page.dart @@ -37,6 +37,11 @@ class _TransactionsAndBalanceWidgetState final PagingController<String?, Transaction> _pagingController = PagingController<String?, Transaction>(firstPageKey: null); + final PagingController<int, Transaction> _pendingController = + PagingController<int, Transaction>(firstPageKey: 0); + + final int _pendingPageSize = 30; + @override void initState() { // Remove in the future @@ -46,6 +51,10 @@ class _TransactionsAndBalanceWidgetState _pagingController.addPageRequestListener((String? cursor) { _bloc.onPageRequestSink.add(cursor); }); + _pendingController.addPageRequestListener((int cursor) { + _fetchPending(cursor); + }); + // We could've used StreamBuilder, but that would unnecessarily recreate // the entire [PagedSliverGrid] every time the state changes. // Instead, handling the subscription ourselves and updating only the @@ -108,6 +117,7 @@ class _TransactionsAndBalanceWidgetState void dispose() { _transScrollController.dispose(); _pagingController.dispose(); + _pendingController.dispose(); _blocListingStateSubscription.cancel(); super.dispose(); } @@ -159,14 +169,9 @@ class _TransactionsAndBalanceWidgetState border: Border.all( color: Theme.of(context).colorScheme.inversePrimary, width: 3), - /* borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(8), - ), */ ), child: Scrollbar( child: ListView( - // controller: scrollController, children: <Widget>[ Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), @@ -196,14 +201,28 @@ class _TransactionsAndBalanceWidgetState color: Colors.white, backgroundColor: Theme.of(context).colorScheme.primary, strokeWidth: 4.0, - onRefresh: () => Future<void>.sync( - () => _pagingController.refresh(), - ), + onRefresh: () => Future<void>.sync(() { + _pagingController.refresh(); + _pendingController.refresh(); + }), child: CustomScrollView( shrinkWrap: true, // scrollDirection: Axis.vertical, slivers: <Widget>[ // Some widget before all, + PagedSliverList<int, Transaction>( + shrinkWrapFirstPageIndicators: true, + pagingController: _pendingController, + builderDelegate: PagedChildBuilderDelegate<Transaction>( + itemBuilder: + (BuildContext context, Transaction tx, int index) => + TransactionListItem( + pubKey: myPubKey, + index: index, + transaction: tx, + ), + noItemsFoundIndicatorBuilder: (_) => Container()), + ), PagedSliverList<String?, Transaction>( pagingController: _pagingController, // separatorBuilder: (BuildContext context, int index) => @@ -215,7 +234,10 @@ class _TransactionsAndBalanceWidgetState int index) { return TransactionListItem( pubKey: myPubKey, - index: index, + index: index + + (_pendingController.itemList != null + ? _pendingController.itemList!.length + : 0), transaction: tx, ); }, @@ -228,4 +250,24 @@ class _TransactionsAndBalanceWidgetState )); }); } + + Future<void> _fetchPending(int pageKey) async { + try { + final bool shouldPaginate = + transCubit.state.pendingTransactions.length > _pendingPageSize; + final List<Transaction> newItems = shouldPaginate + ? transCubit.state.pendingTransactions + .sublist(pageKey, _pendingPageSize) + : transCubit.state.pendingTransactions; + final bool isLastPage = newItems.length < _pendingPageSize; + if (isLastPage) { + _pendingController.appendLastPage(newItems); + } else { + final int nextPageKey = pageKey + newItems.length; + _pendingController.appendPage(newItems, nextPageKey); + } + } catch (error) { + _pendingController.error = error; + } + } } diff --git a/lib/ui/widgets/third_screen/contacts_page.dart b/lib/ui/widgets/third_screen/contacts_page.dart index be4eb1266c420894fb5b25867db1beeb71c2e218..01f9a7636bc5b747690abe807864a8a7fde86abb 100644 --- a/lib/ui/widgets/third_screen/contacts_page.dart +++ b/lib/ui/widgets/third_screen/contacts_page.dart @@ -81,7 +81,7 @@ class _ContactsPageState extends State<ContactsPage> { .read<ContactsCubit>() .removeContact(contact); }, - backgroundColor: const Color(0xFFFE4A49), + backgroundColor: deleteColor, foregroundColor: Colors.white, icon: Icons.delete, label: tr('delete_contact'), diff --git a/test/transactions_test.dart b/test/transactions_test.dart index 10404112a7426133f3476da6348c0a46fef09ea1..ad0a4291f711e928ed355ee5e88ecebe8f635f07 100644 --- a/test/transactions_test.dart +++ b/test/transactions_test.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:ginkgo/data/models/pending_transaction.dart'; import 'package:ginkgo/data/models/transaction.dart'; import 'package:ginkgo/data/models/transaction_state.dart'; import 'package:ginkgo/data/models/transaction_type.dart'; @@ -12,7 +11,7 @@ import 'package:ginkgo/ui/contacts_cache.dart'; void main() { final TransactionState emptyState = TransactionState( transactions: const <Transaction>[], - pendingTransactions: const <PendingTransaction>[], + pendingTransactions: const <Transaction>[], balance: 0, lastChecked: DateTime(1970)); @@ -33,7 +32,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final String txData = await rootBundle.loadString('assets/tx.json'); final TransactionState result = - await transactionParser(txData, <PendingTransaction>[]); + await transactionParser(txData, <Transaction>[]); expect(result.balance, equals(6700)); final List<Transaction> txs = result.transactions; for (final Transaction tx in txs) {