diff --git a/lib/features/authentication/data/repositories/auth_repository_impl.dart b/lib/features/authentication/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..1d1b694 --- /dev/null +++ b/lib/features/authentication/data/repositories/auth_repository_impl.dart @@ -0,0 +1,311 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../../domain/repositories/auth_repository.dart'; +import '../../data/models/auth_user.dart'; +import '../../../../core/errors/auth_exceptions.dart'; + +/// Supabase implementation of AuthRepository +/// +/// This class implements the AuthRepository interface using Supabase authentication +/// services. It handles all authentication operations and maps Supabase responses +/// to our domain models and custom exceptions. +class AuthRepositoryImpl implements AuthRepository { + final SupabaseClient _supabase; + StreamSubscription? _authSubscription; + + /// Creates a new AuthRepositoryImpl instance + /// + /// [supabaseClient] - The Supabase client to use for authentication + AuthRepositoryImpl({SupabaseClient? supabaseClient}) + : _supabase = supabaseClient ?? Supabase.instance.client; + + @override + Future signUp(String email, String password) async { + try { + final response = await _supabase.auth.signUp( + email: email, + password: password, + ); + + if (response.user == null) { + throw const AuthException( + message: 'Failed to create account. Please try again.', + code: 'SIGNUP_FAILED', + ); + } + + final user = response.user!; + return AuthUser.fromSupabase(user); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future signIn(String email, String password) async { + try { + final response = await _supabase.auth.signInWithPassword( + email: email, + password: password, + ); + + if (response.user == null) { + throw const AuthException( + message: 'Failed to sign in. Please check your credentials.', + code: 'SIGNIN_FAILED', + ); + } + + final user = response.user!; + return AuthUser.fromSupabase(user); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future signOut() async { + try { + await _supabase.auth.signOut(); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future resetPassword(String email) async { + try { + await _supabase.auth.resetPasswordForEmail( + email, + redirectTo: 'com.sage.app://reset-password', + ); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future getCurrentUser() async { + try { + final user = _supabase.auth.currentUser; + + if (user == null) { + return null; + } + + return AuthUser.fromSupabase(user); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Stream authStateChanges() { + try { + return _supabase.auth.onAuthStateChange + .map((authState) { + final user = authState.session?.user; + return user != null ? AuthUser.fromSupabase(user) : null; + }); + } catch (e) { + // Return a stream that immediately emits the error + return Stream.error(AuthExceptionFactory.fromSupabaseError(e)); + } + } + + @override + Future refreshSession() async { + try { + final response = await _supabase.auth.refreshSession(); + + if (response.session == null) { + throw const SessionExpiredException(); + } + + final user = response.session!.user; + return AuthUser.fromSupabase(user); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future updateProfile({ + String? displayName, + String? avatarUrl, + }) async { + try { + final user = _supabase.auth.currentUser; + + if (user == null) { + throw const SessionExpiredException(); + } + + final userMetadata = {}; + + if (displayName != null) { + userMetadata['display_name'] = displayName; + } + + if (avatarUrl != null) { + userMetadata['avatar_url'] = avatarUrl; + } + + final response = await _supabase.auth.updateUser( + UserAttributes( + data: userMetadata, + ), + ); + + final updatedUser = response.user; + if (updatedUser == null) { + throw const AuthException( + message: 'Failed to update profile. Please try again.', + code: 'UPDATE_FAILED', + ); + } + + return AuthUser.fromSupabase(updatedUser); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future sendEmailVerification() async { + try { + final user = _supabase.auth.currentUser; + + if (user == null) { + throw const SessionExpiredException(); + } + + await _supabase.auth.resend( + type: OtpType.signup, + email: user.email!, + ); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future changePassword(String currentPassword, String newPassword) async { + try { + final user = _supabase.auth.currentUser; + + if (user == null) { + throw const SessionExpiredException(); + } + + // First verify current password by attempting to sign in + await _supabase.auth.signInWithPassword( + email: user.email!, + password: currentPassword, + ); + + // If sign in succeeded, update password + await _supabase.auth.updateUser( + UserAttributes( + password: newPassword, + ), + ); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future deleteAccount() async { + try { + final user = _supabase.auth.currentUser; + + if (user == null) { + throw const SessionExpiredException(); + } + + await _supabase.rpc('delete_user', params: {'user_id': user.id}); + await _supabase.auth.signOut(); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future isEmailVerified() async { + try { + final user = _supabase.auth.currentUser; + + if (user == null) { + return null; + } + + return user.emailConfirmedAt != null; + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future signInWithOAuth(String provider) async { + try { + OAuthProvider oauthProvider; + + switch (provider.toLowerCase()) { + case 'google': + oauthProvider = OAuthProvider.google; + break; + case 'github': + oauthProvider = OAuthProvider.github; + break; + case 'apple': + oauthProvider = OAuthProvider.apple; + break; + default: + throw AuthException( + message: 'Unsupported OAuth provider: $provider', + code: 'UNSUPPORTED_PROVIDER', + ); + } + + final response = await _supabase.auth.signInWithOAuth( + oauthProvider, + redirectTo: 'com.sage.app://auth-callback', + ); + + // OAuth sign in is handled via redirect, so we'll handle this in the callback + // For now, throw an exception indicating the redirect flow + throw AuthException( + message: 'OAuth sign in initiated. Please complete authentication in your browser.', + code: 'OAUTH_REDIRECT', + ); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + @override + Future signInAnonymously() async { + try { + final response = await _supabase.auth.signInAnonymously(); + + if (response.user == null) { + throw const AuthException( + message: 'Failed to sign in anonymously. Please try again.', + code: 'ANONYMOUS_SIGNIN_FAILED', + ); + } + + final user = response.user!; + return AuthUser.fromSupabase(user); + } catch (e) { + throw AuthExceptionFactory.fromSupabaseError(e); + } + } + + /// Dispose of any active subscriptions + /// + /// Call this when the repository is no longer needed to prevent memory leaks + void dispose() { + _authSubscription?.cancel(); + } +} \ No newline at end of file