data_section_widget.dart 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. // @dart=2.9
  2. import 'dart:async';
  3. import 'dart:io';
  4. import 'dart:ui';
  5. import 'package:ente_auth/core/configuration.dart';
  6. import 'package:ente_auth/ente_theme_data.dart';
  7. import 'package:ente_auth/l10n/l10n.dart';
  8. import 'package:ente_auth/models/code.dart';
  9. import 'package:ente_auth/services/authenticator_service.dart';
  10. import 'package:ente_auth/services/local_authentication_service.dart';
  11. import 'package:ente_auth/store/code_store.dart';
  12. import 'package:ente_auth/theme/ente_theme.dart';
  13. import 'package:ente_auth/ui/components/captioned_text_widget.dart';
  14. import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
  15. import 'package:ente_auth/ui/components/menu_item_widget.dart';
  16. import 'package:ente_auth/ui/settings/common_settings.dart';
  17. import 'package:ente_auth/utils/dialog_util.dart';
  18. import 'package:file_picker/file_picker.dart';
  19. import 'package:flutter/material.dart';
  20. import 'package:logging/logging.dart';
  21. import 'package:share_plus/share_plus.dart';
  22. class DataSectionWidget extends StatelessWidget {
  23. final _logger = Logger("AccountSectionWidget");
  24. final _codeFile = File(
  25. Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
  26. );
  27. DataSectionWidget({Key key}) : super(key: key);
  28. @override
  29. Widget build(BuildContext context) {
  30. return ExpandableMenuItemWidget(
  31. title: "Data",
  32. selectionOptionsWidget: _getSectionOptions(context),
  33. leadingIcon: Icons.key_outlined,
  34. );
  35. }
  36. Column _getSectionOptions(BuildContext context) {
  37. List<Widget> children = [];
  38. children.addAll([
  39. sectionOptionSpacing,
  40. MenuItemWidget(
  41. captionedTextWidget: const CaptionedTextWidget(
  42. title: "Import codes",
  43. ),
  44. pressedColor: getEnteColorScheme(context).fillFaint,
  45. trailingIcon: Icons.chevron_right_outlined,
  46. trailingIconIsMuted: true,
  47. onTap: () async {
  48. _showImportInstructionDialog(context);
  49. },
  50. ),
  51. sectionOptionSpacing,
  52. MenuItemWidget(
  53. captionedTextWidget: const CaptionedTextWidget(
  54. title: "Export codes",
  55. ),
  56. pressedColor: getEnteColorScheme(context).fillFaint,
  57. trailingIcon: Icons.chevron_right_outlined,
  58. trailingIconIsMuted: true,
  59. onTap: () async {
  60. _showExportWarningDialog(context);
  61. },
  62. ),
  63. sectionOptionSpacing,
  64. ]);
  65. return Column(
  66. children: children,
  67. );
  68. }
  69. Future<void> _showImportInstructionDialog(BuildContext context) async {
  70. final AlertDialog alert = AlertDialog(
  71. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  72. title: Text(
  73. "Import codes",
  74. style: Theme.of(context).textTheme.headline6,
  75. ),
  76. content: SingleChildScrollView(
  77. child: Column(
  78. children: [
  79. const Text(
  80. "Please select a file that contains a list of your codes in the following format",
  81. ),
  82. const SizedBox(
  83. height: 20,
  84. ),
  85. Container(
  86. color: Theme.of(context).colorScheme.gNavBackgroundColor,
  87. child: Padding(
  88. padding: const EdgeInsets.all(8),
  89. child: Text(
  90. "otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET",
  91. style: TextStyle(
  92. fontFeatures: const [FontFeature.tabularFigures()],
  93. fontFamily: Platform.isIOS ? "Courier" : "monospace",
  94. fontSize: 13,
  95. ),
  96. ),
  97. ),
  98. ),
  99. const SizedBox(
  100. height: 20,
  101. ),
  102. const Text(
  103. "The codes can be separated by a comma or a new line",
  104. ),
  105. ],
  106. ),
  107. ),
  108. actions: [
  109. TextButton(
  110. child: const Text(
  111. "Cancel",
  112. style: TextStyle(
  113. color: Colors.red,
  114. ),
  115. ),
  116. onPressed: () {
  117. Navigator.of(context, rootNavigator: true).pop('dialog');
  118. },
  119. ),
  120. TextButton(
  121. child: const Text(
  122. "Select file",
  123. ),
  124. onPressed: () {
  125. Navigator.of(context, rootNavigator: true).pop('dialog');
  126. _pickImportFile(context);
  127. },
  128. ),
  129. ],
  130. );
  131. return showDialog(
  132. context: context,
  133. builder: (BuildContext context) {
  134. return alert;
  135. },
  136. barrierColor: Colors.black12,
  137. );
  138. }
  139. Future<void> _showExportWarningDialog(BuildContext context) async {
  140. final AlertDialog alert = AlertDialog(
  141. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  142. title: Text(
  143. "Warning",
  144. style: Theme.of(context).textTheme.headline6,
  145. ),
  146. content: const Text(
  147. "The exported file contains sensitive information. Please store this safely.",
  148. ),
  149. actions: [
  150. TextButton(
  151. child: const Text(
  152. "I understand",
  153. style: TextStyle(
  154. color: Colors.red,
  155. ),
  156. ),
  157. onPressed: () {
  158. Navigator.of(context, rootNavigator: true).pop('dialog');
  159. _exportCodes(context);
  160. },
  161. ),
  162. TextButton(
  163. child: const Text(
  164. "Cancel",
  165. ),
  166. onPressed: () {
  167. Navigator.of(context, rootNavigator: true).pop('dialog');
  168. },
  169. ),
  170. ],
  171. );
  172. return showDialog(
  173. context: context,
  174. builder: (BuildContext context) {
  175. return alert;
  176. },
  177. barrierColor: Colors.black12,
  178. );
  179. }
  180. Future<void> _exportCodes(BuildContext context) async {
  181. final hasAuthenticated =
  182. await LocalAuthenticationService.instance.requestLocalAuthentication(
  183. context,
  184. "Please authenticate to export your codes",
  185. );
  186. if (!hasAuthenticated) {
  187. return;
  188. }
  189. if (_codeFile.existsSync()) {
  190. await _codeFile.delete();
  191. }
  192. final codes = await CodeStore.instance.getAllCodes();
  193. String data = "";
  194. for (final code in codes) {
  195. data += code.rawData + "\n";
  196. }
  197. _codeFile.writeAsStringSync(data);
  198. await Share.shareFiles([_codeFile.path]);
  199. Future.delayed(const Duration(seconds: 15), () async {
  200. if (_codeFile.existsSync()) {
  201. _codeFile.deleteSync();
  202. }
  203. });
  204. }
  205. Future<void> _pickImportFile(BuildContext context) async {
  206. final l10n = context.l10n;
  207. FilePickerResult result = await FilePicker.platform.pickFiles();
  208. if (result == null) {
  209. return;
  210. }
  211. final dialog = createProgressDialog(context, l10n.pleaseWaitTitle);
  212. await dialog.show();
  213. try {
  214. File file = File(result.files.single.path);
  215. final codes = await file.readAsString();
  216. List<String> splitCodes = codes.split(",");
  217. if (splitCodes.length == 1) {
  218. splitCodes = codes.split("\n");
  219. }
  220. final parsedCodes = [];
  221. for (final code in splitCodes) {
  222. try {
  223. parsedCodes.add(Code.fromRawData(code));
  224. } catch (e) {
  225. _logger.severe("Could not parse code", e);
  226. }
  227. }
  228. for (final code in parsedCodes) {
  229. await CodeStore.instance.addCode(code, shouldSync: false);
  230. }
  231. unawaited(AuthenticatorService.instance.sync());
  232. await dialog.hide();
  233. await showConfettiDialog(
  234. context: context,
  235. builder: (BuildContext context) {
  236. return AlertDialog(
  237. shape:
  238. RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  239. title: Text(
  240. "Yay!",
  241. style: Theme.of(context).textTheme.headline6,
  242. ),
  243. content: Text(
  244. "You have imported " + parsedCodes.length.toString() + " codes!",
  245. ),
  246. actions: [
  247. TextButton(
  248. child: Text(
  249. l10n.ok,
  250. style: TextStyle(
  251. color: Theme.of(context).colorScheme.onSurface,
  252. ),
  253. ),
  254. onPressed: () {
  255. Navigator.of(context, rootNavigator: true).pop('dialog');
  256. },
  257. ),
  258. ],
  259. );
  260. },
  261. );
  262. } catch (e) {
  263. await dialog.hide();
  264. await showErrorDialog(
  265. context,
  266. "Sorry",
  267. "Could not parse the selected file.\nPlease write to support@ente.io if you need help!",
  268. );
  269. }
  270. }
  271. }