From 72ef16c6f64bee32550fcd3c583e80b3b0f6fe6f Mon Sep 17 00:00:00 2001 From: NaiJi Date: Thu, 26 May 2022 04:02:06 +0300 Subject: [PATCH] Implement recovery key pages and device cubit Co-authored-by: Inex Code --- assets/translations/en.json | 13 +- lib/logic/common_enum/common_enum.dart | 7 + lib/logic/cubit/devices/devices_cubit.dart | 75 ++++++ lib/logic/cubit/devices/devices_state.dart | 33 +++ .../recovery_key/recovery_key_cubit.dart | 7 +- .../recovery_key/recovery_key_state.dart | 9 +- .../server_installation_cubit.dart | 5 +- .../server_installation_repository.dart | 24 ++ .../brand_button/filled_button.dart | 17 +- lib/ui/pages/recovery_key/recovery_key.dart | 234 +++++++++++++++++- .../recovery_key/recovery_key_receiving.dart | 36 +++ .../recovering/recover_by_old_token.dart | 1 - .../recovering/recovery_method_select.dart | 103 ++++---- .../setup/recovering/recovery_routing.dart | 8 +- 14 files changed, 502 insertions(+), 70 deletions(-) create mode 100644 lib/logic/cubit/devices/devices_cubit.dart create mode 100644 lib/logic/cubit/devices/devices_state.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 7cbfdd4f..73234e0c 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -325,13 +325,24 @@ "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" }, "recovery_key": { + "key_connection_error": "Couldn't connect to the server.", + "key_synchronizing": "Synchronizing...", "key_main_header": "Recovery key", "key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.", "key_amount_toggle": "Limit by number of uses", "key_amount_field_title": "Max number of uses", "key_duedate_toggle": "Limit by time", "key_duedate_field_title": "Due date of expiration", - "key_receive_button": "Receive key" + "key_receive_button": "Receive key", + "key_valid": "Your key is valid", + "key_invalid": "Your key is no longer valid", + "key_valid_until": "Valid until {}", + "key_valid_for": "Valid for {} uses", + "key_creation_date": "Created on {}", + "key_replace_button": "Generate new key", + "key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:", + "key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.", + "key_receiving_done": "Done!" }, "modals": { "_comment": "messages in modals", diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index edaaf567..94acface 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -3,6 +3,13 @@ import 'package:flutter/cupertino.dart'; import 'package:ionicons/ionicons.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; +enum LoadingStatus { + uninitialized, + refreshing, + success, + error, +} + enum InitializingSteps { setHetznerKey, setCloudFlareKey, diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart new file mode 100644 index 00000000..4ec51d84 --- /dev/null +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -0,0 +1,75 @@ +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/json/api_token.dart'; + +part 'devices_state.dart'; + +class ApiDevicesCubit + extends ServerInstallationDependendCubit { + ApiDevicesCubit(ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, const ApiDevicesState.initial()); + + final api = ServerApi(); + + @override + void load() async { + if (serverInstallationCubit.state is ServerInstallationFinished) { + emit(const ApiDevicesState([], LoadingStatus.refreshing)); + final devices = await _getApiTokens(); + if (devices != null) { + emit(ApiDevicesState(devices, LoadingStatus.success)); + } else { + emit(const ApiDevicesState([], LoadingStatus.error)); + } + } + } + + Future refresh() async { + emit(const ApiDevicesState([], LoadingStatus.refreshing)); + final devices = await _getApiTokens(); + if (devices != null) { + emit(ApiDevicesState(devices, LoadingStatus.success)); + } else { + emit(const ApiDevicesState([], LoadingStatus.error)); + } + } + + Future?> _getApiTokens() async { + final response = await api.getApiTokens(); + if (response.isSuccess) { + return response.data; + } else { + return null; + } + } + + Future deleteDevice(ApiToken device) async { + final response = await api.deleteApiToken(device.name); + if (response.isSuccess) { + emit(ApiDevicesState( + state.devices.where((d) => d.name != device.name).toList(), + LoadingStatus.success)); + } else { + getIt() + .showSnackBar(response.errorMessage ?? 'Error deleting device'); + } + } + + Future getNewDeviceKey() async { + final response = await api.createDeviceToken(); + if (response.isSuccess) { + return response.data; + } else { + getIt().showSnackBar( + response.errorMessage ?? 'Error getting new device key'); + return null; + } + } + + @override + void clear() { + emit(const ApiDevicesState.initial()); + } +} diff --git a/lib/logic/cubit/devices/devices_state.dart b/lib/logic/cubit/devices/devices_state.dart new file mode 100644 index 00000000..bccc5e29 --- /dev/null +++ b/lib/logic/cubit/devices/devices_state.dart @@ -0,0 +1,33 @@ +part of 'devices_cubit.dart'; + +class ApiDevicesState extends ServerInstallationDependendState { + const ApiDevicesState(this._devices, this.status); + + const ApiDevicesState.initial() : this(const [], LoadingStatus.uninitialized); + final List _devices; + final LoadingStatus status; + + List get devices => _devices; + ApiToken get thisDevice => _devices.firstWhere((device) => device.isCaller, + orElse: () => ApiToken( + name: 'Error fetching device', + isCaller: true, + date: DateTime.now(), + )); + + List get otherDevices => + _devices.where((device) => !device.isCaller).toList(); + + ApiDevicesState copyWith({ + List? devices, + LoadingStatus? status, + }) { + return ApiDevicesState( + devices ?? _devices, + status ?? this.status, + ); + } + + @override + List get props => [_devices]; +} diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart index 6092a03d..6b120d0e 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -1,4 +1,5 @@ import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; @@ -18,7 +19,8 @@ class RecoveryKeyCubit if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { - emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good)); + emit(state.copyWith( + status: status, loadingStatus: LoadingStatus.success)); } } else { emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); @@ -41,7 +43,8 @@ class RecoveryKeyCubit if (status == null) { emit(state.copyWith(loadingStatus: LoadingStatus.error)); } else { - emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good)); + emit( + state.copyWith(status: status, loadingStatus: LoadingStatus.success)); } } diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart index c88a9138..f5eb1090 100644 --- a/lib/logic/cubit/recovery_key/recovery_key_state.dart +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -1,12 +1,5 @@ part of 'recovery_key_cubit.dart'; -enum LoadingStatus { - uninitialized, - refreshing, - good, - error, -} - class RecoveryKeyState extends ServerInstallationDependendState { const RecoveryKeyState(this._status, this.loadingStatus); @@ -20,7 +13,7 @@ class RecoveryKeyState extends ServerInstallationDependendState { bool get exists => _status.exists; bool get isValid => _status.valid; DateTime? get generatedAt => _status.date; - DateTime? get expiresAt => _status.date; + DateTime? get expiresAt => _status.expiration; int? get usesLeft => _status.usesLeft; @override List get props => [_status, loadingStatus]; diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart index 311f41e1..9aedf6fb 100644 --- a/lib/logic/cubit/server_installation/server_installation_cubit.dart +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -307,8 +307,8 @@ class ServerInstallationCubit extends Cubit { return; } try { - Future Function(ServerDomain, String) - recoveryFunction; + Future Function( + ServerDomain, String, ServerRecoveryCapabilities) recoveryFunction; switch (method) { case ServerRecoveryMethods.newDeviceKey: recoveryFunction = repository.authorizeByNewDeviceKey; @@ -325,6 +325,7 @@ class ServerInstallationCubit extends Cubit { final serverDetails = await recoveryFunction( serverDomain, token, + dataState.recoveryCapabilities, ); await repository.saveServerDetails(serverDetails); emit(dataState.copyWith( diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart index e4e07804..5a1bfb58 100644 --- a/lib/logic/cubit/server_installation/server_installation_repository.dart +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -392,6 +392,7 @@ class ServerInstallationRepository { Future authorizeByNewDeviceKey( ServerDomain serverDomain, String newDeviceKey, + ServerRecoveryCapabilities recoveryCapabilities, ) async { var serverApi = ServerApi( isWithToken: false, @@ -424,6 +425,7 @@ class ServerInstallationRepository { Future authorizeByRecoveryKey( ServerDomain serverDomain, String recoveryKey, + ServerRecoveryCapabilities recoveryCapabilities, ) async { var serverApi = ServerApi( isWithToken: false, @@ -455,12 +457,34 @@ class ServerInstallationRepository { Future authorizeByApiToken( ServerDomain serverDomain, String apiToken, + ServerRecoveryCapabilities recoveryCapabilities, ) async { var serverApi = ServerApi( isWithToken: false, overrideDomain: serverDomain.domainName, customToken: apiToken, ); + if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { + final apiResponse = await serverApi.servicesPowerCheck(); + if (apiResponse.isNotEmpty) { + return ServerHostingDetails( + apiToken: apiToken, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.unknown, + id: 0, + ip4: '', + startTime: null, + createTime: null, + ); + } else { + throw ServerAuthorizationException( + 'Couldn\'t connect to server with this token', + ); + } + } final deviceAuthKey = await serverApi.createDeviceToken(); final apiResponse = await serverApi.authorizeDevice( DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data)); diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart index 06822d4f..a3230ddf 100644 --- a/lib/ui/components/brand_button/filled_button.dart +++ b/lib/ui/components/brand_button/filled_button.dart @@ -6,20 +6,29 @@ class FilledButton extends StatelessWidget { this.onPressed, this.title, this.child, + this.disabled = false, }) : super(key: key); final VoidCallback? onPressed; final String? title; final Widget? child; + final bool disabled; @override Widget build(BuildContext context) { + final ButtonStyle _enabledStyle = ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onPrimary, + primary: Theme.of(context).colorScheme.primary, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); + + final ButtonStyle _disabledStyle = ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onSurface.withAlpha(30), + primary: Theme.of(context).colorScheme.onSurface.withAlpha(98), + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); + return ElevatedButton( onPressed: onPressed, - style: ElevatedButton.styleFrom( - onPrimary: Theme.of(context).colorScheme.onPrimary, - primary: Theme.of(context).colorScheme.primary, - ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + style: disabled ? _disabledStyle : _enabledStyle, child: child ?? Text(title ?? ''), ); } diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart index bf0fdd6d..30a7cc76 100644 --- a/lib/ui/pages/recovery_key/recovery_key.dart +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -1,5 +1,17 @@ -/*import 'package:flutter/src/foundation/key.dart'; -import 'package:flutter/src/widgets/framework.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/ui/pages/recovery_key/recovery_key_receiving.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; class RecoveryKey extends StatefulWidget { const RecoveryKey({Key? key}) : super(key: key); @@ -10,5 +22,219 @@ class RecoveryKey extends StatefulWidget { class _RecoveryKeyState extends State { @override - Widget build(BuildContext context) {} -}*/ + Widget build(BuildContext context) { + var keyStatus = context.watch().state; + + final List widgets; + final String? subtitle = + keyStatus.exists ? null : 'recovery_key.key_main_description'.tr(); + switch (keyStatus.loadingStatus) { + case LoadingStatus.refreshing: + widgets = [ + const Icon(Icons.refresh_outlined), + const SizedBox(height: 18), + BrandText( + 'recovery_key.key_synchronizing'.tr(), + type: TextType.h1, + ), + ]; + break; + case LoadingStatus.success: + widgets = [ + const RecoveryKeyContent(), + ]; + break; + case LoadingStatus.uninitialized: + case LoadingStatus.error: + widgets = [ + const Icon(Icons.sentiment_dissatisfied_outlined), + const SizedBox(height: 18), + BrandText( + 'recovery_key.key_connection_error'.tr(), + type: TextType.h1, + ), + ]; + break; + } + + return BrandHeroScreen( + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: subtitle, + hasBackButton: true, + hasFlashButton: false, + children: widgets, + ); + } +} + +class RecoveryKeyContent extends StatefulWidget { + const RecoveryKeyContent({Key? key}) : super(key: key); + + @override + State createState() => _RecoveryKeyContentState(); +} + +class _RecoveryKeyContentState extends State { + bool _isAmountToggled = true; + bool _isExpirationToggled = true; + bool _isConfigurationVisible = false; + + final _amountController = TextEditingController(); + final _expirationController = TextEditingController(); + + @override + Widget build(BuildContext context) { + var keyStatus = context.read().state; + _isConfigurationVisible = !keyStatus.exists; + + List widgets = []; + + if (keyStatus.exists) { + if (keyStatus.isValid) { + widgets = [ + BrandCards.filled( + child: ListTile( + title: Text('recovery_key.key_valid'.tr()), + leading: const Icon(Icons.check_circle_outlined), + tileColor: Colors.lightGreen, + ), + ), + ...widgets + ]; + } else { + widgets = [ + BrandCards.filled( + child: ListTile( + title: Text('recovery_key.key_invalid'.tr()), + leading: const Icon(Icons.cancel_outlined), + tileColor: Colors.redAccent, + ), + ), + ...widgets + ]; + } + + if (keyStatus.expiresAt != null && !_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Text( + 'recovery_key.key_valid_until'.tr( + args: [keyStatus.expiresAt!.toIso8601String()], + ), + ) + ]; + } + + if (keyStatus.usesLeft != null && !_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Text( + 'recovery_key.key_valid_for'.tr( + args: [keyStatus.usesLeft!.toString()], + ), + ) + ]; + } + + if (keyStatus.generatedAt != null && !_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Text( + 'recovery_key.key_creation_date'.tr( + args: [keyStatus.generatedAt!.toIso8601String()], + ), + ) + ]; + } + + if (!_isConfigurationVisible) { + if (keyStatus.isValid) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + BrandButton.text( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () => _isConfigurationVisible = true, + ), + ]; + } else { + widgets = [ + ...widgets, + const SizedBox(height: 18), + FilledButton( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () => _isConfigurationVisible = true, + ), + ]; + } + } + } + + if (_isConfigurationVisible) { + widgets = [ + ...widgets, + const SizedBox(height: 18), + Row( + children: [ + Text('key_amount_toggle'.tr()), + Switch( + value: _isAmountToggled, + onChanged: (bool toogled) => _isAmountToggled = toogled, + ), + ], + ), + const SizedBox(height: 18), + TextField( + enabled: _isAmountToggled, + controller: _amountController, + decoration: InputDecoration( + labelText: 'recovery_key.key_amount_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 18), + Row( + children: [ + Text('key_duedate_toggle'.tr()), + Switch( + value: _isExpirationToggled, + onChanged: (bool toogled) => _isExpirationToggled = toogled, + ), + ], + ), + const SizedBox(height: 18), + TextField( + enabled: _isExpirationToggled, + controller: _expirationController, + decoration: InputDecoration( + labelText: 'recovery_key.key_duedate_field_title'.tr()), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 18), + FilledButton( + title: 'recovery_key.key_receive_button'.tr(), + disabled: + (_isExpirationToggled && _expirationController.text.isEmpty) || + (_isAmountToggled && _amountController.text.isEmpty), + onPressed: () { + Navigator.of(context).push( + materialRoute( + const RecoveryKeyReceiving(recoveryKey: ''), // TO DO + ), + ); + }, + ), + ]; + } + + return Column(children: widgets); + } +} diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart index 8b137891..8605e871 100644 --- a/lib/ui/pages/recovery_key/recovery_key_receiving.dart +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -1 +1,37 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +class RecoveryKeyReceiving extends StatelessWidget { + const RecoveryKeyReceiving({required this.recoveryKey, Key? key}) + : super(key: key); + + final String recoveryKey; + + @override + Widget build(BuildContext context) { + return BrandHeroScreen( + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: 'recovering.method_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + Text(recoveryKey, style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(height: 18), + const Icon(Icons.info_outlined, size: 14), + Text('recovery_key.key_receiving_info'.tr()), + const SizedBox(height: 18), + FilledButton( + title: 'recovery_key.key_receiving_done'.tr(), + onPressed: () { + Navigator.of(context) + .pushReplacement(materialRoute(const RootPage())); + }, + ), + ], + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recover_by_old_token.dart b/lib/ui/pages/setup/recovering/recover_by_old_token.dart index b16ff964..d62221e9 100644 --- a/lib/ui/pages/setup/recovering/recover_by_old_token.dart +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -21,7 +21,6 @@ class RecoverByOldTokenInstruction extends StatelessWidget { if (state is ServerInstallationRecovery && state.currentStep != RecoveryStep.selecting) { Navigator.of(context).pop(); - Navigator.of(context).pop(); } }, child: BrandHeroScreen( diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart index 554f40eb..a8e4860c 100644 --- a/lib/ui/pages/setup/recovering/recovery_method_select.dart +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -1,5 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; @@ -59,58 +60,68 @@ class RecoveryFallbackMethodSelect extends StatelessWidget { @override Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: 'recovering.recovery_main_header'.tr(), - heroSubtitle: 'recovering.fallback_select_description'.tr(), - hasBackButton: true, - hasFlashButton: false, - children: [ - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_token_copy'.tr(), - style: Theme.of(context).textTheme.titleMedium, + return BlocListener( + listener: (context, state) { + if (state is ServerInstallationRecovery && + state.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens && + state.currentStep != RecoveryStep.selecting) { + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.fallback_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_token_copy'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.vpn_key), + onTap: () => Navigator.of(context) + .push(materialRoute(const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_old', + ))), ), - leading: const Icon(Icons.vpn_key), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_old', - ))), ), - ), - const SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_root_ssh'.tr(), - style: Theme.of(context).textTheme.titleMedium, + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_root_ssh'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.terminal), + onTap: () => Navigator.of(context) + .push(materialRoute(const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_ssh', + ))), ), - leading: const Icon(Icons.terminal), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_ssh', - ))), ), - ), - const SizedBox(height: 16), - BrandCards.outlined( - child: ListTile( - title: Text( - 'recovering.fallback_select_provider_console'.tr(), - style: Theme.of(context).textTheme.titleMedium, + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_provider_console'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + 'recovering.fallback_select_provider_console_hint'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: const Icon(Icons.web), + onTap: () => Navigator.of(context) + .push(materialRoute(const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_terminal', + ))), ), - subtitle: Text( - 'recovering.fallback_select_provider_console_hint'.tr(), - style: Theme.of(context).textTheme.bodyMedium, - ), - leading: const Icon(Icons.web), - onTap: () => Navigator.of(context) - .push(materialRoute(const RecoverByOldTokenInstruction( - instructionFilename: 'how_fallback_terminal', - ))), ), - ), - ], + ], + ), ); } } diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart index 65ff5688..b43ff7e7 100644 --- a/lib/ui/pages/setup/recovering/recovery_routing.dart +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -27,10 +27,14 @@ class RecoveryRouting extends StatelessWidget { if (serverInstallation is ServerInstallationRecovery) { switch (serverInstallation.currentStep) { case RecoveryStep.selecting: - if (serverInstallation.recoveryCapabilities != - ServerRecoveryCapabilities.none) { + if (serverInstallation.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens) { currentPage = const RecoveryMethodSelect(); } + if (serverInstallation.recoveryCapabilities == + ServerRecoveryCapabilities.legacy) { + currentPage = const RecoveryFallbackMethodSelect(); + } break; case RecoveryStep.recoveryKey: currentPage = const RecoverByRecoveryKey();