import 'dart:async'; import 'dart:collection'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:stack_trace/stack_trace.dart'; import '../models/auth/credentials_intf.dart'; import '../models/auth/profile.dart'; import '../models/exec_error.dart'; import 'background_updater_services.dart'; import 'connection_manager_services.dart'; import 'globals_services.dart'; import 'instance_info_services.dart'; import 'networking/friendica_profile_client_services.dart'; import 'status_service.dart'; part 'account_services.g.dart'; @Riverpod(keepAlive: true) class LoggedInProfiles extends _$LoggedInProfiles { @override List build() { return UnmodifiableListView([]); } void add(Profile profile) { state = [...state, profile]; } void remove(Profile profile) { final profilesWithUpdate = List.from(state); profilesWithUpdate.removeWhere((p) => p == profile); state = UnmodifiableListView(profilesWithUpdate); } void clear() { state = UnmodifiableListView([]); } } @Riverpod(keepAlive: true) class LoggedOutProfiles extends _$LoggedOutProfiles { @override List build() { return []; } void add(Profile profile) { state = [...state, profile]; } void remove(Profile profile) { final profilesWithUpdate = List.from(state); profilesWithUpdate.removeWhere((p) => p == profile); state = UnmodifiableListView(profilesWithUpdate); } void clear() { state = UnmodifiableListView([]); } } const _activeProfileIdKey = 'active_profile_id'; @Riverpod(keepAlive: true) class ActiveProfile extends _$ActiveProfile { bool get hasActiveProfile => state != Profile.empty(); @override Profile build() { return Profile.empty(); } void setActiveProfile(Profile profile) { ref.read(backgroundUpdatersProvider(state).notifier).stop(); state = profile; _saveStoredLoginState(); ref.read(backgroundUpdatersProvider(state).notifier).reset(); } void clear() { state = Profile.empty(); _saveStoredLoginState(); } void _saveStoredLoginState() { final prefs = ref.read(sharedPreferencesProvider); prefs.setString(_activeProfileIdKey, state.id); } } @riverpod bool loggedIn(Ref ref) => ref.watch(activeProfileProvider) != Profile.empty(); final _pmLogger = Logger('ProfileManagerProvider'); @riverpod class CredentialSignin extends _$CredentialSignin { @override bool build(ICredentials credentials) { return false; } FutureResult signIn(bool activateProfileOnSuccess) async { final result = await credentials.signIn(ref).andThenAsync((signedInCredentials) async { ref.read(statusServiceProvider.notifier).setStatus( 'Getting user profile from ${signedInCredentials.serverName}'); return await ref.read( myProfileProvider(Profile.credentialsOnly(signedInCredentials)) .future); }).andThenAsync((profileResult) async { final profileData = profileResult.$1; final profile = profileResult.$2; final loginProfile = Profile( credentials: profile.credentials, username: profileData.name, serverName: profile.credentials.serverName, avatar: profileData.avatarUrl, userId: profileData.id, loggedIn: true, ); ref .read(statusServiceProvider.notifier) .setStatus('Loaded user profile ${profileData.handle}'); if (activateProfileOnSuccess) { ref.read(activeProfileProvider.notifier).setActiveProfile(loginProfile); } ref.read(loggedInProfilesProvider.notifier).add(loginProfile); ref.read(loggedOutProfilesProvider.notifier).remove(loginProfile); await ref.read(secretsServiceProvider).addOrUpdateProfile(loginProfile); await ref.read(connectionRepoInitProvider(loginProfile).future); ref.read(instanceRulesManagerProvider(loginProfile)); await ref .read(instanceInfoManagerProvider(loginProfile).notifier) .update(); state = true; return Result.ok(loginProfile); }); if (result.isFailure) { ref .read(statusServiceProvider.notifier) .setStatus('Error signing in: ${result.error}'); _pmLogger.severe( 'Error signing in $credentials: ${result.error}', Trace.current()); } return result.execErrorCast(); } } @Riverpod(keepAlive: true) class ProfileManager extends _$ProfileManager { @override bool build(Profile profile) { return false; } FutureResult signIn(bool activateProfileOnSuccess) async { return await ref .read(credentialSigninProvider(profile.credentials).notifier) .signIn(activateProfileOnSuccess); } Future signOut({bool withNotification = true}) async { if (ref.read(activeProfileProvider) == profile) { ref.read(activeProfileProvider.notifier).clear(); } final updatedProfile = profile.copyWithLoginUpdate(false); ref.read(loggedInProfilesProvider.notifier).remove(profile); ref.read(loggedOutProfilesProvider.notifier).add(updatedProfile); ref.read(secretsServiceProvider).addOrUpdateProfile(updatedProfile); if (ref.read(loggedInProfilesProvider).isNotEmpty) { ref.read(activeProfileProvider.notifier).setActiveProfile( ref.read(loggedInProfilesProvider).first, ); } if (withNotification) { ref.notifyListeners(); } } } @riverpod class AccountServicesInitializer extends _$AccountServicesInitializer { var initialized = false; @override bool build() { initialized = false; return false; } FutureResult initialize() async { var initializing = state; if (initialized) { return Result.ok(true); } if (initializing) { return buildErrorResult( type: ErrorType.localError, message: 'Already initializing'); } final lastActiveProfile = _getStoredLoginState(); final secretsService = ref.read(secretsServiceProvider); final result = await runCatchingAsync(() async { final initialProfiles = secretsService.profiles; for (final p in initialProfiles) { if (!p.loggedIn) { ref.read(loggedOutProfilesProvider.notifier).add(p); continue; } final pr = await ref.read(profileManagerProvider(p).notifier).signIn(false); if (pr.isSuccess) { final profile = pr.value; if (profile.id.isNotEmpty && profile.id == lastActiveProfile) { ref.read(activeProfileProvider.notifier).setActiveProfile(profile); } } else { await ref.read(profileManagerProvider(p).notifier).signOut(); } } if (!ref.read(activeProfileProvider.notifier).hasActiveProfile && ref.read(loggedInProfilesProvider).isNotEmpty) { final firstProfile = ref.read(loggedOutProfilesProvider).first; ref.read(activeProfileProvider.notifier).setActiveProfile(firstProfile); } return Result.ok( ref.read(activeProfileProvider.notifier).hasActiveProfile); }); initialized = true; state = false; ref.notifyListeners(); return result.execErrorCast(); } String _getStoredLoginState() { final prefs = ref.read(sharedPreferencesProvider); return prefs.getString('active_profile_id') ?? ''; } } @riverpod class AccountClearer extends _$AccountClearer { @override bool build() { return true; } void clearAllProfiles() { ref.read(loggedInProfilesProvider.notifier).clear(); ref.read(loggedOutProfilesProvider.notifier).clear(); ref.read(activeProfileProvider.notifier).clear(); ref.read(secretsServiceProvider).clearCredentials(); } }