import 'package:flutter/foundation.dart'; import 'package:flutter_facebook_auth/flutter_facebook_auth.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:real_estate_mobile/core/network/api_client.dart'; import 'package:real_estate_mobile/core/network/api_exceptions.dart'; import 'package:real_estate_mobile/core/services/push_notification_service.dart'; import 'package:real_estate_mobile/core/storage/secure_storage.dart'; import 'package:real_estate_mobile/features/auth/data/auth_repository.dart'; import 'package:real_estate_mobile/features/auth/data/models/register_request.dart'; import 'package:real_estate_mobile/features/auth/data/models/user_model.dart'; import 'package:real_estate_mobile/features/messaging/data/socket_service.dart'; // Auth state enum AuthStatus { initial, loading, authenticated, error } class AuthState { final AuthStatus status; final UserModel? user; final String? errorMessage; final List fieldErrors; final String registeredEmail; final bool requiresTwoFactor; final String? tempToken; final String? twoFactorEmail; final bool registrationSuccess; const AuthState({ this.status = AuthStatus.initial, this.user, this.errorMessage, this.fieldErrors = const [], this.registeredEmail = '', this.requiresTwoFactor = false, this.tempToken, this.twoFactorEmail, this.registrationSuccess = false, }); AuthState copyWith({ AuthStatus? status, UserModel? user, String? errorMessage, List? fieldErrors, String? registeredEmail, bool? requiresTwoFactor, String? tempToken, String? twoFactorEmail, bool? registrationSuccess, }) { return AuthState( status: status ?? this.status, user: user ?? this.user, errorMessage: errorMessage, fieldErrors: fieldErrors ?? this.fieldErrors, registeredEmail: registeredEmail ?? this.registeredEmail, requiresTwoFactor: requiresTwoFactor ?? this.requiresTwoFactor, tempToken: tempToken ?? this.tempToken, twoFactorEmail: twoFactorEmail ?? this.twoFactorEmail, registrationSuccess: registrationSuccess ?? this.registrationSuccess, ); } String? getFieldError(String fieldName) { final match = fieldErrors.where((e) => e.field == fieldName); if (match.isEmpty) return null; return match.first.errors.isNotEmpty ? match.first.errors.first : null; } } // Auth notifier class AuthNotifier extends StateNotifier { final AuthRepository _repository; AuthNotifier(this._repository) : super(const AuthState(status: AuthStatus.loading)) { // Wire up force logout callback so API client can reset auth state // when token refresh fails ApiClient.onForceLogout = () { SocketService().disconnect(); // Delete local FCM token so push notifications stop PushNotificationService().unregister(); // Only reset if still authenticated — avoid showing errors on login screen if (state.status == AuthStatus.authenticated) { state = const AuthState(); } }; checkAuthStatus(); } Future checkAuthStatus() async { state = state.copyWith(status: AuthStatus.loading); final token = await SecureStorage.getAccessToken(); if (token == null) { state = state.copyWith(status: AuthStatus.initial); return; } try { final user = await _repository.getMe(); // Re-enable auth error handling after successful login ApiClient.suppressAuthErrors = false; state = state.copyWith( status: AuthStatus.authenticated, user: user, ); // Connect socket for real-time updates (matching web's PresenceProvider) _connectSocket(); // Register for push notifications after auth confirmed _initPushNotifications(); } catch (_) { // Token is invalid/expired — clear and show login await SecureStorage.clearTokens(); state = state.copyWith(status: AuthStatus.initial); } } Future logout() async { // Suppress 401 errors during logout to prevent SnackBar flashing ApiClient.suppressAuthErrors = true; // Reset state first so UI navigates to login immediately state = const AuthState(); // Disconnect socket immediately SocketService().disconnect(); // Unregister FCM BEFORE clearing tokens — backend call needs auth header, // and deleteToken() stops Firebase from delivering to this device try { await PushNotificationService().unregister(); } catch (_) {} // Logout API call + clear local tokens await _repository.logout(); } Future login({ required String email, required String password, String? loginRole, }) async { // Reset suppressAuthErrors in case a prior logout left it enabled — // otherwise the 401 interceptor rejects errors unwrapped and we lose // the backend's message (e.g. role-mismatch), falling back to generic. ApiClient.suppressAuthErrors = false; state = state.copyWith( status: AuthStatus.loading, errorMessage: null, fieldErrors: [], ); try { final result = await _repository.login( email: email, password: password, loginRole: loginRole, ); if (result['requiresTwoFactor'] == true) { state = state.copyWith( status: AuthStatus.initial, requiresTwoFactor: true, tempToken: result['tempToken'] as String?, twoFactorEmail: email, ); return; } ApiClient.suppressAuthErrors = false; state = state.copyWith( status: AuthStatus.authenticated, user: result['user'] as UserModel, ); _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: e.fieldErrors.isNotEmpty ? 'Please fix the errors below' : e.message, fieldErrors: e.fieldErrors, ); } catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: 'An unexpected error occurred', ); } } Future signInWithGoogle({ String? role, String? agentTypeId, String mode = 'signup', }) async { state = state.copyWith( status: AuthStatus.loading, errorMessage: null, fieldErrors: [], ); try { final googleSignIn = GoogleSignIn( scopes: ['email', 'profile'], serverClientId: '703616926518-9h70kp29oqdgi8umr84u6n59kpb22q97.apps.googleusercontent.com', ); final account = await googleSignIn.signIn(); if (account == null) { // User cancelled state = state.copyWith(status: AuthStatus.initial); return; } final user = await _repository.socialAuth( provider: 'google', providerId: account.id, email: account.email, name: account.displayName, avatar: account.photoUrl, role: role, agentTypeId: agentTypeId, mode: mode, ); state = state.copyWith( status: AuthStatus.authenticated, user: user, ); _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: e.message, ); } catch (e) { debugPrint('Google sign-in error: $e'); state = state.copyWith( status: AuthStatus.error, errorMessage: 'Google sign-in failed. Please try again.', ); } } Future signInWithFacebook({ String? role, String? agentTypeId, String mode = 'signup', }) async { state = state.copyWith( status: AuthStatus.loading, errorMessage: null, fieldErrors: [], ); try { final result = await FacebookAuth.instance.login( permissions: ['email', 'public_profile'], ); if (result.status == LoginStatus.cancelled) { state = state.copyWith(status: AuthStatus.initial); return; } if (result.status != LoginStatus.success) { state = state.copyWith( status: AuthStatus.error, errorMessage: result.message ?? 'Facebook login failed', ); return; } final userData = await FacebookAuth.instance.getUserData( fields: 'id,name,email,picture.width(200)', ); final email = userData['email'] as String?; if (email == null || email.isEmpty) { state = state.copyWith( status: AuthStatus.error, errorMessage: 'Email not available from Facebook. Please use email login.', ); return; } final pictureData = userData['picture']?['data']; final avatarUrl = pictureData?['url'] as String?; final user = await _repository.socialAuth( provider: 'facebook', providerId: userData['id'] as String, email: email, name: userData['name'] as String?, avatar: avatarUrl, role: role, agentTypeId: agentTypeId, mode: mode, ); state = state.copyWith( status: AuthStatus.authenticated, user: user, ); _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: e.message, ); } catch (e) { debugPrint('Facebook sign-in error: $e'); state = state.copyWith( status: AuthStatus.error, errorMessage: 'Facebook sign-in failed. Please try again.', ); } } /// Complete Twitter/X login after webview returns user data. Future completeTwitterLogin( Map twitterUser, { String? role, String? agentTypeId, String mode = 'signup', }) async { state = state.copyWith( status: AuthStatus.loading, errorMessage: null, fieldErrors: [], ); try { final user = await _repository.socialAuth( provider: 'twitter', providerId: twitterUser['id'] as String, email: '${twitterUser['username']}@x.com', name: twitterUser['name'] as String?, avatar: twitterUser['avatar'] as String?, role: role, agentTypeId: agentTypeId, mode: mode, ); state = state.copyWith( status: AuthStatus.authenticated, user: user, ); _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: e.message, ); } catch (e) { debugPrint('Twitter sign-in error: $e'); state = state.copyWith( status: AuthStatus.error, errorMessage: 'X/Twitter sign-in failed. Please try again.', ); } } Future register({ required String name, required String email, required String password, required String role, String? agentTypeId, }) async { state = state.copyWith( status: AuthStatus.loading, errorMessage: null, fieldErrors: [], ); // Split name into firstName and lastName final nameParts = name.trim().split(RegExp(r'\s+')); final firstName = nameParts.first; final lastName = nameParts.length > 1 ? nameParts.sublist(1).join(' ') : null; try { await _repository.register( RegisterRequest( email: email, password: password, role: role, firstName: firstName, lastName: lastName, agentTypeId: role == 'AGENT' ? agentTypeId : null, ), ); // Registration successful — user must verify email before logging in state = state.copyWith( status: AuthStatus.initial, registeredEmail: email, registrationSuccess: true, ); } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: e.fieldErrors.isNotEmpty ? 'Please fix the errors below' : e.message, fieldErrors: e.fieldErrors, ); } catch (e) { state = state.copyWith( status: AuthStatus.error, errorMessage: 'An unexpected error occurred', ); } } Future verifyTwoFactor(String token) async { final tempToken = state.tempToken; if (tempToken == null) { state = state.copyWith( errorMessage: 'Session expired. Please login again.', ); return; } state = state.copyWith( status: AuthStatus.loading, errorMessage: null, ); try { final authResponse = await _repository.verifyTwoFactor( tempToken: tempToken, token: token, ); state = AuthState( status: AuthStatus.authenticated, user: authResponse.user, ); _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { // Preserve tempToken and requiresTwoFactor so user can retry state = state.copyWith( status: AuthStatus.initial, requiresTwoFactor: true, errorMessage: e.message, ); } catch (e) { state = state.copyWith( status: AuthStatus.initial, requiresTwoFactor: true, errorMessage: 'Verification failed. Please try again.', ); } } Future verifyBackupCode(String backupCode) async { final tempToken = state.tempToken; if (tempToken == null) { state = state.copyWith( errorMessage: 'Session expired. Please login again.', ); return; } state = state.copyWith( status: AuthStatus.loading, errorMessage: null, ); try { final authResponse = await _repository.verifyBackupCode( tempToken: tempToken, backupCode: backupCode, ); state = AuthState( status: AuthStatus.authenticated, user: authResponse.user, ); _connectSocket(); _initPushNotifications(); } on ApiException catch (e) { state = state.copyWith( status: AuthStatus.initial, requiresTwoFactor: true, errorMessage: e.message, ); } catch (e) { state = state.copyWith( status: AuthStatus.initial, requiresTwoFactor: true, errorMessage: 'Verification failed. Please try again.', ); } } void clearTwoFactor() { state = const AuthState(); } void _connectSocket() { SocketService().connect(); } void _initPushNotifications() { final push = PushNotificationService(); push.initialize().then((_) => push.registerAfterLogin()); } void reset() { state = const AuthState(); } } // Providers final authRepositoryProvider = Provider((ref) { return AuthRepository(); }); final authProvider = StateNotifierProvider((ref) { return AuthNotifier(ref.watch(authRepositoryProvider)); });