Files
mobile-app/lib/features/auth/presentation/providers/auth_provider.dart

516 lines
14 KiB
Dart

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<FieldError> 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<FieldError>? 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<AuthState> {
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();
// Only reset if still authenticated — avoid showing errors on login screen
if (state.status == AuthStatus.authenticated) {
state = const AuthState();
}
};
checkAuthStatus();
}
Future<void> 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<void> 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();
// Clear tokens BEFORE any API calls to prevent 401 errors during logout
await SecureStorage.clearTokens();
// Best-effort cleanup — tokens already cleared, ignore any errors
try {
await PushNotificationService().unregister();
} catch (_) {}
}
Future<void> login({
required String email,
required String password,
}) async {
state = state.copyWith(
status: AuthStatus.loading,
errorMessage: null,
fieldErrors: [],
);
try {
final result = await _repository.login(
email: email,
password: password,
);
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<void> 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<void> 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<void> completeTwitterLogin(
Map<String, dynamic> 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<void> 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<void> 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<void> 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<AuthRepository>((ref) {
return AuthRepository();
});
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref.watch(authRepositoryProvider));
});