diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2140bca42b3675b554a4fac5071c08606b0d5312..b0b9696f9e75f7b81c1c654cd613bdda917d2aaf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" /> <application - android:preserveLegacyExternalStorage="true" android:requestLegacyExternalStorage="true" android:icon="@mipmap/ic_launcher" android:label="Ginkgo"> diff --git a/assets/translations/en.json b/assets/translations/en.json index c32f7cfad8db8fe9dc4208536c41f41175b8ca41..2c8e6d2adbf8edf15d3cbc12965f54f4f8462ea8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -124,5 +124,6 @@ "error_installing_desktop": "Error installing Äž1nkgo in your Desktop: (error: {error})", "install_desktop": "Install Äž1nkgo in Desktop", "import_failed": "Wallet import failed", - "select_file_to_import": "Select the wallet backup" + "select_file_to_import": "Select the wallet backup", + "You can't send money to yourself.": "You can't send money to yourself." } diff --git a/assets/translations/es.json b/assets/translations/es.json index 46a179451d4bd2ba658e801060e3305ad7611a35..dff193b89bd031e9babc20067b566453b26ca3db 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -31,11 +31,11 @@ "no_internet": "Sin conexión a internet", "skip": "Omitir", "start": "Comenzar", - "offline": "¡Está desconectad@!", + "offline": "¡Estás desconectad@!", "online_terminal": "En lÃnea", "offline_terminal": "Fuera de lÃnea", - "show_qr_to_client": "Muestre su clave pública a su clientæ", - "show_qr_to_client_amount": "Muestre este QR con esa cantidad a otro monedero Äž1nkgo", + "show_qr_to_client": "Muestra su clave pública a su clientæ", + "show_qr_to_client_amount": "Muestra este QR con esa cantidad a otro monedero Äž1nkgo", "keys_tooltip": "Las claves públicas y privadas en Äž1 y Duniter son como un sistema de cerradura y llave, donde la clave pública actúa como la cerradura que puede ser abierta por cualquiera que tenga la clave privada correspondiente, proporcionando una forma segura de autenticar y verificar transacciones", "card_validity": "Validez", "card_validity_tooltip": "Tenga en cuenta que este monedero solo es accesible mientras utiliza este navegador y este dispositivo especÃfico. Si borra o restablece el navegador, perderá el acceso a este monedero y los fondos almacenados en el.", @@ -124,5 +124,6 @@ "error_installing_desktop": "Error al instalar Äž1nkgo en Desktop (error: {error})", "install_desktop": "Instalar Äž1nkgo en tu Desktop", "import_failed": "Error al importar monedero", - "select_file_to_import": "Selecciona el monedero guadado" + "select_file_to_import": "Selecciona el monedero guadado", + "You can't send money to yourself.": "No puedes enviarte dinero a ti mismo/a." } diff --git a/lib/g1/api.dart b/lib/g1/api.dart index 9b113e6892c1bfb26d3b497c6c8d1e44a5d62c8a..e57ee5b5ff98e83417ce636742140e367aa2d57b 100644 --- a/lib/g1/api.dart +++ b/lib/g1/api.dart @@ -544,8 +544,9 @@ Future<T?> gvaFunctionWrapper<T>( final T? result = await specificFunction(gva); return result; } - } catch (e, stacktrace) { - await Sentry.captureException(e, stackTrace: stacktrace); + } catch (e) { + await Sentry.captureMessage( + 'Error trying to use gva node ${node.url} $e'); logger('Error trying ${node.url} $e'); logger('Increasing node errors of ${node.url} (${node.errors})'); NodeManager() diff --git a/lib/g1/transaction_parser.dart b/lib/g1/transaction_parser.dart index 660397144c236c704af6a561d34bb54260448678..656f208b8a5da88025b1cc1446004749d8a12587 100644 --- a/lib/g1/transaction_parser.dart +++ b/lib/g1/transaction_parser.dart @@ -33,7 +33,7 @@ TransactionsAndBalanceState transactionParser(String txData) { balance = balance -= amount; } final DateTime txDate = - DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true); /* if (!kReleaseMode) { logger('Timestamp: $timestamp'); logger('Fecha: $txDate'); diff --git a/lib/ui/contacts_cache.dart b/lib/ui/contacts_cache.dart index 8683fc145c0e907a88d7661471f94c61249d3b12..8837f5a87a6bdff90a1c94450297fea5852b9ae1 100644 --- a/lib/ui/contacts_cache.dart +++ b/lib/ui/contacts_cache.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:convert'; import 'dart:io'; @@ -22,13 +23,18 @@ class ContactsCache { Box<dynamic>? _box; Future<void> init() async { - if (kIsWeb) { - _box = await Hive.openBox(_boxName); - } else { - final Directory appDocDir = await getApplicationDocumentsDirectory(); - final String appDocPath = appDocDir.path; - _box = await Hive.openBox(_boxName, path: appDocPath); + try { + if (kIsWeb) { + _box = await Hive.openBox(_boxName); + } else { + final Directory appDocDir = await getApplicationDocumentsDirectory(); + final String appDocPath = appDocDir.path; + _box = await Hive.openBox(_boxName, path: appDocPath); + } + } catch (e) { + logger('Error opening Hive: $e'); } + _box ??= _MemoryFallbackBox<Contact>(); } Future<void> dispose() async { @@ -37,9 +43,9 @@ class ContactsCache { static ContactsCache? _instance; final Map<String, List<Completer<Contact>>> _pendingRequests = - <String, List<Completer<Contact>>>{}; + <String, List<Completer<Contact>>>{}; static Duration duration = - kReleaseMode ? const Duration(days: 3) : const Duration(hours: 5); + kReleaseMode ? const Duration(days: 3) : const Duration(hours: 5); final String _boxName = 'contacts_cache'; @@ -135,9 +141,9 @@ class ContactsCache { if (record != null) { final Map<String, dynamic> typedRecord = - Map<String, dynamic>.from(record as Map<dynamic, dynamic>); + Map<String, dynamic>.from(record as Map<dynamic, dynamic>); final DateTime timestamp = - DateTime.parse(typedRecord['timestamp'] as String); + DateTime.parse(typedRecord['timestamp'] as String); final bool before = DateTime.now().isBefore(timestamp.add(duration)); if (before) { final Contact contact = Contact.fromJson( @@ -148,3 +154,147 @@ class ContactsCache { return null; } } + +class _MemoryFallbackBox<E> extends Box<E> { + final Map<String, dynamic> _storage = HashMap<String, dynamic>(); + + @override + String get name => '_memory_fallback_box'; + + @override + bool get isOpen => true; + + @override + String? get path => null; + + @override + bool get lazy => false; + + @override + Iterable<dynamic> get keys => _storage.keys; + + @override + int get length => _storage.length; + + @override + bool get isEmpty => _storage.isEmpty; + + @override + bool get isNotEmpty => _storage.isNotEmpty; + + @override + dynamic keyAt(int index) { + return _storage.keys.elementAt(index); + } + + @override + Stream<BoxEvent> watch({dynamic key}) { + throw UnimplementedError('watch() is not supported in _MemoryFallbackBox'); + } + + @override + bool containsKey(dynamic key) { + return _storage.containsKey(key); + } + + @override + Future<void> put(dynamic key, E value) async { + _storage[key as String] = value; + } + + @override + Future<void> putAt(int index, E value) async { + _storage[_storage.keys.elementAt(index)] = value; + } + + @override + Future<void> putAll(Map<dynamic, E> entries) async { + _storage.addAll(entries as Map<String, dynamic>); + } + + @override + Future<int> add(E value) async { + throw UnimplementedError('add() is not supported in _MemoryFallbackBox'); + } + + @override + Future<Iterable<int>> addAll(Iterable<E> values) async { + throw UnimplementedError('addAll() is not supported in _MemoryFallbackBox'); + } + + @override + Future<void> delete(dynamic key) async { + _storage.remove(key); + } + + @override + Future<void> deleteAt(int index) async { + _storage.remove(_storage.keys.elementAt(index)); + } + + @override + Future<void> deleteAll(Iterable<dynamic> keys) async { + // ignore: prefer_foreach + for (final dynamic key in keys) { + _storage.remove(key); + } + } + + @override + Future<void> compact() async {} + + @override + Future<int> clear() async { + final int count = _storage.length; + _storage.clear(); + return count; + } + + @override + Future<void> close() async {} + + @override + Future<void> deleteFromDisk() async {} + + @override + Future<void> flush() async {} + + @override + E? get(dynamic key, {E? defaultValue}) { + return _storage.containsKey(key) ? _storage[key] as E : defaultValue; + } + + @override + E? getAt(int index) { + return _storage.values.elementAt(index) as E?; + } + + @override + Map<dynamic, E> toMap() { + return Map<dynamic, E>.from(_storage); + } + + @override + Iterable<E> get values => _storage.values.cast<E>(); + + @override + Iterable<E> valuesBetween({dynamic startKey, dynamic endKey}) { + if (startKey == null && endKey == null) { + return values; + } + + final int startIndex = startKey != null ? _storage.keys.toList().indexOf( + startKey as String) : 0; + final int endIndex = endKey != null ? _storage.keys.toList().indexOf( + endKey as String) : _storage.length - 1; + + if (startIndex < 0 || endIndex < 0) { + throw ArgumentError('Start key or end key not found in the box.'); + } + + return _storage.values.skip(startIndex) + .take(endIndex - startIndex + 1) + .cast<E>(); + } + +} diff --git a/lib/ui/screens/pay_form.dart b/lib/ui/screens/pay_form.dart index af8e41ca7422e6e5ad7cf0a91a81da7c73545fda..8d4ea40a87452f6d37bb1cd6cb4c252045296c2b 100644 --- a/lib/ui/screens/pay_form.dart +++ b/lib/ui/screens/pay_form.dart @@ -1,3 +1,4 @@ +import 'package:connectivity_wrapper/connectivity_wrapper.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -35,6 +36,20 @@ class _PayFormState extends State<PayForm> { if (state.comment != null && _commentController.text != state.comment) { _commentController.text = state.comment; } + final ButtonStyle payBtnStyle = ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 25), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30.0), + ), + foregroundColor: Colors.white, + backgroundColor: Theme.of(context).colorScheme.primary, + textStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ); + final Widget payBtnText = Text(tr('g1_form_pay_send') + + (!kReleaseMode ? ' ${state.amount} ${state.comment}' : '')); return Form( key: _formKey, child: Column( @@ -70,51 +85,47 @@ class _PayFormState extends State<PayForm> { }, ), const SizedBox(height: 10.0), - ElevatedButton( - onPressed: (!state.canBeSent() || - state.amount == null || - !_commentValidate() || - !_weHaveBalance(context, state.amount!)) - ? null - : () async { - try { - await payWithRetry(context, state, false); - } on RetryException { - // Here the transactions can be lost, so we must implement some manual retry use - await payWithRetry(context, state, true); - } - }, - style: ElevatedButton.styleFrom( - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 25), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30.0), + ConnectivityWidgetWrapper( + stacked: false, + offlineWidget: ElevatedButton( + onPressed: null, + style: payBtnStyle, + child: _buildBtn(Text(tr('offline'))), ), - foregroundColor: Colors.white, - backgroundColor: Theme.of(context).colorScheme.primary, - textStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: <Widget>[ - const Icon(Icons.send), - const SizedBox(width: 10), - Text(tr('g1_form_pay_send') + - (!kReleaseMode - ? ' ${state.amount} ${state.comment}' - : '')), - ], - ), - ) + child: ElevatedButton( + onPressed: (!state.canBeSent() || + state.amount == null || + !_commentValidate() || + !_weHaveBalance(context, state.amount!)) + ? null + : () async { + try { + await payWithRetry(context, state, false); + } on RetryException { + // Here the transactions can be lost, so we must implement some manual retry use + await payWithRetry(context, state, true); + } + }, + style: payBtnStyle, + child: _buildBtn(payBtnText), + )) ], ), ); }); } + Row _buildBtn(Widget payBtnText) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: <Widget>[ + const Icon(Icons.send), + const SizedBox(width: 10), + payBtnText, + ], + ); + } + bool _commentValidate() { final bool? val = _formKey.currentState?.validate(); logger('Validating comment: $val'); diff --git a/lib/ui/ui_helpers.dart b/lib/ui/ui_helpers.dart index 7dbc04845bc0da22fddb62781272bbe2b75864f0..930c10af193518a775ce73c1fd29b33b084d3842 100644 --- a/lib/ui/ui_helpers.dart +++ b/lib/ui/ui_helpers.dart @@ -197,7 +197,7 @@ ListTile contactToListItem(Contact contact, int index, BuildContext context, trailing: trailing); } -bool showShare() => onlyInDevelopment; +bool showShare() => onlyInDevelopment || !kIsWeb; bool get onlyInDevelopment => !inProduction; @@ -207,7 +207,10 @@ bool get onlyInProduction => kReleaseMode; bool get inProduction => onlyInProduction; -String assets(String str) => (kIsWeb && kReleaseMode) ? 'assets/$str' : str; +String assets(String str) => + (kIsWeb && kReleaseMode) || (!kIsWeb && Platform.isAndroid) + ? 'assets/$str' + : str; Future<Directory?> getAppSpecificExternalFilesDirectory( [bool ext = false]) async { diff --git a/lib/ui/widgets/first_screen/pay_contact_search_page.dart b/lib/ui/widgets/first_screen/pay_contact_search_page.dart index a225daab25212576a2cab968bab696793680614b..a6e7ed0179ddf8045bd16d1eb60e545a201287b3 100644 --- a/lib/ui/widgets/first_screen/pay_contact_search_page.dart +++ b/lib/ui/widgets/first_screen/pay_contact_search_page.dart @@ -148,10 +148,11 @@ class _PayContactSearchPageState extends State<PayContactSearchPage> { if (_results.length == 1 && pay != null) { final Contact contact = _results[0]; paymentCubit.selectUser(contact, pay.amount); - } else if (pay!.amount != null) { - paymentCubit.selectKeyAmount(pay.contact!, pay.amount!); - } else { - paymentCubit.selectKey(pay.contact); + if (pay.amount != null) { + paymentCubit.selectKeyAmount(contact, pay.amount!); + } else { + paymentCubit.selectKey(contact); + } } if (!mounted) { return;