feat: bugfixes, finished go_router migration, implemented better visibility of states

This commit is contained in:
Anton Stubenbord
2023-10-06 01:17:08 +02:00
parent ad23df4f89
commit a2c5ced3b7
102 changed files with 1512 additions and 3090 deletions

View File

@@ -1,11 +1,9 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
import 'package:paperless_mobile/core/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/config/hive/hive_extensions.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.dart';
@@ -14,6 +12,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_app_state.dart'
import 'package:paperless_mobile/core/database/tables/local_user_settings.dart';
import 'package:paperless_mobile/core/database/tables/user_credentials.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.dart';
import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart';
import 'package:paperless_mobile/core/model/info_message_exception.dart';
import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
@@ -22,21 +21,26 @@ import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/login/model/login_form_credentials.dart';
import 'package:paperless_mobile/features/login/model/reachability_status.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
part 'authentication_state.dart';
typedef _FutureVoidCallback = Future<void> Function();
class AuthenticationCubit extends Cubit<AuthenticationState> {
final LocalAuthenticationService _localAuthService;
final PaperlessApiFactory _apiFactory;
final SessionManager _sessionManager;
final ConnectivityStatusService _connectivityService;
final LocalNotificationService _notificationService;
AuthenticationCubit(
this._localAuthService,
this._apiFactory,
this._sessionManager,
this._connectivityService,
this._notificationService,
) : super(const UnauthenticatedState());
Future<void> login({
@@ -45,7 +49,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
ClientCertificate? clientCertificate,
}) async {
assert(credentials.username != null && credentials.password != null);
emit(const CheckingLoginState());
if (state is AuthenticatingState) {
// Cancel duplicate login requests
return;
}
emit(const AuthenticatingState(AuthenticatingStage.authenticating));
final localUserId = "${credentials.username}@$serverUrl";
_debugPrintMessage(
"login",
@@ -58,35 +66,63 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
credentials,
clientCertificate,
_sessionManager,
onFetchUserInformation: () async {
emit(const AuthenticatingState(
AuthenticatingStage.fetchingUserInformation));
},
onPerformLogin: () async {
emit(const AuthenticatingState(AuthenticatingStage.authenticating));
},
onPersistLocalUserData: () async {
emit(const AuthenticatingState(
AuthenticatingStage.persistingLocalUserData));
},
);
// Mark logged in user as currently active user.
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.loggedInUserId = localUserId;
await globalSettings.save();
emit(AuthenticatedState(localUserId: localUserId));
_debugPrintMessage(
"login",
"User successfully logged in.",
} catch (e) {
emit(
AuthenticationErrorState(
serverUrl: serverUrl,
username: credentials.username!,
password: credentials.password!,
clientCertificate: clientCertificate,
),
);
} catch (error) {
emit(const UnauthenticatedState());
rethrow;
}
// Mark logged in user as currently active user.
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
globalSettings.loggedInUserId = localUserId;
await globalSettings.save();
emit(AuthenticatedState(localUserId: localUserId));
_debugPrintMessage(
"login",
"User successfully logged in.",
);
}
/// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async {
emit(const SwitchingAccountsState());
_debugPrintMessage(
"switchAccount",
"Trying to switch to user $localUserId...",
);
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
if (globalSettings.loggedInUserId == localUserId) {
emit(AuthenticatedState(localUserId: localUserId));
return;
}
final userAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
// if (globalSettings.loggedInUserId == localUserId) {
// _debugPrintMessage(
// "switchAccount",
// "User $localUserId is already logged in.",
// );
// emit(AuthenticatedState(localUserId: localUserId));
// return;
// }
final userAccountBox = Hive.localUserAccountBox;
if (!userAccountBox.containsKey(localUserId)) {
debugPrint("User $localUserId not yet registered.");
@@ -99,10 +135,18 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authenticated = await _localAuthService
.authenticateLocalUser("Authenticate to switch your account.");
if (!authenticated) {
debugPrint("User not authenticated.");
_debugPrintMessage(
"switchAccount",
"User could not be authenticated.",
);
emit(VerifyIdentityState(userId: localUserId));
return;
}
}
final currentlyLoggedInUser = globalSettings.loggedInUserId;
if (currentlyLoggedInUser != localUserId) {
await _notificationService.cancelUserNotifications(localUserId);
}
await withEncryptedBox<UserCredentials, void>(
HiveBoxes.localUserCredentials, (credentialsBox) async {
if (!credentialsBox.containsKey(localUserId)) {
@@ -131,9 +175,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
apiVersion,
);
emit(AuthenticatedState(
localUserId: localUserId,
));
emit(AuthenticatedState(localUserId: localUserId));
});
}
@@ -142,19 +184,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
required String serverUrl,
ClientCertificate? clientCertificate,
required bool enableBiometricAuthentication,
required String locale,
}) async {
assert(credentials.password != null && credentials.username != null);
final localUserId = "${credentials.username}@$serverUrl";
final sessionManager = SessionManager();
await _addUser(
localUserId,
serverUrl,
credentials,
clientCertificate,
sessionManager,
);
return localUserId;
final sessionManager = SessionManager([
LanguageHeaderInterceptor(locale),
]);
try {
await _addUser(
localUserId,
serverUrl,
credentials,
clientCertificate,
sessionManager,
// onPerformLogin: () async {
// emit(AuthenticatingState(AuthenticatingStage.authenticating));
// await Future.delayed(const Duration(milliseconds: 500));
// },
);
return localUserId;
} catch (error, stackTrace) {
print(error);
debugPrintStack(stackTrace: stackTrace);
rethrow;
}
}
Future<void> removeAccount(String userId) async {
@@ -170,28 +226,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
///
/// Performs a conditional hydration based on the local authentication success.
/// Restores the previous session if exists.
///
Future<void> restoreSessionState() async {
Future<void> restoreSession([String? userId]) async {
emit(const RestoringSessionState());
_debugPrintMessage(
"restoreSessionState",
"Trying to restore previous session...",
);
final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final localUserId = globalSettings.loggedInUserId;
if (localUserId == null) {
final restoreSessionForUser = userId ?? globalSettings.loggedInUserId;
// final localUserId = globalSettings.loggedInUserId;
if (restoreSessionForUser == null) {
_debugPrintMessage(
"restoreSessionState",
"There is nothing to restore.",
);
final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty;
// If there is nothing to restore, we can quit here.
emit(const UnauthenticatedState());
emit(
UnauthenticatedState(redirectToAccountSelection: otherAccountsExist),
);
return;
}
final localUserAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccount = localUserAccountBox.get(localUserId)!;
final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!;
_debugPrintMessage(
"restoreSessionState",
"Checking if biometric authentication is required...",
@@ -207,7 +268,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final localAuthSuccess =
await _localAuthService.authenticateLocalUser(authenticationMesage);
if (!localAuthSuccess) {
emit(const RequiresLocalAuthenticationState());
emit(VerifyIdentityState(userId: restoreSessionForUser));
_debugPrintMessage(
"restoreSessionState",
"User could not be authenticated.",
@@ -231,7 +292,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) {
return box.get(globalSettings.loggedInUserId!);
return box.get(restoreSessionForUser);
});
if (authentication == null) {
@@ -290,8 +351,9 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"Skipping update of server user (server could not be reached).",
);
}
emit(AuthenticatedState(localUserId: localUserId));
globalSettings.loggedInUserId = restoreSessionForUser;
await globalSettings.save();
emit(AuthenticatedState(localUserId: restoreSessionForUser));
_debugPrintMessage(
"restoreSessionState",
@@ -300,7 +362,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
Future<void> logout([bool removeAccount = false]) async {
emit(const LogginOutState());
emit(const LoggingOutState());
_debugPrintMessage(
"logout",
"Trying to log out current user...",
@@ -308,13 +370,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await _resetExternalState();
final globalSettings = Hive.globalSettingsBox.getValue()!;
final userId = globalSettings.loggedInUserId!;
await _notificationService.cancelUserNotifications(userId);
final otherAccountsExist = Hive.localUserAccountBox.length > 1;
emit(UnauthenticatedState(redirectToAccountSelection: otherAccountsExist));
if (removeAccount) {
this.removeAccount(userId);
await this.removeAccount(userId);
}
globalSettings.loggedInUserId = null;
await globalSettings.save();
emit(const UnauthenticatedState());
_debugPrintMessage(
"logout",
"User successfully logged out.",
@@ -322,16 +387,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}
Future<void> _resetExternalState() async {
_debugPrintMessage(
"_resetExternalState",
"Resetting session manager and clearing storage...",
);
_sessionManager.resetSettings();
await HydratedBloc.storage.clear();
_debugPrintMessage(
"_resetExternalState",
"Session manager successfully reset and storage cleared.",
);
}
Future<int> _addUser(
@@ -339,8 +396,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
String serverUrl,
LoginFormCredentials credentials,
ClientCertificate? clientCert,
SessionManager sessionManager,
) async {
SessionManager sessionManager, {
_FutureVoidCallback? onPerformLogin,
_FutureVoidCallback? onPersistLocalUserData,
_FutureVoidCallback? onFetchUserInformation,
}) async {
assert(credentials.username != null && credentials.password != null);
_debugPrintMessage("_addUser", "Adding new user $localUserId...");
@@ -356,6 +416,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"Trying to login user ${credentials.username} on $serverUrl...",
);
await onPerformLogin?.call();
final token = await authApi.login(
username: credentials.username!,
password: credentials.password!,
@@ -384,6 +446,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
);
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
}
await onFetchUserInformation?.call();
final apiVersion = await _getApiVersion(sessionManager.client);
_debugPrintMessage(
"_addUser",
@@ -413,6 +476,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"_addUser",
"Persisting local user account...",
);
await onPersistLocalUserData?.call();
// Create user account
await userAccountBox.put(
localUserId,
@@ -490,7 +554,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
"API version ($apiVersion) successfully retrieved.",
);
return apiVersion;
} on DioException catch (e) {
} on DioException catch (_) {
return defaultValue;
}
}

View File

@@ -7,34 +7,76 @@ sealed class AuthenticationState {
switch (this) { AuthenticatedState() => true, _ => false };
}
class UnauthenticatedState extends AuthenticationState {
const UnauthenticatedState();
class UnauthenticatedState extends AuthenticationState with EquatableMixin {
final bool redirectToAccountSelection;
const UnauthenticatedState({this.redirectToAccountSelection = false});
@override
List<Object?> get props => [redirectToAccountSelection];
}
class RequiresLocalAuthenticationState extends AuthenticationState {
const RequiresLocalAuthenticationState();
class RestoringSessionState extends AuthenticationState {
const RestoringSessionState();
}
class CheckingLoginState extends AuthenticationState {
const CheckingLoginState();
class VerifyIdentityState extends AuthenticationState {
final String userId;
const VerifyIdentityState({required this.userId});
}
class LogginOutState extends AuthenticationState {
const LogginOutState();
class AuthenticatingState extends AuthenticationState with EquatableMixin {
final AuthenticatingStage currentStage;
const AuthenticatingState(this.currentStage);
@override
List<Object?> get props => [currentStage];
}
class AuthenticatedState extends AuthenticationState {
class LoggingOutState extends AuthenticationState {
const LoggingOutState();
}
class AuthenticatedState extends AuthenticationState with EquatableMixin {
final String localUserId;
const AuthenticatedState({
required this.localUserId,
});
const AuthenticatedState({required this.localUserId});
@override
List<Object?> get props => [localUserId];
}
class SwitchingAccountsState extends AuthenticationState {
const SwitchingAccountsState();
}
class AuthenticationErrorState extends AuthenticationState {
const AuthenticationErrorState();
class AuthenticationErrorState extends AuthenticationState with EquatableMixin {
final ErrorCode? errorCode;
final String serverUrl;
final ClientCertificate? clientCertificate;
final String username;
final String password;
const AuthenticationErrorState({
this.errorCode,
required this.serverUrl,
this.clientCertificate,
required this.username,
required this.password,
});
@override
List<Object?> get props => [
errorCode,
serverUrl,
clientCertificate,
username,
password,
];
}
enum AuthenticatingStage {
authenticating,
persistingLocalUserData,
fetchingUserInformation,
}

View File

@@ -1,48 +0,0 @@
import 'package:equatable/equatable.dart';
class OldAuthenticationState with EquatableMixin {
final bool showBiometricAuthenticationScreen;
final bool isAuthenticated;
final String? username;
final String? fullName;
final String? localUserId;
final int? apiVersion;
const OldAuthenticationState({
this.isAuthenticated = false,
this.showBiometricAuthenticationScreen = false,
this.username,
this.fullName,
this.localUserId,
this.apiVersion,
});
OldAuthenticationState copyWith({
bool? isAuthenticated,
bool? showBiometricAuthenticationScreen,
String? username,
String? fullName,
String? localUserId,
int? apiVersion,
}) {
return OldAuthenticationState(
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
showBiometricAuthenticationScreen: showBiometricAuthenticationScreen ??
this.showBiometricAuthenticationScreen,
username: username ?? this.username,
fullName: fullName ?? this.fullName,
localUserId: localUserId ?? this.localUserId,
apiVersion: apiVersion ?? this.apiVersion,
);
}
@override
List<Object?> get props => [
localUserId,
username,
fullName,
isAuthenticated,
showBiometricAuthenticationScreen,
apiVersion,
];
}