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/api/responses/confirmation.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/auth/state.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(); static String get currentUserRef => Constants.nilObjectRef; /// Auth lifecycle state to avoid multiple ad-hoc flags. AuthState _authState = AuthState.idle; AuthState get authState => _authState; // 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; 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, ); } @protected Future onAccountChanged(Account? previous, Account? current) => Future.value(); void _setResource(Resource newResource) { final previousAccount = _resource.data; _resource = newResource; final currentAccount = newResource.data; if (previousAccount != currentAccount) { unawaited(onAccountChanged(previousAccount, currentAccount)); } notifyListeners(); } 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) { _authState = AuthState.ready; _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'); } final confirmation = await VerificationService.requestLoginCode(pending); _pendingLogin = _applyConfirmationMeta(pending, confirmation); _authState = AuthState.idle; _setResource(_resource.copyWith(isLoading: false)); } return outcome; } catch (e) { _authState = AuthState.error; _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } PendingLogin _applyConfirmationMeta(PendingLogin pending, ConfirmationResponse confirmation) { final ttlSeconds = confirmation.ttlSeconds != 0 ? confirmation.ttlSeconds : pending.ttlSeconds; final destination = confirmation.destination.isNotEmpty ? confirmation.destination : pending.destination; final cooldownSeconds = confirmation.cooldownSeconds; return pending.copyWith( ttlSeconds: ttlSeconds, destination: destination, cooldownSeconds: cooldownSeconds > 0 ? cooldownSeconds : null, cooldownUntil: cooldownSeconds > 0 ? DateTime.now().add(confirmation.cooldownDuration) : null, clearCooldown: cooldownSeconds <= 0, ); } void updatePendingLogin(ConfirmationResponse confirmation) { final pending = _pendingLogin; if (pending == null) return; _pendingLogin = _applyConfirmationMeta(pending, confirmation); notifyListeners(); } void completePendingLogin(Account account) { _pendingLogin = null; _authState = AuthState.ready; _setResource(Resource(data: account, isLoading: false, error: null)); _pickupLocale(account.locale); } Future isAuthorizationStored() async => AuthorizationService.isAuthorizationStored(); Future restore() async { _authState = AuthState.checking; notifyListeners(); _setResource(_resource.copyWith(isLoading: true, error: null)); try { final acc = await AccountService.restore(); _authState = AuthState.ready; _setResource(Resource(data: acc, isLoading: false)); _pickupLocale(acc.locale); return acc; } catch (e) { _authState = AuthState.error; _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future signup({ required AccountData account, required Describable organization, required Describable ownerRole, required Describable cryptoWallet, required Describable ledgerWallet, required String timezone, }) async { _setResource(_resource.copyWith(isLoading: true, error: null)); try { await AccountService.signup( SignupRequest.build( account: account, organization: organization, organizationTimeZone: timezone, ownerRole: ownerRole, cryptoWallet: cryptoWallet, ledgerWallet: ledgerWallet, ), ); // 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 { _authState = AuthState.empty; _setResource(_resource.copyWith(isLoading: true, error: null)); _pendingLogin = null; try { await AccountService.logout(); _setResource(Resource(data: null, isLoading: false)); } catch (e) { _authState = AuthState.error; _setResource(_resource.copyWith(isLoading: false, error: toException(e))); rethrow; } } Future update({ Describable? describable, String? lastName, 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, lastName: lastName, 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 resetUsername(String userName, {String? lastName}) async { if (account == null) throw ErrorUnauthorized(); return update( describable: account!.describable.copyWith(name: userName), lastName: lastName ?? account!.lastName, ); } 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() async { if (_authState == AuthState.checking || _authState == AuthState.ready) return; final hasAuth = await AuthorizationService.isAuthorizationStored(); if (!hasAuth) { _authState = AuthState.empty; notifyListeners(); return; } await restore(); } }