import 'dart:async'; import 'package:flutter/material.dart'; import 'package:share_plus/share_plus.dart'; import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/requests/login_data.dart'; import 'package:pshared/config/constants.dart'; import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/auth/login_outcome.dart'; import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/models/describable.dart'; import 'package:pshared/models/storable.dart'; import 'package:pshared/provider/locale.dart'; import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/account.dart'; import 'package:pshared/service/authorization/service.dart'; import 'package:pshared/service/verification.dart'; import 'package:pshared/utils/exception.dart'; class AccountProvider extends ChangeNotifier { AccountProvider({Future Function(Account?)? onAccountChanged}) : _onAccountChanged = onAccountChanged; static String get currentUserRef => Constants.nilObjectRef; // The resource now wraps our Account? state along with its loading/error state. Resource _resource = Resource(data: null); Resource get resource => _resource; late LocaleProvider _localeProvider; PendingLogin? _pendingLogin; Future? _restoreFuture; Future Function(Account?)? _onAccountChanged; Account? get account => _resource.data; PendingLogin? get pendingLogin => _pendingLogin; bool get isLoggedIn => account != null; bool get isLoading => _resource.isLoading; Object? get error => _resource.error; bool get isReady => (!isLoading) && (account != null); Account? currentUser() { final acc = account; if (acc == null) return null; return Account( storable: newStorable( id: currentUserRef, createdAt: acc.createdAt, updatedAt: acc.updatedAt, ), describable: acc.describable, lastName: acc.lastName, avatarUrl: acc.avatarUrl, login: acc.login, locale: acc.locale, ); } // Private helper to update the resource and notify listeners. void setAccountChangedListener(Future Function(Account?)? listener) => _onAccountChanged = listener; void _setResource(Resource newResource) { final previousAccount = _resource.data; _resource = newResource; _notifyAccountChanged(previousAccount, newResource.data); notifyListeners(); } void _notifyAccountChanged(Account? previous, Account? current) { if (previous == current) return; final handler = _onAccountChanged; if (handler != null) unawaited(handler(current)); } void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider; void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale)); Future login({ required String email, required String password, required String locale, }) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { final outcome = await AccountService.login(LoginData.build( login: email, password: password, locale: locale, )); if (outcome.account != null) { _setResource(Resource(data: outcome.account, isLoading: false)); _pickupLocale(outcome.account!.locale); } else { final pending = outcome.pending; if (pending == null) { throw Exception('Pending login data is missing'); } await VerificationService.requestLoginCode(pending); _pendingLogin = pending; _setResource(_resource.copyWith(isLoading: false)); } return outcome; } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } void completePendingLogin(Account account) { _pendingLogin = null; _setResource(Resource(data: account, isLoading: false, error: null)); _pickupLocale(account.locale); } Future isAuthorizationStored() async => AuthorizationService.isAuthorizationStored(); Future restore() async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { final acc = await AccountService.restore(); _setResource(Resource(data: acc, isLoading: false)); _pickupLocale(acc.locale); return acc; } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future signup({ required AccountData account, required Describable organization, required String timezone, required Describable ownerRole, }) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { await AccountService.signup( SignupRequest.build( account: account, organization: organization, organizationTimeZone: timezone, ownerRole: ownerRole, ), ); // Signup might not automatically log in the user, // so we just mark the request as complete. _setResource(_resource.copyWith(isLoading: false)); } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future logout() async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { await AccountService.logout(); _setResource(Resource(data: null, isLoading: false)); } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future update({ Describable? describable, String? locale, String? avatarUrl, String? notificationFrequency, }) async { if (account == null) throw ErrorUnauthorized(); _setResource(_resource.copyWith(isLoading: true, error: null)); try { final updated = await AccountService.update( account!.copyWith( describable: describable, avatarUrl: () => avatarUrl ?? account!.avatarUrl, locale: locale ?? account!.locale, ), ); _setResource(Resource(data: updated, isLoading: false)); return updated; } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future changePassword(String oldPassword, String newPassword) async { if (account == null) throw ErrorUnauthorized(); _setResource(_resource.copyWith(isLoading: true, error: null)); try { final updated = await AccountService.changePassword(oldPassword, newPassword); _setResource(Resource(data: updated, isLoading: false)); } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future uploadAvatar(XFile avatarFile) async { if (account == null) throw ErrorUnauthorized(); _setResource(_resource.copyWith(isLoading: true, error: null)); try { final avatarUrl = await AccountService.uploadAvatar(account!.id, avatarFile); // Reuse the update method to update the avatar URL. return update(avatarUrl: avatarUrl); } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future forgotPassword(String email) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { await AccountService.forgotPassword(email); _setResource(_resource.copyWith(isLoading: false)); } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future resetPassword(String accountId, String token, String newPassword) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { await AccountService.resetPassword(accountId, token, newPassword); _setResource(_resource.copyWith(isLoading: false)); } catch (e) { _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future restoreIfPossible() { return _restoreFuture ??= AuthorizationService.isAuthorizationStored().then((hasAuth) async { if (!hasAuth) return; await restore(); }); } }