193 lines
5.2 KiB
Dart
193 lines
5.2 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'biometric_service.dart';
|
|
import 'inou_api.dart';
|
|
|
|
/// Authentication state
|
|
enum AuthState {
|
|
unknown, // Initial state, checking stored credentials
|
|
loggedOut, // No valid session
|
|
loggedIn, // Authenticated
|
|
}
|
|
|
|
/// Authentication service - manages login state with email + OTP code
|
|
class AuthService extends ChangeNotifier {
|
|
static final AuthService _instance = AuthService._internal();
|
|
factory AuthService() => _instance;
|
|
AuthService._internal();
|
|
|
|
final BiometricService _biometricService = BiometricService();
|
|
final InouApi _api = InouApi();
|
|
|
|
AuthState _state = AuthState.unknown;
|
|
String? _userEmail;
|
|
String? _error;
|
|
|
|
// Preference keys
|
|
static const String _prefKeyLoggedIn = 'auth_logged_in';
|
|
static const String _prefKeyEmail = 'auth_email';
|
|
static const String _prefKeyBiometricSetup = 'auth_biometric_setup';
|
|
|
|
/// Current auth state
|
|
AuthState get state => _state;
|
|
|
|
/// Current user email (if logged in)
|
|
String? get userEmail => _userEmail;
|
|
|
|
/// Last error message
|
|
String? get error => _error;
|
|
|
|
/// Whether user has completed biometric setup prompt
|
|
Future<bool> get hasBiometricSetupCompleted async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
return prefs.getBool(_prefKeyBiometricSetup) ?? false;
|
|
}
|
|
|
|
/// Initialize - check stored login state
|
|
Future<void> initialize() async {
|
|
await _api.initialize();
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final isLoggedIn = prefs.getBool(_prefKeyLoggedIn) ?? false;
|
|
final email = prefs.getString(_prefKeyEmail);
|
|
|
|
if (isLoggedIn && email != null && _api.isAuthenticated) {
|
|
_userEmail = email;
|
|
_state = AuthState.loggedIn;
|
|
} else {
|
|
_state = AuthState.loggedOut;
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Request login code to be sent to email
|
|
/// Returns true if code was sent successfully
|
|
Future<bool> requestLoginCode(String email) async {
|
|
_error = null;
|
|
|
|
if (email.isEmpty) {
|
|
_error = 'Please enter your email';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
if (!_isValidEmail(email)) {
|
|
_error = 'Please enter a valid email address';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
// Call the real API
|
|
final apiError = await _api.sendLoginCode(email);
|
|
|
|
if (apiError != null) {
|
|
_error = apiError;
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
/// Verify the login code
|
|
/// Returns true if code is valid and user is now logged in
|
|
Future<bool> verifyLoginCode(String email, String code) async {
|
|
_error = null;
|
|
|
|
if (code.isEmpty) {
|
|
_error = 'Please enter the verification code';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
if (code.length != 6) {
|
|
_error = 'Code must be 6 digits';
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
// Call the real API
|
|
final apiError = await _api.verifyCode(email, code);
|
|
|
|
if (apiError != null) {
|
|
_error = apiError;
|
|
notifyListeners();
|
|
return false;
|
|
}
|
|
|
|
// Store login state
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(_prefKeyLoggedIn, true);
|
|
await prefs.setString(_prefKeyEmail, email);
|
|
|
|
_userEmail = email;
|
|
_state = AuthState.loggedIn;
|
|
notifyListeners();
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Logout - clear all auth state
|
|
Future<void> logout() async {
|
|
await _api.logout();
|
|
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove(_prefKeyLoggedIn);
|
|
await prefs.remove(_prefKeyEmail);
|
|
await prefs.remove(_prefKeyBiometricSetup);
|
|
|
|
// Also disable biometric
|
|
await _biometricService.setBiometricEnabled(false);
|
|
_biometricService.resetAuthState();
|
|
|
|
_userEmail = null;
|
|
_state = AuthState.loggedOut;
|
|
_error = null;
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Mark biometric setup as completed (user saw the prompt)
|
|
Future<void> completeBiometricSetup() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setBool(_prefKeyBiometricSetup, true);
|
|
}
|
|
|
|
/// Enable biometric authentication for future logins
|
|
Future<void> enableBiometric() async {
|
|
// User just logged in - no need to verify again, just enable
|
|
await _biometricService.setBiometricEnabled(true);
|
|
await completeBiometricSetup();
|
|
}
|
|
|
|
/// Skip biometric setup
|
|
Future<void> skipBiometricSetup() async {
|
|
await completeBiometricSetup();
|
|
}
|
|
|
|
/// Check if biometric login is available and enabled
|
|
Future<bool> canUseBiometricLogin() async {
|
|
final available = await _biometricService.isBiometricsAvailable();
|
|
final enabled = await _biometricService.isBiometricEnabled();
|
|
return available && enabled;
|
|
}
|
|
|
|
/// Attempt biometric login (for returning users)
|
|
Future<bool> biometricLogin() async {
|
|
final canUse = await canUseBiometricLogin();
|
|
if (!canUse) return false;
|
|
|
|
final result = await _biometricService.authenticate(
|
|
reason: 'Login to inou',
|
|
);
|
|
|
|
return result == BiometricResult.success;
|
|
}
|
|
|
|
bool _isValidEmail(String email) {
|
|
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
|
|
}
|
|
}
|