diff --git a/assets/translations/en.json b/assets/translations/en.json index d24b2eede382c96ab529cf21aff14a0eb4e8ed9b..e180cddc7ae4123bd451604f40287ceab3f60acf 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -94,6 +94,7 @@ "transaction_sent": "Sent", "transaction_received": "Received", "transaction_failed": "Payment failed", + "transaction_waiting_network": "Retry with Internet", "valid_comment": "The comment cannot have accents or commas", "search_limitation": "The search term must have at least 3 characters", "faq_title": "Frequently Asked Questions", @@ -186,5 +187,8 @@ "cancel_payment": "Cancel Payment", "payment_canceled": "Payment canceled although we cannot ensure that it has not already been executed", "pay_again": "Pay Again", - "telegram_group": "Telegram Group" + "telegram_group": "Telegram Group", + "payment_waiting_internet_title": "No Internet", + "payment_waiting_internet_desc": "We'll retry your payment when internet is back", + "payment_waiting_internet_desc_beta": "Retry your payment when internet is back" } diff --git a/assets/translations/es.json b/assets/translations/es.json index e7d33ab03a958b64accb0d7118a549ab11dc5dfc..4e3d3d9bd4071aad356dc75257a38107920f0a4f 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -94,6 +94,7 @@ "transaction_sent": "Enviado", "transaction_received": "Recibido", "transaction_fallido": "Pago fallido", + "transaction_waiting_network": "Reintenta con Internet", "valid_comment": "El comentario no puede tener tildes o comas", "search_limitation": "La búsqueda debe tener al menos 3 caracteres", "faq_title": "Preguntas frecuentes", @@ -188,5 +189,8 @@ "payment_canceled": "Pago cancelado aunque no podemos asegurar que no se haya hecho ejecutado ya", "transaction_failed": "Pago fallido", "pay_again": "Paga de nuevo", - "telegram_group": "Grupo de Telegram" + "telegram_group": "Grupo de Telegram", + "payment_waiting_internet_title": "Sin Internet", + "payment_waiting_internet_desc": "Reintentaremos el pago cuando vuelvas a tener conexión", + "payment_waiting_internet_desc_beta": "Reintenta el pago cuando vuelvas a tener conexión" } diff --git a/lib/data/models/transaction.dart b/lib/data/models/transaction.dart index a65b5a434e38398b086e1e72ef1a388e4cb53fa3..bf03832af9068bc6053f26401caf4ef78d5a0537 100644 --- a/lib/data/models/transaction.dart +++ b/lib/data/models/transaction.dart @@ -31,22 +31,12 @@ class Transaction extends Equatable { final DateTime time; final String? debugInfo; -/* - bool get isOutgoing => - type == TransactionType.sending || - type == TransactionType.sent || - type == TransactionType.pending; - - -bool get isProcessing => - type == TransactionType.sending || - type == TransactionType.receiving || - type == TransactionType.pending; */ - bool get isFailed => type == TransactionType.failed; bool get isPending => - type == TransactionType.pending || type == TransactionType.failed; + type == TransactionType.pending || + type == TransactionType.failed || + type == TransactionType.waitingNetwork; bool get isIncoming => type == TransactionType.receiving || type == TransactionType.received; diff --git a/lib/data/models/transaction.g.dart b/lib/data/models/transaction.g.dart index 5200e68969b348c3502a4aa32c0b538104160f53..b721984bab7b327a763e5402896308bd3ac27967 100644 --- a/lib/data/models/transaction.g.dart +++ b/lib/data/models/transaction.g.dart @@ -153,4 +153,5 @@ const _$TransactionTypeEnumMap = { TransactionType.sent: 'sent', TransactionType.pending: 'pending', TransactionType.failed: 'failed', + TransactionType.waitingNetwork: 'waitingNetwork', }; diff --git a/lib/data/models/transaction_cubit.dart b/lib/data/models/transaction_cubit.dart index 846c6a7e933e5126765d28bcff305414e00f64d2..3dad7d21163ad9b8ef910c00cb79bd664f85a296 100644 --- a/lib/data/models/transaction_cubit.dart +++ b/lib/data/models/transaction_cubit.dart @@ -117,6 +117,9 @@ class TransactionCubit extends HydratedCubit<TransactionState> { // Adjust pending transactions for (final Transaction pend in newState.pendingTransactions) { + if (pend.type == TransactionType.waitingNetwork) { + continue; + } if (txMap[getTxKey(pend)] != null) { // Found a match // VER SI SENT o que diff --git a/lib/data/models/transaction_type.dart b/lib/data/models/transaction_type.dart index 195dc1dce7b05360b01dfa4ec1e88781e4bb9d38..2027f67eb678ee1731700e4bf6dda8d453829246 100644 --- a/lib/data/models/transaction_type.dart +++ b/lib/data/models/transaction_type.dart @@ -1 +1,9 @@ -enum TransactionType { sending, received, receiving, sent, pending, failed } +enum TransactionType { + sending, + received, + receiving, + sent, + pending, + failed, + waitingNetwork +} diff --git a/lib/data/models/transactions_bloc.dart b/lib/data/models/transactions_bloc.dart index c31fd78c04e23e6c89905a72c4ffa8d317f85c8a..868daa14c17ae03293b0fdde85cdbcbaec330fe0 100644 --- a/lib/data/models/transactions_bloc.dart +++ b/lib/data/models/transactions_bloc.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:rxdart/rxdart.dart'; import '../../g1/g1_helper.dart'; +import '../../ui/logger.dart'; import '../../ui/pay_helper.dart'; +import '../../ui/widgets/connectivity_widget_wrapper_wrapper.dart'; import 'app_cubit.dart'; import 'node_list_cubit.dart'; import 'transaction.dart'; @@ -33,7 +35,7 @@ class TransactionsBloc { final CompositeSubscription _subscriptions = CompositeSubscription(); final BehaviorSubject<TransactionsState> _onNewListingStateController = - BehaviorSubject<TransactionsState>.seeded( + BehaviorSubject<TransactionsState>.seeded( TransactionsState(), ); @@ -45,7 +47,7 @@ class TransactionsBloc { Sink<String?> get onPageRequestSink => _onPageRequest.sink; final BehaviorSubject<String?> _onSearchInputChangedSubject = - BehaviorSubject<String?>.seeded(null); + BehaviorSubject<String?>.seeded(null); Sink<String?> get onSearchInputChangedSink => _onSearchInputChangedSubject.sink; @@ -54,7 +56,7 @@ class TransactionsBloc { if (_onNewListingStateController.value.itemList != null) { return _onNewListingStateController.value.itemList! .where((Transaction tx) => - areDatesClose(DateTime.now(), tx.time, paymentTimeRange)) + areDatesClose(DateTime.now(), tx.time, paymentTimeRange)) .toList(); } else { return <Transaction>[]; @@ -85,25 +87,36 @@ class TransactionsBloc { searchTerm: _searchInputValue, ); */ - final List<Transaction> fetchedItems = await transCubit.fetchTransactions( - nodeListCubit, appCubit, - cursor: pageKey, - pageSize: _pageSize); - final bool isLastPage = fetchedItems.length < _pageSize; - final String? nextPageKey = - isLastPage ? null : transCubit.state.endCursor; - - yield TransactionsState( - // error: null, - nextPageKey: nextPageKey, - itemList: pageKey == null - ? fetchedItems - : <Transaction>[ - ...lastListingState.itemList ?? <Transaction>[], - ...fetchedItems - ], - ); + final bool isConnected = + await ConnectivityWidgetWrapperWrapper.isConnected; + logger('isConnected: $isConnected'); + + if (!isConnected) { + yield TransactionsState( + nextPageKey: pageKey, + itemList: transCubit.transactions, + ); + } else { + final List<Transaction> fetchedItems = + await transCubit.fetchTransactions(nodeListCubit, appCubit, + cursor: pageKey, pageSize: _pageSize); + + final bool isLastPage = fetchedItems.length < _pageSize; + final String? nextPageKey = + isLastPage ? null : transCubit.state.endCursor; + + yield TransactionsState( + // error: null, + nextPageKey: nextPageKey, + itemList: pageKey == null + ? fetchedItems + : <Transaction>[ + ...lastListingState.itemList ?? <Transaction>[], + ...fetchedItems + ], + ); + } } catch (e) { yield TransactionsState( error: e, diff --git a/lib/ui/pay_helper.dart b/lib/ui/pay_helper.dart index b484cf6c7a142e4034a87008c71bd84a33fdfebc..9c2c81884d5b0b682152a339dc63a5c37f5c94e2 100644 --- a/lib/ui/pay_helper.dart +++ b/lib/ui/pay_helper.dart @@ -20,6 +20,7 @@ import '../g1/g1_helper.dart'; import 'contacts_cache.dart'; import 'logger.dart'; import 'ui_helpers.dart'; +import 'widgets/connectivity_widget_wrapper_wrapper.dart'; Future<void> payWithRetry( {required BuildContext context, @@ -31,73 +32,95 @@ Future<void> payWithRetry( required bool isG1, required double currentUd}) async { logger('Trying to pay state with useMempool: $useMempool'); + assert(amount > 0); + final TransactionCubit txCubit = context.read<TransactionCubit>(); final PaymentCubit paymentCubit = context.read<PaymentCubit>(); final AppCubit appCubit = context.read<AppCubit>(); paymentCubit.sending(); final String fromPubKey = SharedPreferencesHelper().getPubKey(); final String contactPubKey = to.pubKey; + final bool isConnected = await ConnectivityWidgetWrapperWrapper.isConnected; + logger('isConnected: $isConnected'); final bool? confirmed = await _confirmSend(context, amount.toString(), humanizeContact(fromPubKey, to, true), isRetry, appCubit.currency); final Contact fromContact = await ContactsCache().getContact(fromPubKey); final double convertedAmount = toG1(amount, isG1, currentUd); + if (confirmed == null || !confirmed) { paymentCubit.sentFailed(); } else { - final PayResult result = - await pay(to: contactPubKey, comment: comment, amount: convertedAmount); final Transaction tx = Transaction( type: TransactionType.pending, from: fromContact, to: to, amount: -toCG1(convertedAmount).toDouble(), comment: comment, - debugInfo: - 'Node used: ${result.node != null ? result.node!.url : 'unknown'}', time: DateTime.now()); - if (result.message == 'success') { + if (isConnected != null && !isConnected && !isRetry) { paymentCubit.sent(); if (!context.mounted) { return; } - showTooltip( - context, tr('payment_successful'), tr('payment_successful_desc')); + showTooltip(context, tr('payment_waiting_internet_title'), + tr('payment_waiting_internet_desc_beta')); + final Transaction pending = + tx.copyWith(type: TransactionType.waitingNetwork); + txCubit.addPendingTransaction(pending); + context.read<BottomNavCubit>().updateIndex(3); + return; + } else { + final PayResult result = await pay( + to: contactPubKey, comment: comment, amount: convertedAmount); + final Transaction pending = tx.copyWith( + debugInfo: + 'Node used: ${result.node != null ? result.node!.url : 'unknown'}'); + if (result.message == 'success') { + paymentCubit.sent(); + // ignore: use_build_context_synchronously + if (!context.mounted) { + return; + } + showTooltip( + context, tr('payment_successful'), tr('payment_successful_desc')); - if (!isRetry) { - // Add here the transaction to the pending list (so we can check it the tx is confirmed) - txCubit.addPendingTransaction(tx); + if (!isRetry) { + // Add here the transaction to the pending list (so we can check it the tx is confirmed) + txCubit.addPendingTransaction(pending); + } else { + // Update the previously failed tx with an update time and type pending + txCubit.updatePendingTransaction(pending); + } } else { - // Update the previously failed tx with an update time and type pending - txCubit.updatePendingTransaction(tx); - } - } else { - /* this retry didn't work + /* this retry didn't work if (!useMempool) { throw RetryException(); } */ - paymentCubit.pendingPayment(); - if (!context.mounted) { - return; - } - final bool failedWithBalance = result.message == '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(result.message) - })); - if (!isRetry) { - txCubit.insertPendingTransaction( - tx.copyWith(type: TransactionType.failed)); - context.read<BottomNavCubit>().updateIndex(3); - } else { - // Update the previously failed tx with an update time and type pending - txCubit.updatePendingTransaction( - tx.copyWith(type: TransactionType.failed)); + paymentCubit.pendingPayment(); + if (!context.mounted) { + return; + } + final bool failedWithBalance = + result.message == '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(result.message) + })); + if (!isRetry) { + txCubit.insertPendingTransaction( + pending.copyWith(type: TransactionType.failed)); + context.read<BottomNavCubit>().updateIndex(3); + } else { + // Update the previously failed tx with an update time and type pending + txCubit.updatePendingTransaction( + pending.copyWith(type: TransactionType.failed)); + } } } } diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart index f8a8026f0a9f30056d6cf29b28fa1d7c7d41637c..72ab13ed385a8d6a6121a9ade43b8f9ec12bcc9b 100644 --- a/lib/ui/ui_helpers.dart +++ b/lib/ui/ui_helpers.dart @@ -514,3 +514,5 @@ void showQrDialog({ }, ); } + +bool get isIOS => !kIsWeb && Platform.isIOS; diff --git a/lib/ui/widgets/connectivity_widget_wrapper_wrapper.dart b/lib/ui/widgets/connectivity_widget_wrapper_wrapper.dart index 1953a75ae6bd663a96fa63e16ac4d5598b4096d2..a4017bfbea1a8d0271b7777cd172d679c2668116 100644 --- a/lib/ui/widgets/connectivity_widget_wrapper_wrapper.dart +++ b/lib/ui/widgets/connectivity_widget_wrapper_wrapper.dart @@ -1,16 +1,21 @@ -import 'dart:io'; - import 'package:connectivity_wrapper/connectivity_wrapper.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import '../ui_helpers.dart'; + class ConnectivityWidgetWrapperWrapper extends ConnectivityWidgetWrapper { ConnectivityWidgetWrapperWrapper( {super.key, Widget? offlineWidget, required super.child, super.stacked, + super.disableInteraction, super.message, super.height}) - : super(offlineWidget: !kIsWeb && Platform.isIOS ? child : offlineWidget); + : super(offlineWidget: isIOS ? child : offlineWidget); + + // This package does not work in IOS so we just return true + static Future<bool> get isConnected => isIOS + ? Future<bool>.value(true) + : ConnectivityWrapper.instance.isConnected; } diff --git a/lib/ui/widgets/fourth_screen/transaction_item.dart b/lib/ui/widgets/fourth_screen/transaction_item.dart index b66f76ab831abf807879049b95e9baadcdca854b..1d14392c0a9ffcd100c3f812e524fcc58ea14e6b 100644 --- a/lib/ui/widgets/fourth_screen/transaction_item.dart +++ b/lib/ui/widgets/fourth_screen/transaction_item.dart @@ -51,8 +51,7 @@ class TransactionListItem extends StatelessWidget { _buildTransactionItem(context, transaction)); } - Slidable _buildTransactionItem( - BuildContext context, Transaction transaction) { + Widget _buildTransactionItem(BuildContext context, Transaction transaction) { IconData? icon; Color? iconColor; String statusText; @@ -66,9 +65,15 @@ class TransactionListItem extends StatelessWidget { final String amountS = '${transaction.amount < 0 ? "" : "+"}$amountWithSymbol'; - statusText = tr('transaction_${transaction.type.name}'); + statusText = transaction.type != TransactionType.waitingNetwork + ? tr('transaction_${transaction.type.name}') + : tr('transaction_waiting_network'); switch (transaction.type) { + case TransactionType.waitingNetwork: + icon = Icons.schedule_send; + iconColor = grey; + break; case TransactionType.pending: icon = Icons.schedule; iconColor = grey; @@ -136,6 +141,16 @@ class TransactionListItem extends StatelessWidget { endActionPane: ActionPane( motion: const ScrollMotion(), children: <SlidableAction>[ + if (transaction.type == TransactionType.waitingNetwork) + SlidableAction( + onPressed: (BuildContext c) async { + await _payAgain(context, transaction, true); + }, + backgroundColor: Theme.of(context).primaryColorDark, + foregroundColor: Colors.white, + icon: Icons.replay, + label: tr('retry_payment'), + ), if (transaction.type == TransactionType.failed) SlidableAction( onPressed: (BuildContext c) async {