import 'dart:async'; import 'package:example/entities/user_role.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../entities/auth.dart'; part 'auth_controller.g.dart'; /// A mock of an Authenticated User const _dummyUser = Auth.signedIn( id: -1, displayName: 'My Name', email: 'My Email', token: 'some-updated-secret-auth-token', ); /// This controller is an [AsyncNotifier] that holds and handles our authentication state @riverpod class AuthController extends _$AuthController { late SharedPreferences _sharedPreferences; static const _sharedPrefsKey = 'token'; @override Future build() async { _sharedPreferences = await SharedPreferences.getInstance(); _persistenceRefreshLogic(); return _loginRecoveryAttempt(); } /// Tries to perform a login with the saved token on the persistent storage. /// If _anything_ goes wrong, deletes the internal token and returns a [User.signedOut]. Future _loginRecoveryAttempt() { try { final savedToken = _sharedPreferences.getString(_sharedPrefsKey); if (savedToken == null) throw const UnauthorizedException('No auth token found'); return _loginWithToken(savedToken); } catch (_, __) { _sharedPreferences.remove(_sharedPrefsKey).ignore(); return Future.value(const Auth.signedOut()); } } /// Mock of a request performed on logout (might be common, or not, whatevs). Future logout() async { await Future.delayed(networkRoundTripTime); state = const AsyncData(Auth.signedOut()); } /// Mock of a successful login attempt, which results come from the network. Future login(String email, String password) async { final result = await Future.delayed( networkRoundTripTime, () => _dummyUser, ); state = AsyncData(result); } /// Mock of a login request performed with a saved token. /// If such request fails, this method will throw an [UnauthorizedException]. Future _loginWithToken(String token) async { final logInAttempt = await Future.delayed( networkRoundTripTime, () => true, // edit this if you wanna play around ); if (logInAttempt) return _dummyUser; throw const UnauthorizedException('401 Unauthorized or something'); } /// Internal method used to listen authentication state changes. /// When the auth object is in a loading state, nothing happens. /// When the auth object is in an error state, we choose to remove the token /// Otherwise, we expect the current auth value to be reflected in our persistence API void _persistenceRefreshLogic() { ref.listenSelf((_, next) { if (next.isLoading) return; if (next.hasError) { _sharedPreferences.remove(_sharedPrefsKey); return; } next.requireValue.map( signedIn: (signedIn) => _sharedPreferences.setString(_sharedPrefsKey, signedIn.token), signedOut: (signedOut) { _sharedPreferences.remove(_sharedPrefsKey); }, ); }); } } /// Simple mock of a 401 exception class UnauthorizedException implements Exception { const UnauthorizedException(this.message); final String message; } /// Mock of the duration of a network request final networkRoundTripTime = 2.seconds;