mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-15 06:12:29 -06:00
feat: bugfixes, finished go_router migration, implemented better visibility of states
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user