diff --git a/lib/ui/settings/account_section_widget.dart b/lib/ui/settings/account_section_widget.dart index 8d7259e2b..09fa9f2b7 100644 --- a/lib/ui/settings/account_section_widget.dart +++ b/lib/ui/settings/account_section_widget.dart @@ -1,15 +1,7 @@ // @dart=2.9 -import 'dart:async'; -import 'dart:io'; -import 'dart:ui'; - import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/ente_theme_data.dart'; -import 'package:ente_auth/models/code.dart'; -import 'package:ente_auth/services/authenticator_service.dart'; import 'package:ente_auth/services/local_authentication_service.dart'; -import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/account/change_email_dialog.dart'; import 'package:ente_auth/ui/account/password_entry_page.dart'; @@ -20,19 +12,10 @@ import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:logging/logging.dart'; -import 'package:share_plus/share_plus.dart'; class AccountSectionWidget extends StatelessWidget { - final _logger = Logger("AccountSectionWidget"); - - final _codeFile = File( - Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt", - ); - AccountSectionWidget({Key key}) : super(key: key); @override @@ -136,240 +119,9 @@ class AccountSectionWidget extends StatelessWidget { }, ), sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: const CaptionedTextWidget( - title: "Import codes", - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - _showImportInstructionDialog(context); - }, - ), - sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: const CaptionedTextWidget( - title: "Export codes", - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - _showExportWarningDialog(context); - }, - ), - sectionOptionSpacing, ]); return Column( children: children, ); } - - Future _showImportInstructionDialog(BuildContext context) async { - final AlertDialog alert = AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - title: Text( - "Import codes", - style: Theme.of(context).textTheme.headline6, - ), - content: SingleChildScrollView( - child: Column( - children: [ - const Text( - "Please select a file that contains a list of your codes in the following format", - ), - const SizedBox( - height: 20, - ), - Container( - color: Theme.of(context).colorScheme.gNavBackgroundColor, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - "otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET", - style: TextStyle( - fontFeatures: const [FontFeature.tabularFigures()], - fontFamily: Platform.isIOS ? "Courier" : "monospace", - fontSize: 13, - ), - ), - ), - ), - const SizedBox( - height: 20, - ), - const Text( - "The codes can be separated by a comma or a new line", - ), - ], - ), - ), - actions: [ - TextButton( - child: const Text( - "Cancel", - style: TextStyle( - color: Colors.red, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - TextButton( - child: const Text( - "Select file", - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - _pickImportFile(context); - }, - ), - ], - ); - - return showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - barrierColor: Colors.black12, - ); - } - - Future _showExportWarningDialog(BuildContext context) async { - final AlertDialog alert = AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - title: Text( - "Warning", - style: Theme.of(context).textTheme.headline6, - ), - content: const Text( - "The exported file contains sensitive information. Please store this safely.", - ), - actions: [ - TextButton( - child: const Text( - "I understand", - style: TextStyle( - color: Colors.red, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - _exportCodes(context); - }, - ), - TextButton( - child: const Text( - "Cancel", - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - ], - ); - - return showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - barrierColor: Colors.black12, - ); - } - - Future _exportCodes(BuildContext context) async { - final hasAuthenticated = - await LocalAuthenticationService.instance.requestLocalAuthentication( - context, - "Please authenticate to export your codes", - ); - if (!hasAuthenticated) { - return; - } - if (_codeFile.existsSync()) { - await _codeFile.delete(); - } - final codes = await CodeStore.instance.getAllCodes(); - String data = ""; - for (final code in codes) { - data += code.rawData + "\n"; - } - _codeFile.writeAsStringSync(data); - await Share.shareFiles([_codeFile.path]); - Future.delayed(const Duration(seconds: 15), () async { - if (_codeFile.existsSync()) { - _codeFile.deleteSync(); - } - }); - } - - Future _pickImportFile(BuildContext context) async { - FilePickerResult result = await FilePicker.platform.pickFiles(); - if (result == null) { - return; - } - final dialog = createProgressDialog(context, "Please wait..."); - await dialog.show(); - try { - File file = File(result.files.single.path); - final codes = await file.readAsString(); - List splitCodes = codes.split(","); - if (splitCodes.length == 1) { - splitCodes = codes.split("\n"); - } - final parsedCodes = []; - for (final code in splitCodes) { - try { - parsedCodes.add(Code.fromRawData(code)); - } catch (e) { - _logger.severe("Could not parse code", e); - } - } - for (final code in parsedCodes) { - await CodeStore.instance.addCode(code, shouldSync: false); - } - unawaited(AuthenticatorService.instance.sync()); - await dialog.hide(); - await showConfettiDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - title: Text( - "Yay!", - style: Theme.of(context).textTheme.headline6, - ), - content: Text( - "You have imported " + parsedCodes.length.toString() + " codes!", - ), - actions: [ - TextButton( - child: Text( - "Okay", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - ], - ); - }, - ); - } catch (e) { - await dialog.hide(); - await showErrorDialog( - context, - "Sorry", - "Could not parse the selected file.\nPlease write to support@ente.io if you need help!", - ); - } - } } diff --git a/lib/ui/settings/data_section_widget.dart b/lib/ui/settings/data_section_widget.dart new file mode 100644 index 000000000..0603672f1 --- /dev/null +++ b/lib/ui/settings/data_section_widget.dart @@ -0,0 +1,282 @@ +// @dart=2.9 + +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:ente_auth/core/configuration.dart'; +import 'package:ente_auth/ente_theme_data.dart'; +import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/services/authenticator_service.dart'; +import 'package:ente_auth/services/local_authentication_service.dart'; +import 'package:ente_auth/store/code_store.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/ui/components/captioned_text_widget.dart'; +import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; +import 'package:ente_auth/ui/components/menu_item_widget.dart'; +import 'package:ente_auth/ui/settings/common_settings.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:share_plus/share_plus.dart'; + +class DataSectionWidget extends StatelessWidget { + final _logger = Logger("AccountSectionWidget"); + + final _codeFile = File( + Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt", + ); + + DataSectionWidget({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ExpandableMenuItemWidget( + title: "Data", + selectionOptionsWidget: _getSectionOptions(context), + leadingIcon: Icons.key_outlined, + ); + } + + Column _getSectionOptions(BuildContext context) { + List children = []; + children.addAll([ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Import codes", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + _showImportInstructionDialog(context); + }, + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Export codes", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + _showExportWarningDialog(context); + }, + ), + sectionOptionSpacing, + ]); + return Column( + children: children, + ); + } + + Future _showImportInstructionDialog(BuildContext context) async { + final AlertDialog alert = AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Text( + "Import codes", + style: Theme.of(context).textTheme.headline6, + ), + content: SingleChildScrollView( + child: Column( + children: [ + const Text( + "Please select a file that contains a list of your codes in the following format", + ), + const SizedBox( + height: 20, + ), + Container( + color: Theme.of(context).colorScheme.gNavBackgroundColor, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + "otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET", + style: TextStyle( + fontFeatures: const [FontFeature.tabularFigures()], + fontFamily: Platform.isIOS ? "Courier" : "monospace", + fontSize: 13, + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + const Text( + "The codes can be separated by a comma or a new line", + ), + ], + ), + ), + actions: [ + TextButton( + child: const Text( + "Cancel", + style: TextStyle( + color: Colors.red, + ), + ), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop('dialog'); + }, + ), + TextButton( + child: const Text( + "Select file", + ), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop('dialog'); + _pickImportFile(context); + }, + ), + ], + ); + + return showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + barrierColor: Colors.black12, + ); + } + + Future _showExportWarningDialog(BuildContext context) async { + final AlertDialog alert = AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Text( + "Warning", + style: Theme.of(context).textTheme.headline6, + ), + content: const Text( + "The exported file contains sensitive information. Please store this safely.", + ), + actions: [ + TextButton( + child: const Text( + "I understand", + style: TextStyle( + color: Colors.red, + ), + ), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop('dialog'); + _exportCodes(context); + }, + ), + TextButton( + child: const Text( + "Cancel", + ), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop('dialog'); + }, + ), + ], + ); + + return showDialog( + context: context, + builder: (BuildContext context) { + return alert; + }, + barrierColor: Colors.black12, + ); + } + + Future _exportCodes(BuildContext context) async { + final hasAuthenticated = + await LocalAuthenticationService.instance.requestLocalAuthentication( + context, + "Please authenticate to export your codes", + ); + if (!hasAuthenticated) { + return; + } + if (_codeFile.existsSync()) { + await _codeFile.delete(); + } + final codes = await CodeStore.instance.getAllCodes(); + String data = ""; + for (final code in codes) { + data += code.rawData + "\n"; + } + _codeFile.writeAsStringSync(data); + await Share.shareFiles([_codeFile.path]); + Future.delayed(const Duration(seconds: 15), () async { + if (_codeFile.existsSync()) { + _codeFile.deleteSync(); + } + }); + } + + Future _pickImportFile(BuildContext context) async { + FilePickerResult result = await FilePicker.platform.pickFiles(); + if (result == null) { + return; + } + final dialog = createProgressDialog(context, "Please wait..."); + await dialog.show(); + try { + File file = File(result.files.single.path); + final codes = await file.readAsString(); + List splitCodes = codes.split(","); + if (splitCodes.length == 1) { + splitCodes = codes.split("\n"); + } + final parsedCodes = []; + for (final code in splitCodes) { + try { + parsedCodes.add(Code.fromRawData(code)); + } catch (e) { + _logger.severe("Could not parse code", e); + } + } + for (final code in parsedCodes) { + await CodeStore.instance.addCode(code, shouldSync: false); + } + unawaited(AuthenticatorService.instance.sync()); + await dialog.hide(); + await showConfettiDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + title: Text( + "Yay!", + style: Theme.of(context).textTheme.headline6, + ), + content: Text( + "You have imported " + parsedCodes.length.toString() + " codes!", + ), + actions: [ + TextButton( + child: Text( + "Okay", + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + onPressed: () { + Navigator.of(context, rootNavigator: true).pop('dialog'); + }, + ), + ], + ); + }, + ); + } catch (e) { + await dialog.hide(); + await showErrorDialog( + context, + "Sorry", + "Could not parse the selected file.\nPlease write to support@ente.io if you need help!", + ); + } + } +} diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index cfef73fce..22e4a472d 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -8,6 +8,7 @@ import 'package:ente_auth/ui/settings/about_section_widget.dart'; import 'package:ente_auth/ui/settings/account_section_widget.dart'; import 'package:ente_auth/ui/settings/app_version_widget.dart'; import 'package:ente_auth/ui/settings/danger_section_widget.dart'; +import 'package:ente_auth/ui/settings/data_section_widget.dart'; import 'package:ente_auth/ui/settings/made_with_love_widget.dart'; import 'package:ente_auth/ui/settings/security_section_widget.dart'; import 'package:ente_auth/ui/settings/social_section_widget.dart'; @@ -61,6 +62,8 @@ class SettingsPage extends StatelessWidget { contents.addAll([ AccountSectionWidget(), sectionSpacing, + DataSectionWidget(), + sectionSpacing, const SecuritySectionWidget(), sectionSpacing, ]);