diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f2c3952357..f5588c3a97 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -1385,6 +1385,12 @@ "type": "text", "placeholders": {} }, + "cannotEnableKeyBackup": "Cannot enable Chat Backup. Please Go to Settings to try it again.", + "@cannotEnableKeyBackup": { + "type": "text", + "placeholders": {} + }, + "cannotUploadKey": "Cannot store Key Backup.", "oopsPushError": "Oops! Unfortunately, an error occurred when setting up the push notifications.", "@oopsPushError": { "type": "text", @@ -2443,6 +2449,8 @@ "@enableAutoBackups": {}, "unlockOldMessages": "Unlock old messages", "@unlockOldMessages": {}, + "cannotUnlockBackupKey": "Cannot unlock Key backup.", + "@cannotUnlockBackupKey": {}, "storeInSecureStorageDescription": "Store the recovery key in the secure storage of this device.", "@storeInSecureStorageDescription": {}, "saveKeyManuallyDescription": "Save this key manually by triggering the system share dialog or clipboard.", diff --git a/lib/data/datasource/recovery_words_data_source.dart b/lib/data/datasource/recovery_words_data_source.dart index 8689310520..12006a7850 100644 --- a/lib/data/datasource/recovery_words_data_source.dart +++ b/lib/data/datasource/recovery_words_data_source.dart @@ -4,4 +4,6 @@ abstract class RecoveryWordsDataSource { Future getRecoveryWords(); Future saveRecoveryWords(String recoveryWords); + + Future deleteRecoveryWords(); } diff --git a/lib/data/datasource_impl/recovery_words_data_source_impl.dart b/lib/data/datasource_impl/recovery_words_data_source_impl.dart index e9cfc0c683..f685de3d60 100644 --- a/lib/data/datasource_impl/recovery_words_data_source_impl.dart +++ b/lib/data/datasource_impl/recovery_words_data_source_impl.dart @@ -18,4 +18,9 @@ class RecoveryWordsDataSourceImpl implements RecoveryWordsDataSource { Future saveRecoveryWords(String recoveryWords) async { return _recoveryWordsAPI.saveRecoveryWords(recoveryWords); } + + @override + Future deleteRecoveryWords() { + return _recoveryWordsAPI.deleteRecoveryWords(); + } } diff --git a/lib/data/network/contact/tom_contact_api.dart b/lib/data/network/contact/tom_contact_api.dart index 9f1f18b061..206c20df5d 100644 --- a/lib/data/network/contact/tom_contact_api.dart +++ b/lib/data/network/contact/tom_contact_api.dart @@ -26,7 +26,7 @@ class TomContactAPI { ); final response = await _client - .post( + .postToGetBody( IdentityEndpoint.matchUserIdServicePath .generateTwakeIdentityEndpoint(), data: requestBody.toJson(), diff --git a/lib/data/network/dio_client.dart b/lib/data/network/dio_client.dart index 9491761eaa..1a7f211acb 100644 --- a/lib/data/network/dio_client.dart +++ b/lib/data/network/dio_client.dart @@ -26,7 +26,7 @@ class DioClient { .catchError((error) => throw error); } - Future post( + Future postToGetBody( String path, { data, Map? queryParameters, @@ -49,6 +49,28 @@ class DioClient { .catchError((error) => throw error); } + Future post( + String path, { + data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + return await _dio + .post( + path, + data: data, + queryParameters: queryParameters, + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ) + .catchError((error) => throw error); + } + Future delete( String path, { data, @@ -64,7 +86,6 @@ class DioClient { options: options, cancelToken: cancelToken, ) - .then((value) => value.data) .catchError((error) => throw (error)); } diff --git a/lib/data/network/media/media_api.dart b/lib/data/network/media/media_api.dart index bfec887724..d8c1d552b0 100644 --- a/lib/data/network/media/media_api.dart +++ b/lib/data/network/media/media_api.dart @@ -22,7 +22,7 @@ class MediaAPI { await File(fileInfo.filePath).length(); dioHeaders[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; final response = await _client - .post( + .postToGetBody( HomeserverEndpoint.uploadMediaServicePath .generateHomeserverMediaEndpoint(), data: fileInfo.readStream ?? File(fileInfo.filePath).openRead(), diff --git a/lib/data/network/recovery_words/recovery_words_api.dart b/lib/data/network/recovery_words/recovery_words_api.dart index 431433aaac..0550171ab4 100644 --- a/lib/data/network/recovery_words/recovery_words_api.dart +++ b/lib/data/network/recovery_words/recovery_words_api.dart @@ -1,8 +1,10 @@ +import 'package:dio/dio.dart'; import 'package:fluffychat/data/model/recovery_words_json.dart'; import 'package:fluffychat/data/network/dio_client.dart'; import 'package:fluffychat/data/network/tom_endpoint.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/di/global/network_di.dart'; +import 'package:matrix/matrix.dart'; class RecoveryWordsAPI { final DioClient _client = @@ -26,4 +28,20 @@ class RecoveryWordsAPI { .onError((error, stackTrace) => throw Exception(error)); return response.statusCode >= 200 && response.statusCode <= 299; } + + Future deleteRecoveryWords() async { + final options = Options(validateStatus: _deleteRecoverySuccess); + final response = await _client + .delete(TomEndpoint.recoveryWordsServicePath.path, options: options) + .onError((error, stackTrace) { + Logs().e('RecoveryWordsAPI::deleteRecoveryWords() [Exception]', error); + throw Exception(error); + }); + return _deleteRecoverySuccess(response?.statusCode); + } + + bool _deleteRecoverySuccess(int? statusCode) { + if (statusCode == null) return false; + return statusCode >= 200 && statusCode <= 299 || statusCode == 404; + } } diff --git a/lib/data/repository/recovery_words_repository_impl.dart b/lib/data/repository/recovery_words_repository_impl.dart index 5b37b1e160..064a389328 100644 --- a/lib/data/repository/recovery_words_repository_impl.dart +++ b/lib/data/repository/recovery_words_repository_impl.dart @@ -16,4 +16,9 @@ class RecoveryWordsRepositoryImpl implements RecoveryWordsRepository { Future saveRecoveryWords(String recoveryWords) async { return await recoveryWordsDataSource.saveRecoveryWords(recoveryWords); } + + @override + Future deleteRecoveryWords() async { + return await recoveryWordsDataSource.deleteRecoveryWords(); + } } diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index bfb0d23415..ec77ea63c7 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -1,4 +1,5 @@ import 'dart:collection'; + import 'package:fluffychat/data/datasource/media/media_data_source.dart'; import 'package:fluffychat/data/datasource/recovery_words_data_source.dart'; import 'package:fluffychat/data/datasource/tom_configurations_datasource.dart'; @@ -25,6 +26,9 @@ import 'package:fluffychat/domain/usecase/download_file_for_preview_interactor.d import 'package:fluffychat/domain/usecase/forward/forward_message_interactor.dart'; import 'package:fluffychat/domain/usecase/get_contacts_interactor.dart'; import 'package:fluffychat/domain/usecase/preview_url/get_preview_url_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/delete_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/save_recovery_words_interactor.dart'; import 'package:fluffychat/domain/usecase/room/chat_room_search_interactor.dart'; import 'package:fluffychat/domain/usecase/room/create_new_group_chat_interactor.dart'; import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; @@ -34,8 +38,6 @@ import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; import 'package:fluffychat/domain/usecase/send_image_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; -import 'package:fluffychat/domain/usecases/get_recovery_words_interactor.dart'; -import 'package:fluffychat/domain/usecases/save_recovery_words_interactor.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:get_it/get_it.dart'; @@ -122,6 +124,9 @@ class GetItInitializer { getIt.registerLazySingleton( () => SaveRecoveryWordsInteractor(), ); + getIt.registerLazySingleton( + () => DeleteRecoveryWordsInteractor(), + ); getIt.registerFactory( () => GetContactsInteractor(), ); diff --git a/lib/domain/app_state/recovery_words/delete_recovery_states.dart b/lib/domain/app_state/recovery_words/delete_recovery_states.dart new file mode 100644 index 0000000000..1e5ba3f42a --- /dev/null +++ b/lib/domain/app_state/recovery_words/delete_recovery_states.dart @@ -0,0 +1,18 @@ +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; + +class DeleteRecoveryWordsFailed extends Failure { + final dynamic exception; + + const DeleteRecoveryWordsFailed({this.exception}); + + @override + List get props => [exception]; +} + +class DeleteRecoveryWordsSuccess extends Success { + const DeleteRecoveryWordsSuccess(); + + @override + List get props => []; +} diff --git a/lib/domain/repository/recovery_words_repository.dart b/lib/domain/repository/recovery_words_repository.dart index 0a8e2ad1f2..bdd21efd2f 100644 --- a/lib/domain/repository/recovery_words_repository.dart +++ b/lib/domain/repository/recovery_words_repository.dart @@ -4,4 +4,6 @@ abstract class RecoveryWordsRepository { Future getRecoveryWords(); Future saveRecoveryWords(String recoveryWords); + + Future deleteRecoveryWords(); } diff --git a/lib/domain/usecase/recovery/delete_recovery_words_interactor.dart b/lib/domain/usecase/recovery/delete_recovery_words_interactor.dart new file mode 100644 index 0000000000..bda2cff8ad --- /dev/null +++ b/lib/domain/usecase/recovery/delete_recovery_words_interactor.dart @@ -0,0 +1,24 @@ +import 'package:dartz/dartz.dart'; +import 'package:fluffychat/app_state/failure.dart'; +import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/app_state/recovery_words/delete_recovery_states.dart'; +import 'package:fluffychat/domain/repository/recovery_words_repository.dart'; +import 'package:matrix/matrix.dart'; + +class DeleteRecoveryWordsInteractor { + final RecoveryWordsRepository recoveryWordsRepository = + getIt.get(); + + Future> execute() async { + try { + final bool response = await recoveryWordsRepository.deleteRecoveryWords(); + return response + ? const Right(DeleteRecoveryWordsSuccess()) + : const Left(DeleteRecoveryWordsFailed()); + } catch (e) { + Logs().e('DeleteRecoveryWordsInteractor::execute() [Exception]', e); + return Left(DeleteRecoveryWordsFailed(exception: e)); + } + } +} diff --git a/lib/domain/usecases/get_recovery_words_interactor.dart b/lib/domain/usecase/recovery/get_recovery_words_interactor.dart similarity index 100% rename from lib/domain/usecases/get_recovery_words_interactor.dart rename to lib/domain/usecase/recovery/get_recovery_words_interactor.dart diff --git a/lib/domain/usecases/save_recovery_words_interactor.dart b/lib/domain/usecase/recovery/save_recovery_words_interactor.dart similarity index 89% rename from lib/domain/usecases/save_recovery_words_interactor.dart rename to lib/domain/usecase/recovery/save_recovery_words_interactor.dart index 0d0ece1de0..faa4abbac1 100644 --- a/lib/domain/usecases/save_recovery_words_interactor.dart +++ b/lib/domain/usecase/recovery/save_recovery_words_interactor.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/app_state/recovery_words/save_recovery_words_failed.dart'; import 'package:fluffychat/domain/app_state/recovery_words/save_recovery_words_success.dart'; import 'package:fluffychat/domain/repository/recovery_words_repository.dart'; +import 'package:matrix/matrix.dart'; class SaveRecoveryWordsInteractor { final RecoveryWordsRepository recoveryWordsRepository = @@ -18,6 +19,7 @@ class SaveRecoveryWordsInteractor { ? const Right(SaveRecoveryWordsSuccess()) : const Left(SaveRecoveryWordsFailed()); } catch (e) { + Logs().e('SaveRecoveryWordsInteractor::execute() [Exception]', e); return Left(SaveRecoveryWordsFailed(exception: e)); } } diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 8f3eebdad2..d5b8772d83 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -1,5 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_flat_button.dart'; import 'package:flutter/cupertino.dart'; @@ -374,7 +375,16 @@ class BootstrapDialogState extends State { cancelLabel: L10n.of(context)!.cancel, isDestructiveAction: true, )) { - setState(() => _createBootstrap(true)); + await TomBootstrapDialog( + wipe: true, + wipeRecovery: true, + client: widget.client, + ).show(context).then( + (value) => Navigator.of( + context, + rootNavigator: false, + ).pop(false), + ); } }, ) diff --git a/lib/pages/bootstrap/tom_bootstrap_dialog.dart b/lib/pages/bootstrap/tom_bootstrap_dialog.dart index d55143e227..c9c14b498e 100644 --- a/lib/pages/bootstrap/tom_bootstrap_dialog.dart +++ b/lib/pages/bootstrap/tom_bootstrap_dialog.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; -import 'package:fluffychat/domain/usecases/save_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/delete_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/save_recovery_words_interactor.dart'; import 'package:fluffychat/widgets/adaptive_flat_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -8,14 +9,19 @@ import 'package:matrix/encryption.dart'; import 'package:matrix/encryption/utils/bootstrap.dart'; import 'package:matrix/matrix.dart'; +import 'bootstrap_dialog.dart'; + class TomBootstrapDialog extends StatefulWidget { final bool wipe; + final bool wipeRecovery; final Client client; final RecoveryWords? recoveryWords; + const TomBootstrapDialog({ Key? key, this.recoveryWords, this.wipe = false, + this.wipeRecovery = false, required this.client, }) : super(key: key); @@ -32,8 +38,13 @@ class TomBootstrapDialog extends StatefulWidget { class TomBootstrapDialogState extends State { final _saveRecoveryWordsInteractor = getIt.get(); + final _deleteRecoveryWordsInteractor = + getIt.get(); Bootstrap? bootstrap; + String? titleText; + Widget? body; + final buttons = []; UploadRecoveryKeyState _uploadRecoveryKeyState = UploadRecoveryKeyState.initial; @@ -49,140 +60,278 @@ class TomBootstrapDialogState extends State { void _createBootstrap(bool wipe) async { _wipe = wipe; titleText = null; - _uploadRecoveryKeyState = UploadRecoveryKeyState.initial; + _uploadRecoveryKeyState = _initializeRecoveryKeyState(); WidgetsBinding.instance.addPostFrameCallback((_) { bootstrap = widget.client.encryption!.bootstrap(onUpdate: (_) => setState(() {})); }); } + UploadRecoveryKeyState _initializeRecoveryKeyState() { + if (widget.wipeRecovery) { + return UploadRecoveryKeyState.wipeRecovery; + } + + if (widget.recoveryWords != null) { + return UploadRecoveryKeyState.useExisting; + } + + return UploadRecoveryKeyState.initial; + } + @override Widget build(BuildContext context) { Logs().d( 'TomBootstrapDialogState::build(): BootstrapState = ${bootstrap?.state}', ); _wipe ??= widget.wipe; - final buttons = []; - Widget body = const LinearProgressIndicator(); - titleText = L10n.of(context)!.loadingPleaseWait; - - if (bootstrap?.newSsssKey?.recoveryKey != null && - _uploadRecoveryKeyState == UploadRecoveryKeyState.initial) { - Logs().d( - 'TomBootstrapDialogState::build(): start backup process with key ${bootstrap?.newSsssKey!.recoveryKey}', - ); - final key = bootstrap?.newSsssKey!.recoveryKey; - WidgetsBinding.instance.addPostFrameCallback( - (_) => _backUpInRecoveryVault(key), - ); - } else { - if (bootstrap != null) { - switch (bootstrap!.state) { - case BootstrapState.loading: - break; - case BootstrapState.askWipeSsss: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeSsss(_wipe!), - ); - break; - case BootstrapState.askBadSsss: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.ignoreBadSecrets(true), - ); - break; - case BootstrapState.askUseExistingSsss: - _uploadRecoveryKeyState = UploadRecoveryKeyState.useExisting; - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.useExistingSsss(!_wipe!), - ); - break; - case BootstrapState.askUnlockSsss: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.unlockedSsss(), - ); - break; - case BootstrapState.askNewSsss: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.newSsss(), - ); - break; - case BootstrapState.openExistingSsss: - WidgetsBinding.instance.addPostFrameCallback( - (_) => _unlockBackUp(), - ); - break; - case BootstrapState.askWipeCrossSigning: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeCrossSigning(_wipe!), - ); - break; - case BootstrapState.askSetupCrossSigning: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.askSetupCrossSigning( - setupMasterKey: true, - setupSelfSigningKey: true, - setupUserSigningKey: true, - ), - ); - break; - case BootstrapState.askWipeOnlineKeyBackup: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.wipeOnlineKeyBackup(_wipe!), - ); + body = _loadingContent(context); - break; - case BootstrapState.askSetupOnlineKeyBackup: - WidgetsBinding.instance.addPostFrameCallback( - (_) => bootstrap?.askSetupOnlineKeyBackup(true), - ); - break; - case BootstrapState.error: - titleText = L10n.of(context)!.oopsSomethingWentWrong; - body = const Icon(Icons.error_outline, color: Colors.red, size: 40); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => Navigator.of(context, rootNavigator: false) - .pop(false), - ), - ); - break; - case BootstrapState.done: - titleText = L10n.of(context)!.everythingReady; - body = Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset('assets/backup.png', fit: BoxFit.contain), - Text(L10n.of(context)!.yourChatBackupHasBeenSetUp), - ], - ); - buttons.add( - AdaptiveFlatButton( - label: L10n.of(context)!.close, - onPressed: () => Navigator.of(context, rootNavigator: false) - .pop(false), + switch (_uploadRecoveryKeyState) { + case UploadRecoveryKeyState.wipeRecovery: + WidgetsBinding.instance.addPostFrameCallback((_) { + _wipeRecoveryWord(); + }); + break; + case UploadRecoveryKeyState.wipeRecoveryFailed: + titleText = L10n.of(context)!.chatBackup; + body = Text( + L10n.of(context)!.cannotEnableKeyBackup, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), + ); + buttons.clear(); + buttons.add( + AdaptiveFlatButton( + label: L10n.of(context)!.close, + onPressed: () => + Navigator.of(context, rootNavigator: false).pop(false), + ), + ); + break; + case UploadRecoveryKeyState.created: + if (_createNewRecoveryKeySuccess()) { + Logs().d( + 'TomBootstrapDialogState::build(): start backup process with key ${bootstrap?.newSsssKey!.recoveryKey}', + ); + final key = bootstrap?.newSsssKey!.recoveryKey; + WidgetsBinding.instance.addPostFrameCallback((_) { + Logs().d( + 'TomBootstrapDialogState::build(): check if key is already in TOM = ${_existedRecoveryWordsInTom( + key, + )} - ${widget.recoveryWords?.words}', ); - break; + if (_existedRecoveryWordsInTom(key)) { + _uploadRecoveryKeyState = UploadRecoveryKeyState.uploaded; + return; + } + _backUpInRecoveryVault(key); + }); } - } + break; + case UploadRecoveryKeyState.uploaded: + _handleBootstrapState(); + break; + case UploadRecoveryKeyState.useExisting: + _handleBootstrapState(); + break; + case UploadRecoveryKeyState.unlockError: + titleText = L10n.of(context)!.chatBackup; + body = Text( + L10n.of(context)!.cannotUnlockBackupKey, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + buttons.clear(); + buttons + ..add( + AdaptiveFlatButton( + label: L10n.of(context)!.close, + onPressed: () => + Navigator.of(context, rootNavigator: false).pop(false), + ), + ) + ..add( + AdaptiveFlatButton( + label: L10n.of(context)!.next, + onPressed: () async { + await BootstrapDialog(client: widget.client).show(context).then( + (value) => Navigator.of(context, rootNavigator: false) + .pop(false), + ); + }, + ), + ); + break; + case UploadRecoveryKeyState.uploadError: + Logs().e('TomBootstrapDialogState::build(): upload recovery key error'); + titleText = L10n.of(context)!.chatBackup; + body = Text( + L10n.of(context)!.cannotUploadKey, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + buttons.clear(); + buttons.add( + AdaptiveFlatButton( + label: L10n.of(context)!.close, + onPressed: () => + Navigator.of(context, rootNavigator: false).pop(false), + ), + ); + break; + default: + _handleBootstrapState(); + break; } - final title = Text(titleText!); return AlertDialog( - title: title, + title: titleText != null ? Text(titleText!) : null, content: body, actions: buttons, ); } + bool _existedRecoveryWordsInTom(String? key) { + if (key == null && widget.recoveryWords != null) { + return true; + } + return widget.recoveryWords != null && widget.recoveryWords!.words == key; + } + + bool _createNewRecoveryKeySuccess() { + return bootstrap?.newSsssKey?.recoveryKey != null && + _uploadRecoveryKeyState == UploadRecoveryKeyState.created; + } + + void _handleBootstrapState() { + if (bootstrap != null) { + switch (bootstrap!.state) { + case BootstrapState.loading: + break; + case BootstrapState.askWipeSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.wipeSsss(_wipe!), + ); + break; + case BootstrapState.askBadSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.ignoreBadSecrets(true), + ); + break; + case BootstrapState.askUseExistingSsss: + _uploadRecoveryKeyState = UploadRecoveryKeyState.useExisting; + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.useExistingSsss(!_wipe!), + ); + break; + case BootstrapState.askUnlockSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.unlockedSsss(), + ); + break; + case BootstrapState.askNewSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.newSsss().then( + (_) => + _uploadRecoveryKeyState = UploadRecoveryKeyState.created, + ), + ); + break; + case BootstrapState.openExistingSsss: + WidgetsBinding.instance.addPostFrameCallback( + (_) => _unlockBackUp(), + ); + break; + case BootstrapState.askWipeCrossSigning: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.wipeCrossSigning(_wipe!), + ); + break; + case BootstrapState.askSetupCrossSigning: + _uploadRecoveryKeyState = + UploadRecoveryKeyState.uploadingCrossSigningKeys; + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.askSetupCrossSigning( + setupMasterKey: true, + setupSelfSigningKey: true, + setupUserSigningKey: true, + ), + ); + break; + case BootstrapState.askWipeOnlineKeyBackup: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.wipeOnlineKeyBackup(_wipe!), + ); + break; + case BootstrapState.askSetupOnlineKeyBackup: + WidgetsBinding.instance.addPostFrameCallback( + (_) => bootstrap?.askSetupOnlineKeyBackup(true), + ); + break; + case BootstrapState.error: + titleText = L10n.of(context)!.oopsSomethingWentWrong; + body = const Icon(Icons.error_outline, color: Colors.red, size: 40); + buttons.clear(); + buttons.add( + AdaptiveFlatButton( + label: L10n.of(context)!.close, + onPressed: () => + Navigator.of(context, rootNavigator: false).pop(false), + ), + ); + break; + case BootstrapState.done: + titleText = L10n.of(context)!.everythingReady; + body = Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/backup.png', fit: BoxFit.contain), + Text(L10n.of(context)!.yourChatBackupHasBeenSetUp), + ], + ); + buttons.clear(); + buttons.add( + AdaptiveFlatButton( + label: L10n.of(context)!.close, + onPressed: () => + Navigator.of(context, rootNavigator: false).pop(false), + ), + ); + break; + } + } + } + + Future _wipeRecoveryWord() async { + await _deleteRecoveryWordsInteractor.execute().then( + (either) => either.fold( + (failure) { + Logs().e( + 'TomBootstrapDialogState::_wipeRecoveryWord(): wipe recoveryWords failed', + ); + setState( + () => _uploadRecoveryKeyState = + UploadRecoveryKeyState.wipeRecoveryFailed, + ); + }, + (success) => setState( + () => _uploadRecoveryKeyState = UploadRecoveryKeyState.initial, + ), + ), + ); + } + Future _backUpInRecoveryVault(String? key) async { if (key == null) { setState(() { Logs().d( 'TomBootstrapDialogState::_backUpInRecoveryVault(): key null, upload failed', ); - _uploadRecoveryKeyState = UploadRecoveryKeyState.error; + _uploadRecoveryKeyState = UploadRecoveryKeyState.uploadError; }); } await _saveRecoveryWordsInteractor.execute(key!).then( @@ -192,7 +341,8 @@ class TomBootstrapDialogState extends State { 'TomBootstrapDialogState::_backUpInRecoveryVault(): upload recoveryWords failed', ); setState( - () => _uploadRecoveryKeyState = UploadRecoveryKeyState.error, + () => _uploadRecoveryKeyState = + UploadRecoveryKeyState.uploadError, ); }, (success) => setState( @@ -205,33 +355,64 @@ class TomBootstrapDialogState extends State { Future _unlockBackUp() async { final recoveryWords = widget.recoveryWords; if (recoveryWords == null) { - // error handling + Logs().e('TomBootstrapDialogState::_unlockBackUp(): recoveryWords null'); + setState(() { + _uploadRecoveryKeyState = UploadRecoveryKeyState.unlockError; + }); return; } try { + Logs().d('TomBootstrapDialogState::_unlockBackUp() unlocking'); await bootstrap?.newSsssKey!.unlock( keyOrPassphrase: recoveryWords.words, ); - Logs().d('SSSS unlocked'); + Logs().d('TomBootstrapDialogState::_unlockBackUp() self Signing'); await bootstrap?.client.encryption!.crossSigning.selfSign( keyOrPassphrase: recoveryWords.words, ); - Logs().d('Successful elfsigned'); + Logs().d('TomBootstrapDialogState::_unlockBackUp() open existing SSSS'); await bootstrap?.openExistingSsss(); } catch (e, s) { - Logs().w('Unable to unlock SSSS', e, s); - setState( - () => titleText = L10n.of(context)!.oopsSomethingWentWrong, + Logs().w( + 'TomBootstrapDialogState::_unlockBackUp() Unable to unlock SSSS', + e, + s, ); + setState(() { + _uploadRecoveryKeyState = UploadRecoveryKeyState.unlockError; + }); } finally { setState(() {}); } } + + Widget _loadingContent(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(right: 16.0), + child: CircularProgressIndicator.adaptive(), + ), + Expanded( + child: Text( + L10n.of(context)!.loadingPleaseWait, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } } enum UploadRecoveryKeyState { initial, + wipeRecovery, + wipeRecoveryFailed, + created, + uploadingCrossSigningKeys, uploaded, - error, + uploadError, useExisting, + unlockError, } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 43bcd63928..a7a327c67b 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -5,8 +5,9 @@ import 'package:async/async.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; -import 'package:fluffychat/domain/usecases/get_recovery_words_interactor.dart'; +import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; import 'package:fluffychat/mixin/comparable_presentation_contact_mixin.dart'; +import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/chat_list/receive_sharing_intent_mixin.dart'; @@ -27,7 +28,6 @@ import 'package:matrix/matrix.dart'; import '../../../utils/account_bundles.dart'; import '../../utils/voip/callkeep_manager.dart'; import '../../widgets/matrix.dart'; -import '../bootstrap/bootstrap_dialog.dart'; enum SelectMode { normal, @@ -477,6 +477,7 @@ class ChatListController extends State await client.accountDataLoading; if (client.prevBatch == null) { await client.onSync.stream.first; + await client.initCompleter?.future; // Display first login bootstrap if enabled if (client.encryption?.keyManager.enabled == true) { @@ -504,9 +505,10 @@ class ChatListController extends State 'ChatListController::_waitForFirstSync(): encryption is not enabled', ); final recoveryWords = await _getRecoveryWords(); - if (recoveryWords == null) { - await TomBootstrapDialog(client: client).show(context); - } + await TomBootstrapDialog( + client: client, + wipeRecovery: recoveryWords != null, + ).show(context); } } if (!mounted) return; diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index fbab65fd1c..293f838a7a 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -1,14 +1,15 @@ +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/utils/fluffy_share.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; -import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/utils/fluffy_share.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; -import 'package:fluffychat/widgets/matrix.dart'; + import 'settings.dart'; class SettingsView extends StatelessWidget { @@ -18,7 +19,6 @@ class SettingsView extends StatelessWidget { @override Widget build(BuildContext context) { - final showChatBackupBanner = controller.showChatBackupBanner; return Scaffold( appBar: AppBar( leading: CloseButton( @@ -138,20 +138,6 @@ class SettingsView extends StatelessWidget { }, ), const Divider(thickness: 1), - if (showChatBackupBanner == null) - ListTile( - leading: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.chatBackup), - trailing: const CircularProgressIndicator.adaptive(), - ) - else - SwitchListTile.adaptive( - controlAffinity: ListTileControlAffinity.trailing, - value: controller.showChatBackupBanner == false, - secondary: const Icon(Icons.backup_outlined), - title: Text(L10n.of(context)!.chatBackup), - onChanged: controller.firstRunBootstrapAction, - ), const Divider(thickness: 1), // ListTile( // leading: const Icon(Icons.format_paint_outlined), diff --git a/lib/widgets/adaptive_flat_button.dart b/lib/widgets/adaptive_flat_button.dart index 7faa7eed48..b2d58a94f1 100644 --- a/lib/widgets/adaptive_flat_button.dart +++ b/lib/widgets/adaptive_flat_button.dart @@ -1,8 +1,5 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; - class AdaptiveFlatButton extends StatelessWidget { final String label; final Color? textColor; @@ -17,18 +14,13 @@ class AdaptiveFlatButton extends StatelessWidget { @override Widget build(BuildContext context) { - if (PlatformInfos.isCupertinoStyle) { - return CupertinoDialogAction( - onPressed: onPressed, - textStyle: textColor != null ? TextStyle(color: textColor) : null, - child: Text(label), - ); - } return TextButton( onPressed: onPressed, child: Text( label, - style: TextStyle(color: textColor), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index db6aaa2bc0..fcaa50ec55 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1534,7 +1534,7 @@ packages: description: path: "." ref: twake-supported - resolved-ref: "186ded3be6d5acbe3476e61a38f6aeb41cac5347" + resolved-ref: "3cc4ded9cd70652b35b56bfbeb4ca0525ada1166" url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.2"