dev
dart 245 lines 8.82 KB
Raw
1 import 'dart:io';
2 import 'dart:ui';
3 import 'package:cake_wallet/core/execution_state.dart';
4 import 'package:cake_wallet/src/screens/base_page.dart';
5 import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
6 import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
7 import 'package:cake_wallet/src/widgets/primary_button.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/routes.dart';
10 import 'package:cake_wallet/utils/clipboard_util.dart';
11 import 'package:cake_wallet/utils/exception_handler.dart';
12 import 'package:cake_wallet/utils/share_util.dart';
13 import 'package:cake_wallet/utils/show_bar.dart';
14 import 'package:cake_wallet/utils/show_pop_up.dart';
15 import 'package:cake_wallet/view_model/backup_view_model.dart';
16 import 'package:file_picker/file_picker.dart';
17 import 'package:flutter/cupertino.dart';
18 import 'package:flutter/material.dart';
19 import 'package:flutter/services.dart';
20 import 'package:flutter_mobx/flutter_mobx.dart';
21
22 class BackupPage extends BasePage {
23 BackupPage(this.backupViewModelBase);
24
25 final BackupViewModelBase backupViewModelBase;
26
27 @override
28 String get title => S.current.backup;
29
30 @override
31 Widget body(BuildContext context) {
32 return Stack(
33 fit: StackFit.expand,
34 children: [
35 Center(
36 child: Container(
37 padding: EdgeInsets.only(left: 24, right: 24),
38 height: 300,
39 child: Column(
40 children: [
41 Text(
42 S.of(context).backup_password + ':',
43 textAlign: TextAlign.center,
44 style: Theme.of(context).textTheme.headlineMedium,
45 ),
46 Padding(
47 padding: EdgeInsets.only(top: 20, bottom: 10),
48 child: Observer(
49 builder: (_) => GestureDetector(
50 onTap: () {
51 ClipboardUtil.setSensitiveDataToClipboard(
52 ClipboardData(text: backupViewModelBase.backupPassword),
53 );
54 showBar<void>(
55 context,
56 S.of(context).transaction_details_copied(S.of(context).backup_password),
57 );
58 },
59 child: Text(
60 backupViewModelBase.backupPassword,
61 style: Theme.of(context).textTheme.titleLarge,
62 textAlign: TextAlign.center,
63 ),
64 ),
65 ),
66 ),
67 Padding(
68 padding: EdgeInsets.all(20),
69 child: Text(
70 S.of(context).write_down_backup_password,
71 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
72 color: Theme.of(context).colorScheme.onSurfaceVariant,
73 ),
74 textAlign: TextAlign.center,
75 ),
76 )
77 ],
78 ),
79 ),
80 ),
81 Positioned(
82 child: Column(
83 children: [
84 PrimaryButton(
85 onPressed: () => Navigator.of(context).pushNamed(Routes.editBackupPassword),
86 text: S.of(context).change_password,
87 color: Theme.of(context).colorScheme.surfaceContainer,
88 textColor: Theme.of(context).colorScheme.onSecondaryContainer,
89 ),
90 const SizedBox(height: 10),
91 Observer(
92 builder: (_) => LoadingPrimaryButton(
93 isLoading: backupViewModelBase.state is IsExecutingState,
94 onPressed: () => onExportBackup(context),
95 text: S.of(context).export_backup,
96 color: Theme.of(context).colorScheme.primary,
97 textColor: Theme.of(context).colorScheme.onPrimary,
98 ),
99 ),
100 ],
101 ),
102 bottom: 24,
103 left: 24,
104 right: 24,
105 )
106 ],
107 );
108 }
109
110 void onExportBackup(BuildContext context) {
111 if (backupViewModelBase.backupPassword.isEmpty) return;
112 showPopUp<void>(
113 context: context,
114 builder: (dialogContext) {
115 return AlertWithTwoActions(
116 alertTitle: S.of(context).export_backup,
117 alertContent: S.of(context).save_backup_password,
118 rightButtonText: S.of(context).seed_alert_yes,
119 leftButtonText: S.of(context).seed_alert_back,
120 leftAlertButtonStyle: AlertButtonStyle.error(context),
121 rightAlertButtonStyle: AlertButtonStyle.secondary(context),
122 actionRightButton: () async {
123 Navigator.of(dialogContext).pop();
124 final backupFuture = backupViewModelBase.exportBackup();
125 showPersistentActionOverlay(context, backupFuture, text: S.of(context).creating_backup);
126 final backup = await backupFuture;
127
128 if (backup == null) {
129 return;
130 }
131
132 if (Platform.isAndroid) {
133 onExportAndroid(context, backup);
134 } else if (Platform.isIOS) {
135 await share(backup, context);
136 } else {
137 await _saveFile(backup);
138 }
139 },
140 actionLeftButton: () => Navigator.of(dialogContext).pop(),
141 );
142 },
143 );
144 }
145
146 void onExportAndroid(BuildContext context, BackupExportFile backup) {
147 showPopUp<void>(
148 context: context,
149 builder: (dialogContext) {
150 return AlertWithTwoActions(
151 alertTitle: S.of(context).export_backup,
152 alertContent: S.of(context).select_destination,
153 rightButtonText: S.of(context).save_to_downloads,
154 leftButtonText: S.of(context).share,
155 actionRightButton: () async {
156 await backupViewModelBase.saveToDownload(backup.name, backup.file);
157 if (dialogContext.mounted && Navigator.canPop(dialogContext)) {
158 Navigator.of(dialogContext).pop();
159 }
160 if (context.mounted && Navigator.canPop(context)) {
161 await showBar<void>(context, S.of(context).file_saved);
162 }
163 },
164 actionLeftButton: () async {
165 Navigator.of(dialogContext).pop();
166 await share(backup, context);
167 });
168 });
169 }
170
171 Future<void> share(BackupExportFile backup, BuildContext context) async {
172 final path = await backupViewModelBase.saveBackupFileLocally(backup);
173 await ShareUtil.shareFile(filePath: path, fileName: backup.name, context: context);
174 await backupViewModelBase.removeBackupFileLocally(backup);
175 }
176
177 Future<void> _saveFile(BackupExportFile backup) async {
178 String? outputFile = await FilePicker.platform.saveFile(
179 dialogTitle: 'Save Your File to desired location',
180 fileName: backup.name,
181 lockParentWindow: true);
182
183 if (outputFile == null) return;
184
185 try {
186 await backup.file.copy(outputFile);
187 } catch (exception, stackTrace) {
188 await ExceptionHandler.onError(FlutterErrorDetails(
189 exception: exception,
190 stack: stackTrace,
191 library: "Export Backup",
192 ));
193 }
194 }
195 }
196
197 void showPersistentActionOverlay(BuildContext context, Future action, {String text = ""}) async {
198 final navigator = Navigator.of(context, rootNavigator: true);
199 showPopUp(
200 useRootNavigator: true,
201 context: context,
202 builder: (_) => PersistentActionOverlay(text: text),
203 barrierDismissible: false);
204 try {
205 await action;
206 } finally {
207 // this way even if action throws, it'll still pop and not softlock the app
208 navigator.pop();
209 }
210 }
211
212 class PersistentActionOverlay extends StatelessWidget {
213 const PersistentActionOverlay({super.key, required this.text});
214
215 final String text;
216
217 @override
218 Widget build(BuildContext context) {
219 return Center(
220 child: Padding(
221 padding: EdgeInsets.all(20),
222 child: ClipRRect(
223 borderRadius: BorderRadius.circular(20),
224 child: BackdropFilter(
225 filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
226 child: Container(
227 decoration: BoxDecoration(
228 borderRadius: BorderRadius.circular(20),
229 color: Theme.of(context).colorScheme.surfaceContainer.withAlpha(80)),
230 child: Padding(
231 padding: EdgeInsets.all(20),
232 child: Column(
233 mainAxisSize: MainAxisSize.min,
234 children: [
235 SizedBox(width: 36, height: 36, child: CupertinoActivityIndicator()),
236 if (text.isNotEmpty)
237 Text(text, style: TextStyle(fontSize: 24, fontWeight: FontWeight.w500))
238 ],
239 ),
240 ),
241 ))),
242 ),
243 );
244 }
245 }