mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-26 18:56:38 +00:00
Implement recovery key pages and device cubit
Co-authored-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
parent
5dcaa060a1
commit
72ef16c6f6
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
75
lib/logic/cubit/devices/devices_cubit.dart
Normal file
75
lib/logic/cubit/devices/devices_cubit.dart
Normal file
|
@ -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<ApiDevicesState> {
|
||||
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<void> 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<List<ApiToken>?> _getApiTokens() async {
|
||||
final response = await api.getApiTokens();
|
||||
if (response.isSuccess) {
|
||||
return response.data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<NavigationService>()
|
||||
.showSnackBar(response.errorMessage ?? 'Error deleting device');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getNewDeviceKey() async {
|
||||
final response = await api.createDeviceToken();
|
||||
if (response.isSuccess) {
|
||||
return response.data;
|
||||
} else {
|
||||
getIt<NavigationService>().showSnackBar(
|
||||
response.errorMessage ?? 'Error getting new device key');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
emit(const ApiDevicesState.initial());
|
||||
}
|
||||
}
|
33
lib/logic/cubit/devices/devices_state.dart
Normal file
33
lib/logic/cubit/devices/devices_state.dart
Normal file
|
@ -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<ApiToken> _devices;
|
||||
final LoadingStatus status;
|
||||
|
||||
List<ApiToken> get devices => _devices;
|
||||
ApiToken get thisDevice => _devices.firstWhere((device) => device.isCaller,
|
||||
orElse: () => ApiToken(
|
||||
name: 'Error fetching device',
|
||||
isCaller: true,
|
||||
date: DateTime.now(),
|
||||
));
|
||||
|
||||
List<ApiToken> get otherDevices =>
|
||||
_devices.where((device) => !device.isCaller).toList();
|
||||
|
||||
ApiDevicesState copyWith({
|
||||
List<ApiToken>? devices,
|
||||
LoadingStatus? status,
|
||||
}) {
|
||||
return ApiDevicesState(
|
||||
devices ?? _devices,
|
||||
status ?? this.status,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [_devices];
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Object> get props => [_status, loadingStatus];
|
||||
|
|
|
@ -307,8 +307,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
Future<ServerHostingDetails> Function(ServerDomain, String)
|
||||
recoveryFunction;
|
||||
Future<ServerHostingDetails> Function(
|
||||
ServerDomain, String, ServerRecoveryCapabilities) recoveryFunction;
|
||||
switch (method) {
|
||||
case ServerRecoveryMethods.newDeviceKey:
|
||||
recoveryFunction = repository.authorizeByNewDeviceKey;
|
||||
|
@ -325,6 +325,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
|||
final serverDetails = await recoveryFunction(
|
||||
serverDomain,
|
||||
token,
|
||||
dataState.recoveryCapabilities,
|
||||
);
|
||||
await repository.saveServerDetails(serverDetails);
|
||||
emit(dataState.copyWith(
|
||||
|
|
|
@ -392,6 +392,7 @@ class ServerInstallationRepository {
|
|||
Future<ServerHostingDetails> authorizeByNewDeviceKey(
|
||||
ServerDomain serverDomain,
|
||||
String newDeviceKey,
|
||||
ServerRecoveryCapabilities recoveryCapabilities,
|
||||
) async {
|
||||
var serverApi = ServerApi(
|
||||
isWithToken: false,
|
||||
|
@ -424,6 +425,7 @@ class ServerInstallationRepository {
|
|||
Future<ServerHostingDetails> authorizeByRecoveryKey(
|
||||
ServerDomain serverDomain,
|
||||
String recoveryKey,
|
||||
ServerRecoveryCapabilities recoveryCapabilities,
|
||||
) async {
|
||||
var serverApi = ServerApi(
|
||||
isWithToken: false,
|
||||
|
@ -455,12 +457,34 @@ class ServerInstallationRepository {
|
|||
Future<ServerHostingDetails> 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));
|
||||
|
|
|
@ -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 ?? ''),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<RecoveryKey> {
|
||||
@override
|
||||
Widget build(BuildContext context) {}
|
||||
}*/
|
||||
Widget build(BuildContext context) {
|
||||
var keyStatus = context.watch<RecoveryKeyCubit>().state;
|
||||
|
||||
final List<Widget> 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<RecoveryKeyContent> createState() => _RecoveryKeyContentState();
|
||||
}
|
||||
|
||||
class _RecoveryKeyContentState extends State<RecoveryKeyContent> {
|
||||
bool _isAmountToggled = true;
|
||||
bool _isExpirationToggled = true;
|
||||
bool _isConfigurationVisible = false;
|
||||
|
||||
final _amountController = TextEditingController();
|
||||
final _expirationController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var keyStatus = context.read<RecoveryKeyCubit>().state;
|
||||
_isConfigurationVisible = !keyStatus.exists;
|
||||
|
||||
List<Widget> 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: <TextInputFormatter>[
|
||||
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: <TextInputFormatter>[
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<ServerInstallationCubit, ServerInstallationState>(
|
||||
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',
|
||||
))),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue