| 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 | } |