Hot bug fixing of recovery flow

Co-authored-by: Inex Code <inex.code@selfprivacy.org>
pull/90/head
NaiJi ✨ 2022-05-24 20:45:13 +03:00
parent a096e7e732
commit edce25ec55
19 changed files with 220 additions and 75 deletions

View File

@ -23,7 +23,7 @@ linter:
# producing the lint. # producing the lint.
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -313,11 +313,25 @@
"choose_server_description": "We couldn't figure out which server your are trying to connect to.", "choose_server_description": "We couldn't figure out which server your are trying to connect to.",
"no_servers": "There is no available servers on your account.", "no_servers": "There is no available servers on your account.",
"domain_not_available_on_token": "Selected domain is not available on this token.", "domain_not_available_on_token": "Selected domain is not available on this token.",
"modal_confirmation_title": "Is it really your server?",
"modal_confirmation_description": "If you connect to a wrong server you may lose all your data.",
"modal_confirmation_dns_valid": "Reverse DNS is valid",
"modal_confirmation_dns_invalid": "Reverse DNS points to another domain",
"modal_confirmation_ip_valid": "IP is the same as in DNS record",
"modal_confirmation_ip_invalid": "IP is not the same as in DNS record",
"confirm_cloudflare": "Connect to CloudFlare", "confirm_cloudflare": "Connect to CloudFlare",
"confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:", "confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:",
"confirm_backblze": "Connect to Backblaze", "confirm_backblaze": "Connect to Backblaze",
"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": {
"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"
}, },
"modals": { "modals": {
"_comment": "messages in modals", "_comment": "messages in modals",
@ -331,7 +345,8 @@
"8": "Remove task", "8": "Remove task",
"9": "Reboot", "9": "Reboot",
"10": "You cannot use this API for domains with such TLD.", "10": "You cannot use this API for domains with such TLD.",
"yes": "Yes" "yes": "Yes",
"no": "No"
}, },
"timer": { "timer": {
"sec": "{} sec" "sec": "{} sec"

View File

@ -315,11 +315,22 @@
"choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.",
"no_servers": "На вашем аккаунте нет доступных серверов.", "no_servers": "На вашем аккаунте нет доступных серверов.",
"domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.", "domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.",
"modal_confirmation_title": "Это действительно ваш сервер?",
"modal_confirmation_description": "Подключение к неправильному серверу может привести к деструктивным последствиям.",
"confirm_cloudflare": "Подключение к Cloudflare", "confirm_cloudflare": "Подключение к Cloudflare",
"confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:", "confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:",
"confirm_backblze": "Подключение к Backblaze", "confirm_backblze": "Подключение к Backblaze",
"confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:" "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:"
}, },
"recovery_key": {
"key_main_header": "Ключ восстановления",
"key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.",
"key_amount_toggle": "Ограничить использования",
"key_amount_field_title": "Макс. кол-во использований",
"key_duedate_toggle": "Ограничить срок использования",
"key_duedate_field_title": "Дата окончания срока",
"key_receive_button": "Получить ключ"
},
"modals": { "modals": {
"_comment": "messages in modals", "_comment": "messages in modals",
"1": "Сервер с таким именем уже существует", "1": "Сервер с таким именем уже существует",
@ -332,7 +343,8 @@
"8": "Удалить задачу", "8": "Удалить задачу",
"9": "Перезагрузить", "9": "Перезагрузить",
"10": "API не поддерживает домены с таким TLD.", "10": "API не поддерживает домены с таким TLD.",
"yes": "Да" "yes": "Да",
"no": "Нет"
}, },
"timer": { "timer": {
"sec": "{} сек" "sec": "{} сек"

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.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/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
@ -22,6 +23,7 @@ class BlocAndProviderConfig extends StatelessWidget {
var servicesCubit = ServicesCubit(serverInstallationCubit); var servicesCubit = ServicesCubit(serverInstallationCubit);
var backupsCubit = BackupsCubit(serverInstallationCubit); var backupsCubit = BackupsCubit(serverInstallationCubit);
var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); var dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
var recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
return MultiProvider( return MultiProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
@ -36,6 +38,7 @@ class BlocAndProviderConfig extends StatelessWidget {
BlocProvider(create: (_) => servicesCubit..load(), lazy: false), BlocProvider(create: (_) => servicesCubit..load(), lazy: false),
BlocProvider(create: (_) => backupsCubit..load(), lazy: false), BlocProvider(create: (_) => backupsCubit..load(), lazy: false),
BlocProvider(create: (_) => dnsRecordsCubit..load()), BlocProvider(create: (_) => dnsRecordsCubit..load()),
BlocProvider(create: (_) => recoveryKeyCubit..load()),
BlocProvider( BlocProvider(
create: (_) => create: (_) =>
JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit),

View File

@ -628,7 +628,7 @@ class ServerApi extends ApiMap {
.replaceAll('"', ''); .replaceAll('"', '');
} }
Future<ApiResponse<RecoveryKeyStatus>> getRecoveryTokenStatus() async { Future<ApiResponse<RecoveryKeyStatus?>> getRecoveryTokenStatus() async {
Response response; Response response;
var client = await getClient(); var client = await getClient();
@ -649,7 +649,7 @@ class ServerApi extends ApiMap {
return ApiResponse( return ApiResponse(
statusCode: code, statusCode: code,
data: response.data != null data: response.data != null
? response.data.fromJson(response.data) ? RecoveryKeyStatus.fromJson(response.data)
: null); : null);
} }

View File

@ -51,6 +51,7 @@ class BackblazeFormCubit extends FormCubit {
isKeyValid = await apiClient.isValid(encodedApiKey); isKeyValid = await apiClient.isValid(encodedApiKey);
} catch (e) { } catch (e) {
addError(e); addError(e);
isKeyValid = false;
} }
if (!isKeyValid) { if (!isKeyValid) {

View File

@ -26,7 +26,7 @@ class RecoveryKeyCubit
} }
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async { Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
final ApiResponse<RecoveryKeyStatus> response = final ApiResponse<RecoveryKeyStatus?> response =
await api.getRecoveryTokenStatus(); await api.getRecoveryTokenStatus();
if (response.isSuccess) { if (response.isSuccess) {
return response.data; return response.data;

View File

@ -64,6 +64,10 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
} }
void setCloudflareKey(String cloudFlareKey) async { void setCloudflareKey(String cloudFlareKey) async {
if (state is ServerInstallationRecovery) {
setAndValidateCloudflareToken(cloudFlareKey);
return;
}
await repository.saveCloudFlareKey(cloudFlareKey); await repository.saveCloudFlareKey(cloudFlareKey);
emit((state as ServerInstallationNotFinished) emit((state as ServerInstallationNotFinished)
.copyWith(cloudFlareKey: cloudFlareKey)); .copyWith(cloudFlareKey: cloudFlareKey));
@ -431,12 +435,19 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
.showSnackBar('recovering.domain_not_available_on_token'.tr()); .showSnackBar('recovering.domain_not_available_on_token'.tr());
return; return;
} }
await repository.saveDomain(ServerDomain(
domainName: serverDomain.domainName,
zoneId: zoneId,
provider: DnsProvider.Cloudflare,
));
await repository.saveCloudFlareKey(token);
emit(dataState.copyWith( emit(dataState.copyWith(
serverDomain: ServerDomain( serverDomain: ServerDomain(
domainName: serverDomain.domainName, domainName: serverDomain.domainName,
zoneId: zoneId, zoneId: zoneId,
provider: DnsProvider.Cloudflare, provider: DnsProvider.Cloudflare,
), ),
cloudFlareKey: token,
currentStep: RecoveryStep.BackblazeToken, currentStep: RecoveryStep.BackblazeToken,
)); ));
} }

View File

@ -88,8 +88,9 @@ class MyApp extends StatelessWidget {
: RootPage(), : RootPage(),
builder: (BuildContext context, Widget? widget) { builder: (BuildContext context, Widget? widget) {
Widget error = Text('...rendering error...'); Widget error = Text('...rendering error...');
if (widget is Scaffold || widget is Navigator) if (widget is Scaffold || widget is Navigator) {
error = Scaffold(body: Center(child: error)); error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder = ErrorWidget.builder =
(FlutterErrorDetails errorDetails) => error; (FlutterErrorDetails errorDetails) => error;
return widget!; return widget!;

View File

@ -0,0 +1,14 @@
/*import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
class RecoveryKey extends StatefulWidget {
const RecoveryKey({Key? key}) : super(key: key);
@override
State<RecoveryKey> createState() => _RecoveryKeyState();
}
class _RecoveryKeyState extends State<RecoveryKey> {
@override
Widget build(BuildContext context) {}
}*/

View File

@ -0,0 +1 @@

View File

@ -40,34 +40,44 @@ class RecoverByNewDeviceKeyInput extends StatelessWidget {
FieldCubitFactory(context), FieldCubitFactory(context),
ServerRecoveryMethods.newDeviceKey, ServerRecoveryMethods.newDeviceKey,
), ),
child: Builder( child: BlocListener<ServerInstallationCubit, ServerInstallationState>(
builder: (context) { listener: (context, state) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state; if (state is ServerInstallationRecovery &&
state.currentStep != RecoveryStep.NewDeviceKey) {
return BrandHeroScreen( Navigator.of(context).pop();
heroTitle: "recovering.recovery_main_header".tr(), }
heroSubtitle: "recovering.method_device_input_description".tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
CubitFormTextField(
formFieldCubit:
context.read<RecoveryDeviceFormCubit>().tokenField,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "recovering.method_device_input_placeholder".tr(),
),
),
SizedBox(height: 16),
FilledButton(
title: "more.continue".tr(),
onPressed: formCubitState.isSubmitting
? null
: () => context.read<RecoveryDeviceFormCubit>().trySubmit(),
)
],
);
}, },
child: Builder(
builder: (context) {
var formCubitState = context.watch<RecoveryDeviceFormCubit>().state;
return BrandHeroScreen(
heroTitle: "recovering.recovery_main_header".tr(),
heroSubtitle: "recovering.method_device_input_description".tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
CubitFormTextField(
formFieldCubit:
context.read<RecoveryDeviceFormCubit>().tokenField,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText:
"recovering.method_device_input_placeholder".tr(),
),
),
SizedBox(height: 16),
FilledButton(
title: "more.continue".tr(),
onPressed: formCubitState.isSubmitting
? null
: () =>
context.read<RecoveryDeviceFormCubit>().trySubmit(),
)
],
);
},
),
), ),
); );
} }

View File

@ -13,24 +13,33 @@ class RecoverByOldTokenInstruction extends StatelessWidget {
RecoverByOldTokenInstruction({required this.instructionFilename}); RecoverByOldTokenInstruction({required this.instructionFilename});
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BrandHeroScreen( return BlocListener<ServerInstallationCubit, ServerInstallationState>(
heroTitle: "recovering.recovery_main_header".tr(), listener: (context, state) {
hasBackButton: true, if (state is ServerInstallationRecovery &&
hasFlashButton: false, state.currentStep != RecoveryStep.Selecting) {
onBackButtonPressed: () => Navigator.of(context).pop();
context.read<ServerInstallationCubit>().revertRecoveryStep(), Navigator.of(context).pop();
children: [ }
BrandMarkdown( },
fileName: instructionFilename, child: BrandHeroScreen(
), heroTitle: "recovering.recovery_main_header".tr(),
SizedBox(height: 16), hasBackButton: true,
FilledButton( hasFlashButton: false,
title: "recovering.method_device_button".tr(), onBackButtonPressed: () =>
onPressed: () => context context.read<ServerInstallationCubit>().revertRecoveryStep(),
.read<ServerInstallationCubit>() children: [
.selectRecoveryMethod(ServerRecoveryMethods.oldToken), BrandMarkdown(
) fileName: instructionFilename,
], ),
SizedBox(height: 18),
FilledButton(
title: "recovering.method_device_button".tr(),
onPressed: () => context
.read<ServerInstallationCubit>()
.selectRecoveryMethod(ServerRecoveryMethods.oldToken),
)
],
),
); );
} }
@ -66,7 +75,7 @@ class RecoverByOldToken extends StatelessWidget {
labelText: "recovering.method_device_input_placeholder".tr(), labelText: "recovering.method_device_input_placeholder".tr(),
), ),
), ),
SizedBox(height: 16), SizedBox(height: 18),
FilledButton( FilledButton(
title: "more.continue".tr(), title: "more.continue".tr(),
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting

View File

@ -30,26 +30,28 @@ class RecoveryConfirmBackblaze extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
scrollPadding: EdgeInsets.only(bottom: 70), scrollPadding: EdgeInsets.only(bottom: 70),
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'KeyID', hintText: 'KeyID',
), ),
), ),
Spacer(), const SizedBox(height: 18),
CubitFormTextField( CubitFormTextField(
formFieldCubit: context.read<BackblazeFormCubit>().applicationKey, formFieldCubit: context.read<BackblazeFormCubit>().applicationKey,
textAlign: TextAlign.center, textAlign: TextAlign.center,
scrollPadding: EdgeInsets.only(bottom: 70), scrollPadding: EdgeInsets.only(bottom: 70),
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'Master Application Key', hintText: 'Master Application Key',
), ),
), ),
Spacer(), const SizedBox(height: 18),
BrandButton.rised( BrandButton.rised(
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting
? null ? null
: () => context.read<BackblazeFormCubit>().trySubmit(), : () => context.read<BackblazeFormCubit>().trySubmit(),
text: 'basis.connect'.tr(), text: 'basis.connect'.tr(),
), ),
SizedBox(height: 10), const SizedBox(height: 18),
BrandButton.text( BrandButton.text(
onPressed: () => showModalBottomSheet<void>( onPressed: () => showModalBottomSheet<void>(
context: context, context: context,

View File

@ -32,17 +32,18 @@ class RecoveryConfirmCloudflare extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
scrollPadding: EdgeInsets.only(bottom: 70), scrollPadding: EdgeInsets.only(bottom: 70),
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: 'initializing.5'.tr(), hintText: 'initializing.5'.tr(),
), ),
), ),
Spacer(), const SizedBox(height: 18),
BrandButton.rised( BrandButton.rised(
onPressed: formCubitState.isSubmitting onPressed: formCubitState.isSubmitting
? null ? null
: () => context.read<CloudFlareFormCubit>().trySubmit(), : () => context.read<CloudFlareFormCubit>().trySubmit(),
text: 'basis.connect'.tr(), text: 'basis.connect'.tr(),
), ),
SizedBox(height: 10), const SizedBox(height: 18),
BrandButton.text( BrandButton.text(
onPressed: () => showModalBottomSheet<void>( onPressed: () => showModalBottomSheet<void>(
context: context, context: context,

View File

@ -36,11 +36,11 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BrandHeroScreen( return BrandHeroScreen(
heroTitle: _isExtended heroTitle: _isExtended
? "recovering.choose_server".tr() ? 'recovering.choose_server'.tr()
: "recovering.confirm_server".tr(), : 'recovering.confirm_server'.tr(),
heroSubtitle: _isExtended heroSubtitle: _isExtended
? "recovering.choose_server_description".tr() ? 'recovering.choose_server_description'.tr()
: "recovering.confirm_server_description".tr(), : 'recovering.confirm_server_description'.tr(),
hasBackButton: true, hasBackButton: true,
hasFlashButton: false, hasFlashButton: false,
children: [ children: [
@ -68,7 +68,7 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
if (servers?.isEmpty ?? true) if (servers?.isEmpty ?? true)
Center( Center(
child: Text( child: Text(
"recovering.no_servers".tr(), 'recovering.no_servers'.tr(),
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.headline6,
), ),
), ),
@ -99,7 +99,7 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
), ),
SizedBox(height: 16), SizedBox(height: 16),
FilledButton( FilledButton(
title: "recovering.confirm_server_accept".tr(), title: 'recovering.confirm_server_accept'.tr(),
onPressed: () => _showConfirmationDialog(context, server), onPressed: () => _showConfirmationDialog(context, server),
), ),
SizedBox(height: 16), SizedBox(height: 16),
@ -166,19 +166,65 @@ class _RecoveryConfirmServerState extends State<RecoveryConfirmServer> {
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('ssh.delete'.tr()), title: Column(
content: SingleChildScrollView( crossAxisAlignment: CrossAxisAlignment.center,
child: ListBody( children: [
children: <Widget>[ const Icon(Icons.warning_amber_outlined),
Text("WOW DIALOGUE TEXT WOW :)"), const SizedBox(height: 8),
], Text(
), 'recovering.modal_confirmation_title'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('recovering.modal_confirmation_description'.tr(),
style: Theme.of(context).textTheme.bodyMedium),
const Divider(),
Text(
server.name,
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: 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()),
],
),
],
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text('basis.cancel'.tr()), child: Text('modals.no'.tr()),
onPressed: () { onPressed: () {
Navigator.of(context)..pop(); Navigator.of(context).pop();
},
),
TextButton(
child: Text('modals.yes'.tr()),
onPressed: () {
context.read<ServerInstallationCubit>().setServerId(server);
Navigator.of(context).pop();
}, },
), ),
], ],

View File

@ -9,6 +9,8 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart';
import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart';
@ -43,8 +45,10 @@ class RecoveryRouting extends StatelessWidget {
currentPage = RecoveryConfirmServer(); currentPage = RecoveryConfirmServer();
break; break;
case RecoveryStep.CloudflareToken: case RecoveryStep.CloudflareToken:
currentPage = RecoveryConfirmCloudflare();
break; break;
case RecoveryStep.BackblazeToken: case RecoveryStep.BackblazeToken:
currentPage = RecoveryConfirmBackblaze();
break; break;
} }
} }

View File

@ -363,6 +363,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.9.2" 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: flutter_localizations:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -560,6 +567,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.2.0" version: "6.2.0"
lints:
dependency: transitive
description:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
local_auth: local_auth:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -52,6 +52,7 @@ dev_dependencies:
flutter_launcher_icons: ^0.9.2 flutter_launcher_icons: ^0.9.2
hive_generator: ^1.0.0 hive_generator: ^1.0.0
json_serializable: ^6.1.4 json_serializable: ^6.1.4
flutter_lints: ^2.0.1
flutter_icons: flutter_icons:
android: "launcher_icon" android: "launcher_icon"