diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f660c0d86..8361e1424 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -122,7 +122,7 @@ PODS: - SDWebImage (5.18.3): - SDWebImage/Core (= 5.18.3) - SDWebImage/Core (5.18.3) - - share (0.0.1): + - share_plus (0.0.1): - Flutter - shared_preferences_ios (0.0.1): - Flutter @@ -147,7 +147,7 @@ DEPENDENCIES: - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - - share (from `.symlinks/plugins/share/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -201,8 +201,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" - share: - :path: ".symlinks/plugins/share/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" shared_preferences_ios: :path: ".symlinks/plugins/shared_preferences_ios/ios" sqflite: @@ -242,7 +242,7 @@ SPEC CHECKSUMS: Protobuf: 351e9022fe13a6e2af00e9aefc22077cb88520f8 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 96e0c18ef14010b7485210e92fac888587ebb958 - share: 0b2c3e82132f5888bccca3351c504d0003b3b410 + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f diff --git a/lib/app_config/coins_updater.dart b/lib/app_config/coins_updater.dart index ecf8fbb3a..7b2f4bccc 100644 --- a/lib/app_config/coins_updater.dart +++ b/lib/app_config/coins_updater.dart @@ -1,10 +1,11 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; -import 'dart:isolate'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart' as http; import 'package:komodo_dex/utils/log.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:komodo_dex/utils/utils.dart'; /// Provides methods for fetching coin data either from local assets or a remote Git repository. /// @@ -49,109 +50,152 @@ class CoinUpdater { String _cachedConfig; String _cachedCoins; - Future _fetchAsset(String path) async { - return await rootBundle.loadString(path); - } + Future _fetchAsset(String path) => + rootBundle.loadString(path, cache: false); Future _getLocalFile(String filename) async { - final directory = await getApplicationDocumentsDirectory(); - return File('${directory.path}/$filename'); + final directory = await applicationDocumentsDirectory; + return File('${directory.path}/config_updates/$filename'); } - Future _fetchOrCache( - String localPath, - String remoteUrl, - String cacheName, - String cacheProperty, - ) async { - try { - if (cacheProperty != null) { - return cacheProperty; - } + Future _fetchCoinFileOrAsset(UpdateCacheParams params) async { + File cacheFile; + String property; - File cacheFile = await _getLocalFile(cacheName); + try { + try { + cacheFile = await _getLocalFile(params.cacheFileName); - final cacheFileExists = await cacheFile.exists(); + final maybeCacheValue = await compute( + _tryReadValidJsonFile, + cacheFile.path, + ); - if (isUpdateEnabled) { - scheduleMicrotask( - () => _updateCacheInBackground(remoteUrl, cacheFile), + property = maybeCacheValue ?? property; + } catch (e) { + Log( + 'CoinUpdater', + 'Error reading coin config cache file: ${e.toString()}', ); } - if (cacheFileExists) { - cacheProperty = await cacheFile.readAsString(); + property ??= await _fetchAsset(params.localPath); - return cacheProperty; - } else { - String localData = await _fetchAsset(localPath); - cacheProperty = localData; - return localData; - } + return property; } catch (e) { - // If there's an error, first try to return the cached value, - // if that's null too, then fall back to the local asset. - if (cacheProperty != null) { - return cacheProperty; - } else { - return await _fetchAsset(localPath); + Log('CoinUpdater', 'Error fetching or caching ${params.cacheKey}: $e'); + rethrow; + } finally { + if (isUpdateEnabled) { + _startUpdateCacheInBackground(params.remoteUrl, cacheFile); } } } - void _updateCacheInBackground(String remoteUrl, File cacheFile) async { - final ReceivePort receivePort = ReceivePort(); - + void _startUpdateCacheInBackground(String remoteUrl, File cacheFile) async { try { - await Isolate.spawn( - _isolateEntry, - [remoteUrl, cacheFile.path], - onExit: receivePort.sendPort, - errorsAreFatal: false, + Log('CoinUpdater', 'Updating coins in background...'); + await compute, void>( + _updateFileFromServer, + { + 'remoteUrl': remoteUrl, + 'filePath': cacheFile.path, + }, ); - receivePort.listen((data) { - // Close the receive port when the isolate is done - receivePort.close(); - Log( - 'CoinUpdater', - 'Coin updater updated coins to latest commit on branch ' - '$coinsRepoBranch from $coinsRepoUrl. \n $remoteUrl', - ); - }); + Log( + 'CoinUpdater', + 'Coin updater updated coins to latest commit on branch $coinsRepoBranch' + ' from $coinsRepoUrl. Changes will take effect on next app launch.', + ); } catch (e) { - Log('CoinUpdater', 'Error updating coins: $e'); - } - } - - static void _isolateEntry(List data) async { - final String remoteUrl = data[0]; - final String filePath = data[1]; - - final response = await http.get(Uri.parse(remoteUrl)); - if (response.statusCode == 200) { - final file = File(filePath); - file.writeAsString(response.body); + Log('CoinUpdater', 'Error updating coins in background: $e'); } } Future getConfig() async { - _cachedConfig = await _fetchOrCache( - localAssetPathConfig, - remotePathConfig, - 'coins_config_cache.json', - _cachedConfig, + return _cachedConfig ??= await _fetchCoinFileOrAsset( + UpdateCacheParams( + localPath: localAssetPathConfig, + remoteUrl: remotePathConfig, + cacheFileName: 'coins_config_cache.json', + cacheKey: 'config', + ), ); - return _cachedConfig; } Future getCoins() async { - _cachedCoins = await _fetchOrCache( - localAssetPathCoins, - remotePathCoins, - 'coins_cache.json', - _cachedCoins, + return _cachedCoins ??= await _fetchCoinFileOrAsset( + UpdateCacheParams( + localPath: localAssetPathCoins, + remoteUrl: remotePathCoins, + cacheFileName: 'coins_cache.json', + cacheKey: 'coins', + ), ); - return _cachedCoins; + } +} + +class UpdateCacheParams { + final String localPath; + final String remoteUrl; + final String cacheFileName; + final String cacheKey; + + UpdateCacheParams({ + @required this.localPath, + @required this.remoteUrl, + @required this.cacheFileName, + @required this.cacheKey, + }); +} + +/// Isolate-safe method for returning the contents of a JSON file if it is valid +Future _tryReadValidJsonFile(String path) async { + try { + final contents = await File(path).readAsString(); + + if (!_isJsonValid(contents)) return null; + + return contents; + } catch (e) { + return null; + } +} + +/// An isolate-safe method for checking if a string is valid JSON. +bool _isJsonValid(String json) { + try { + if (json?.isEmpty ?? true) return false; + + jsonDecode(json); + return true; + } catch (e) { + return false; + } +} + +/// Isolate-safe method for fetching and updating a JSON file from a +/// remote server. +Future _updateFileFromServer(Map params) async { + final String remoteUrl = params['remoteUrl']; + final String filePath = params['filePath']; + + try { + final response = await http.get(Uri.parse(remoteUrl)); + + if (response.statusCode != 200 || !_isJsonValid(response.body)) return; + + final file = File(filePath); + + if (!file.existsSync()) { + file.createSync(recursive: true); + } + + file.writeAsStringSync(response.body, flush: true); + } catch (e) { + print('Error in isolate: $e'); + + rethrow; } } diff --git a/lib/screens/authentification/authenticate_page.dart b/lib/screens/authentification/authenticate_page.dart index 8f77e0034..b85443b00 100644 --- a/lib/screens/authentification/authenticate_page.dart +++ b/lib/screens/authentification/authenticate_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:komodo_dex/utils/log.dart'; +import 'package:komodo_dex/widgets/repeated_tap_detector.dart'; import '../../blocs/authenticate_bloc.dart'; import '../../blocs/wallet_bloc.dart'; import '../../localizations.dart'; @@ -119,8 +121,9 @@ class BuildScreenAuthMultiWallets extends StatelessWidget { ), ), Align( - alignment: Alignment.centerRight, - child: const SelectLanguageButton()), + alignment: Alignment.centerRight, + child: const SelectLanguageButton(), + ), SizedBox(height: 16), IntrinsicHeight( child: Row( @@ -251,13 +254,7 @@ class _BuildScreenAuthState extends State { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - SizedBox( - height: 240, - width: 240, - child: Image.asset(Theme.of(context).brightness == - Brightness.light - ? 'assets/branding/mark_and_text_vertical_dark.png' - : 'assets/branding/mark_and_text_vertical_light.png')), + _FullAppLogo(), ], ), Padding( @@ -295,6 +292,45 @@ class _BuildScreenAuthState extends State { } } +class _FullAppLogo extends StatelessWidget { + const _FullAppLogo({ + Key key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return RepeatedTapDetector( + onRepeatedTap: () => _downloadLogs(context), + tapTriggerCount: 7, + child: SizedBox( + height: 240, + width: 240, + child: Image.asset(Theme.of(context).brightness == Brightness.light + ? 'assets/branding/mark_and_text_vertical_dark.png' + : 'assets/branding/mark_and_text_vertical_light.png')), + ); + } + + /// If the user taps the branding logo 7 times in a row, the app will + /// download the logs and share them via the system share sheet. This is so + /// that users can download logs even if they can't access the settings page. + /// E.g. if the app crashes on login. + void _downloadLogs(BuildContext context) { + Log.downloadLogs().catchError((e) { + _showSnackbar(context, e.toString()); + }); + } + + void _showSnackbar(BuildContext context, String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 3), + ), + ); + } +} + class CreateWalletButton extends StatelessWidget { const CreateWalletButton({Key key}) : super(key: key); diff --git a/lib/screens/dex/orders/swap/final_trade_success.dart b/lib/screens/dex/orders/swap/final_trade_success.dart index 8fc3b089a..eb48cb30e 100644 --- a/lib/screens/dex/orders/swap/final_trade_success.dart +++ b/lib/screens/dex/orders/swap/final_trade_success.dart @@ -1,18 +1,20 @@ import 'dart:io'; import 'dart:ui' as ui; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:share_plus/share_plus.dart'; + import '../../../../app_config/app_config.dart'; import '../../../../blocs/dialog_bloc.dart'; import '../../../../localizations.dart'; import '../../../../model/swap.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import '../../../dex/orders/swap/detail_swap.dart'; -import '../../../dex/orders/swap/share_preview_overlay.dart'; import '../../../../utils/utils.dart'; import '../../../../widgets/swap_share_card.dart'; -import 'package:share/share.dart'; +import '../../../dex/orders/swap/detail_swap.dart'; +import '../../../dex/orders/swap/share_preview_overlay.dart'; class FinalTradeSuccess extends StatefulWidget { const FinalTradeSuccess({@required this.swap}); @@ -167,10 +169,9 @@ class _FinalTradeSuccessState extends State ' on my phone! You can try it too: https://komodoplatform.com\n' '#blockchain #dex #atomicdex #komodoplatform #atomicswap'; - await Share.shareFiles( - [imgFile.path], + await Share.shareXFiles( + [XFile(imgFile.path, mimeType: 'image/png')], text: shareText, - mimeTypes: ['image/png'], ); if (Platform.isIOS) { diff --git a/lib/screens/import-export/export_page.dart b/lib/screens/import-export/export_page.dart index fca8a60d6..ae1e90bcf 100644 --- a/lib/screens/import-export/export_page.dart +++ b/lib/screens/import-export/export_page.dart @@ -1,26 +1,28 @@ import 'dart:convert'; import 'dart:io'; -import 'package:intl/intl.dart'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; + +import '../../localizations.dart'; import '../../model/addressbook_provider.dart'; import '../../model/backup.dart'; +import '../../model/export_import_list_item.dart'; import '../../model/recent_swaps.dart'; import '../../model/swap.dart'; import '../../model/swap_provider.dart'; -import '../authentification/lock_screen.dart'; -import '../import-export/export_import_success.dart'; +import '../../services/db/database.dart'; import '../../utils/encryption_tool.dart'; import '../../utils/utils.dart'; import '../../widgets/password_visibility_control.dart'; import '../../widgets/primary_button.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:provider/provider.dart'; -import 'package:share/share.dart'; -import '../../model/export_import_list_item.dart'; -import '../../services/db/database.dart'; -import '../../localizations.dart'; +import '../authentification/lock_screen.dart'; import '../import-export/export_import_list.dart'; +import '../import-export/export_import_success.dart'; class ExportPage extends StatefulWidget { @override @@ -355,8 +357,10 @@ class _ExportPageState extends State { final encrypted = EncryptionTool().encryptData(_ctrlPass1.text, encoded); await tempFile.writeAsString(encrypted); - await Share.shareFiles([tempFile.path], - mimeTypes: ['application/octet-stream'], subject: 'atomicDEX_backup'); + await Share.shareXFiles( + [XFile(tempFile.path, mimeType: 'application/octet-stream')], + subject: 'atomicDEX_backup', + ); setState(() { _done = true; }); diff --git a/lib/screens/portfolio/coin_detail/coin_detail.dart b/lib/screens/portfolio/coin_detail/coin_detail.dart index 8424134ee..e8622b49c 100644 --- a/lib/screens/portfolio/coin_detail/coin_detail.dart +++ b/lib/screens/portfolio/coin_detail/coin_detail.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; import '../../../../app_config/app_config.dart'; import '../../../../blocs/coin_detail_bloc.dart'; diff --git a/lib/screens/portfolio/transaction_detail.dart b/lib/screens/portfolio/transaction_detail.dart index ea3e42bab..cba184c41 100644 --- a/lib/screens/portfolio/transaction_detail.dart +++ b/lib/screens/portfolio/transaction_detail.dart @@ -2,7 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:komodo_dex/model/coin_type.dart'; import 'package:provider/provider.dart'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; import '../../blocs/coins_bloc.dart'; import '../../blocs/main_bloc.dart'; diff --git a/lib/screens/settings/setting_page.dart b/lib/screens/settings/setting_page.dart index 4543899c5..b9638a24b 100644 --- a/lib/screens/settings/setting_page.dart +++ b/lib/screens/settings/setting_page.dart @@ -7,7 +7,7 @@ import 'package:komodo_dex/packages/z_coin_activation/widgets/z_coin_status_list import 'package:komodo_dex/utils/log_storage.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:provider/provider.dart'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; import '../../app_config/app_config.dart'; import '../../blocs/authenticate_bloc.dart'; @@ -556,44 +556,20 @@ class _SettingPageState extends State { Future _shareLogs() async { Navigator.of(context).pop(); - final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - final String os = Platform.isAndroid ? 'Android' : 'iOS'; - - final now = DateTime.now(); - // final log = FileAndSink(_logStorage.getLogFilePath(now)); - if (swapMonitor.swaps.isEmpty) await swapMonitor.update(); - try { - await Log.appendRawLog('\n\n--- my recent swaps ---\n\n'); - for (Swap swap in swapMonitor.swaps) { - final started = swap.started; - if (started == null) continue; - final tim = DateTime.fromMillisecondsSinceEpoch(started.timestamp); - final delta = now.difference(tim); - if (delta.inDays > 7) continue; // Skip old swaps. - await Log.appendRawLog(json.encode(swap.toJson) + '\n\n'); - } - await Log.appendRawLog('\n\n--- / my recent swaps ---\n\n'); - // TBD: Replace these with a pretty-printed metrics JSON - await Log.appendRawLog('Komodo Wallet ${packageInfo.version} $os\n'); - await Log.appendRawLog( - 'mm_version ${mmSe.mmVersion} mm_date ${mmSe.mmDate}\n'); - await Log.appendRawLog('netid ${mmSe.netid}\n'); - } catch (ex) { - Log('setting_page:723', ex); - await Log.appendRawLog('Error saving swaps for log export: $ex'); - } - - // Discord attachment size limit is about 25 MiB - final exportedLogFiles = await LogStorage().exportLogs(); - - final paths = exportedLogFiles.map((f) => f.path).toList(); - - mainBloc.isUrlLaucherIsOpen = true; - - await Share.shareFiles( - paths, - // mimeTypes: ['application/octet-stream'], - subject: 'Komodo Wallet at ${DateTime.now().toIso8601String()}', + + Log.downloadLogs().catchError((dynamic e) { + _showSnackbar(e.toString()); + }); + } + + void _showSnackbar(String message) { + if (context == null) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 3), + ), ); } diff --git a/lib/services/mm_service.dart b/lib/services/mm_service.dart index c5718a877..5f5747a9e 100644 --- a/lib/services/mm_service.dart +++ b/lib/services/mm_service.dart @@ -319,19 +319,6 @@ class MMService { _directorySizeCache['$dirPath**$endsWith'] = size; } - List removeZhtlcCheckPointBlock(List coinsJson) { - return coinsJson.map((dynamic coinDynamic) { - Map coin = coinDynamic as Map; - if (coin.containsKey('protocol') && - coin['protocol'].containsKey('type') && - coin['protocol']['type'] == 'ZHTLC' && - coin['protocol']['protocol_data'].containsKey('check_point_block')) { - coin['protocol']['protocol_data'].remove('check_point_block'); - } - return coin; - }).toList(); - } - /// returns directory size in MB static double dirStatSync(String dirPath, {String endsWith = 'log'}) { int totalSize = 0; @@ -374,7 +361,7 @@ class MMService { userhome: filesPath, passphrase: passphrase, rpcPassword: rpcPass, - coins: removeZhtlcCheckPointBlock(await readJsonCoinInit()), + coins: await readJsonCoinInit(), dbdir: filesPath, allowWeakPassword: false, rpcPort: appConfig.rpcPort, @@ -457,18 +444,46 @@ class MMService { } Future> readJsonCoinInit() async { + List coinsJson; + try { - return jsonDecode(await CoinUpdater().getCoins()); + coinsJson = await jsonDecode(await CoinUpdater().getCoins()); + + coinsJson = coinsJson.map((dynamic coinDynamic) { + try { + if (coinDynamic is Map) { + coinDynamic = coinModifier(coinDynamic); + } + } catch (e) { + // Coin modification failed. This is not a critical error, so we can, + // but developers should be aware of it. + Log('mm_service', 'Coin modification failed. ${e.toString()}'); + } + return coinDynamic; + }).toList(); } catch (e) { - if (kDebugMode) { - Log('mm_service', 'readJsonCoinInit] $e'); - printError('$e'); - printError('Try to run `\$sh fetch_coins.sh`.' - ' See README.md for details.'); - SystemChannels.platform.invokeMethod('SystemNavigator.pop'); - } + Log('mm_service', 'Error loading coin config: ${e.toString()}'); + return []; } + + return coinsJson; + } + + /// A function to modify each loaded coin in the list of coins before it is + /// passed to MM. + Map coinModifier(Map coin) { + // Remove the check_point_block from ZHTLC coins because this is required + // if we want to activate ZHTLC coins and only sync from the current date. + // The check_point_block will be removed from the coin config repo in the + // future, so this is a temporary workaround. + if (coin.containsKey('protocol') && + coin['protocol'].containsKey('type') && + coin['protocol']['type'] == 'ZHTLC' && + coin['protocol']['protocol_data'].containsKey('check_point_block')) { + coin['protocol']['protocol_data'].remove('check_point_block'); + } + return coin; } Future initCoinsAndLoad() async { diff --git a/lib/utils/log.dart b/lib/utils/log.dart index 9be6d2e0a..a7747a10f 100644 --- a/lib/utils/log.dart +++ b/lib/utils/log.dart @@ -2,10 +2,15 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:cross_file/cross_file.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:intl/intl.dart'; +import 'package:komodo_dex/blocs/main_bloc.dart'; +import 'package:komodo_dex/model/swap_provider.dart'; import 'package:komodo_dex/utils/log_storage.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:share_plus/share_plus.dart'; import '../services/mm_service.dart'; import '../utils/utils.dart'; @@ -110,6 +115,56 @@ class Log { await _updateLastClearedDate(); } + + static Future downloadLogs() async { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + final String os = Platform.operatingSystem; + + final now = DateTime.now(); + + try { + if (swapMonitor.swaps.isEmpty) await swapMonitor.update(); + + await Log.appendRawLog('\n\n--- my recent swaps ---\n\n'); + + final recentSwaps = swapMonitor.swaps.where((swap) => + swap.started != null && + DateTime.fromMillisecondsSinceEpoch(swap.started.timestamp) + .difference(now) + .inDays + .abs() < + 7); + + for (final swap in recentSwaps) { + await Log.appendRawLog('${swap.toJson}\n'); + } + + await Log.appendRawLog('\n\n--- / my recent swaps ---\n\n'); + // TBD: Replace these with a pretty-printed metrics JSON + await Log.appendRawLog('Komodo Wallet ${packageInfo.version} $os\n'); + await Log.appendRawLog( + 'mm_version ${mmSe.mmVersion} mm_date ${mmSe.mmDate}\n'); + await Log.appendRawLog('netid ${mmSe.netid}\n'); + } catch (ex) { + Log('setting_page:723', ex); + await Log.appendRawLog('Error saving swaps for log export: $ex'); + } + + // Discord attachment size limit is about 25 MiB + final exportedLogFiles = + (await LogStorage().exportLogs()).map((f) => XFile(f.path)).toList(); + if (exportedLogFiles.isEmpty) { + throw Exception('No logs to download'); + } + + mainBloc.isUrlLaucherIsOpen = true; + + await Share.shareXFiles( + exportedLogFiles, + // mimeTypes: ['application/octet-stream'], + subject: 'Komodo Wallet Logs at ${DateTime.now().toIso8601String()}', + ); + } } Future _doMaintainInSeparateIsolate(Map params) async { diff --git a/lib/utils/log_storage.dart b/lib/utils/log_storage.dart index 3ae829b12..7e5ef4b93 100644 --- a/lib/utils/log_storage.dart +++ b/lib/utils/log_storage.dart @@ -155,11 +155,7 @@ class LogStorage { Future> exportLogs() async { await deleteExportedArchives(); - final logFiles = Directory(logFolderPath()) - .listSync(followLinks: false, recursive: true) - .whereType() - .where((f) => f.path.endsWith('.log')) - .toList(); + final logFiles = await getLogFiles(); final compressedFiles = []; final compressedSizes = []; @@ -168,7 +164,7 @@ class LogStorage { const compressionLevel = Deflate.DEFAULT_COMPRESSION; const maxSizeBytes = 24 * 1000 * 1000; // 24MB. Discord limit is 25MB. - for (var logFile in logFiles) { + for (final logFile in logFiles.values) { List fileBytes = logFile.readAsBytesSync(); final fileName = p.basename(logFile.path); @@ -191,11 +187,11 @@ class LogStorage { compressedSizes.add(compressedBytes.length); - final isLastFile = logFile == logFiles.last; + final isLastFile = logFile == logFiles.values.last; if (wouldBeOverLimit || isLastFile) { final archiveFile = File( - '${logFolderPath()}/komodo_wallet_logs_archive_${compressedFiles.length}.g.zip', + '${logFolderPath()}/komodo_wallet_logs_archive_${compressedFiles.length}.gz', ); final archiveBytes = @@ -218,7 +214,7 @@ class LogStorage { final archives = Directory(logFolderPath()) .listSync(followLinks: false, recursive: true) .whereType() - .where((f) => f.path.endsWith('.g.zip')) + .where((f) => f.path.endsWith('.gz')) .toList(); for (var archive in archives) { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 7534a9a54..8103a013e 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -12,7 +12,7 @@ import 'package:local_auth/local_auth.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:rational/rational.dart'; -import 'package:share/share.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import '../app_config/app_config.dart'; diff --git a/lib/widgets/repeated_tap_detector.dart b/lib/widgets/repeated_tap_detector.dart new file mode 100644 index 000000000..3a480d24f --- /dev/null +++ b/lib/widgets/repeated_tap_detector.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +/// A widget which detects if a user repeatedly taps on a widget. +/// +/// +/// [child] The widget to be wrapped by the repeated tap detector. +/// [tapTriggerCount] The number of taps required to trigger the [onRepeatedTap] callback. +/// [onRepeatedTap] The callback to be triggered when the user taps on the widget [tapTriggerCount] times. +/// [cooldown] The maximum time in milliseconds between taps to be considered a repeated tap. +class RepeatedTapDetector extends StatefulWidget { + const RepeatedTapDetector({ + Key key, + @required this.child, + @required this.tapTriggerCount, + @required this.onRepeatedTap, + this.cooldown = const Duration(seconds: 1), + }) : super(key: key); + + final Widget child; + final int tapTriggerCount; + final VoidCallback onRepeatedTap; + final Duration cooldown; + + @override + _RepeatedTapDetectorState createState() => _RepeatedTapDetectorState(); +} + +class _RepeatedTapDetectorState extends State { + int _tapCount = 0; + DateTime _lastTapTime; + + bool get _isCooldownExpired => + _lastTapTime != null && + DateTime.now().difference(_lastTapTime) > widget.cooldown; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + print( + 'RepeatedTapDetector: onTap. _tapCount: $_tapCount, _isCooldownExpired: $_isCooldownExpired'); + if (_tapCount == 0 || _isCooldownExpired) { + _resetLastTap(); + } + _tapCount++; + if (_tapCount == widget.tapTriggerCount) { + widget.onRepeatedTap?.call(); + _tapCount = 0; + } + + _lastTapTime = DateTime.now(); + }, + child: widget.child, + ); + } + + void _resetLastTap() { + _lastTapTime = DateTime.now(); + _tapCount = 0; + } +} diff --git a/pubspec.lock b/pubspec.lock index e5dea6c62..0fb987b6e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -35,7 +35,7 @@ packages: name: asn1lib url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.4.1" async: dependency: transitive description: @@ -170,6 +170,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3+2" crypto: dependency: "direct main" description: @@ -358,7 +365,7 @@ packages: name: flutter_secure_storage_linux url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: @@ -372,14 +379,14 @@ packages: name: flutter_secure_storage_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_windows: dependency: transitive description: @@ -705,7 +712,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.5" + version: "6.1.1" pub_semver: dependency: transitive description: @@ -736,13 +743,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" - share: + share_plus: dependency: "direct main" description: - name: share + name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "4.5.3" + share_plus_linux: + dependency: transitive + description: + name: share_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + share_plus_macos: + dependency: transitive + description: + name: share_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + share_plus_web: + dependency: transitive + description: + name: share_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + share_plus_windows: + dependency: transitive + description: + name: share_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index cb8243851..c4b2717c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,8 +36,6 @@ dependencies: url_launcher: 6.1.5 # flutter.dev (Note: Supports null safety) - share: 2.0.4 # flutter.dev (Note: Supports null safety) - local_auth: 1.1.11 # flutter.dev (Note: Supports null safety) http: 0.13.4 # dart.dev (Note: Supports null safety) @@ -61,6 +59,9 @@ dependencies: path: packages/package_info_plus/package_info_plus/ ref: 08a7a35502d2498ea3dd1473445b4511dea35518 #1.4.2 + share_plus: ^4.5.3 + + #------------3rd party-------------