refactor: Replace RecoveryKeyCubit with RecoveryKeyBloc

pull/440/head
Inex Code 2024-02-08 18:08:29 +03:00
parent 1daf957245
commit 3a525f0d11
10 changed files with 294 additions and 205 deletions

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/connection_status/connection_status_bloc.dart';
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
@ -9,7 +10,6 @@ import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
@ -31,7 +31,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
late final ServicesBloc servicesBloc;
late final BackupsBloc backupsBloc;
late final DnsRecordsCubit dnsRecordsCubit;
late final RecoveryKeyCubit recoveryKeyCubit;
late final RecoveryKeyBloc recoveryKeyBloc;
late final ApiDevicesCubit apiDevicesCubit;
late final ServerJobsBloc serverJobsBloc;
late final ConnectionStatusBloc connectionStatusBloc;
@ -47,7 +47,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
servicesBloc = ServicesBloc();
backupsBloc = BackupsBloc();
dnsRecordsCubit = DnsRecordsCubit();
recoveryKeyCubit = RecoveryKeyCubit();
recoveryKeyBloc = RecoveryKeyBloc();
apiDevicesCubit = ApiDevicesCubit();
serverJobsBloc = ServerJobsBloc();
connectionStatusBloc = ConnectionStatusBloc();
@ -90,7 +90,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
create: (final _) => dnsRecordsCubit,
),
BlocProvider(
create: (final _) => recoveryKeyCubit,
create: (final _) => recoveryKeyBloc,
),
BlocProvider(
create: (final _) => apiDevicesCubit,

View File

@ -0,0 +1,94 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
part 'recovery_key_event.dart';
part 'recovery_key_state.dart';
class RecoveryKeyBloc extends Bloc<RecoveryKeyEvent, RecoveryKeyState> {
RecoveryKeyBloc() : super(RecoveryKeyInitial()) {
on<RecoveryKeyStatusChanged>(
_mapRecoveryKeyStatusChangedToState,
transformer: sequential(),
);
on<CreateNewRecoveryKey>(
_mapCreateNewRecoveryKeyToState,
transformer: sequential(),
);
on<ConsumedNewRecoveryKey>(
_mapRecoveryKeyStatusRefreshToState,
transformer: sequential(),
);
on<RecoveryKeyStatusRefresh>(
_mapRecoveryKeyStatusRefreshToState,
transformer: droppable(),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
add(
RecoveryKeyStatusChanged(apiData.recoveryKeyStatus.data),
);
},
);
}
StreamSubscription? _apiDataSubscription;
Future<void> _mapRecoveryKeyStatusChangedToState(
final RecoveryKeyStatusChanged event,
final Emitter<RecoveryKeyState> emit,
) async {
if (state is RecoveryKeyCreating) {
return;
}
if (event.recoveryKeyStatus == null) {
emit(RecoveryKeyError());
return;
}
emit(RecoveryKeyLoaded(keyStatus: event.recoveryKeyStatus));
}
Future<void> _mapCreateNewRecoveryKeyToState(
final CreateNewRecoveryKey event,
final Emitter<RecoveryKeyState> emit,
) async {
emit(RecoveryKeyCreating());
final GenericResult<String> response =
await getIt<ApiConnectionRepository>().api.generateRecoveryToken(
event.expirationDate,
event.numberOfUses,
);
if (response.success) {
emit(RecoveryKeyCreating(recoveryKey: response.data));
} else {
emit(RecoveryKeyCreating(error: response.message ?? 'Unknown error'));
}
}
Future<void> _mapRecoveryKeyStatusRefreshToState(
final RecoveryKeyEvent event,
final Emitter<RecoveryKeyState> emit,
) async {
emit(RecoveryKeyRefreshing(keyStatus: state._status));
getIt<ApiConnectionRepository>().apiData.recoveryKeyStatus.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
}
@override
void onChange(final Change<RecoveryKeyState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,41 @@
part of 'recovery_key_bloc.dart';
sealed class RecoveryKeyEvent extends Equatable {
const RecoveryKeyEvent();
}
class RecoveryKeyStatusChanged extends RecoveryKeyEvent {
const RecoveryKeyStatusChanged(this.recoveryKeyStatus);
final RecoveryKeyStatus? recoveryKeyStatus;
@override
List<Object?> get props => [recoveryKeyStatus];
}
class CreateNewRecoveryKey extends RecoveryKeyEvent {
const CreateNewRecoveryKey({
this.expirationDate,
this.numberOfUses,
});
final DateTime? expirationDate;
final int? numberOfUses;
@override
List<Object?> get props => [expirationDate, numberOfUses];
}
class ConsumedNewRecoveryKey extends RecoveryKeyEvent {
const ConsumedNewRecoveryKey();
@override
List<Object?> get props => [];
}
class RecoveryKeyStatusRefresh extends RecoveryKeyEvent {
const RecoveryKeyStatusRefresh();
@override
List<Object?> get props => [];
}

View File

@ -0,0 +1,67 @@
part of 'recovery_key_bloc.dart';
sealed class RecoveryKeyState extends Equatable {
RecoveryKeyState({
required final RecoveryKeyStatus? keyStatus,
}) : _hashCode = keyStatus.hashCode;
final int _hashCode;
RecoveryKeyStatus get _status =>
getIt<ApiConnectionRepository>().apiData.recoveryKeyStatus.data ??
const RecoveryKeyStatus(exists: false, valid: false);
bool get exists => _status.exists;
bool get isValid => _status.valid;
DateTime? get generatedAt => _status.date;
DateTime? get expiresAt => _status.expiration;
int? get usesLeft => _status.usesLeft;
bool get isInvalidBecauseExpired =>
_status.expiration != null &&
_status.expiration!.isBefore(DateTime.now());
bool get isInvalidBecauseUsed =>
_status.usesLeft != null && _status.usesLeft == 0;
}
class RecoveryKeyInitial extends RecoveryKeyState {
RecoveryKeyInitial()
: super(keyStatus: const RecoveryKeyStatus(exists: false, valid: false));
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyRefreshing extends RecoveryKeyState {
RecoveryKeyRefreshing({required super.keyStatus});
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyLoaded extends RecoveryKeyState {
RecoveryKeyLoaded({required super.keyStatus});
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyError extends RecoveryKeyState {
RecoveryKeyError()
: super(keyStatus: const RecoveryKeyStatus(exists: false, valid: false));
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyCreating extends RecoveryKeyState {
RecoveryKeyCreating({this.recoveryKey, this.error})
: super(keyStatus: const RecoveryKeyStatus(exists: false, valid: false));
final String? recoveryKey;
final String? error;
@override
List<Object?> get props => [_hashCode, recoveryKey, error];
}

View File

@ -1,80 +0,0 @@
import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/server_connection_dependent/server_connection_dependent_cubit.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
part 'recovery_key_state.dart';
class RecoveryKeyCubit
extends ServerConnectionDependentCubit<RecoveryKeyState> {
RecoveryKeyCubit() : super(const RecoveryKeyState.initial());
final ServerApi api = ServerApi();
@override
void load() async {
// if (serverInstallationCubit.state is ServerInstallationFinished) {
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
if (status == null) {
emit(state.copyWith(loadingStatus: LoadingStatus.error));
} else {
emit(
state.copyWith(
status: status,
loadingStatus: LoadingStatus.success,
),
);
}
// } else {
// emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
// }
}
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
final GenericResult<RecoveryKeyStatus?> response =
await api.getRecoveryTokenStatus();
if (response.success) {
return response.data;
} else {
return null;
}
}
Future<void> refresh() async {
emit(state.copyWith(loadingStatus: LoadingStatus.refreshing));
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
if (status == null) {
emit(state.copyWith(loadingStatus: LoadingStatus.error));
} else {
emit(
state.copyWith(status: status, loadingStatus: LoadingStatus.success),
);
}
}
Future<String> generateRecoveryKey({
final DateTime? expirationDate,
final int? numberOfUses,
}) async {
final GenericResult<String> response =
await api.generateRecoveryToken(expirationDate, numberOfUses);
if (response.success) {
unawaited(refresh());
return response.data;
} else {
throw GenerationError(response.message ?? 'Unknown error');
}
}
@override
void clear() {
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
}
}
class GenerationError extends Error {
GenerationError(this.message);
final String message;
}

View File

@ -1,39 +0,0 @@
part of 'recovery_key_cubit.dart';
class RecoveryKeyState extends ServerInstallationDependendState {
const RecoveryKeyState(this._status, this.loadingStatus);
const RecoveryKeyState.initial()
: this(
const RecoveryKeyStatus(exists: false, valid: false),
LoadingStatus.refreshing,
);
final RecoveryKeyStatus _status;
final LoadingStatus loadingStatus;
bool get exists => _status.exists;
bool get isValid => _status.valid;
DateTime? get generatedAt => _status.date;
DateTime? get expiresAt => _status.expiration;
int? get usesLeft => _status.usesLeft;
bool get isInvalidBecauseExpired =>
_status.expiration != null &&
_status.expiration!.isBefore(DateTime.now());
bool get isInvalidBecauseUsed =>
_status.usesLeft != null && _status.usesLeft == 0;
@override
List<Object> get props => [_status, loadingStatus];
RecoveryKeyState copyWith({
final RecoveryKeyStatus? status,
final LoadingStatus? loadingStatus,
}) =>
RecoveryKeyState(
status ?? _status,
loadingStatus ?? this.loadingStatus,
);
}

View File

@ -8,6 +8,7 @@ import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.da
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
import 'package:selfprivacy/logic/models/json/server_disk_volume.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -101,6 +102,8 @@ class ApiConnectionRepository {
_apiData.backups.data = await _apiData.backups.fetchData();
_apiData.services.data = await _apiData.services.fetchData();
_apiData.volumes.data = await _apiData.volumes.fetchData();
_apiData.recoveryKeyStatus.data =
await _apiData.recoveryKeyStatus.fetchData();
_dataStream.add(_apiData);
connectionStatus = ConnectionStatus.connected;
@ -140,6 +143,8 @@ class ApiConnectionRepository {
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.volumes
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.recoveryKeyStatus
.refetchData(version, () => _dataStream.add(_apiData));
}
void emitData() {
@ -159,10 +164,12 @@ class ApiData {
backupConfig = ApiDataElement<BackupConfiguration>(
fetchData: () async => api.getBackupsConfiguration(),
requiredApiVersion: '>=2.4.2',
ttl: 120,
),
backups = ApiDataElement<List<Backup>>(
fetchData: () async => api.getBackups(),
requiredApiVersion: '>=2.4.2',
ttl: 120,
),
services = ApiDataElement<List<Service>>(
fetchData: () async => api.getAllServices(),
@ -170,6 +177,10 @@ class ApiData {
),
volumes = ApiDataElement<List<ServerDiskVolume>>(
fetchData: () async => api.getServerDiskVolumes(),
),
recoveryKeyStatus = ApiDataElement<RecoveryKeyStatus>(
fetchData: () async => (await api.getRecoveryTokenStatus()).data,
ttl: 300,
);
ApiDataElement<List<ServerJob>> serverJobs;
@ -178,6 +189,7 @@ class ApiData {
ApiDataElement<List<Backup>> backups;
ApiDataElement<List<Service>> services;
ApiDataElement<List<ServerDiskVolume>> volumes;
ApiDataElement<RecoveryKeyStatus> recoveryKeyStatus;
}
enum ConnectionStatus {

View File

@ -4,8 +4,6 @@ import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
@RoutePage()
@ -89,18 +87,6 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
),
),
),
ListTile(
title: const Text('ApiDevicesCubit'),
subtitle: Text(
context.watch<ApiDevicesCubit>().state.status.toString(),
),
),
ListTile(
title: const Text('RecoveryKeyCubit'),
subtitle: Text(
context.watch<RecoveryKeyCubit>().state.loadingStatus.toString(),
),
),
ListTile(
title: const Text('ApiConnectionRepository status'),
subtitle: Text(

View File

@ -2,9 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:selfprivacy/config/get_it_config.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/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
@ -21,34 +19,29 @@ class RecoveryKeyPage extends StatefulWidget {
}
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
@override
void initState() {
super.initState();
context.read<RecoveryKeyCubit>().load();
}
@override
Widget build(final BuildContext context) {
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state;
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyBloc>().state;
final List<Widget> widgets;
String? subtitle =
keyStatus.exists ? null : 'recovery_key.key_main_description'.tr();
switch (keyStatus.loadingStatus) {
case LoadingStatus.refreshing:
switch (keyStatus) {
case RecoveryKeyRefreshing():
subtitle = 'recovery_key.key_synchronizing'.tr();
widgets = [
const Center(child: CircularProgressIndicator()),
];
break;
case LoadingStatus.success:
case RecoveryKeyLoaded():
widgets = [
const RecoveryKeyContent(),
];
break;
case LoadingStatus.uninitialized:
case LoadingStatus.error:
case RecoveryKeyInitial():
case RecoveryKeyError():
case RecoveryKeyCreating():
subtitle = 'recovery_key.key_connection_error'.tr();
widgets = [
const Icon(Icons.sentiment_dissatisfied_outlined),
@ -58,7 +51,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
return RefreshIndicator(
onRefresh: () async {
context.read<RecoveryKeyCubit>().load();
context.read<RecoveryKeyBloc>().add(const RecoveryKeyStatusRefresh());
},
child: BrandHeroScreen(
heroTitle: 'recovery_key.key_main_header'.tr(),
@ -83,7 +76,7 @@ class _RecoveryKeyContentState extends State<RecoveryKeyContent> {
@override
Widget build(final BuildContext context) {
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state;
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyBloc>().state;
return Column(
children: [
@ -241,34 +234,24 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
setState(() {
_isLoading = true;
});
try {
final String token =
await context.read<RecoveryKeyCubit>().generateRecoveryKey(
numberOfUses: _isAmountToggled
? int.tryParse(_amountController.text)
: null,
expirationDate: _isExpirationToggled ? _selectedDate : null,
);
if (!mounted) {
return;
}
setState(() {
_isLoading = false;
});
await Navigator.of(context).push(
materialRoute(
RecoveryKeyReceiving(recoveryKey: token), // TO DO
),
);
} on GenerationError catch (e) {
setState(() {
_isLoading = false;
});
getIt<NavigationService>().showSnackBar(
'recovery_key.generation_error'.tr(args: [e.message]),
);
context.read<RecoveryKeyBloc>().add(
CreateNewRecoveryKey(
expirationDate: _isExpirationToggled ? _selectedDate : null,
numberOfUses:
_isAmountToggled ? int.tryParse(_amountController.text) : null,
),
);
if (!mounted) {
return;
}
setState(() {
_isLoading = false;
});
await Navigator.of(context).push(
materialRoute(
const RecoveryKeyReceiving(),
),
);
}
void _updateErrorStatuses() {

View File

@ -1,23 +1,36 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
class RecoveryKeyReceiving extends StatelessWidget {
const RecoveryKeyReceiving({required this.recoveryKey, super.key});
final String recoveryKey;
const RecoveryKeyReceiving({super.key});
@override
Widget build(final BuildContext context) => BrandHeroScreen(
heroTitle: 'recovery_key.key_main_header'.tr(),
heroSubtitle: 'recovery_key.key_receiving_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
const Divider(),
const SizedBox(height: 16),
Widget build(final BuildContext context) {
final recoveryKeyState = context.watch<RecoveryKeyBloc>().state;
final String? recoveryKey = recoveryKeyState is RecoveryKeyCreating
? recoveryKeyState.recoveryKey
: null;
final String? error =
recoveryKeyState is RecoveryKeyCreating ? recoveryKeyState.error : null;
return BrandHeroScreen(
heroTitle: 'recovery_key.key_main_header'.tr(),
heroSubtitle: 'recovery_key.key_receiving_description'.tr(),
hasBackButton: true,
hasFlashButton: false,
children: [
const Divider(),
const SizedBox(height: 16),
if (recoveryKey == null && error == null)
const Center(child: CircularProgressIndicator()),
if (recoveryKey != null)
Text(
recoveryKey,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
@ -26,19 +39,31 @@ class RecoveryKeyReceiving extends StatelessWidget {
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
InfoBox(
text: 'recovery_key.key_receiving_info'.tr(),
if (error != null)
Text(
'recovery_key.generation_error'.tr(args: [error]),
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 24,
fontFamily: 'RobotoMono',
color: Theme.of(context).colorScheme.error,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
BrandButton.filled(
child: Text('recovery_key.key_receiving_done'.tr()),
onPressed: () {
Navigator.of(context).popUntil((final route) => route.isFirst);
},
),
],
);
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 16),
InfoBox(
text: 'recovery_key.key_receiving_info'.tr(),
),
const SizedBox(height: 16),
BrandButton.filled(
child: Text('recovery_key.key_receiving_done'.tr()),
onPressed: () {
context.read<RecoveryKeyBloc>().add(const ConsumedNewRecoveryKey());
Navigator.of(context).popUntil((final route) => route.isFirst);
},
),
],
);
}
}