mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2025-01-27 11:16:45 +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:"
|
"confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:"
|
||||||
},
|
},
|
||||||
"recovery_key": {
|
"recovery_key": {
|
||||||
|
"key_connection_error": "Couldn't connect to the server.",
|
||||||
|
"key_synchronizing": "Synchronizing...",
|
||||||
"key_main_header": "Recovery key",
|
"key_main_header": "Recovery key",
|
||||||
"key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.",
|
"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_toggle": "Limit by number of uses",
|
||||||
"key_amount_field_title": "Max number of uses",
|
"key_amount_field_title": "Max number of uses",
|
||||||
"key_duedate_toggle": "Limit by time",
|
"key_duedate_toggle": "Limit by time",
|
||||||
"key_duedate_field_title": "Due date of expiration",
|
"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": {
|
"modals": {
|
||||||
"_comment": "messages in modals",
|
"_comment": "messages in modals",
|
||||||
|
|
|
@ -3,6 +3,13 @@ import 'package:flutter/cupertino.dart';
|
||||||
import 'package:ionicons/ionicons.dart';
|
import 'package:ionicons/ionicons.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
|
||||||
|
|
||||||
|
enum LoadingStatus {
|
||||||
|
uninitialized,
|
||||||
|
refreshing,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
enum InitializingSteps {
|
enum InitializingSteps {
|
||||||
setHetznerKey,
|
setHetznerKey,
|
||||||
setCloudFlareKey,
|
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/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/cubit/app_config_dependent/authentication_dependend_cubit.dart';
|
||||||
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
|
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
|
||||||
|
|
||||||
|
@ -18,7 +19,8 @@ class RecoveryKeyCubit
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
emit(state.copyWith(loadingStatus: LoadingStatus.error));
|
emit(state.copyWith(loadingStatus: LoadingStatus.error));
|
||||||
} else {
|
} else {
|
||||||
emit(state.copyWith(status: status, loadingStatus: LoadingStatus.good));
|
emit(state.copyWith(
|
||||||
|
status: status, loadingStatus: LoadingStatus.success));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
|
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
|
||||||
|
@ -41,7 +43,8 @@ class RecoveryKeyCubit
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
emit(state.copyWith(loadingStatus: LoadingStatus.error));
|
emit(state.copyWith(loadingStatus: LoadingStatus.error));
|
||||||
} else {
|
} 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';
|
part of 'recovery_key_cubit.dart';
|
||||||
|
|
||||||
enum LoadingStatus {
|
|
||||||
uninitialized,
|
|
||||||
refreshing,
|
|
||||||
good,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
|
|
||||||
class RecoveryKeyState extends ServerInstallationDependendState {
|
class RecoveryKeyState extends ServerInstallationDependendState {
|
||||||
const RecoveryKeyState(this._status, this.loadingStatus);
|
const RecoveryKeyState(this._status, this.loadingStatus);
|
||||||
|
|
||||||
|
@ -20,7 +13,7 @@ class RecoveryKeyState extends ServerInstallationDependendState {
|
||||||
bool get exists => _status.exists;
|
bool get exists => _status.exists;
|
||||||
bool get isValid => _status.valid;
|
bool get isValid => _status.valid;
|
||||||
DateTime? get generatedAt => _status.date;
|
DateTime? get generatedAt => _status.date;
|
||||||
DateTime? get expiresAt => _status.date;
|
DateTime? get expiresAt => _status.expiration;
|
||||||
int? get usesLeft => _status.usesLeft;
|
int? get usesLeft => _status.usesLeft;
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [_status, loadingStatus];
|
List<Object> get props => [_status, loadingStatus];
|
||||||
|
|
|
@ -307,8 +307,8 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Future<ServerHostingDetails> Function(ServerDomain, String)
|
Future<ServerHostingDetails> Function(
|
||||||
recoveryFunction;
|
ServerDomain, String, ServerRecoveryCapabilities) recoveryFunction;
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case ServerRecoveryMethods.newDeviceKey:
|
case ServerRecoveryMethods.newDeviceKey:
|
||||||
recoveryFunction = repository.authorizeByNewDeviceKey;
|
recoveryFunction = repository.authorizeByNewDeviceKey;
|
||||||
|
@ -325,6 +325,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
|
||||||
final serverDetails = await recoveryFunction(
|
final serverDetails = await recoveryFunction(
|
||||||
serverDomain,
|
serverDomain,
|
||||||
token,
|
token,
|
||||||
|
dataState.recoveryCapabilities,
|
||||||
);
|
);
|
||||||
await repository.saveServerDetails(serverDetails);
|
await repository.saveServerDetails(serverDetails);
|
||||||
emit(dataState.copyWith(
|
emit(dataState.copyWith(
|
||||||
|
|
|
@ -392,6 +392,7 @@ class ServerInstallationRepository {
|
||||||
Future<ServerHostingDetails> authorizeByNewDeviceKey(
|
Future<ServerHostingDetails> authorizeByNewDeviceKey(
|
||||||
ServerDomain serverDomain,
|
ServerDomain serverDomain,
|
||||||
String newDeviceKey,
|
String newDeviceKey,
|
||||||
|
ServerRecoveryCapabilities recoveryCapabilities,
|
||||||
) async {
|
) async {
|
||||||
var serverApi = ServerApi(
|
var serverApi = ServerApi(
|
||||||
isWithToken: false,
|
isWithToken: false,
|
||||||
|
@ -424,6 +425,7 @@ class ServerInstallationRepository {
|
||||||
Future<ServerHostingDetails> authorizeByRecoveryKey(
|
Future<ServerHostingDetails> authorizeByRecoveryKey(
|
||||||
ServerDomain serverDomain,
|
ServerDomain serverDomain,
|
||||||
String recoveryKey,
|
String recoveryKey,
|
||||||
|
ServerRecoveryCapabilities recoveryCapabilities,
|
||||||
) async {
|
) async {
|
||||||
var serverApi = ServerApi(
|
var serverApi = ServerApi(
|
||||||
isWithToken: false,
|
isWithToken: false,
|
||||||
|
@ -455,12 +457,34 @@ class ServerInstallationRepository {
|
||||||
Future<ServerHostingDetails> authorizeByApiToken(
|
Future<ServerHostingDetails> authorizeByApiToken(
|
||||||
ServerDomain serverDomain,
|
ServerDomain serverDomain,
|
||||||
String apiToken,
|
String apiToken,
|
||||||
|
ServerRecoveryCapabilities recoveryCapabilities,
|
||||||
) async {
|
) async {
|
||||||
var serverApi = ServerApi(
|
var serverApi = ServerApi(
|
||||||
isWithToken: false,
|
isWithToken: false,
|
||||||
overrideDomain: serverDomain.domainName,
|
overrideDomain: serverDomain.domainName,
|
||||||
customToken: apiToken,
|
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 deviceAuthKey = await serverApi.createDeviceToken();
|
||||||
final apiResponse = await serverApi.authorizeDevice(
|
final apiResponse = await serverApi.authorizeDevice(
|
||||||
DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data));
|
DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data));
|
||||||
|
|
|
@ -6,20 +6,29 @@ class FilledButton extends StatelessWidget {
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.title,
|
this.title,
|
||||||
this.child,
|
this.child,
|
||||||
|
this.disabled = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final String? title;
|
final String? title;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
final bool disabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return ElevatedButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
style: ElevatedButton.styleFrom(
|
style: disabled ? _disabledStyle : _enabledStyle,
|
||||||
onPrimary: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
primary: Theme.of(context).colorScheme.primary,
|
|
||||||
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
|
|
||||||
child: child ?? Text(title ?? ''),
|
child: child ?? Text(title ?? ''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
/*import 'package:flutter/src/foundation/key.dart';
|
import 'package:cubit_form/cubit_form.dart';
|
||||||
import 'package:flutter/src/widgets/framework.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 {
|
class RecoveryKey extends StatefulWidget {
|
||||||
const RecoveryKey({Key? key}) : super(key: key);
|
const RecoveryKey({Key? key}) : super(key: key);
|
||||||
|
@ -10,5 +22,219 @@ class RecoveryKey extends StatefulWidget {
|
||||||
|
|
||||||
class _RecoveryKeyState extends State<RecoveryKey> {
|
class _RecoveryKeyState extends State<RecoveryKey> {
|
||||||
@override
|
@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 &&
|
if (state is ServerInstallationRecovery &&
|
||||||
state.currentStep != RecoveryStep.selecting) {
|
state.currentStep != RecoveryStep.selecting) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BrandHeroScreen(
|
child: BrandHeroScreen(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.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/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/brand_button.dart';
|
||||||
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart';
|
||||||
|
@ -59,58 +60,68 @@ class RecoveryFallbackMethodSelect extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BrandHeroScreen(
|
return BlocListener<ServerInstallationCubit, ServerInstallationState>(
|
||||||
heroTitle: 'recovering.recovery_main_header'.tr(),
|
listener: (context, state) {
|
||||||
heroSubtitle: 'recovering.fallback_select_description'.tr(),
|
if (state is ServerInstallationRecovery &&
|
||||||
hasBackButton: true,
|
state.recoveryCapabilities ==
|
||||||
hasFlashButton: false,
|
ServerRecoveryCapabilities.loginTokens &&
|
||||||
children: [
|
state.currentStep != RecoveryStep.selecting) {
|
||||||
BrandCards.outlined(
|
Navigator.of(context).pop();
|
||||||
child: ListTile(
|
}
|
||||||
title: Text(
|
},
|
||||||
'recovering.fallback_select_token_copy'.tr(),
|
child: BrandHeroScreen(
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
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),
|
||||||
const SizedBox(height: 16),
|
BrandCards.outlined(
|
||||||
BrandCards.outlined(
|
child: ListTile(
|
||||||
child: ListTile(
|
title: Text(
|
||||||
title: Text(
|
'recovering.fallback_select_root_ssh'.tr(),
|
||||||
'recovering.fallback_select_root_ssh'.tr(),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
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),
|
||||||
const SizedBox(height: 16),
|
BrandCards.outlined(
|
||||||
BrandCards.outlined(
|
child: ListTile(
|
||||||
child: ListTile(
|
title: Text(
|
||||||
title: Text(
|
'recovering.fallback_select_provider_console'.tr(),
|
||||||
'recovering.fallback_select_provider_console'.tr(),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
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) {
|
if (serverInstallation is ServerInstallationRecovery) {
|
||||||
switch (serverInstallation.currentStep) {
|
switch (serverInstallation.currentStep) {
|
||||||
case RecoveryStep.selecting:
|
case RecoveryStep.selecting:
|
||||||
if (serverInstallation.recoveryCapabilities !=
|
if (serverInstallation.recoveryCapabilities ==
|
||||||
ServerRecoveryCapabilities.none) {
|
ServerRecoveryCapabilities.loginTokens) {
|
||||||
currentPage = const RecoveryMethodSelect();
|
currentPage = const RecoveryMethodSelect();
|
||||||
}
|
}
|
||||||
|
if (serverInstallation.recoveryCapabilities ==
|
||||||
|
ServerRecoveryCapabilities.legacy) {
|
||||||
|
currentPage = const RecoveryFallbackMethodSelect();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case RecoveryStep.recoveryKey:
|
case RecoveryStep.recoveryKey:
|
||||||
currentPage = const RecoverByRecoveryKey();
|
currentPage = const RecoverByRecoveryKey();
|
||||||
|
|
Loading…
Reference in a new issue