From 40d60ae2ecb665f21c86ea0cb9d8359edea78b4d Mon Sep 17 00:00:00 2001 From: vjrj <vjrj@comunes.org> Date: Wed, 5 Apr 2023 00:10:01 +0200 Subject: [PATCH] Improved export/import and android support --- .../widgets/fifth_screen/export_dialog.dart | 65 ++++++++++++++++-- .../widgets/fifth_screen/import_dialog.dart | 67 +++++++++++++++++-- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/lib/ui/widgets/fifth_screen/export_dialog.dart b/lib/ui/widgets/fifth_screen/export_dialog.dart index 86553530..df1fa33f 100644 --- a/lib/ui/widgets/fifth_screen/export_dialog.dart +++ b/lib/ui/widgets/fifth_screen/export_dialog.dart @@ -1,15 +1,20 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart'; import 'package:pattern_lock/pattern_lock.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/html.dart' as html; import '../../../g1/g1_helper.dart'; import '../../../shared_prefs.dart'; +import '../../logger.dart'; import '../../ui_helpers.dart'; import 'pattern_util.dart'; @@ -100,13 +105,11 @@ class _ExportDialogState extends State<ExportDialog> { final String fileJson = jsonEncode(jsonData); final List<int> bytes = utf8.encode(fileJson); - final html.Blob blob = html.Blob(<dynamic>[bytes]); - final String url = html.Url.createObjectUrlFromBlob(blob); - - final html.AnchorElement anchor = html.AnchorElement(href: url); - anchor.download = - 'ginkgo-wallet-${simplifyPubKey(SharedPreferencesHelper().getPubKey())}.json'; - anchor.click(); + if (kIsWeb) { + webDownload(bytes); + } else { + saveFile(bytes); + } if (!mounted) { return; @@ -118,4 +121,52 @@ class _ExportDialogState extends State<ExportDialog> { ), ); } + + void webDownload(List<int> bytes) { + final html.Blob blob = html.Blob(<dynamic>[bytes]); + final String url = html.Url.createObjectUrlFromBlob(blob); + + final html.AnchorElement anchor = html.AnchorElement(href: url); + anchor.download = + 'ginkgo-wallet-${simplifyPubKey(SharedPreferencesHelper().getPubKey())}.json'; + anchor.click(); + } + + Future<void> saveFile(List<int> bytes) async { + try { + final Directory? externalDirectory = + await getAppSpecificExternalFilesDirectory(); // ensureDownloadsDirectoryExists(); + if (externalDirectory == null) { + logger('Downloads directory not found'); + return; + } + final String fileName = walletFileName(); + final File file = File(join(externalDirectory.path, fileName)); + await file.writeAsBytes(bytes); + + logger('File saved at: ${file.path}'); + } catch (e, stacktrace) { + logger('Error saving wallet file $e'); + await Sentry.captureException(e, stackTrace: stacktrace); + } + } + + Future<void> saveFileApp(List<int> bytesList) async { + final Uint8List bytes = Uint8List.fromList(bytesList); + + final String fileName = walletFileName(); + + await FileSaver.instance.saveFile( + name: fileName, + bytes: bytes, + // 'application/json', + mimeType: MimeType.json, + ); + } + + String walletFileName() { + final String fileName = + 'ginkgo-wallet-${simplifyPubKey(SharedPreferencesHelper().getPubKey())}.json'; + return fileName; + } } diff --git a/lib/ui/widgets/fifth_screen/import_dialog.dart b/lib/ui/widgets/fifth_screen/import_dialog.dart index aa7be351..72580713 100644 --- a/lib/ui/widgets/fifth_screen/import_dialog.dart +++ b/lib/ui/widgets/fifth_screen/import_dialog.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:filesystem_picker/filesystem_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,7 +17,6 @@ import '../../../shared_prefs.dart'; import '../../logger.dart'; import '../../ui_helpers.dart'; import '../custom_error_widget.dart'; -import '../loading_box.dart'; import 'pattern_util.dart'; class ImportDialog extends StatefulWidget { @@ -31,9 +32,11 @@ class _ImportDialogState extends State<ImportDialog> { @override Widget build(BuildContext context) { return FutureBuilder<String>( - future: _importWallet(), + future: kIsWeb ? _importWalletWeb() : _importWallet(context), builder: (BuildContext context, AsyncSnapshot<String> snapshot) { - if (snapshot.hasData) { + if (snapshot.hasData && + snapshot.data != null && + snapshot.data!.isNotEmpty) { final String keyEncString = snapshot.data!; final Map<String, dynamic> keyJson = jsonDecode(keyEncString) as Map<String, dynamic>; @@ -112,12 +115,65 @@ class _ImportDialogState extends State<ImportDialog> { } else if (snapshot.hasError) { return CustomErrorWidget(snapshot.error); } else { - return const LoadingBox(); + return CustomErrorWidget(tr('import_failed')); } }); } - Future<String> _importWallet() async { + Future<String> _importWallet(BuildContext context) async { + try { + // Use file_picker to pick a file + + /* final bool hasPermission = await requestStoragePermission(context); + + if (hasPermission == null || !hasPermission) { + logger('No permission to access storage'); + return ''; + }*/ + + final Directory? appDocDir = await getAppSpecificExternalFilesDirectory(); + if (appDocDir == null) { + return ''; + } + logger('appDocDir: ${appDocDir.path}'); + + if (!mounted) { + return ''; + } + + final String? filePath = await FilesystemPicker.openDialog( + title: tr('select_file_to_import'), + context: context, + rootDirectory: appDocDir, + fsType: FilesystemType.file, + allowedExtensions: <String>['.json'], + // requestPermission: () async => _requestStoragePermission(context), + fileTileSelectMode: FileTileSelectMode.wholeTile, + ); + + if (filePath == null || filePath.isEmpty) { + return ''; + } + + final File file = File(filePath); + final String jsonString = await file.readAsString(); + + // Log the content if not in release mode + if (!kReleaseMode) { + logger(jsonString); + } + + return jsonString; + } catch (e, stacktrace) { + logger('Error importing wallet $e'); + await Sentry.captureException(e, stackTrace: stacktrace); + // Handle the exception using Sentry or any other error reporting tool + // await Sentry.captureException(e, stackTrace: stacktrace); + return ''; + } + } + + Future<String> _importWalletWeb() async { final Completer<String> completer = Completer<String>(); final html.InputElement input = html.InputElement()..type = 'file'; @@ -147,6 +203,7 @@ class _ImportDialogState extends State<ImportDialog> { } catch (e, stacktrace) { logger('Error importing wallet $e'); await Sentry.captureException(e, stackTrace: stacktrace); + completer.complete(''); } }); return completer.future; -- GitLab