Finish recovery key workflow and pages

Co-authored-by: Inex Code <inex.code@selfprivacy.org>
pull/90/head
NaiJi ✨ 2022-05-30 16:55:52 +03:00
parent b60fb19ecc
commit ead19d2210
13 changed files with 404 additions and 190 deletions

View File

@ -80,12 +80,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
);
await repository.saveBackblazeKey(backblazeCredential);
if (state is ServerInstallationRecovery) {
final mainUser = await repository.getMainUser();
final updatedState = (state as ServerInstallationRecovery).copyWith(
backblazeCredential: backblazeCredential,
rootUser: mainUser,
);
emit(updatedState.finish());
finishRecoveryProcess(backblazeCredential);
return;
}
emit((state as ServerInstallationNotFinished)
@ -458,6 +453,19 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
));
}
void finishRecoveryProcess(BackblazeCredential backblazeCredential) async {
await repository.saveIsServerStarted(true);
await repository.saveIsServerResetedFirstTime(true);
await repository.saveIsServerResetedSecondTime(true);
await repository.saveHasFinalChecked(true);
final mainUser = await repository.getMainUser();
final updatedState = (state as ServerInstallationRecovery).copyWith(
backblazeCredential: backblazeCredential,
rootUser: mainUser,
);
emit(updatedState.finish());
}
@override
void onChange(Change<ServerInstallationState> change) {
super.onChange(change);
@ -474,6 +482,9 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
print(
'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}');
}
if (change.nextState is TimerState) {
print('Timer: ${(change.nextState as TimerState).duration}');
}
}
void clearAppConfig() {

View File

@ -431,6 +431,7 @@ class ServerInstallationRepository {
isWithToken: false,
overrideDomain: serverDomain.domainName,
);
final serverIp = await getServerIpFromDomain(serverDomain);
final apiResponse = await serverApi.useRecoveryToken(
DeviceToken(device: await getDeviceName(), token: recoveryKey));
@ -443,7 +444,7 @@ class ServerInstallationRepository {
),
provider: ServerProvider.unknown,
id: 0,
ip4: '',
ip4: serverIp,
startTime: null,
createTime: null,
);
@ -464,6 +465,7 @@ class ServerInstallationRepository {
overrideDomain: serverDomain.domainName,
customToken: apiToken,
);
final serverIp = await getServerIpFromDomain(serverDomain);
if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) {
final apiResponse = await serverApi.servicesPowerCheck();
if (apiResponse.isNotEmpty) {
@ -475,7 +477,7 @@ class ServerInstallationRepository {
),
provider: ServerProvider.unknown,
id: 0,
ip4: '',
ip4: serverIp,
startTime: null,
createTime: null,
);

View File

@ -34,7 +34,7 @@ class BrandButton {
}) =>
ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 48,
minHeight: 40,
minWidth: double.infinity,
),
child: TextButton(onPressed: onPressed, child: Text(title)),

View File

@ -26,10 +26,16 @@ class FilledButton extends StatelessWidget {
primary: Theme.of(context).colorScheme.onSurface.withAlpha(98),
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0));
return ElevatedButton(
onPressed: onPressed,
style: disabled ? _disabledStyle : _enabledStyle,
child: child ?? Text(title ?? ''),
return ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 40,
minWidth: double.infinity,
),
child: ElevatedButton(
onPressed: onPressed,
style: disabled ? _disabledStyle : _enabledStyle,
child: child ?? Text(title ?? ''),
),
);
}
}

View File

@ -52,13 +52,17 @@ class BrandHeroScreen extends StatelessWidget {
if (heroTitle != null)
Text(
heroTitle!,
style: Theme.of(context).textTheme.headlineMedium,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
textAlign: TextAlign.start,
),
const SizedBox(height: 8.0),
if (heroSubtitle != null)
Text(heroSubtitle!,
style: Theme.of(context).textTheme.bodyMedium,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onBackground,
),
textAlign: TextAlign.start),
const SizedBox(height: 16.0),
...children,

View File

@ -8,6 +8,7 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_text/brand_text.dart';
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
import 'package:selfprivacy/ui/pages/setup/initializing.dart';
import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
@ -77,6 +78,13 @@ class MorePage extends StatelessWidget {
goTo: SshKeysPage(
user: context.read<UsersCubit>().state.rootUser,
)),
_NavItem(
isEnabled: context.read<ServerInstallationCubit>().state
is ServerInstallationFinished,
iconData: Icons.password_outlined,
goTo: const RecoveryKey(),
title: 'recovery_key.key_main_header'.tr(),
)
],
),
)

View File

@ -31,8 +31,8 @@ class _RecoveryKeyState extends State<RecoveryKey> {
switch (keyStatus.loadingStatus) {
case LoadingStatus.refreshing:
widgets = [
const Icon(Icons.refresh_outlined),
const SizedBox(height: 18),
const Center(child: CircularProgressIndicator()),
const SizedBox(height: 16),
BrandText(
'recovery_key.key_synchronizing'.tr(),
type: TextType.h1,
@ -48,7 +48,7 @@ class _RecoveryKeyState extends State<RecoveryKey> {
case LoadingStatus.error:
widgets = [
const Icon(Icons.sentiment_dissatisfied_outlined),
const SizedBox(height: 18),
const SizedBox(height: 16),
BrandText(
'recovery_key.key_connection_error'.tr(),
type: TextType.h1,
@ -75,78 +75,25 @@ class RecoveryKeyContent extends StatefulWidget {
}
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;
final keyStatus = context.watch<RecoveryKeyCubit>().state;
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
];
}
widgets = [
RecoveryKeyStatusCard(isValid: keyStatus.isValid),
RecoveryKeyInformation(state: keyStatus),
...widgets,
];
if (keyStatus.expiresAt != null && !_isConfigurationVisible) {
if (_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()],
),
)
const RecoveryKeyConfiguration(),
];
}
@ -154,87 +101,274 @@ class _RecoveryKeyContentState extends State<RecoveryKeyContent> {
if (keyStatus.isValid) {
widgets = [
...widgets,
const SizedBox(height: 18),
const SizedBox(height: 16),
BrandButton.text(
title: 'recovery_key.key_replace_button'.tr(),
onPressed: () => _isConfigurationVisible = true,
onPressed: () {
setState(() {
_isConfigurationVisible = true;
});
},
),
];
} else {
widgets = [
...widgets,
const SizedBox(height: 18),
const SizedBox(height: 16),
FilledButton(
title: 'recovery_key.key_replace_button'.tr(),
onPressed: () => _isConfigurationVisible = true,
onPressed: () {
setState(() {
_isConfigurationVisible = true;
});
},
),
];
}
}
}
if (_isConfigurationVisible) {
if (!keyStatus.exists) {
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
),
);
},
),
const RecoveryKeyConfiguration(),
];
}
return Column(children: widgets);
}
}
class RecoveryKeyStatusCard extends StatelessWidget {
const RecoveryKeyStatusCard({required this.isValid, Key? key})
: super(key: key);
final bool isValid;
@override
Widget build(BuildContext context) {
return BrandCards.filled(
child: ListTile(
title: isValid
? Text(
'recovery_key.key_valid'.tr(),
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
)
: Text(
'recovery_key.key_invalid'.tr(),
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
),
leading: isValid
? Icon(
Icons.check_circle_outlined,
color: Theme.of(context).colorScheme.onSurfaceVariant,
)
: Icon(
Icons.cancel_outlined,
color: Theme.of(context).colorScheme.onErrorContainer,
),
tileColor: isValid
? Theme.of(context).colorScheme.surfaceVariant
: Theme.of(context).colorScheme.errorContainer,
),
);
}
}
class RecoveryKeyInformation extends StatelessWidget {
const RecoveryKeyInformation({required this.state, Key? key})
: super(key: key);
final RecoveryKeyState state;
@override
Widget build(BuildContext context) {
const padding = EdgeInsets.symmetric(vertical: 8.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.expiresAt != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_valid_until'.tr(
args: [state.expiresAt!.toIso8601String()],
),
),
),
if (state.usesLeft != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_valid_for'.tr(
args: [state.usesLeft!.toString()],
),
),
),
if (state.generatedAt != null)
Padding(
padding: padding,
child: Text(
'recovery_key.key_creation_date'.tr(
args: [state.generatedAt!.toIso8601String()],
),
textAlign: TextAlign.start,
),
),
],
);
}
}
class RecoveryKeyConfiguration extends StatefulWidget {
const RecoveryKeyConfiguration({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _RecoveryKeyConfigurationState();
}
class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
bool _isAmountToggled = false;
bool _isExpirationToggled = false;
bool _isAmountError = false;
bool _isExpirationError = false;
final TextEditingController _amountController = TextEditingController();
final TextEditingController _expirationController = TextEditingController();
DateTime _selectedDate = DateTime.now();
bool _isDateSelected = false;
@override
Widget build(BuildContext context) {
if (_isDateSelected) {
_expirationController.text = _selectedDate.toIso8601String();
}
return Column(
children: [
const SizedBox(height: 16),
Row(
children: [
Text('recovery_key.key_amount_toggle'.tr()),
Switch(
value: _isAmountToggled,
onChanged: (bool toogled) {
setState(
() {
_isAmountToggled = toogled;
_isExpirationToggled = _isExpirationToggled;
},
);
},
),
],
),
const SizedBox(height: 16),
TextField(
enabled: _isAmountToggled,
controller: _amountController,
decoration: InputDecoration(
errorText: _isAmountError ? ' ' : null,
labelText: 'recovery_key.key_amount_field_title'.tr()),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
], // Only numbers can be entered
),
const SizedBox(height: 16),
Row(
children: [
Text('recovery_key.key_duedate_toggle'.tr()),
Switch(
value: _isExpirationToggled,
onChanged: (bool toogled) {
setState(
() {
_isAmountToggled = _isAmountToggled;
_isExpirationToggled = toogled;
},
);
},
),
],
),
const SizedBox(height: 16),
TextField(
enabled: _isExpirationToggled,
controller: _expirationController,
onTap: () {
_selectDate(context);
},
decoration: InputDecoration(
errorText: _isExpirationError ? ' ' : null,
labelText: 'recovery_key.key_duedate_field_title'.tr()),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
], // Only numbers can be entered
),
const SizedBox(height: 16),
FilledButton(
title: 'recovery_key.key_receive_button'.tr(),
onPressed: () {
if (_isExpirationToggled && _expirationController.text.isEmpty) {
setState(() {
_isExpirationError = true;
_isAmountError = false;
_isAmountToggled = _isAmountToggled;
_isExpirationToggled = _isExpirationToggled;
});
return;
} else if (_isAmountToggled && _amountController.text.isEmpty) {
setState(() {
_isAmountError = true;
_isExpirationError = false;
_isAmountToggled = _isAmountToggled;
_isExpirationToggled = _isExpirationToggled;
});
return;
} else {
setState(() {
_isAmountError = false;
_isExpirationError = false;
_isAmountToggled = _isAmountToggled;
_isExpirationToggled = _isExpirationToggled;
});
Navigator.of(context).push(
materialRoute(
const RecoveryKeyReceiving(recoveryKey: ''), // TO DO
),
);
}
},
),
],
);
}
Future<DateTime> _selectDate(BuildContext context) async {
final selected = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime.now(),
lastDate: DateTime(DateTime.now().year + 50));
if (selected != null && selected != _selectedDate) {
setState(
() {
_selectedDate = selected;
_isDateSelected = true;
},
);
}
return _selectedDate;
}
}

View File

@ -20,10 +20,10 @@ class RecoveryKeyReceiving extends StatelessWidget {
hasFlashButton: false,
children: [
Text(recoveryKey, style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 18),
const SizedBox(height: 16),
const Icon(Icons.info_outlined, size: 14),
Text('recovery_key.key_receiving_info'.tr()),
const SizedBox(height: 18),
const SizedBox(height: 16),
FilledButton(
title: 'recovery_key.key_receiving_done'.tr(),
onPressed: () {

View File

@ -33,7 +33,7 @@ class RecoverByOldTokenInstruction extends StatelessWidget {
BrandMarkdown(
fileName: instructionFilename,
),
const SizedBox(height: 18),
const SizedBox(height: 16),
FilledButton(
title: 'recovering.method_device_button'.tr(),
onPressed: () => context
@ -79,7 +79,7 @@ class RecoverByOldToken extends StatelessWidget {
labelText: 'recovering.method_device_input_placeholder'.tr(),
),
),
const SizedBox(height: 18),
const SizedBox(height: 16),
FilledButton(
title: 'more.continue'.tr(),
onPressed: formCubitState.isSubmitting

View File

@ -36,7 +36,7 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
hintText: 'KeyID',
),
),
const SizedBox(height: 18),
const SizedBox(height: 16),
CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().applicationKey,
textAlign: TextAlign.center,
@ -46,14 +46,14 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
hintText: 'Master Application Key',
),
),
const SizedBox(height: 18),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<BackblazeFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 18),
const SizedBox(height: 16),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,

View File

@ -38,14 +38,14 @@ class RecoveryConfirmCloudflare extends StatelessWidget {
hintText: 'initializing.5'.tr(),
),
),
const SizedBox(height: 18),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: formCubitState.isSubmitting
? null
: () => context.read<CloudFlareFormCubit>().trySubmit(),
text: 'basis.connect'.tr(),
),
const SizedBox(height: 18),
const SizedBox(height: 16),
BrandButton.text(
onPressed: () => showModalBottomSheet<void>(
context: context,

View File

@ -133,22 +133,54 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
VoidCallback? onTap}) {
return BrandCards.filled(
child: ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
onTap: onTap,
title: Text(server.name),
leading: const Icon(Icons.dns),
title: Text(
server.name,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
leading: Icon(
Icons.dns,
color: Theme.of(context).colorScheme.onSurface,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(server.isReverseDnsValid ? Icons.check : Icons.close),
Text('rDNS: ${server.reverseDns}'),
Icon(
server.isReverseDnsValid ? Icons.check : Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'rDNS: ${server.reverseDns}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
Row(
children: [
Icon(server.isIpValid ? Icons.check : Icons.close),
Text('IP: ${server.ip}'),
Icon(
server.isIpValid ? Icons.check : Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'IP: ${server.ip}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
],
@ -186,27 +218,19 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.start,
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(server.isReverseDnsValid ? Icons.check : Icons.close),
const SizedBox(width: 8),
Text(server.isReverseDnsValid
? 'recovering.modal_confirmation_dns_valid'.tr()
: 'recovering.modal_confirmation_dns_invalid'.tr()),
],
const SizedBox(height: 8),
IsValidStringDisplay(
isValid: server.isReverseDnsValid,
textIfValid: 'recovering.modal_confirmation_dns_valid'.tr(),
textIfInvalid:
'recovering.modal_confirmation_dns_invalid'.tr(),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(server.isIpValid ? Icons.check : Icons.close),
const SizedBox(width: 8),
Text(server.isIpValid
? 'recovering.modal_confirmation_ip_valid'.tr()
: 'recovering.modal_confirmation_ip_invalid'.tr()),
],
const SizedBox(height: 8),
IsValidStringDisplay(
isValid: server.isIpValid,
textIfValid: 'recovering.modal_confirmation_ip_valid'.tr(),
textIfInvalid:
'recovering.modal_confirmation_ip_invalid'.tr(),
),
],
),
@ -229,3 +253,42 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
},
);
}
class IsValidStringDisplay extends StatelessWidget {
const IsValidStringDisplay({
Key? key,
required this.isValid,
required this.textIfValid,
required this.textIfInvalid,
}) : super(key: key);
final bool isValid;
final String textIfValid;
final String textIfInvalid;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
isValid
? Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface)
: Icon(Icons.close, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 8),
isValid
? Text(
textIfValid,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
)
: Text(
textIfInvalid,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.error,
),
)
],
);
}
}

View File

@ -363,13 +363,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.2"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flutter_localizations:
dependency: transitive
description: flutter
@ -526,7 +519,7 @@ packages:
source: hosted
version: "3.1.3"
intl:
dependency: "direct main"
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
@ -567,13 +560,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "6.2.0"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
local_auth:
dependency: "direct main"
description: