Skip to content

Commit 5a7ce70

Browse files
committed
feat: Added the ability to export a given backup. Fixes #3.
1 parent 61b58f4 commit 5a7ce70

File tree

10 files changed

+137
-61
lines changed

10 files changed

+137
-61
lines changed

lib/i18n/en/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@
137137
"deleteBackupConfirmationDialog": {
138138
"title": "Delete backup",
139139
"message": "Do you want to delete this backup ?"
140+
},
141+
"exportBackupDialog": {
142+
"subject": "Export backup",
143+
"text": "Export the backup to save it or to share it."
144+
},
145+
"button": {
146+
"export": "Export",
147+
"delete": "Delete",
148+
"restore": "Restore"
140149
}
141150
}
142151
},

lib/i18n/fr/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@
137137
"deleteBackupConfirmationDialog": {
138138
"title": "Supprimer la sauvegarde",
139139
"message": "Voulez-vous vraiment supprimer cette sauvegarde ?"
140+
},
141+
"exportBackupDialog": {
142+
"subject": "Exporter la sauvegarde",
143+
"text": "Exporter la sauvegarde pour l'enregistrer ou la partager."
144+
},
145+
"button": {
146+
"export": "Exporter",
147+
"delete": "Supprimer",
148+
"restore": "Restaurer"
140149
}
141150
}
142151
},

lib/model/app_unlock/method.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,20 @@ import 'package:open_authenticator/widgets/dialog/text_input_dialog.dart';
1616
sealed class AppUnlockMethod {
1717
/// Tries to unlock the app.
1818
/// [context] is required so that we can interact with the user.
19-
Future<Result> tryUnlock(BuildContext context, AsyncNotifierProviderRef ref, UnlockReason reason);
19+
Future<Result> tryUnlock(BuildContext context, Ref ref, UnlockReason reason);
2020

2121
/// Triggered when this method has been chosen has the app unlock method.
2222
/// [unlockResult] is the result of the [tryUnlock] call.
23-
Future<void> onMethodChosen(AsyncNotifierProviderRef ref, {ResultSuccess? enableResult}) => Future.value();
23+
Future<void> onMethodChosen(Ref ref, {ResultSuccess? enableResult}) => Future.value();
2424

2525
/// Triggered when a new method will be used for app unlocking.
26-
Future<void> onMethodChanged(AsyncNotifierProviderRef ref, {ResultSuccess? disableResult}) => Future.value();
26+
Future<void> onMethodChanged(Ref ref, {ResultSuccess? disableResult}) => Future.value();
2727
}
2828

2929
/// Local authentication.
3030
class LocalAuthenticationAppUnlockMethod extends AppUnlockMethod {
3131
@override
32-
Future<Result> tryUnlock(BuildContext context, AsyncNotifierProviderRef ref, UnlockReason reason) async {
32+
Future<Result> tryUnlock(BuildContext context, Ref ref, UnlockReason reason) async {
3333
LocalAuthentication auth = LocalAuthentication();
3434
if (!(await auth.isDeviceSupported())) {
3535
return ResultError();
@@ -71,7 +71,7 @@ class LocalAuthenticationAppUnlockMethod extends AppUnlockMethod {
7171
/// Enter master password.
7272
class MasterPasswordAppUnlockMethod extends AppUnlockMethod {
7373
@override
74-
Future<Result> tryUnlock(BuildContext context, AsyncNotifierProviderRef ref, UnlockReason reason) async {
74+
Future<Result> tryUnlock(BuildContext context, Ref ref, UnlockReason reason) async {
7575
if (reason != UnlockReason.openApp && reason != UnlockReason.sensibleAction) {
7676
TotpList totps = await ref.read(totpRepositoryProvider.future);
7777
if (totps.isEmpty) {
@@ -107,15 +107,15 @@ class MasterPasswordAppUnlockMethod extends AppUnlockMethod {
107107
}
108108

109109
@override
110-
Future<void> onMethodChosen(AsyncNotifierProviderRef ref, {ResultSuccess? enableResult}) async {
110+
Future<void> onMethodChosen(Ref ref, {ResultSuccess? enableResult}) async {
111111
String? password = enableResult?.valueOrNull;
112112
if (await ref.read(passwordSignatureVerificationMethodProvider.notifier).enable(password)) {
113113
await ref.read(cryptoStoreProvider.notifier).deleteFromLocalStorage();
114114
}
115115
}
116116

117117
@override
118-
Future<void> onMethodChanged(AsyncNotifierProviderRef ref, {ResultSuccess? disableResult}) async {
118+
Future<void> onMethodChanged(Ref ref, {ResultSuccess? disableResult}) async {
119119
await ref.read(passwordSignatureVerificationMethodProvider.notifier).disable();
120120
await ref.read(cryptoStoreProvider.notifier).saveCurrentOnLocalStorage(checkSettings: false);
121121
}
@@ -124,7 +124,7 @@ class MasterPasswordAppUnlockMethod extends AppUnlockMethod {
124124
/// No unlock.
125125
class NoneAppUnlockMethod extends AppUnlockMethod {
126126
@override
127-
Future<Result> tryUnlock(BuildContext context, AsyncNotifierProviderRef ref, UnlockReason reason) => Future.value(const ResultSuccess());
127+
Future<Result> tryUnlock(BuildContext context, Ref ref, UnlockReason reason) => Future.value(const ResultSuccess());
128128
}
129129

130130
/// Configures the unlock reason for [UnlockChallenge]s.

lib/model/backup.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,21 @@ class Backup implements Comparable<Backup> {
7575
static const String kPasswordSignatureKey = 'passwordSignature';
7676

7777
/// The Riverpod ref.
78-
final AsyncNotifierProviderRef _ref;
78+
final Ref _ref;
7979

8080
/// The backup time.
8181
final DateTime dateTime;
8282

8383
/// Creates a new backup instance.
8484
Backup._({
85-
required AsyncNotifierProviderRef ref,
85+
required Ref ref,
8686
required this.dateTime,
8787
}) : _ref = ref;
8888

8989
/// Restore this backup.
9090
Future<Result> restore(String password) async {
9191
try {
92-
File file = await _getBackupPath();
92+
File file = await getBackupPath();
9393
if (!file.existsSync()) {
9494
throw _BackupFileDoesNotExistException(path: file.path);
9595
}
@@ -146,7 +146,7 @@ class Backup implements Comparable<Backup> {
146146
toBackup.add(decryptedTotp ?? totp);
147147
}
148148
HmacSecretKey hmacSecretKey = await HmacSecretKey.importRawKey(await newStore.key.exportRawKey(), Hash.sha256);
149-
File file = await _getBackupPath(createDirectory: true);
149+
File file = await getBackupPath(createDirectory: true);
150150
file.writeAsString(jsonEncode({
151151
kPasswordSignatureKey: base64.encode(await hmacSecretKey.signBytes(utf8.encode(password))),
152152
kSaltKey: base64.encode(newStore.salt.value),
@@ -164,7 +164,7 @@ class Backup implements Comparable<Backup> {
164164
/// Deletes this backup.
165165
Future<Result> delete() async {
166166
try {
167-
File file = await _getBackupPath();
167+
File file = await getBackupPath();
168168
if (file.existsSync()) {
169169
file.deleteSync();
170170
}
@@ -179,7 +179,7 @@ class Backup implements Comparable<Backup> {
179179
}
180180

181181
/// Returns the backup path (TOTPs and salt).
182-
Future<File> _getBackupPath({bool createDirectory = false}) async {
182+
Future<File> getBackupPath({bool createDirectory = false}) async {
183183
Directory directory = await BackupStore._getBackupsDirectory(create: createDirectory);
184184
return File(join(directory.path, '${dateTime.millisecondsSinceEpoch}.bak'));
185185
}

lib/pages/settings/entries/manage_backups.dart

Lines changed: 82 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import 'dart:io';
2+
13
import 'package:flutter/material.dart';
24
import 'package:flutter_riverpod/flutter_riverpod.dart';
35
import 'package:intl/intl.dart';
46
import 'package:open_authenticator/i18n/translations.g.dart';
57
import 'package:open_authenticator/model/backup.dart';
6-
import 'package:open_authenticator/utils/platform.dart';
78
import 'package:open_authenticator/utils/result.dart';
89
import 'package:open_authenticator/widgets/centered_circular_progress_indicator.dart';
910
import 'package:open_authenticator/widgets/dialog/confirmation_dialog.dart';
1011
import 'package:open_authenticator/widgets/dialog/text_input_dialog.dart';
12+
import 'package:open_authenticator/widgets/list/expand_list_tile.dart';
1113
import 'package:open_authenticator/widgets/waiting_overlay.dart';
14+
import 'package:share_plus/share_plus.dart';
1215

1316
/// Allows the user to restore a backup.
1417
class ManageBackupSettingsEntryWidget extends ConsumerWidget {
@@ -47,35 +50,31 @@ class _RestoreBackupDialog extends ConsumerStatefulWidget {
4750

4851
/// The restore backup dialog state.
4952
class _RestoreBackupDialogState extends ConsumerState<_RestoreBackupDialog> {
53+
/// The list global key.
54+
late GlobalKey listKey = GlobalKey();
55+
5056
@override
5157
Widget build(BuildContext context) {
5258
DateFormat formatter = DateFormat(_RestoreBackupDialog.kDateFormat);
5359
AsyncValue<List<Backup>> backups = ref.watch(backupStoreProvider);
5460
Widget content;
5561
switch (backups) {
5662
case AsyncData(:final value):
57-
content = ListView(
58-
shrinkWrap: true,
59-
children: [
60-
for (Backup backup in value)
61-
ListTile(
62-
title: Text(formatter.format(backup.dateTime)),
63-
onLongPress: currentPlatform.isDesktop ? null : () => deleteBackup(backup),
64-
contentPadding: EdgeInsets.zero,
65-
trailing: currentPlatform.isDesktop
66-
? Row(
67-
mainAxisSize: MainAxisSize.min,
68-
children: [
69-
createRestoreButton(backup),
70-
IconButton(
71-
onPressed: () => deleteBackup(backup),
72-
icon: const Icon(Icons.delete),
73-
),
74-
],
75-
)
76-
: createRestoreButton(backup),
77-
),
78-
],
63+
content = SizedBox(
64+
width: MediaQuery.of(context).size.width,
65+
child: ListView(
66+
key: listKey,
67+
shrinkWrap: true,
68+
children: [
69+
for (Backup backup in value)
70+
ExpandListTile(
71+
title: Text(
72+
formatter.format(backup.dateTime),
73+
),
74+
children: createBackupActions(backup),
75+
),
76+
],
77+
),
7978
);
8079
break;
8180
case AsyncError(:final error):
@@ -102,32 +101,68 @@ class _RestoreBackupDialogState extends ConsumerState<_RestoreBackupDialog> {
102101
);
103102
}
104103

105-
/// Creates the button that allows to restore the given [backup].
106-
Widget createRestoreButton(Backup backup) => IconButton(
107-
onPressed: () async {
108-
String? password = await TextInputDialog.prompt(
109-
context,
110-
title: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.title,
111-
message: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.message,
112-
password: true,
113-
);
114-
if (password == null || !mounted) {
115-
return;
116-
}
117-
Result result = await showWaitingOverlay(
118-
context,
119-
future: backup.restore(password),
120-
);
121-
if (mounted) {
122-
context.showSnackBarForResult(result);
123-
Navigator.pop(context);
124-
}
125-
},
126-
icon: const Icon(Icons.upload),
127-
);
104+
/// Creates the buttons to interact with a given [backup].
105+
List<Widget> createBackupActions(Backup backup) => [
106+
ListTile(
107+
dense: true,
108+
onTap: () => restoreBackup(backup),
109+
title: Text(translations.settings.backups.manageBackups.button.restore),
110+
leading: const Icon(Icons.upload),
111+
),
112+
ListTile(
113+
dense: true,
114+
onTap: () => exportBackup(backup),
115+
title: Text(translations.settings.backups.manageBackups.button.export),
116+
leading: const Icon(Icons.share),
117+
),
118+
ListTile(
119+
dense: true,
120+
onTap: () => deleteBackup(backup),
121+
title: Text(translations.settings.backups.manageBackups.button.delete),
122+
leading: const Icon(Icons.delete),
123+
),
124+
];
125+
126+
/// Asks the user for the given [backup] restoring.
127+
Future<void> restoreBackup(Backup backup) async {
128+
String? password = await TextInputDialog.prompt(
129+
context,
130+
title: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.title,
131+
message: translations.settings.backups.manageBackups.restoreBackupPasswordDialog.message,
132+
password: true,
133+
);
134+
if (password == null || !mounted) {
135+
return;
136+
}
137+
Result result = await showWaitingOverlay(
138+
context,
139+
future: backup.restore(password),
140+
);
141+
if (mounted) {
142+
context.showSnackBarForResult(result);
143+
Navigator.pop(context);
144+
}
145+
}
146+
147+
/// Asks the user for the given [backup] export.
148+
Future<void> exportBackup(Backup backup) async {
149+
RenderBox? box = listKey.currentContext?.findRenderObject() as RenderBox?;
150+
File file = await backup.getBackupPath();
151+
await Share.shareXFiles(
152+
[
153+
XFile(
154+
file.path,
155+
mimeType: 'application/json',
156+
),
157+
],
158+
subject: translations.settings.backups.manageBackups.exportBackupDialog.subject,
159+
text: translations.settings.backups.manageBackups.exportBackupDialog.text,
160+
sharePositionOrigin: box == null ? Rect.zero : (box.localToGlobal(Offset.zero) & box.size),
161+
);
162+
}
128163

129164
/// Asks the user for the given [backup] deletion.
130-
void deleteBackup(Backup backup) async {
165+
Future<void> deleteBackup(Backup backup) async {
131166
bool result = await ConfirmationDialog.ask(
132167
context,
133168
title: translations.settings.backups.manageBackups.deleteBackupConfirmationDialog.title,

macos/Flutter/GeneratedPluginRegistrant.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import path_provider_foundation
1717
import purchases_flutter
1818
import rate_my_app
1919
import screen_retriever
20+
import share_plus
2021
import shared_preferences_foundation
2122
import simple_secure_storage_darwin
2223
import sqlite3_flutter_libs
@@ -37,6 +38,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
3738
PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin"))
3839
SwiftRateMyAppPlugin.register(with: registry.registrar(forPlugin: "SwiftRateMyAppPlugin"))
3940
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
41+
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
4042
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
4143
SimpleSecureStoragePlugin.register(with: registry.registrar(forPlugin: "SimpleSecureStoragePlugin"))
4244
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))

pubspec.lock

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,22 @@ packages:
11701170
url: "https://pub.dev"
11711171
source: hosted
11721172
version: "2.4.0+4"
1173+
share_plus:
1174+
dependency: "direct main"
1175+
description:
1176+
name: share_plus
1177+
sha256: "3af2cda1752e5c24f2fc04b6083b40f013ffe84fb90472f30c6499a9213d5442"
1178+
url: "https://pub.dev"
1179+
source: hosted
1180+
version: "10.1.1"
1181+
share_plus_platform_interface:
1182+
dependency: transitive
1183+
description:
1184+
name: share_plus_platform_interface
1185+
sha256: c57c0bbfec7142e3a0f55633be504b796af72e60e3c791b44d5a017b985f7a48
1186+
url: "https://pub.dev"
1187+
source: hosted
1188+
version: "5.0.1"
11731189
shared_preferences:
11741190
dependency: "direct main"
11751191
description:

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dependencies:
5555
hashlib: ^1.21.0
5656
hashlib_codecs: ^2.6.0
5757
scrollable_positioned_list: ^0.3.8
58+
share_plus: ^10.1.1
5859

5960
dev_dependencies:
6061
# The "flutter_lints" package below contains a set of recommended lints to

windows/flutter/generated_plugin_registrant.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <firebase_core/firebase_core_plugin_c_api.h>
1313
#include <local_auth_windows/local_auth_plugin.h>
1414
#include <screen_retriever/screen_retriever_plugin.h>
15+
#include <share_plus/share_plus_windows_plugin_c_api.h>
1516
#include <simple_secure_storage_windows/simple_secure_storage_windows_plugin_c_api.h>
1617
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
1718
#include <url_launcher_windows/url_launcher_windows.h>
@@ -31,6 +32,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
3132
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
3233
ScreenRetrieverPluginRegisterWithRegistrar(
3334
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
35+
SharePlusWindowsPluginCApiRegisterWithRegistrar(
36+
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
3437
SimpleSecureStorageWindowsPluginCApiRegisterWithRegistrar(
3538
registry->GetRegistrarForPlugin("SimpleSecureStorageWindowsPluginCApi"));
3639
Sqlite3FlutterLibsPluginRegisterWithRegistrar(

windows/flutter/generated_plugins.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
99
firebase_core
1010
local_auth_windows
1111
screen_retriever
12+
share_plus
1213
simple_secure_storage_windows
1314
sqlite3_flutter_libs
1415
url_launcher_windows

0 commit comments

Comments
 (0)