From 46a73ca28f6b475ec18a24d80b438c87a1da4475 Mon Sep 17 00:00:00 2001 From: Isaias Cuvula <68303716+IsaiasCuvula@users.noreply.github.com> Date: Sun, 18 Jun 2023 03:46:30 +0300 Subject: [PATCH] auth feature --- android/app/build.gradle | 3 +- lib/app/app.dart | 6 +- lib/common/common.dart | 3 +- .../translations/app_localizations.dart | 18 ++++++ .../translations/app_localizations_en.dart | 9 +++ .../translations/app_localizations_pt.dart | 9 +++ .../translations/quote_generator_en.arb | 3 + .../translations/quote_generator_pt.arb | 3 + .../navigation/routers/error_screen.dart | 31 +++++++++++ lib/common/navigation/routers/routers.dart | 4 +- lib/common/navigation/routers/routes.dart | 49 +++++++++-------- .../navigation/routers/routes_config.dart | 13 ----- .../navigation/routers/routes_location.dart | 16 ++++++ .../navigation/routers/routes_name.dart | 9 --- .../navigation/routers/routes_provider.dart | 32 +++++++++++ .../settings_screen.dart | 20 ++++++- lib/common/screens/splash_screen.dart | 24 ++++++++ lib/features/auth/data/data.dart | 3 + .../auth/data/datasource/datasource.dart | 3 + .../remote/auth_remote_datasource.dart | 8 +++ .../remote/auth_remote_datasource_impl.dart | 42 ++++++++++++++ .../auth_remote_datasource_provider.dart | 6 ++ .../auth/data/models/auth_result.dart | 1 + .../repositories/auth_repository_impl.dart | 36 ++++++++++++ .../auth/data/repositories/repositories.dart | 1 + lib/features/auth/domain/domain.dart | 3 + .../auth/domain/entities/app_user.dart | 18 ++++++ .../domain/repositories/auth_repository.dart | 10 ++++ .../auth_repository_provider.dart | 7 +++ .../domain/repositories/repositories.dart | 2 + .../auth_state_changes.dart | 2 + .../auth_state_changes_usecase.dart | 12 ++++ .../auth_state_changes_usecase_provider.dart | 9 +++ .../sign_in/google_sign_in_usecase.dart | 13 +++++ .../google_sign_in_usecase_provider.dart | 7 +++ .../auth/domain/usecases/sign_in/sign_in.dart | 2 + .../domain/usecases/sign_out/sign_out.dart | 2 + .../usecases/sign_out/sign_out_usecase.dart | 10 ++++ .../sign_out/sign_out_usecase_provider.dart | 7 +++ .../auth/domain/usecases/usecases.dart | 3 + .../auth/presentation/presentation.dart | 1 + .../presentation/providers/auth_notifier.dart | 37 +++++++++++++ .../presentation/providers/auth_provider.dart | 8 +++ .../presentation/providers/auth_state.dart | 33 +++++++++++ .../auth_state_changes_provider.dart | 9 +++ .../presentation/providers/providers.dart | 4 ++ .../presentation/screens/auth_screen.dart | 55 +++++++++++++++++++ lib/features/auth/utils/constants.dart | 4 ++ lib/features/auth/utils/utils.dart | 2 +- 49 files changed, 560 insertions(+), 52 deletions(-) create mode 100644 lib/common/navigation/routers/error_screen.dart delete mode 100644 lib/common/navigation/routers/routes_config.dart create mode 100644 lib/common/navigation/routers/routes_location.dart delete mode 100644 lib/common/navigation/routers/routes_name.dart create mode 100644 lib/common/navigation/routers/routes_provider.dart rename lib/common/{settings => screens}/settings_screen.dart (72%) create mode 100644 lib/common/screens/splash_screen.dart create mode 100644 lib/features/auth/data/datasource/datasource.dart create mode 100644 lib/features/auth/data/datasource/remote/auth_remote_datasource.dart create mode 100644 lib/features/auth/data/datasource/remote/auth_remote_datasource_impl.dart create mode 100644 lib/features/auth/data/datasource/remote/auth_remote_datasource_provider.dart create mode 100644 lib/features/auth/data/models/auth_result.dart create mode 100644 lib/features/auth/data/repositories/auth_repository_impl.dart create mode 100644 lib/features/auth/data/repositories/repositories.dart create mode 100644 lib/features/auth/domain/entities/app_user.dart create mode 100644 lib/features/auth/domain/repositories/auth_repository.dart create mode 100644 lib/features/auth/domain/repositories/auth_repository_provider.dart create mode 100644 lib/features/auth/domain/repositories/repositories.dart create mode 100644 lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes.dart create mode 100644 lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase.dart create mode 100644 lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase_provider.dart create mode 100644 lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase.dart create mode 100644 lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase_provider.dart create mode 100644 lib/features/auth/domain/usecases/sign_in/sign_in.dart create mode 100644 lib/features/auth/domain/usecases/sign_out/sign_out.dart create mode 100644 lib/features/auth/domain/usecases/sign_out/sign_out_usecase.dart create mode 100644 lib/features/auth/domain/usecases/sign_out/sign_out_usecase_provider.dart create mode 100644 lib/features/auth/domain/usecases/usecases.dart create mode 100644 lib/features/auth/presentation/providers/auth_notifier.dart create mode 100644 lib/features/auth/presentation/providers/auth_provider.dart create mode 100644 lib/features/auth/presentation/providers/auth_state.dart create mode 100644 lib/features/auth/presentation/providers/auth_state_changes/auth_state_changes_provider.dart create mode 100644 lib/features/auth/presentation/providers/providers.dart create mode 100644 lib/features/auth/utils/constants.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 6bba119..9ecabdd 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -47,10 +47,11 @@ android { defaultConfig { applicationId "com.bersyte.quote_generator" - minSdkVersion flutter.minSdkVersion + minSdkVersion 19 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } buildTypes { diff --git a/lib/app/app.dart b/lib/app/app.dart index 8ee5bf0..bf21c11 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -10,6 +10,8 @@ class QuoteGeneratorApp extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final appLocale = ref.watch(appLocaleProvider); final theme = ref.watch(themeProvider); + final routeConfig = ref.watch(routerProvider); + return ScreenUtilInit( designSize: const Size(375, 812), builder: (context, child) { @@ -21,7 +23,9 @@ class QuoteGeneratorApp extends ConsumerWidget { themeMode: theme, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - routerConfig: RoutesConfig.routeConfig, + routerDelegate: routeConfig.routerDelegate, + routeInformationParser: routeConfig.routeInformationParser, + routeInformationProvider: routeConfig.routeInformationProvider, locale: appLocale, ); }, diff --git a/lib/common/common.dart b/lib/common/common.dart index 9d7d38b..a65223c 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -2,4 +2,5 @@ export 'navigation/navigation.dart'; export 'theme/theme.dart'; export 'localization/localization.dart'; export 'shared_prefs/shared_prefs.dart'; -export 'settings/settings_screen.dart'; +export 'screens/settings_screen.dart'; +export 'screens/splash_screen.dart'; diff --git a/lib/common/localization/translations/app_localizations.dart b/lib/common/localization/translations/app_localizations.dart index 54ba320..515df42 100644 --- a/lib/common/localization/translations/app_localizations.dart +++ b/lib/common/localization/translations/app_localizations.dart @@ -351,6 +351,18 @@ abstract class AppLocalizations { /// **'Photo Saved on Library'** String get image_saved; + /// No description provided for @something_went_wrong. + /// + /// In en, this message translates to: + /// **'Oops, Something went wrong!'** + String get something_went_wrong; + + /// No description provided for @go_to_home. + /// + /// In en, this message translates to: + /// **'Go to home screen'** + String get go_to_home; + /// No description provided for @copied_to_clipboard. /// /// In en, this message translates to: @@ -362,6 +374,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Sign In with Google'** String get sign_in_google; + + /// No description provided for @sign_out. + /// + /// In en, this message translates to: + /// **'Sign Out'** + String get sign_out; } class _AppLocalizationsDelegate extends LocalizationsDelegate { diff --git a/lib/common/localization/translations/app_localizations_en.dart b/lib/common/localization/translations/app_localizations_en.dart index bcb09ef..eae325a 100644 --- a/lib/common/localization/translations/app_localizations_en.dart +++ b/lib/common/localization/translations/app_localizations_en.dart @@ -133,9 +133,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get image_saved => 'Photo Saved on Library'; + @override + String get something_went_wrong => 'Oops, Something went wrong!'; + + @override + String get go_to_home => 'Go to home screen'; + @override String get copied_to_clipboard => 'quote copied to clipboard'; @override String get sign_in_google => 'Sign In with Google'; + + @override + String get sign_out => 'Sign Out'; } diff --git a/lib/common/localization/translations/app_localizations_pt.dart b/lib/common/localization/translations/app_localizations_pt.dart index d6f3172..0538642 100644 --- a/lib/common/localization/translations/app_localizations_pt.dart +++ b/lib/common/localization/translations/app_localizations_pt.dart @@ -133,9 +133,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get image_saved => 'Foto salva na biblioteca'; + @override + String get something_went_wrong => 'Oops, algo deu errado!'; + + @override + String get go_to_home => 'Ir para a tela inicial'; + @override String get copied_to_clipboard => 'citação copiada'; @override String get sign_in_google => 'Faça login no Google'; + + @override + String get sign_out => 'Sair'; } diff --git a/lib/common/localization/translations/quote_generator_en.arb b/lib/common/localization/translations/quote_generator_en.arb index 9c4a3af..ec58a71 100644 --- a/lib/common/localization/translations/quote_generator_en.arb +++ b/lib/common/localization/translations/quote_generator_en.arb @@ -50,9 +50,12 @@ "dark_mode": "Dark", "light_mode": "Light", "image_saved": "Photo Saved on Library", + "something_went_wrong": "Oops, Something went wrong!", + "go_to_home": "Go to home screen", "@CLIPBOARD": {}, "copied_to_clipboard": "quote copied to clipboard", "@AUTH": {}, "sign_in_google": "Sign In with Google", + "sign_out": "Sign Out", "@END_OF_FILE": {} } \ No newline at end of file diff --git a/lib/common/localization/translations/quote_generator_pt.arb b/lib/common/localization/translations/quote_generator_pt.arb index 7115515..ad6dc95 100644 --- a/lib/common/localization/translations/quote_generator_pt.arb +++ b/lib/common/localization/translations/quote_generator_pt.arb @@ -51,9 +51,12 @@ "dark_mode": "Modo escuro", "light_mode": "Modo claro ", "image_saved": "Foto salva na biblioteca", + "something_went_wrong": "Oops, algo deu errado!", + "go_to_home": "Ir para a tela inicial", "@CLIPBOARD": {}, "copied_to_clipboard": "citação copiada", "@AUTH": {}, "sign_in_google": "Faça login no Google", + "sign_out": "Sair", "@END_OF_FILE": {} } \ No newline at end of file diff --git a/lib/common/navigation/routers/error_screen.dart b/lib/common/navigation/routers/error_screen.dart new file mode 100644 index 0000000..07368de --- /dev/null +++ b/lib/common/navigation/routers/error_screen.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:quote_generator/common/common.dart'; + +class ErrorScreen extends StatelessWidget { + const ErrorScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final textTheme = context.textTheme; + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + l10n.something_went_wrong, + style: textTheme.headlineMedium, + ), + ), + body: Center( + child: ElevatedButton( + onPressed: () => context.go(RouteLocation.createdByYou), + child: Text( + l10n.go_to_home, + style: textTheme.bodyMedium, + ), + ), + ), + ); + } +} diff --git a/lib/common/navigation/routers/routers.dart b/lib/common/navigation/routers/routers.dart index c4667b5..296c5ae 100644 --- a/lib/common/navigation/routers/routers.dart +++ b/lib/common/navigation/routers/routers.dart @@ -1,3 +1,3 @@ -export 'routes_config.dart'; +export 'routes_provider.dart'; export 'routes.dart'; -export 'routes_name.dart'; +export 'routes_location.dart'; diff --git a/lib/common/navigation/routers/routes.dart b/lib/common/navigation/routers/routes.dart index 5ed677b..c325104 100644 --- a/lib/common/navigation/routers/routes.dart +++ b/lib/common/navigation/routers/routes.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:quote_generator/common/common.dart'; -import 'package:quote_generator/common/navigation/navigation.dart'; +import 'package:quote_generator/features/auth/auth.dart'; import 'package:quote_generator/features/discovery/discovery.dart'; import 'package:quote_generator/features/quote/quote.dart'; @@ -9,18 +9,19 @@ final _shellNavigatorKey = GlobalKey(); final routes = [ GoRoute( - path: RoutesName.createQuote, - parentNavigatorKey: RoutesConfig.navigationKey, - pageBuilder: (context, state) { - return NoTransitionPage( - child: CreateQuoteScreen.builder(context, state), - ); - }, + path: RouteLocation.splash, + parentNavigatorKey: navigationKey, + builder: SplashScreen.builder, ), GoRoute( - name: RoutesName.detailScreen, - path: '${RoutesName.detailScreen}/:id', - parentNavigatorKey: RoutesConfig.navigationKey, + path: RouteLocation.createQuote, + parentNavigatorKey: navigationKey, + builder: CreateQuoteScreen.builder, + ), + GoRoute( + name: RouteLocation.detailScreen, + path: '${RouteLocation.detailScreen}/:id', + parentNavigatorKey: navigationKey, pageBuilder: (context, state) { return NoTransitionPage( child: QuoteCardDetails.builder( @@ -32,14 +33,16 @@ final routes = [ }, ), GoRoute( - path: RoutesName.settings, - parentNavigatorKey: RoutesConfig.navigationKey, - pageBuilder: (context, state) { - return NoTransitionPage( - child: SettingsScreen.builder(context, state), - ); - }, + path: RouteLocation.settings, + parentNavigatorKey: navigationKey, + builder: SettingsScreen.builder, ), + GoRoute( + path: RouteLocation.auth, + parentNavigatorKey: navigationKey, + builder: AuthScreen.builder, + ), + //Bottom Nav bar shell ShellRoute( navigatorKey: _shellNavigatorKey, @@ -54,16 +57,16 @@ final routes = [ }, routes: [ GoRoute( - path: RoutesName.createByYou, + path: RouteLocation.createdByYou, parentNavigatorKey: _shellNavigatorKey, pageBuilder: (context, state) { return NoTransitionPage( - child: CreateByYouScreen.builder(context, state), + child: CreatedByYouScreen.builder(context, state), ); }, ), GoRoute( - path: RoutesName.discovery, + path: RouteLocation.discovery, parentNavigatorKey: _shellNavigatorKey, pageBuilder: (context, state) { return NoTransitionPage( @@ -72,7 +75,7 @@ final routes = [ }, ), GoRoute( - path: RoutesName.search, + path: RouteLocation.search, parentNavigatorKey: _shellNavigatorKey, pageBuilder: (context, state) { return NoTransitionPage( @@ -81,7 +84,7 @@ final routes = [ }, ), GoRoute( - path: RoutesName.favorites, + path: RouteLocation.favorites, parentNavigatorKey: _shellNavigatorKey, pageBuilder: (context, state) { return NoTransitionPage( diff --git a/lib/common/navigation/routers/routes_config.dart b/lib/common/navigation/routers/routes_config.dart deleted file mode 100644 index fcb6da3..0000000 --- a/lib/common/navigation/routers/routes_config.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:quote_generator/common/common.dart'; - -@immutable -class RoutesConfig { - static final navigationKey = GlobalKey(); - static final routeConfig = GoRouter( - initialLocation: '/createdByYou', - navigatorKey: navigationKey, - routes: routes, - ); -} diff --git a/lib/common/navigation/routers/routes_location.dart b/lib/common/navigation/routers/routes_location.dart new file mode 100644 index 0000000..f9638c0 --- /dev/null +++ b/lib/common/navigation/routers/routes_location.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart' show immutable; + +@immutable +class RouteLocation { + const RouteLocation._(); + //routeLocation + static String get splash => '/splash'; + static String get detailScreen => '/quoteDetails'; + static String get discovery => '/discovery'; + static String get createQuote => '/createQuote'; + static String get settings => '/settings'; + static String get createdByYou => '/createdByYou'; + static String get search => '/search'; + static String get favorites => '/favorites'; + static String get auth => '/auth'; +} diff --git a/lib/common/navigation/routers/routes_name.dart b/lib/common/navigation/routers/routes_name.dart deleted file mode 100644 index da42168..0000000 --- a/lib/common/navigation/routers/routes_name.dart +++ /dev/null @@ -1,9 +0,0 @@ -class RoutesName { - static const String discovery = '/discovery'; - static const String detailScreen = '/quoteDetails'; - static const String createQuote = '/createQuote'; - static const String settings = '/settings'; - static const String createByYou = '/createdByYou'; - static const String search = '/search'; - static const String favorites = '/favorites'; -} diff --git a/lib/common/navigation/routers/routes_provider.dart b/lib/common/navigation/routers/routes_provider.dart new file mode 100644 index 0000000..648d8ee --- /dev/null +++ b/lib/common/navigation/routers/routes_provider.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:quote_generator/common/common.dart'; +import 'package:quote_generator/common/navigation/routers/error_screen.dart'; +import 'package:quote_generator/features/auth/auth.dart'; + +final navigationKey = GlobalKey(); + +final routerProvider = Provider( + (ref) { + final authState = ref.watch(authStateChangesProvider); + + return GoRouter( + initialLocation: RouteLocation.splash, + navigatorKey: navigationKey, + routes: routes, + errorBuilder: (context, state) => const ErrorScreen(), + redirect: (context, state) { + if (authState.isLoading || authState.hasError) return null; + final isAuth = authState.valueOrNull != null; + final isSplash = state.location == RouteLocation.splash; + if (isSplash) { + return isAuth ? RouteLocation.createdByYou : RouteLocation.auth; + } + final isLoggedIn = state.location == RouteLocation.auth; + if (isLoggedIn) return isAuth ? RouteLocation.createdByYou : null; + return isAuth ? null : RouteLocation.splash; + }, + ); + }, +); diff --git a/lib/common/settings/settings_screen.dart b/lib/common/screens/settings_screen.dart similarity index 72% rename from lib/common/settings/settings_screen.dart rename to lib/common/screens/settings_screen.dart index f3252ca..2737f13 100644 --- a/lib/common/settings/settings_screen.dart +++ b/lib/common/screens/settings_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:quote_generator/common/common.dart'; +import 'package:quote_generator/features/auth/auth.dart'; import 'package:quote_generator/features/shared/shared.dart'; class SettingsScreen extends ConsumerWidget { @@ -16,8 +18,9 @@ class SettingsScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final l10n = context.l10n; final textTheme = context.textTheme; + final colorScheme = context.colorScheme; final themeState = ref.watch(themeProvider); - final switchColor = context.colorScheme.primary; + final switchColor = colorScheme.primary; final themeLabelDisplay = themeState == ThemeMode.dark ? l10n.dark_mode : l10n.light_mode; return Scaffold( @@ -52,6 +55,21 @@ class SettingsScreen extends ConsumerWidget { }, ), ), + ListTile( + leading: Text( + l10n.sign_out, + style: textTheme.bodyMedium, + ), + trailing: IconButton( + onPressed: () async { + await ref.read(authProvider.notifier).signOut(); + }, + icon: FaIcon( + FontAwesomeIcons.arrowRightFromBracket, + color: colorScheme.primary, + ), + ), + ), ], ), ), diff --git a/lib/common/screens/splash_screen.dart b/lib/common/screens/splash_screen.dart new file mode 100644 index 0000000..d227b9d --- /dev/null +++ b/lib/common/screens/splash_screen.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:quote_generator/common/common.dart'; + +class SplashScreen extends StatelessWidget { + static SplashScreen builder( + BuildContext context, + GoRouterState state, + ) => + const SplashScreen(); + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Text( + 'Spalsh Screen', + style: context.textTheme.bodyLarge, + ), + ), + ); + } +} diff --git a/lib/features/auth/data/data.dart b/lib/features/auth/data/data.dart index e69de29..4445cc3 100644 --- a/lib/features/auth/data/data.dart +++ b/lib/features/auth/data/data.dart @@ -0,0 +1,3 @@ +export 'datasource/datasource.dart'; +export 'models/auth_result.dart'; +export 'repositories/repositories.dart'; diff --git a/lib/features/auth/data/datasource/datasource.dart b/lib/features/auth/data/datasource/datasource.dart new file mode 100644 index 0000000..e170092 --- /dev/null +++ b/lib/features/auth/data/datasource/datasource.dart @@ -0,0 +1,3 @@ +export 'remote/auth_remote_datasource.dart'; +export 'remote/auth_remote_datasource_impl.dart'; +export 'remote/auth_remote_datasource_provider.dart'; diff --git a/lib/features/auth/data/datasource/remote/auth_remote_datasource.dart b/lib/features/auth/data/datasource/remote/auth_remote_datasource.dart new file mode 100644 index 0000000..f1d98a2 --- /dev/null +++ b/lib/features/auth/data/datasource/remote/auth_remote_datasource.dart @@ -0,0 +1,8 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:quote_generator/features/auth/data/models/auth_result.dart'; + +abstract class AuthRemoteDataSource { + Future googleSignIn(); + Future googleSignOut(); + Stream authStateChanges(); +} diff --git a/lib/features/auth/data/datasource/remote/auth_remote_datasource_impl.dart b/lib/features/auth/data/datasource/remote/auth_remote_datasource_impl.dart new file mode 100644 index 0000000..2b9572f --- /dev/null +++ b/lib/features/auth/data/datasource/remote/auth_remote_datasource_impl.dart @@ -0,0 +1,42 @@ +import 'package:quote_generator/features/auth/auth.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class AuthRemoteDataSourceImpl implements AuthRemoteDataSource { + @override + Stream authStateChanges() async* { + yield* FirebaseAuth.instance.authStateChanges(); + } + + @override + Future googleSignIn() async { + final GoogleSignIn googleSignIn = GoogleSignIn(scopes: [ + Constants.emailScope, + ]); + final signInAccount = await googleSignIn.signIn(); + if (signInAccount == null) { + return AuthResult.aborted; + } + + final googleAuth = await signInAccount.authentication; + final oauthCredentials = GoogleAuthProvider.credential( + idToken: googleAuth.idToken, + accessToken: googleAuth.accessToken, + ); + + try { + await FirebaseAuth.instance.signInWithCredential( + oauthCredentials, + ); + return AuthResult.success; + } catch (e) { + return AuthResult.failure; + } + } + + @override + Future googleSignOut() async { + await FirebaseAuth.instance.signOut(); + await GoogleSignIn().signOut(); + } +} diff --git a/lib/features/auth/data/datasource/remote/auth_remote_datasource_provider.dart b/lib/features/auth/data/datasource/remote/auth_remote_datasource_provider.dart new file mode 100644 index 0000000..a6d3a46 --- /dev/null +++ b/lib/features/auth/data/datasource/remote/auth_remote_datasource_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/data/datasource/datasource.dart'; + +final authRemoteDatasourceProvider = Provider((ref) { + return AuthRemoteDataSourceImpl(); +}); diff --git a/lib/features/auth/data/models/auth_result.dart b/lib/features/auth/data/models/auth_result.dart new file mode 100644 index 0000000..1e9d6d6 --- /dev/null +++ b/lib/features/auth/data/models/auth_result.dart @@ -0,0 +1 @@ +enum AuthResult { none, aborted, success, failure } diff --git a/lib/features/auth/data/repositories/auth_repository_impl.dart b/lib/features/auth/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..6df0820 --- /dev/null +++ b/lib/features/auth/data/repositories/auth_repository_impl.dart @@ -0,0 +1,36 @@ +import 'package:dartz/dartz.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:quote_generator/features/auth/data/datasource/datasource.dart'; +import 'package:quote_generator/features/auth/data/models/auth_result.dart'; +import 'package:quote_generator/features/auth/domain/domain.dart'; +import 'package:quote_generator/features/shared/errors/failure.dart'; + +class AuthRepositoryImpl implements AuthRepository { + final AuthRemoteDataSource _authRemoteDataSource; + + AuthRepositoryImpl(this._authRemoteDataSource); + @override + Stream authStateChanges() { + try { + final result = _authRemoteDataSource.authStateChanges(); + return result; + } catch (e) { + throw const Failure('there is no user'); + } + } + + @override + Future> signIn() async { + try { + final result = await _authRemoteDataSource.googleSignIn(); + return Right(result); + } catch (e) { + return const Left(Failure('something went wrong')); + } + } + + @override + Future signOut() async { + return await _authRemoteDataSource.googleSignOut(); + } +} diff --git a/lib/features/auth/data/repositories/repositories.dart b/lib/features/auth/data/repositories/repositories.dart new file mode 100644 index 0000000..23c7844 --- /dev/null +++ b/lib/features/auth/data/repositories/repositories.dart @@ -0,0 +1 @@ +export 'auth_repository_impl.dart'; diff --git a/lib/features/auth/domain/domain.dart b/lib/features/auth/domain/domain.dart index e69de29..e0a3bae 100644 --- a/lib/features/auth/domain/domain.dart +++ b/lib/features/auth/domain/domain.dart @@ -0,0 +1,3 @@ +export 'repositories/repositories.dart'; +export 'entities/app_user.dart'; +export 'usecases/usecases.dart'; diff --git a/lib/features/auth/domain/entities/app_user.dart b/lib/features/auth/domain/entities/app_user.dart new file mode 100644 index 0000000..e08a015 --- /dev/null +++ b/lib/features/auth/domain/entities/app_user.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart' show immutable; + +@immutable +class AppUser extends Equatable { + final String userId; + final String displayName; + final String email; + + const AppUser({ + required this.userId, + required this.displayName, + required this.email, + }); + + @override + List get props => [userId, displayName, email]; +} diff --git a/lib/features/auth/domain/repositories/auth_repository.dart b/lib/features/auth/domain/repositories/auth_repository.dart new file mode 100644 index 0000000..e313787 --- /dev/null +++ b/lib/features/auth/domain/repositories/auth_repository.dart @@ -0,0 +1,10 @@ +import 'package:dartz/dartz.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:quote_generator/features/auth/auth.dart'; +import 'package:quote_generator/features/shared/shared.dart'; + +abstract class AuthRepository { + Future> signIn(); + Future signOut(); + Stream authStateChanges(); +} diff --git a/lib/features/auth/domain/repositories/auth_repository_provider.dart b/lib/features/auth/domain/repositories/auth_repository_provider.dart new file mode 100644 index 0000000..94f5c79 --- /dev/null +++ b/lib/features/auth/domain/repositories/auth_repository_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/auth.dart'; + +final authRepositorProvider = Provider((ref) { + final remoteDataSource = ref.watch(authRemoteDatasourceProvider); + return AuthRepositoryImpl(remoteDataSource); +}); diff --git a/lib/features/auth/domain/repositories/repositories.dart b/lib/features/auth/domain/repositories/repositories.dart new file mode 100644 index 0000000..b39b64d --- /dev/null +++ b/lib/features/auth/domain/repositories/repositories.dart @@ -0,0 +1,2 @@ +export 'auth_repository.dart'; +export 'auth_repository_provider.dart'; diff --git a/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes.dart b/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes.dart new file mode 100644 index 0000000..e4f9e12 --- /dev/null +++ b/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes.dart @@ -0,0 +1,2 @@ +export 'auth_state_changes_usecase.dart'; +export 'auth_state_changes_usecase_provider.dart'; diff --git a/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase.dart b/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase.dart new file mode 100644 index 0000000..a969bde --- /dev/null +++ b/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase.dart @@ -0,0 +1,12 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:quote_generator/features/auth/auth.dart'; + +class AuthStateChangesUseCase { + final AuthRepository _repository; + + AuthStateChangesUseCase(this._repository); + + Stream call() { + return _repository.authStateChanges(); + } +} diff --git a/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase_provider.dart b/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase_provider.dart new file mode 100644 index 0000000..da58e8f --- /dev/null +++ b/lib/features/auth/domain/usecases/auth_state_changes/auth_state_changes_usecase_provider.dart @@ -0,0 +1,9 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/domain/domain.dart'; + +final authStateChangesUseCaseProvider = Provider( + (ref) { + final authRepository = ref.watch(authRepositorProvider); + return AuthStateChangesUseCase(authRepository); + }, +); diff --git a/lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase.dart b/lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase.dart new file mode 100644 index 0000000..57cabae --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase.dart @@ -0,0 +1,13 @@ +import 'package:dartz/dartz.dart'; +import 'package:quote_generator/features/auth/auth.dart'; +import 'package:quote_generator/features/shared/shared.dart'; + +class GoogleSignInUseCase implements UseCase { + final AuthRepository _authRepository; + + GoogleSignInUseCase(this._authRepository); + @override + Future> call(NoParams params) async { + return await _authRepository.signIn(); + } +} diff --git a/lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase_provider.dart b/lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase_provider.dart new file mode 100644 index 0000000..b46ce12 --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_in/google_sign_in_usecase_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/domain/domain.dart'; + +final googleSignInUseCaseProvider = Provider((ref) { + final authRepository = ref.watch(authRepositorProvider); + return GoogleSignInUseCase(authRepository); +}); diff --git a/lib/features/auth/domain/usecases/sign_in/sign_in.dart b/lib/features/auth/domain/usecases/sign_in/sign_in.dart new file mode 100644 index 0000000..7f0a95c --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_in/sign_in.dart @@ -0,0 +1,2 @@ +export 'google_sign_in_usecase.dart'; +export 'google_sign_in_usecase_provider.dart'; diff --git a/lib/features/auth/domain/usecases/sign_out/sign_out.dart b/lib/features/auth/domain/usecases/sign_out/sign_out.dart new file mode 100644 index 0000000..f62199c --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_out/sign_out.dart @@ -0,0 +1,2 @@ +export 'sign_out_usecase.dart'; +export 'sign_out_usecase_provider.dart'; diff --git a/lib/features/auth/domain/usecases/sign_out/sign_out_usecase.dart b/lib/features/auth/domain/usecases/sign_out/sign_out_usecase.dart new file mode 100644 index 0000000..b8cf07b --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_out/sign_out_usecase.dart @@ -0,0 +1,10 @@ +import 'package:quote_generator/features/auth/auth.dart'; + +class SignOutUseCase { + final AuthRepository _repository; + SignOutUseCase(this._repository); + + Future call() async { + return await _repository.signOut(); + } +} diff --git a/lib/features/auth/domain/usecases/sign_out/sign_out_usecase_provider.dart b/lib/features/auth/domain/usecases/sign_out/sign_out_usecase_provider.dart new file mode 100644 index 0000000..16eb2d2 --- /dev/null +++ b/lib/features/auth/domain/usecases/sign_out/sign_out_usecase_provider.dart @@ -0,0 +1,7 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/domain/domain.dart'; + +final signOutUseCaseProvider = Provider((ref) { + final authRepository = ref.watch(authRepositorProvider); + return SignOutUseCase(authRepository); +}); diff --git a/lib/features/auth/domain/usecases/usecases.dart b/lib/features/auth/domain/usecases/usecases.dart new file mode 100644 index 0000000..137d22b --- /dev/null +++ b/lib/features/auth/domain/usecases/usecases.dart @@ -0,0 +1,3 @@ +export 'sign_in/sign_in.dart'; +export 'auth_state_changes/auth_state_changes.dart'; +export 'sign_out/sign_out.dart'; diff --git a/lib/features/auth/presentation/presentation.dart b/lib/features/auth/presentation/presentation.dart index a7ec029..9c85406 100644 --- a/lib/features/auth/presentation/presentation.dart +++ b/lib/features/auth/presentation/presentation.dart @@ -1 +1,2 @@ export 'screens/auth_screen.dart'; +export 'providers/providers.dart'; diff --git a/lib/features/auth/presentation/providers/auth_notifier.dart b/lib/features/auth/presentation/providers/auth_notifier.dart new file mode 100644 index 0000000..4797441 --- /dev/null +++ b/lib/features/auth/presentation/providers/auth_notifier.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/auth.dart'; +import 'package:quote_generator/features/shared/shared.dart'; + +class AuthNotifier extends StateNotifier { + final GoogleSignInUseCase _googleSignInUseCase; + final SignOutUseCase _signOut; + + AuthNotifier(this._googleSignInUseCase, this._signOut) + : super(const AuthState.initial()); + + Future googleSignIn() async { + state.copyWith(isLoading: true); + final result = await _googleSignInUseCase(NoParams()); + state = result.fold( + (failure) => state.copyWith( + isLoading: false, + ), + (authResult) { + return state.copyWith( + authResult: authResult, + isLoading: false, + ); + }, + ); + } + + Future signOut() async { + state.copyWith(isLoading: true); + await _signOut().then((value) { + state = state.copyWith( + authResult: AuthResult.none, + isLoading: false, + ); + }); + } +} diff --git a/lib/features/auth/presentation/providers/auth_provider.dart b/lib/features/auth/presentation/providers/auth_provider.dart new file mode 100644 index 0000000..a0c900e --- /dev/null +++ b/lib/features/auth/presentation/providers/auth_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/auth.dart'; + +final authProvider = StateNotifierProvider((ref) { + final usecaseSignIn = ref.watch(googleSignInUseCaseProvider); + final usecaseSignOut = ref.watch(signOutUseCaseProvider); + return AuthNotifier(usecaseSignIn, usecaseSignOut); +}); diff --git a/lib/features/auth/presentation/providers/auth_state.dart b/lib/features/auth/presentation/providers/auth_state.dart new file mode 100644 index 0000000..9cf152e --- /dev/null +++ b/lib/features/auth/presentation/providers/auth_state.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +import 'package:quote_generator/features/auth/auth.dart'; + +@immutable +class AuthState extends Equatable { + final AuthResult authResult; + final bool isLoading; + + const AuthState({ + required this.authResult, + required this.isLoading, + }); + + const AuthState.initial({ + this.authResult = AuthResult.none, + this.isLoading = false, + }); + + @override + List get props => [authResult, isLoading]; + + AuthState copyWith({ + AuthResult? authResult, + bool? isLoading, + }) { + return AuthState( + authResult: authResult ?? this.authResult, + isLoading: isLoading ?? this.isLoading, + ); + } +} diff --git a/lib/features/auth/presentation/providers/auth_state_changes/auth_state_changes_provider.dart b/lib/features/auth/presentation/providers/auth_state_changes/auth_state_changes_provider.dart new file mode 100644 index 0000000..01beb54 --- /dev/null +++ b/lib/features/auth/presentation/providers/auth_state_changes/auth_state_changes_provider.dart @@ -0,0 +1,9 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:quote_generator/features/auth/auth.dart'; + +final authStateChangesProvider = StreamProvider((ref) { + final repository = ref.watch(authRepositorProvider); + final authStateChanges = AuthStateChangesUseCase(repository); + return authStateChanges(); +}); diff --git a/lib/features/auth/presentation/providers/providers.dart b/lib/features/auth/presentation/providers/providers.dart new file mode 100644 index 0000000..5f13bbe --- /dev/null +++ b/lib/features/auth/presentation/providers/providers.dart @@ -0,0 +1,4 @@ +export 'auth_state_changes/auth_state_changes_provider.dart'; +export 'auth_state.dart'; +export 'auth_notifier.dart'; +export 'auth_provider.dart'; diff --git a/lib/features/auth/presentation/screens/auth_screen.dart b/lib/features/auth/presentation/screens/auth_screen.dart index e69de29..d658de2 100644 --- a/lib/features/auth/presentation/screens/auth_screen.dart +++ b/lib/features/auth/presentation/screens/auth_screen.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:quote_generator/common/common.dart'; +import 'package:quote_generator/features/auth/auth.dart'; + +class AuthScreen extends ConsumerWidget { + static AuthScreen builder( + BuildContext context, + GoRouterState state, + ) => + const AuthScreen(); + const AuthScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textTheme = context.textTheme; + return Scaffold( + body: Padding( + padding: Dimensions.kPaddingAllLarge, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.l10n.app_title, + style: textTheme.headlineLarge, + ).animate().fade(duration: 1500.ms).fadeIn(), + Dimensions.kVerticalSpaceLarge, + ElevatedButton( + onPressed: () async { + await ref.read(authProvider.notifier).googleSignIn(); + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + FontAwesomeIcons.google, + color: context.colorScheme.error, + ), + Dimensions.kHorizontalSpaceSmall, + Text( + context.l10n.sign_in_google, + style: textTheme.bodyMedium, + ) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/auth/utils/constants.dart b/lib/features/auth/utils/constants.dart new file mode 100644 index 0000000..5b95310 --- /dev/null +++ b/lib/features/auth/utils/constants.dart @@ -0,0 +1,4 @@ +class Constants { + static String emailScope = 'email'; + const Constants._(); +} diff --git a/lib/features/auth/utils/utils.dart b/lib/features/auth/utils/utils.dart index 8b13789..e56e456 100644 --- a/lib/features/auth/utils/utils.dart +++ b/lib/features/auth/utils/utils.dart @@ -1 +1 @@ - +export 'constants.dart';