mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 02:12:13 +00:00
Seamless recovery for iOS reinstallations
This commit is contained in:
parent
7634177191
commit
4d39eb0bf4
16 changed files with 594 additions and 48 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
## 0.2.11
|
||||
|
||||
- New: Seamless recovery for iOS reinstallations
|
||||
- Improved: Redesigned snackbar notifications
|
||||
- Improved: New backup mechanism to allow larger backup files
|
||||
- Improved: Move keys into a centralized Rust-owned structure stored in secure storage
|
||||
|
|
|
|||
21
lib/app.dart
21
lib/app.dart
|
|
@ -15,14 +15,20 @@ import 'package:twonly/src/visual/themes/dark.dart';
|
|||
import 'package:twonly/src/visual/themes/light.dart';
|
||||
import 'package:twonly/src/visual/views/critical_error.view.dart';
|
||||
import 'package:twonly/src/visual/views/home.view.dart';
|
||||
import 'package:twonly/src/visual/views/recovery.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/register.view.dart';
|
||||
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
|
||||
import 'package:twonly/src/visual/views/unlock_twonly.view.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({required this.storageError, super.key});
|
||||
const App({
|
||||
required this.storageError,
|
||||
required this.recoveryPossible,
|
||||
super.key,
|
||||
});
|
||||
final bool storageError;
|
||||
final bool recoveryPossible;
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
|
@ -88,6 +94,19 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||
);
|
||||
}
|
||||
|
||||
if (widget.recoveryPossible) {
|
||||
return MaterialApp(
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
debugShowCheckedModeBanner: false,
|
||||
supportedLocales: supportedLocales,
|
||||
title: 'twonly',
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
themeMode: context.read<SettingsChangeProvider>().themeMode,
|
||||
home: const RecoveryView(),
|
||||
);
|
||||
}
|
||||
|
||||
return MaterialApp.router(
|
||||
routerConfig: routerProvider,
|
||||
localizationsDelegates: localizationsDelegates,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ class RustKeyManager {
|
|||
.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity();
|
||||
|
||||
static Future<PlatformInt64?> getUserId() => RustLib.instance.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
||||
|
||||
static Future<void> importSignalIdentity({
|
||||
required List<int> identityKeyPairStructure,
|
||||
required PlatformInt64 registrationId,
|
||||
|
|
@ -40,6 +43,9 @@ class RustKeyManager {
|
|||
.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys();
|
||||
|
||||
static Future<void> removeKeyManager() => RustLib.instance.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager();
|
||||
|
||||
static Future<void> removeSignedPrekey({
|
||||
required PlatformInt64 signedPreKeyId,
|
||||
}) => RustLib.instance.api
|
||||
|
|
@ -47,6 +53,11 @@ class RustKeyManager {
|
|||
signedPreKeyId: signedPreKeyId,
|
||||
);
|
||||
|
||||
static Future<void> setUserId({required PlatformInt64 userId}) => RustLib
|
||||
.instance
|
||||
.api
|
||||
.crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId);
|
||||
|
||||
static Future<void> storeSignedPrekey({
|
||||
required PlatformInt64 signedPreKeyId,
|
||||
required List<int> record,
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
|||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => 1215442517;
|
||||
int get rustContentHash => -1867463121;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
|
|
@ -200,6 +200,8 @@ abstract class RustLibApi extends BaseApi {
|
|||
Future<(Uint8List, PlatformInt64)>
|
||||
crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity();
|
||||
|
||||
Future<PlatformInt64?> crateBridgeWrapperKeyManagerRustKeyManagerGetUserId();
|
||||
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({
|
||||
required List<int> identityKeyPairStructure,
|
||||
required PlatformInt64 registrationId,
|
||||
|
|
@ -214,10 +216,16 @@ abstract class RustLibApi extends BaseApi {
|
|||
Future<Map<PlatformInt64, Uint8List>>
|
||||
crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys();
|
||||
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager();
|
||||
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey({
|
||||
required PlatformInt64 signedPreKeyId,
|
||||
});
|
||||
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerSetUserId({
|
||||
required PlatformInt64 userId,
|
||||
});
|
||||
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey({
|
||||
required PlatformInt64 signedPreKeyId,
|
||||
required List<int> record,
|
||||
|
|
@ -1035,6 +1043,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
argNames: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<PlatformInt64?> crateBridgeWrapperKeyManagerRustKeyManagerGetUserId() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 20,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_opt_box_autoadd_i_64,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperKeyManagerRustKeyManagerGetUserIdConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperKeyManagerRustKeyManagerGetUserIdConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_key_manager_get_user_id",
|
||||
argNames: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({
|
||||
required List<int> identityKeyPairStructure,
|
||||
|
|
@ -1054,7 +1094,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 20,
|
||||
funcId: 21,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -1098,7 +1138,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 21,
|
||||
funcId: 22,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -1131,7 +1171,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 22,
|
||||
funcId: 23,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -1154,6 +1194,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
argNames: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 24,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManagerConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManagerConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_key_manager_remove_key_manager",
|
||||
argNames: [],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey({
|
||||
required PlatformInt64 signedPreKeyId,
|
||||
|
|
@ -1166,7 +1238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 23,
|
||||
funcId: 25,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
@ -1189,6 +1261,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
argNames: ["signedPreKeyId"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerSetUserId({
|
||||
required PlatformInt64 userId,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_i_64(userId, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 26,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta:
|
||||
kCrateBridgeWrapperKeyManagerRustKeyManagerSetUserIdConstMeta,
|
||||
argValues: [userId],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta
|
||||
get kCrateBridgeWrapperKeyManagerRustKeyManagerSetUserIdConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "rust_key_manager_set_user_id",
|
||||
argNames: ["userId"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey({
|
||||
required PlatformInt64 signedPreKeyId,
|
||||
|
|
@ -1203,7 +1310,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 24,
|
||||
funcId: 27,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mutex/mutex.dart';
|
||||
|
|
@ -90,6 +88,8 @@ void main() async {
|
|||
|
||||
var userExists = false;
|
||||
|
||||
var recoveryPossible = false;
|
||||
|
||||
if (!storageError) {
|
||||
try {
|
||||
userExists = await userService.tryInit();
|
||||
|
|
@ -99,12 +99,14 @@ void main() async {
|
|||
}
|
||||
}
|
||||
|
||||
if (Platform.isIOS && userExists) {
|
||||
final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite');
|
||||
if (!dbFile.existsSync()) {
|
||||
Log.error('[twonly] IOS: App was removed and then reinstalled again...');
|
||||
await SecureStorage.instance.deleteAll();
|
||||
userExists = false;
|
||||
if (!userExists && !storageError) {
|
||||
try {
|
||||
final userId = await RustKeyManager.getUserId();
|
||||
if (userId != null) {
|
||||
recoveryPossible = true;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Could not check KeyManager userId for iOS recovery: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +154,10 @@ void main() async {
|
|||
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
|
||||
ChangeNotifierProvider(create: (_) => PurchasesProvider()),
|
||||
],
|
||||
child: App(storageError: storageError),
|
||||
child: App(
|
||||
storageError: storageError,
|
||||
recoveryPossible: recoveryPossible,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3079,6 +3079,42 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Click here to open the app again'**
|
||||
String get recoverSuccessBody;
|
||||
|
||||
/// No description provided for @iosRecoveryWelcomeBack.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Welcome Back'**
|
||||
String get iosRecoveryWelcomeBack;
|
||||
|
||||
/// No description provided for @iosRecoveryPrompt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?'**
|
||||
String get iosRecoveryPrompt;
|
||||
|
||||
/// No description provided for @iosRecoveryNoBackupFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No backup archive could be retrieved from the server for this device.\n\nError: {error}\n\nPlease proceed to register a new twonly account.'**
|
||||
String iosRecoveryNoBackupFound(Object error);
|
||||
|
||||
/// No description provided for @registerNewAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Register New Account'**
|
||||
String get registerNewAccount;
|
||||
|
||||
/// No description provided for @tryRestoreAgain.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try Restore Again'**
|
||||
String get tryRestoreAgain;
|
||||
|
||||
/// No description provided for @registeringNewAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Registering new account'**
|
||||
String get registeringNewAccount;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -1736,4 +1736,25 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recoverSuccessBody => 'Klicke hier, um die App wieder zu öffnen';
|
||||
|
||||
@override
|
||||
String get iosRecoveryWelcomeBack => 'Willkommen zurück';
|
||||
|
||||
@override
|
||||
String get iosRecoveryPrompt =>
|
||||
'Wir haben eine zuvor gesicherte twonly-Identität auf diesem Gerät erkannt. Möchtest du deine Kontakte, Nachrichten und Einstellungen automatisch aus deinem Cloud-Archiv herunterladen und wiederherstellen?';
|
||||
|
||||
@override
|
||||
String iosRecoveryNoBackupFound(Object error) {
|
||||
return 'Für dieses Gerät konnte kein Backup-Archiv vom Server abgerufen werden.\n\nFehler: $error\n\nBitte fahre mit der Registrierung eines neuen twonly-Kontos fort.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get registerNewAccount => 'Neues Konto registrieren';
|
||||
|
||||
@override
|
||||
String get tryRestoreAgain => 'Wiederherstellung erneut versuchen';
|
||||
|
||||
@override
|
||||
String get registeringNewAccount => 'Neues Konto wird registriert';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1721,4 +1721,25 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recoverSuccessBody => 'Click here to open the app again';
|
||||
|
||||
@override
|
||||
String get iosRecoveryWelcomeBack => 'Welcome Back';
|
||||
|
||||
@override
|
||||
String get iosRecoveryPrompt =>
|
||||
'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?';
|
||||
|
||||
@override
|
||||
String iosRecoveryNoBackupFound(Object error) {
|
||||
return 'No backup archive could be retrieved from the server for this device.\n\nError: $error\n\nPlease proceed to register a new twonly account.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get registerNewAccount => 'Register New Account';
|
||||
|
||||
@override
|
||||
String get tryRestoreAgain => 'Try Restore Again';
|
||||
|
||||
@override
|
||||
String get registeringNewAccount => 'Registering new account';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 65bf6a4d161bfa0cd2db698446c58b4cd03db92c
|
||||
Subproject commit 75b97e912f2e72a8e2a5da65e8ad12f0d1091855
|
||||
|
|
@ -7,6 +7,7 @@ import 'package:clock/clock.dart' as clock;
|
|||
import 'package:http/http.dart' as http;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/backup.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||
|
|
@ -102,6 +103,11 @@ class BackupService {
|
|||
backup.identityLastSuccessFull!.isBefore(
|
||||
lastWeek.subtract(const Duration(days: 1)),
|
||||
))) {
|
||||
final backupId = await RustBackupIdentity.getBackupId();
|
||||
if (backupId == null) {
|
||||
Log.error('No backup password was set by the user.');
|
||||
backup.identityState = LastBackupUploadState.failed;
|
||||
} else {
|
||||
Log.info('Performing a identity backup.');
|
||||
final encryptedBackup =
|
||||
await RustBackupIdentity.getIdentityBackupBytes();
|
||||
|
|
@ -114,11 +120,6 @@ class BackupService {
|
|||
'Identity backup has a size of ${backupTempFile.statSync().size}.',
|
||||
);
|
||||
|
||||
final backupId = await RustBackupIdentity.getBackupId();
|
||||
if (backupId == null) {
|
||||
Log.error('Got empty backup id.');
|
||||
backup.identityState = LastBackupUploadState.failed;
|
||||
} else {
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: 'backup_identity',
|
||||
httpRequestMethod: 'PUT',
|
||||
|
|
@ -290,6 +291,19 @@ class BackupService {
|
|||
});
|
||||
}
|
||||
|
||||
static Future<RecoveryError?> tryToReinstallTheArchive() async {
|
||||
final userId = await RustKeyManager.getUserId();
|
||||
if (userId == null) return null;
|
||||
|
||||
final state = BackupRecovery(
|
||||
username: '',
|
||||
userId: userId,
|
||||
password: '',
|
||||
)..state = BackupRecoveryState.archiveBackupStarted;
|
||||
await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson());
|
||||
return _nextBackupStage();
|
||||
}
|
||||
|
||||
static Future<RecoveryError?> startFullBackupRecovery(
|
||||
String username,
|
||||
String password,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/constants/secure_storage.keys.dart';
|
||||
import 'package:twonly/src/model/json/userdata.model.dart';
|
||||
|
|
@ -30,7 +31,9 @@ class UserService {
|
|||
// 1. Try to load from KeyValueStore (user.json)
|
||||
final userDataMap = await KeyValueStore.get('user');
|
||||
if (userDataMap != null) {
|
||||
return UserData.fromJson(userDataMap);
|
||||
final userData = UserData.fromJson(userDataMap);
|
||||
await RustKeyManager.setUserId(userId: userData.userId);
|
||||
return userData;
|
||||
}
|
||||
|
||||
// 2. If not found, try to load from SecureStorage (Migration path)
|
||||
|
|
@ -58,6 +61,11 @@ class UserService {
|
|||
static Future<void> _migrateFromSecureStorage(UserData userData) async {
|
||||
// Currently empty migration logic as requested, but we MUST store the data
|
||||
await KeyValueStore.put('user', userData.toJson());
|
||||
try {
|
||||
await RustKeyManager.setUserId(userId: userData.userId);
|
||||
} catch (e) {
|
||||
Log.error('Could not set userId in RustKeyManager during migration: $e');
|
||||
}
|
||||
|
||||
// Optional: Log migration
|
||||
Log.info('Migrated user data from SecureStorage to KeyValueStore');
|
||||
|
|
@ -87,6 +95,11 @@ class UserService {
|
|||
|
||||
static Future<void> save(UserData user) async {
|
||||
await KeyValueStore.put('user', user.toJson());
|
||||
try {
|
||||
await RustKeyManager.setUserId(userId: user.userId);
|
||||
} catch (e) {
|
||||
Log.error('Could not set userId in RustKeyManager during save: $e');
|
||||
}
|
||||
await userService.tryInit();
|
||||
}
|
||||
|
||||
|
|
|
|||
178
lib/src/visual/views/recovery.view.dart
Normal file
178
lib/src/visual/views/recovery.view.dart
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:restart_app/restart_app.dart';
|
||||
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/backup.service.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class RecoveryView extends StatefulWidget {
|
||||
const RecoveryView({super.key});
|
||||
|
||||
@override
|
||||
State<RecoveryView> createState() => _RecoveryViewState();
|
||||
}
|
||||
|
||||
class _RecoveryViewState extends State<RecoveryView> {
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
bool _showRegisterNewPrompt = false;
|
||||
|
||||
Future<void> _startRestore() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final error = await BackupService.tryToReinstallTheArchive();
|
||||
if (!mounted) return;
|
||||
|
||||
if (error != null) {
|
||||
String msg;
|
||||
switch (error) {
|
||||
case RecoveryError.noInternet:
|
||||
msg = context.lang.recoverErrorNoInternet;
|
||||
case RecoveryError.usernameNotValid:
|
||||
msg = context.lang.recoverErrorUsernameNotValid;
|
||||
case RecoveryError.passwordInvalid:
|
||||
msg = context.lang.recoverErrorPasswordInvalid;
|
||||
case RecoveryError.tryAgainLater:
|
||||
msg = context.lang.recoverErrorTryAgainLater;
|
||||
case RecoveryError.unkownError:
|
||||
msg = context.lang.recoverErrorUnknown;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = msg;
|
||||
_showRegisterNewPrompt = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final userExists = await userService.tryInit();
|
||||
if (userExists && mounted) {
|
||||
await Restart.restartApp(
|
||||
notificationTitle: context.lang.recoverSuccessTitle,
|
||||
notificationBody: context.lang.recoverSuccessBody,
|
||||
forceKill: true,
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = context.lang.recoverErrorUnknown;
|
||||
_showRegisterNewPrompt = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _registerNewAccount() async {
|
||||
try {
|
||||
await RustKeyManager.removeKeyManager();
|
||||
} catch (e) {
|
||||
Log.error('Could not remove KeyManager during account reset: $e');
|
||||
}
|
||||
await deleteLocalUserData();
|
||||
if (!mounted) return;
|
||||
await Restart.restartApp(
|
||||
notificationTitle: 'twonly',
|
||||
notificationBody: context.lang.registeringNewAccount,
|
||||
forceKill: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.twonlySafeRecoverTitle),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 100),
|
||||
Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.cloudArrowDown,
|
||||
size: 80,
|
||||
color: context.color.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
context.lang.iosRecoveryWelcomeBack,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_showRegisterNewPrompt
|
||||
? context.lang.iosRecoveryNoBackupFound(
|
||||
_errorMessage ?? '',
|
||||
)
|
||||
: context.lang.iosRecoveryPrompt,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (!_showRegisterNewPrompt) ...[
|
||||
FilledButton.icon(
|
||||
onPressed: _isLoading ? null : _startRestore,
|
||||
icon: _isLoading
|
||||
? const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.restore_rounded),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
context.lang.twonlySafeRecoverBtn,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : _registerNewAccount,
|
||||
child: Text(context.lang.registerNewAccount),
|
||||
),
|
||||
] else ...[
|
||||
FilledButton.icon(
|
||||
onPressed: _registerNewAccount,
|
||||
icon: const Icon(Icons.person_add_rounded),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.redAccent,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
context.lang.registerNewAccount,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _startRestore,
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
label: Text(context.lang.tryRestoreAgain),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,19 @@ impl RustKeyManager {
|
|||
Ok(key_manager.main_key.get_login_token().to_vec())
|
||||
}
|
||||
|
||||
pub async fn get_user_id() -> Result<Option<i64>> {
|
||||
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
|
||||
Ok(key_manager.user_id)
|
||||
}
|
||||
|
||||
pub async fn set_user_id(user_id: i64) -> Result<()> {
|
||||
let ctx = get_twonly_flutter()?;
|
||||
let mut key_manager = ctx.key_manager.lock().await;
|
||||
key_manager.user_id = Some(user_id);
|
||||
key_manager.store_to_keychain(&ctx.secure_storage)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn import_signal_identity(
|
||||
identity_key_pair_structure: Vec<u8>,
|
||||
registration_id: i64,
|
||||
|
|
@ -89,4 +102,10 @@ impl RustKeyManager {
|
|||
Err(TwonlyError::SignalIdentityNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_key_manager() -> Result<()> {
|
||||
let ctx = get_twonly_flutter()?;
|
||||
crate::keys::KeyManager::remove_from_keychain(&ctx.secure_storage)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1215442517;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1867463121;
|
||||
|
||||
// Section: executor
|
||||
|
||||
|
|
@ -424,6 +424,43 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identi
|
|||
})().await)
|
||||
} })
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "rust_key_manager_get_user_id",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||
(move || async move {
|
||||
let output_ok =
|
||||
crate::bridge::wrapper::key_manager::RustKeyManager::get_user_id()
|
||||
.await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
|
|
@ -471,6 +508,21 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_preke
|
|||
})().await)
|
||||
} })
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_remove_key_manager", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
|
||||
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
|
||||
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
deserializer.end(); move |context| async move {
|
||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
|
||||
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::remove_key_manager().await?; Ok(output_ok)
|
||||
})().await)
|
||||
} })
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
|
|
@ -486,6 +538,46 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_pre
|
|||
})().await)
|
||||
} })
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "rust_key_manager_set_user_id",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_user_id = <i64>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||
(move || async move {
|
||||
let output_ok =
|
||||
crate::bridge::wrapper::key_manager::RustKeyManager::set_user_id(
|
||||
api_user_id,
|
||||
)
|
||||
.await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
|
|
@ -1327,11 +1419,14 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||
17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,4 +53,10 @@ impl KeyManager {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes the KeyManager from the secure keychain/local storage.
|
||||
pub fn remove_from_keychain(storage: &SecureStorage) -> Result<()> {
|
||||
storage.delete(KEY_MANAGER_ID)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,15 +92,15 @@ impl SecureStorage {
|
|||
/// Deletes the secret associated with the given key from the secure keyring.
|
||||
///
|
||||
/// If the key does not exist, this function returns `Ok(())` (idempotent).
|
||||
// pub fn delete(&self, key: &str) -> Result<(), String> {
|
||||
// let entry = self.get_entry(key)?;
|
||||
pub fn delete(&self, key: &str) -> Result<(), String> {
|
||||
let entry = self.get_entry(key)?;
|
||||
|
||||
// match entry.delete_credential() {
|
||||
// Ok(()) => Ok(()),
|
||||
// Err(KeyringError::NoEntry) => Ok(()),
|
||||
// Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)),
|
||||
// }
|
||||
// }
|
||||
match entry.delete_credential() {
|
||||
Ok(()) => Ok(()),
|
||||
Err(KeyringError::NoEntry) => Ok(()),
|
||||
Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a keyring entry with the appropriate platform modifiers.
|
||||
fn get_entry(&self, key: &str) -> Result<Entry, String> {
|
||||
|
|
@ -142,10 +142,10 @@ mod tests {
|
|||
assert_eq!(read_val, Some(secret.to_string()));
|
||||
|
||||
// 3. Delete the secret
|
||||
// storage.delete(key).expect("Failed to delete secret");
|
||||
storage.delete(key).expect("Failed to delete secret");
|
||||
|
||||
// 4. Verify the secret is gone
|
||||
// let after_delete = storage.read(key).expect("Failed to read after delete");
|
||||
// assert_eq!(after_delete, None);
|
||||
let after_delete = storage.read(key).expect("Failed to read after delete");
|
||||
assert_eq!(after_delete, None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue