Skip to content
67 changes: 45 additions & 22 deletions lib/data/repository/authentication_repository.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@

import 'dart:developer';

import 'package:flutter/foundation.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:supabase_flutter/supabase_flutter.dart' hide User;

import '../../core/constants/app_constants.dart';
import '../../core/errors/error_model.dart';
import '../../core/errors/result.dart';
import '../../core/errors/supabase_auth_error.dart';
import '../../domain/repository/authentication_repository.dart';
import '../../domain/entity/user.dart';
import '../../domain/repository/authentication_repository.dart';
import '../service/app_secrets_provider.dart';
import '../service/supabase_service.dart';

Expand All @@ -24,7 +22,7 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
});

@override
void signInWithGoogle() async {
Future<Result<bool>> signInWithGoogle() async {
try {
final env = await appSecrets.getEnvVariables();
final GoogleSignIn signIn = GoogleSignIn.instance;
Expand All @@ -43,25 +41,23 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
final accessToken = googleAuthorization?.accessToken;

if (idToken == null) {
throw 'No ID Token found.';
return Result.error(ErrorModel('No ID Token found from Google.'));
}
final client = await supabaseService.getClient();
await client.auth.signInWithIdToken(
provider: OAuthProvider.google,
idToken: idToken,
accessToken: accessToken,
);
return Result.success(true);
} on AuthException catch (error) {
return Result.error(SupabaseAuthError.fromAuthException(error));
} catch (error) {
if (kDebugMode) {
print('Caught error during Google Sign-In: $error');
}
rethrow;
log('error in data $error');
return Result.error(ErrorModel(error.toString()));
}
}

static const String _googleWebClientId = "GOOGLE_WEB_CLIENT_ID";
static const String _googleIosClientId = "GOOGLE_IOS_CLIENT_ID";
static const List<String> _googleScopes = ['email', 'profile', 'openid'];
@override
Stream<AuthState> get onAuthStateChange {
final supabaseClientFuture = supabaseService.getClient();
Expand All @@ -71,17 +67,34 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
}

@override
Future<void> resetPasswordForEmail(String email) async {
final client = await supabaseService.getClient();
await client.auth.resetPasswordForEmail(
email,
redirectTo: AppConstants.resetPasswordRedirect,
);
Future<Result<bool>> resetPasswordForEmail(String email) async {
try {
final client = await supabaseService.getClient();
await client.auth.resetPasswordForEmail(
email,
redirectTo: AppConstants.resetPasswordRedirect,
);
return Result.success(true);
} on AuthException catch (error) {
return Result.error(SupabaseAuthError.fromAuthException(error));
} catch (error) {
log('error in data $error');
return Result.error(ErrorModel(error.toString()));
}
}

@override
Future<void> updatePassword(String password) async {
final client = await supabaseService.getClient();
await client.auth.updateUser(UserAttributes(password: password));
Future<Result<bool>> updatePassword(String password) async {
try {
final client = await supabaseService.getClient();
await client.auth.updateUser(UserAttributes(password: password));
return Result.success(true);
} on AuthException catch (error) {
return Result.error(SupabaseAuthError.fromAuthException(error));
} catch (error) {
log('error in data $error');
return Result.error(ErrorModel(error.toString()));
}
}

@override
Expand Down Expand Up @@ -111,4 +124,14 @@ class AuthenticationRepositoryImpl implements AuthenticationRepository {
return Result.error(ErrorModel(error.toString()));
}
}
}

static const String _googleWebClientId = "GOOGLE_WEB_CLIENT_ID";
static const String _googleIosClientId = "GOOGLE_IOS_CLIENT_ID";
static const List<String> _googleScopes = ['email', 'profile', 'openid'];

@override
Future<String?> get userEmail async {
final supabaseClientFuture = await supabaseService.getClient();
return supabaseClientFuture.auth.currentUser?.email;
}
}
18 changes: 12 additions & 6 deletions lib/domain/repository/authentication_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@ import 'dart:async';
import 'package:supabase_flutter/supabase_flutter.dart' hide User;

import '../../core/errors/result.dart';
import '../../domain/entity/user.dart';
import '../entity/user.dart';

abstract class AuthenticationRepository {
void signInWithGoogle();
Future<void> resetPasswordForEmail(String email);
Future<Result<User>> signIn({
required String email,
required String password,
});

Stream<AuthState> get onAuthStateChange;

Future<void> updatePassword(String password);
Future<String?> get userEmail;

Future<Result<User>> signIn({required String email, required String password});
}
Future<Result<bool>> resetPasswordForEmail(String email);

Future<Result<bool>> signInWithGoogle();

Future<Result<bool>> updatePassword(String password);
}
35 changes: 35 additions & 0 deletions lib/money_app.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,48 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:moneyplus/design_system/theme/money_theme.dart';
import 'package:moneyplus/domain/repository/authentication_repository.dart';
import 'package:moneyplus/presentation/navigation/routes.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import 'core/l10n/app_localizations.dart';
import 'di/injection.dart';

class AuthRedirectNotifier extends ChangeNotifier {
final AuthenticationRepository _authRepository;
late final StreamSubscription<AuthState> _subscription;
bool _isPasswordRecovery = false;

AuthRedirectNotifier(this._authRepository) {
_subscription = _authRepository.onAuthStateChange.listen((data) {
if (data.event == AuthChangeEvent.passwordRecovery) {
_isPasswordRecovery = true;
notifyListeners();
}
});
}

@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
final _authRedirectNotifier = AuthRedirectNotifier(getIt<AuthenticationRepository>());

final _router = GoRouter(
routes: $appRoutes,
refreshListenable: _authRedirectNotifier,
redirect: (context, state) {
if (_authRedirectNotifier._isPasswordRecovery) {
_authRedirectNotifier._isPasswordRecovery = false;
return '/update_password';
}
return null;
},
);


Expand Down
28 changes: 9 additions & 19 deletions lib/presentation/forget_password/cubit/forget_password_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,28 @@ import 'dart:async';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:moneyplus/domain/repository/authentication_repository.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:moneyplus/domain/validator/authentication_validator.dart';

import 'forget_password_state.dart';

class ForgetPasswordCubit extends Cubit<ForgetPasswordState> {
final AuthenticationRepository authenticationRepository;
late final StreamSubscription<AuthState> _authSubscription;
final AuthenticationValidator validator;
ForgetPasswordCubit(this.authenticationRepository, this.validator)
: super(const ForgetPasswordState());

ForgetPasswordCubit(this.authenticationRepository)
: super(ForgetPasswordState.initial()) {
_authSubscription = authenticationRepository.onAuthStateChange.listen((
data,
) {
if (data.event == AuthChangeEvent.passwordRecovery) {
emit(state.copyWith(status: ForgetPasswordStatus.passwordRecovery));
}
});
void onEmailChanged(String email) {
final isEmailValid = validator.isEmailValid(email);
emit(state.copyWith(email: email, isEmailValid: isEmailValid));
}

Future<void> onClickForgetPassword(String email) async {
Future<void> onClickForgetPassword() async {
emit(state.copyWith(status: ForgetPasswordStatus.loading));
try {
await authenticationRepository.resetPasswordForEmail(email);
await authenticationRepository.resetPasswordForEmail(state.email);
emit(state.copyWith(status: ForgetPasswordStatus.success));
} catch (e) {
emit(state.copyWith(status: ForgetPasswordStatus.error));
}
}

@override
Future<void> close() {
_authSubscription.cancel();
return super.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@ enum ForgetPasswordStatus { initial, loading, success, error, passwordRecovery }
class ForgetPasswordState {
const ForgetPasswordState({
this.status = ForgetPasswordStatus.initial,
this.email = '',
this.isEmailValid = false,
});

final ForgetPasswordStatus status;
final String email;
final bool isEmailValid;

ForgetPasswordState copyWith({
ForgetPasswordStatus? status,
String? email,
bool? isEmailValid,
}) {
return ForgetPasswordState(
status: status ?? this.status,
email: email ?? this.email,
isEmailValid: isEmailValid ?? this.isEmailValid,
);
}

factory ForgetPasswordState.initial() => const ForgetPasswordState();
}
37 changes: 10 additions & 27 deletions lib/presentation/forget_password/screen/forget_password_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:moneyplus/design_system/widgets/text_field.dart';
import 'package:moneyplus/di/injection.dart';
import 'package:moneyplus/domain/repository/authentication_repository.dart';
import 'package:moneyplus/presentation/forget_password/cubit/forget_password_cubit.dart';
import 'package:moneyplus/presentation/update_password/screen/update_password_screen.dart';
import 'package:svg_flutter/svg.dart';

import '../../../design_system/theme/money_extension_context.dart';
Expand All @@ -21,38 +20,24 @@ class ForgetPasswordScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ForgetPasswordCubit(getIt<AuthenticationRepository>()),
create: (_) =>
ForgetPasswordCubit(getIt<AuthenticationRepository>(), getIt()),
child: const _ForgetPasswordView(),
);
}
}

class _ForgetPasswordView extends StatefulWidget {
class _ForgetPasswordView extends StatelessWidget {
const _ForgetPasswordView();

@override
State<_ForgetPasswordView> createState() => _ForgetPasswordViewState();
}

class _ForgetPasswordViewState extends State<_ForgetPasswordView> {
String _email = '';

@override
Widget build(BuildContext context) {
final colors = context.colors;
final typography = context.typography;
final l10n = AppLocalizations.of(context)!;

return BlocConsumer<ForgetPasswordCubit, ForgetPasswordState>(
listener: (context, state) {
if (state.status == ForgetPasswordStatus.passwordRecovery) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => UpdatePasswordScreen(email: _email),
),
);
}
},
listener: (context, state) {},
builder: (context, state) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
Expand All @@ -67,12 +52,10 @@ class _ForgetPasswordViewState extends State<_ForgetPasswordView> {
padding: const EdgeInsets.all(16),
child: DefaultButton(
text: l10n.forgetPasswordButton,
isEnabled: _email.isNotEmpty,
isEnabled: state.isEmailValid,
isLoading: state.status == ForgetPasswordStatus.loading,
onPressed: () {
context.read<ForgetPasswordCubit>().onClickForgetPassword(
_email,
);
context.read<ForgetPasswordCubit>().onClickForgetPassword();
},
),
),
Expand Down Expand Up @@ -125,11 +108,11 @@ class _ForgetPasswordViewState extends State<_ForgetPasswordView> {
height: 24,
AppAssets.icEmail,
),
value: _email,
value: state.email,
onChanged: (String value) {
setState(() {
_email = value;
});
context.read<ForgetPasswordCubit>().onEmailChanged(
value,
);
},
),
],
Expand Down
17 changes: 16 additions & 1 deletion lib/presentation/login/cubit/login_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class LoginCubit extends Cubit<LoginState> {
checkIsInputsValid();
}

Future<void> login() async {
void login() async {
emit(state.copyWith(status: LoginStatus.loading));

final result = await authRepository.signIn(
Expand All @@ -44,4 +44,19 @@ class LoginCubit extends Cubit<LoginState> {
},
);
}

void signInWithGoogle() async {
emit(state.copyWith(status: LoginStatus.loading));

final result = await authRepository.signInWithGoogle();

result.when(
onSuccess: (success) {
emit(state.copyWith(status: LoginStatus.success));
},
onError: (error) {
emit(state.copyWith(status: LoginStatus.failure, error: error));
},
);
}
}
4 changes: 3 additions & 1 deletion lib/presentation/login/screen/login_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ class _SocialMediaButtons extends StatelessWidget {
return Column(
children: [
MoneyButton(
onPressed: () {},
onPressed: () {
context.read<LoginCubit>().signInWithGoogle();
},
backgroundColor: colors.surfaceLow,
disabledBackgroundColor: Colors.red,
borderWidth: 0.5,
Expand Down
Loading