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