import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../app/providers.dart'; import '../../../domain/entities/store_account.dart'; /// Sign-in state for the terminal. sealed class AuthState { const AuthState(); bool get isAuthenticated => this is Authenticated; } class Unauthenticated extends AuthState { const Unauthenticated(); } class Authenticating extends AuthState { const Authenticating(); } class Authenticated extends AuthState { const Authenticated({required this.store, required this.user}); final StoreAccount store; final StaffUser user; } class AuthFailure extends AuthState { const AuthFailure(this.message); final String message; } /// Store-level credentials for the unregistered build. /// /// Still a constant, and deliberately so: this is the *store* login, not a /// person's, and it is replaced wholesale when the terminal is registered /// against a real back office. Staff PINs — the credential that actually opens /// a till drawer — are no longer here. They live hashed in the database. class DemoCredentials { const DemoCredentials._(); static const String email = 'admin@nearle.in'; static const String password = 'nearle123'; } /// Validates store credentials and holds the signed-in session. class AuthController extends StateNotifier { AuthController(this._ref) : super(const Unauthenticated()); final Ref _ref; Future signIn({ required String email, required String password, }) async { state = const Authenticating(); // Stand-in for the network round trip. await Future.delayed(const Duration(milliseconds: 600)); final normalised = email.trim().toLowerCase(); if (normalised != DemoCredentials.email) { state = const AuthFailure('No store is registered against that email.'); return false; } if (password != DemoCredentials.password) { state = const AuthFailure('Incorrect password. Please try again.'); return false; } final store = await _ref.read(storeAccountProvider.future); final staff = store.staff; if (staff.isEmpty) { state = const AuthFailure( 'This terminal has no staff accounts. Reinstall to seed them.', ); return false; } // The first admin, or whoever is there. A person switches to their own // account at the till. final opener = staff.firstWhere( (s) => s.role == StaffRole.admin, orElse: () => staff.first, ); state = Authenticated(store: store, user: opener); return true; } /// Switches the active operator, checking their PIN. /// /// Every bill is stamped with whoever is active, so this is the boundary that /// decides who a sale is attributed to — it cannot be a bare selection from a /// list. Future switchUser(String pin) async { final current = state; if (current is! Authenticated) return false; final store = _ref.read(localStoreProvider); final user = await store.staff.authenticate(pin); if (user == null) return false; state = Authenticated(store: current.store, user: user); return true; } /// Re-reads the store after staff or details change, keeping the session. Future refreshStore() async { final current = state; if (current is! Authenticated) return; _ref.invalidate(storeAccountProvider); final store = await _ref.read(storeAccountProvider.future); final me = store.staff.where((s) => s.id == current.user.id); state = Authenticated( store: store, // Signed out if the active operator was just deactivated — carrying on // would keep stamping bills with an account the shop has revoked. user: me.isEmpty ? store.staff.first : me.first, ); } void signOut() => state = const Unauthenticated(); void clearError() { if (state is AuthFailure) state = const Unauthenticated(); } } final authControllerProvider = StateNotifierProvider( AuthController.new, ); /// The signed-in store, or null before sign-in. final currentStoreProvider = Provider((ref) { final s = ref.watch(authControllerProvider); return s is Authenticated ? s.store : null; }); /// The active operator, or null before sign-in. final currentUserProvider = Provider((ref) { final s = ref.watch(authControllerProvider); return s is Authenticated ? s.user : null; }); /// True while anyone is still on a seeded or admin-reset PIN. final mustChangePinProvider = Provider((ref) { final user = ref.watch(currentUserProvider); return user?.mustChangePin ?? false; });