From 361c50fc3367ca7a0991c86dad0100eb3eacd2b9 Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 9 Dec 2024 18:59:17 +0300 Subject: [PATCH 1/7] chore: setup user retriving on application launch --- lib/database/database.dart | 23 ++- lib/features/auth/cubit/auth_cubit.dart | 148 ++---------------- .../auth/{repository => models}/user.dart | 0 .../user_credentials.dart | 0 .../{repository => models}/user_profile.dart | 2 +- .../repository/user_local_repository.dart | 61 ++++++++ .../repository/user_remote_repository.dart | 1 + .../auth/repository/user_repository.dart | 27 ++++ lib/features/auth/views/login_page.dart | 30 ++-- lib/features/auth/views/sign_up_page.dart | 64 ++++---- lib/features/core/widgets/academia_logo.dart | 25 +++ lib/utils/router/default_route.dart | 34 ++++ lib/utils/router/router.dart | 48 ++---- 13 files changed, 236 insertions(+), 227 deletions(-) rename lib/features/auth/{repository => models}/user.dart (100%) rename lib/features/auth/{repository => models}/user_credentials.dart (100%) rename lib/features/auth/{repository => models}/user_profile.dart (94%) create mode 100644 lib/features/auth/repository/user_local_repository.dart create mode 100644 lib/features/auth/repository/user_remote_repository.dart create mode 100644 lib/features/auth/repository/user_repository.dart create mode 100644 lib/features/core/widgets/academia_logo.dart create mode 100644 lib/utils/router/default_route.dart diff --git a/lib/database/database.dart b/lib/database/database.dart index 2966f96..8624189 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -1,8 +1,8 @@ import 'dart:io'; -import 'package:academia/features/auth/repository/user.dart'; -import 'package:academia/features/auth/repository/user_credentials.dart'; -import 'package:academia/features/auth/repository/user_profile.dart'; +import 'package:academia/features/auth/models/user.dart'; +import 'package:academia/features/auth/models/user_credentials.dart'; +import 'package:academia/features/auth/models/user_profile.dart'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:drift_flutter/drift_flutter.dart'; @@ -59,3 +59,20 @@ class AppDatabase extends _$AppDatabase { return driftDatabase(name: 'academia'); } } + +/// A singleton class to reference the local database. +/// Use this instead of AppDatabase to ensure you always +/// have an initialized instance +final class LocalDatabase { + static final LocalDatabase _instance = LocalDatabase._internal(); + + LocalDatabase._internal(); + + factory LocalDatabase() { + return _instance; + } + + AppDatabase getInstance() { + return AppDatabase(); + } +} diff --git a/lib/features/auth/cubit/auth_cubit.dart b/lib/features/auth/cubit/auth_cubit.dart index 888aa4c..add884f 100644 --- a/lib/features/auth/cubit/auth_cubit.dart +++ b/lib/features/auth/cubit/auth_cubit.dart @@ -1,145 +1,17 @@ -import 'package:academia/database/database.dart'; -import 'package:academia/utils/network/dio_client.dart'; -import 'package:dartz/dartz.dart'; -import 'package:dio/dio.dart'; +import 'package:academia/features/auth/repository/user_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; -import 'package:intl/intl.dart'; -import 'package:magnet/magnet.dart'; class AuthCubit extends Cubit { - final AppDatabase appDatabase; - AuthCubit(this.appDatabase) : super(AuthInitialState()) { - _loadStoredUser().then((val) {}); - } - - Future _loadStoredUser() async { - try { - final allUsers = await appDatabase.select(appDatabase.user).get(); - if (allUsers.isEmpty) { - emit(AuthFirstAppLaunch()); - return; - } - emit(AuthCachedUsersRetrieved(cachedUsers: allUsers)); - } catch (e) { - emit(AuthErrorState(e.toString())); - rethrow; - } - } - - /// Authenticates a user - Future> authenticate( - UserCredentialData creds) async { - emit(AuthLoadingState()); - final result = await _fetchUserDataFromMagnet( - creds.admno, - creds.password, - ); - - return result.fold((l) { - emit(AuthErrorState(l)); - return left(l); - }, (r) async { - // authenticate with verisafe - final response = await _authenticateWithVerisafe(creds); - return response.fold((l) { - emit(PartiallyAuthenticatedState(user: r)); - return left(l); - }, (r) async { - /// Write the user data - await appDatabase - .into(appDatabase.user) - .insertOnConflictUpdate(r.toCompanion(true)); - - final credentials = UserCredentialData( - userId: r.id, - username: r.username, - email: r.email!, - admno: creds.admno, - password: creds.password, - lastLogin: DateTime.now(), - ); - - /// Write the credential data - await appDatabase - .into(appDatabase.userCredential) - .insertOnConflictUpdate(credentials); - - // emit the fully autheticated state - emit(FullyAuthenticatedState(user: r, creds: credentials)); - - return right(r); - }); - // emit the authenticated state - }); - } - - Future> _authenticateWithVerisafe( - UserCredentialData creds) async { - final DioClient dioClient = DioClient( - creds: creds, - database: appDatabase, - ); - - try { - final response = await dioClient.dio.post( - "/auth/authenticate", - data: { - "admission_number": creds.admno, - "password": creds.password, - }, - ); - - if (response.statusCode == 200) { - return right(UserData.fromJson(response.data)); - } - - return left(response.data["error"]); - } on DioException catch (e) { - return left(e.toString()); - } catch (e) { - return left(e.toString()); - } - } - - Future> _fetchUserDataFromMagnet( - String admno, String password) async { - final Magnet magnet = Magnet( - admno, - password, - ); - - final loginRes = await magnet.login(); - - return loginRes.fold((l) { - return left(l.toString()); - }, (r) async { - final data = await magnet.fetchUserDetails(); - - return data.fold((l) { - return left(l.toString()); - }, (r) { - var dOB = DateFormat('MM/dd/yyyy').parse(r["dateofbirth"] ?? ""); - String name = r['name'] ?? ""; - List nameParts = name.split(' '); - String firstName = nameParts.first; - String lastName = nameParts.sublist(1).join(' '); - - final userData = UserData( - id: "", - username: "", - firstname: firstName, - othernames: lastName, - phone: "", - email: r["email"] ?? "", - gender: r["gender"] ?? "", - nationalId: r["idno"] ?? "", - active: (r["academicstatus"] ?? "true") == "true" ? true : false, - modifiedAt: DateTime.now(), - dateOfBirth: dOB, - createdAt: DateTime.now(), - ); - return right(userData); + final UserRepository _userRepository = UserRepository(); + + // Load the cached user information + AuthCubit() : super(AuthInitialState()) { + _userRepository.fetchAllUsersFromCache().then((value) { + value.fold((error) { + emit(AuthErrorState(error)); + }, (users) { + emit(AuthCachedUsersRetrieved(cachedUsers: users)); }); }); } diff --git a/lib/features/auth/repository/user.dart b/lib/features/auth/models/user.dart similarity index 100% rename from lib/features/auth/repository/user.dart rename to lib/features/auth/models/user.dart diff --git a/lib/features/auth/repository/user_credentials.dart b/lib/features/auth/models/user_credentials.dart similarity index 100% rename from lib/features/auth/repository/user_credentials.dart rename to lib/features/auth/models/user_credentials.dart diff --git a/lib/features/auth/repository/user_profile.dart b/lib/features/auth/models/user_profile.dart similarity index 94% rename from lib/features/auth/repository/user_profile.dart rename to lib/features/auth/models/user_profile.dart index 852c604..06c2512 100644 --- a/lib/features/auth/repository/user_profile.dart +++ b/lib/features/auth/models/user_profile.dart @@ -1,5 +1,5 @@ -import 'package:academia/features/auth/repository/user.dart'; import 'package:drift/drift.dart'; +import './user.dart'; class UserProfile extends Table { IntColumn get id => integer().autoIncrement()(); diff --git a/lib/features/auth/repository/user_local_repository.dart b/lib/features/auth/repository/user_local_repository.dart new file mode 100644 index 0000000..5bc1ffc --- /dev/null +++ b/lib/features/auth/repository/user_local_repository.dart @@ -0,0 +1,61 @@ +import 'package:academia/database/database.dart'; +import 'package:dartz/dartz.dart'; +import 'package:drift/drift.dart'; + +/// Repository for manipulation of user related information +/// in the application's local cache +final class UserLocalRepository { + // the db's instance + final AppDatabase _localDb = LocalDatabase().getInstance(); + + /// Fetches all users from the local cache + /// incase of an error it will return a [String] to the left + /// and a [List] to the right incase users were retrived + Future>> fetchAllUsers() async { + try { + final users = await _localDb.user.select().get(); + return right(users); + } catch (e) { + return left("Failed to retrieve users with message ${e.toString()}"); + } + } + + /// Adds or updates a user's information into local cache depending + /// on whether the user data exists + Future> addUserToCache(UserData userData) async { + try { + final ok = await _localDb.into(_localDb.user).insertOnConflictUpdate( + userData.toCompanion(true), + ); + if (ok != 0) { + return right(true); + } + return left( + "The specified user data was not inserted since it exists and confliced", + ); + } catch (e) { + return left( + "Failed to append user to cache with error description ${e.toString()}", + ); + } + } + + /// Delete the user specified by [userData] from local cache + /// It wil return a string describing the error that it might have + /// encountered or a boolean [true] incase it was a success + Future> deleteUserFromCache(UserData userData) async { + try { + final ok = await _localDb.delete(_localDb.user).delete(userData); + if (ok != 0) { + return right(true); + } + return left( + "The specified user was not deleted because they do not exist", + ); + } catch (e) { + return left( + "Failed to delete user from cache with error description ${e.toString()}", + ); + } + } +} diff --git a/lib/features/auth/repository/user_remote_repository.dart b/lib/features/auth/repository/user_remote_repository.dart new file mode 100644 index 0000000..5850f1a --- /dev/null +++ b/lib/features/auth/repository/user_remote_repository.dart @@ -0,0 +1 @@ +final class UserRemoteRepository {} diff --git a/lib/features/auth/repository/user_repository.dart b/lib/features/auth/repository/user_repository.dart new file mode 100644 index 0000000..7194648 --- /dev/null +++ b/lib/features/auth/repository/user_repository.dart @@ -0,0 +1,27 @@ +import 'package:academia/database/database.dart'; +import 'package:academia/features/auth/repository/user_local_repository.dart'; +import 'package:dartz/dartz.dart'; + +final class UserRepository { + final UserLocalRepository _userLocalRepository = UserLocalRepository(); + + /// Fetches all users from the local cache + /// incase of an error it will return a [String] to the left + /// and a [List] to the right incase users were retrived + Future>> fetchAllUsersFromCache() async { + return await _userLocalRepository.fetchAllUsers(); + } + + /// Adds or updates a user's information into local cache depending + /// on whether the user data exists + Future> addUserToCache(UserData userData) async { + return await _userLocalRepository.addUserToCache(userData); + } + + /// Delete the user specified by [userData] from local cache + /// It wil return a string describing the error that it might have + /// encountered or a boolean [true] incase it was a success + Future> deleteUserFromCache(UserData userData) async { + return await _userLocalRepository.deleteUserFromCache(userData); + } +} diff --git a/lib/features/auth/views/login_page.dart b/lib/features/auth/views/login_page.dart index af3085b..5d238f9 100644 --- a/lib/features/auth/views/login_page.dart +++ b/lib/features/auth/views/login_page.dart @@ -102,7 +102,7 @@ class _LoginPageState extends State { IconButton( onPressed: () { GoRouter.of(context) - .pushNamed(AcademiaRouter.registerRoute); + .pushNamed(AcademiaRouter.register); }, icon: const Icon(Bootstrap.question_circle), ), @@ -181,20 +181,20 @@ class _LoginPageState extends State { ); return; } - final result = await authCubit - .authenticate(UserCredentialData( - username: "", - email: "", - admno: _admissionController.text, - password: _passwordController.text, - lastLogin: DateTime.now(), - )); - - result.fold((l) { - _showMessageDialog("Authentication Error", l); - }, (r) { - context.pushReplacementNamed("/home"); - }); + // final result = await authCubit + // .authenticate(UserCredentialData( + // username: "", + // email: "", + // admno: _admissionController.text, + // password: _passwordController.text, + // lastLogin: DateTime.now(), + // )); + // + // result.fold((l) { + // _showMessageDialog("Authentication Error", l); + // }, (r) { + // context.pushReplacementNamed("/home"); + // }); }, child: state is AuthLoadingState ? const CircularProgressIndicator.adaptive() diff --git a/lib/features/auth/views/sign_up_page.dart b/lib/features/auth/views/sign_up_page.dart index c31e966..ba677cc 100644 --- a/lib/features/auth/views/sign_up_page.dart +++ b/lib/features/auth/views/sign_up_page.dart @@ -76,38 +76,38 @@ class _SignUpPageState extends State { const SnackBar(content: Text("Checking validity...", softWrap: true)), ); - final result = await authCubit.authenticate(UserCredentialData( - username: "", - email: "", - admno: _admissionNumberController.text, - password: _passwordController.text, - lastLogin: DateTime.now(), - )); - - result.fold((l) { - _showAlertDialog(l); - }, (r) { - // print(r); - // print(r.toJson()); - - _nationalIdController.text = r.nationalId; - _firstNameController.text = r.firstname; - _lastNameController.text = r.othernames!; - _phoneController.text = r.phone; - _emailController.text = r.email!; - _usernameController.text = r.username; - _dateOfBirth = r.dateOfBirth; - _dateOfBirthController.text = - DateFormat.yMMMMEEEEd().format(r.dateOfBirth); - - setState(() {}); - }); - - setState(() { - _dataFetched = true; - }); - } else { - _showAlertDialog("Please fill in all required fields."); + // final result = await authCubit.authenticate(UserCredentialData( + // username: "", + // email: "", + // admno: _admissionNumberController.text, + // password: _passwordController.text, + // lastLogin: DateTime.now(), + // )); + // + // result.fold((l) { + // _showAlertDialog(l); + // }, (r) { + // // print(r); + // // print(r.toJson()); + // + // _nationalIdController.text = r.nationalId; + // _firstNameController.text = r.firstname; + // _lastNameController.text = r.othernames!; + // _phoneController.text = r.phone; + // _emailController.text = r.email!; + // _usernameController.text = r.username; + // _dateOfBirth = r.dateOfBirth; + // _dateOfBirthController.text = + // DateFormat.yMMMMEEEEd().format(r.dateOfBirth); + // + // setState(() {}); + // }); + // + // setState(() { + // _dataFetched = true; + // }); + // } else { + // _showAlertDialog("Please fill in all required fields."); } } diff --git a/lib/features/core/widgets/academia_logo.dart b/lib/features/core/widgets/academia_logo.dart new file mode 100644 index 0000000..619b8a0 --- /dev/null +++ b/lib/features/core/widgets/academia_logo.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class AcademiaLogo extends StatelessWidget { + const AcademiaLogo({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(4), + child: Row( + children: [ + SizedBox( + width: 40, + child: Image.asset("assets/icons/academia.png"), + ), + const SizedBox(width: 2), + Text( + "Academia", + style: Theme.of(context).textTheme.headlineSmall, + ) + ], + ), + ); + } +} diff --git a/lib/utils/router/default_route.dart b/lib/utils/router/default_route.dart new file mode 100644 index 0000000..50e2993 --- /dev/null +++ b/lib/utils/router/default_route.dart @@ -0,0 +1,34 @@ +import 'package:academia/features/auth/cubit/auth_cubit.dart'; +import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:academia/features/auth/views/widgets/user_selection_page.dart'; +import 'package:academia/features/onboarding/views/onboarding_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DefaultRoute extends StatelessWidget { + const DefaultRoute({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => AuthCubit()), + ], + child: BlocBuilder( + builder: (context, state) { + switch (state.runtimeType) { + case AuthErrorState: + return Center( + child: Text( + (state as AuthErrorState).message, + ), + ); + case AuthCachedUsersRetrieved: + return const UserSelectionPage(); + } + return const OnboardingPage(); + }, + ), + ); + } +} diff --git a/lib/utils/router/router.dart b/lib/utils/router/router.dart index 566f96f..520f4c7 100644 --- a/lib/utils/router/router.dart +++ b/lib/utils/router/router.dart @@ -1,7 +1,5 @@ -import 'package:academia/features/auth/cubit/auth_cubit.dart'; -import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:academia/utils/router/default_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:academia/features/features.dart'; @@ -10,9 +8,10 @@ class AcademiaRouter { static GlobalKey get globalNavigatorKey => GlobalKey(); - static const String registerRoute = "/register"; - static const String auth = "/auth"; + static const String register = "register"; + static const String auth = "auth"; static const String profile = "profile"; + static const String home = "home"; static final GoRouter _router = GoRouter( initialLocation: "/", @@ -24,18 +23,18 @@ class AcademiaRouter { builder: (context, state) => const DefaultRoute(), ), GoRoute( - path: "/auth", - name: "/auth", + path: "/$auth", + name: auth, builder: (context, state) => const LoginPage(), ), GoRoute( - path: "/register", - name: "/register", + path: "/$register", + name: register, builder: (context, state) => const SignUpPage(), ), GoRoute( - path: "/home", - name: "/home", + path: "/$home", + name: home, builder: (context, state) => const Layout(), ), GoRoute( @@ -46,30 +45,3 @@ class AcademiaRouter { ], ); } - -class DefaultRoute extends StatelessWidget { - const DefaultRoute({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Text( - "Academia", - style: Theme.of(context).textTheme.headlineLarge, - ), - ), - ); - // return BlocBuilder(builder: (context, state) { - // switch (state.runtimeType) { - // case AuthErrorState: - // return Center( - // child: Text((state as AuthErrorState).message), - // ); - // case AuthCachedUsersRetrieved: - // return const UserSelectionPage(); - // } - // return const OnboardingPage(); - // }); - } -} From 7061c95a0374f810fe744bddcbfb6335ea87c76d Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 9 Dec 2024 23:58:17 +0300 Subject: [PATCH 2/7] feat: authentication with backends and basic caching --- lib/app.dart | 23 +- lib/features/auth/auth.dart | 1 - lib/features/auth/cubit/auth_cubit.dart | 38 +- lib/features/auth/cubit/auth_states.dart | 20 - .../repository/user_remote_repository.dart | 30 +- .../auth/repository/user_repository.dart | 32 ++ lib/features/auth/views/login_page.dart | 29 +- lib/features/auth/views/sign_up_page.dart | 405 ------------------ .../onboarding/views/onboarding_page.dart | 2 +- lib/utils/network/auth_interceptor.dart | 30 +- lib/utils/network/dio_client.dart | 40 +- lib/utils/router/default_route.dart | 33 +- lib/utils/router/router.dart | 5 - pubspec.yaml | 1 + 14 files changed, 177 insertions(+), 512 deletions(-) delete mode 100644 lib/features/auth/views/sign_up_page.dart diff --git a/lib/app.dart b/lib/app.dart index ec46a57..4e85e0e 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,6 +1,8 @@ +import 'package:academia/features/auth/cubit/auth_cubit.dart'; import 'package:academia/utils/router/router.dart'; import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; class Academia extends StatelessWidget { @@ -12,14 +14,19 @@ class Academia extends StatelessWidget { @override Widget build(BuildContext context) { - return DynamicColorBuilder( - builder: (lightscheme, darkscheme) => MaterialApp.router( - title: flavor, - routerConfig: AcademiaRouter.router, - theme: ThemeData( - colorScheme: lightscheme, - useMaterial3: true, - fontFamily: GoogleFonts.inter().fontFamily, + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => AuthCubit()), + ], + child: DynamicColorBuilder( + builder: (lightscheme, darkscheme) => MaterialApp.router( + title: flavor, + routerConfig: AcademiaRouter.router, + theme: ThemeData( + colorScheme: lightscheme, + useMaterial3: true, + fontFamily: GoogleFonts.inter().fontFamily, + ), ), ), ); diff --git a/lib/features/auth/auth.dart b/lib/features/auth/auth.dart index b7c0384..fe68583 100644 --- a/lib/features/auth/auth.dart +++ b/lib/features/auth/auth.dart @@ -1,3 +1,2 @@ export './views/login_page.dart'; -export './views/sign_up_page.dart'; export './views/widgets/user_selection_page.dart'; diff --git a/lib/features/auth/cubit/auth_cubit.dart b/lib/features/auth/cubit/auth_cubit.dart index add884f..d0e2655 100644 --- a/lib/features/auth/cubit/auth_cubit.dart +++ b/lib/features/auth/cubit/auth_cubit.dart @@ -1,4 +1,6 @@ +import 'package:academia/database/database.dart'; import 'package:academia/features/auth/repository/user_repository.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; @@ -7,12 +9,34 @@ class AuthCubit extends Cubit { // Load the cached user information AuthCubit() : super(AuthInitialState()) { - _userRepository.fetchAllUsersFromCache().then((value) { - value.fold((error) { - emit(AuthErrorState(error)); - }, (users) { - emit(AuthCachedUsersRetrieved(cachedUsers: users)); - }); - }); + _userRepository.fetchAllUsersFromCache().then( + (value) { + value.fold((error) { + emit(AuthErrorState(error)); + }, (users) { + if (users.isEmpty) { + emit(AuthFirstAppLaunch()); + return; + } + emit(AuthCachedUsersRetrieved(cachedUsers: users)); + }); + }, + ); + } + + /// Authenticate performs authentication mechanisms with both verisafe + /// and magnet to authenticate a user + Future> authenticate( + UserCredentialData credentials) async { + emit(AuthLoadingState()); + final result = await _userRepository.authenticateRemotely(credentials); + + if (result.isLeft()) { + emit(AuthErrorState((result as Left).value)); + return left((result as Left).value); + } + + emit(AuthInitialState()); + return right(true); } } diff --git a/lib/features/auth/cubit/auth_states.dart b/lib/features/auth/cubit/auth_states.dart index 3c77108..f0c1eed 100644 --- a/lib/features/auth/cubit/auth_states.dart +++ b/lib/features/auth/cubit/auth_states.dart @@ -1,5 +1,4 @@ import 'package:academia/database/database.dart'; -import 'package:magnet/magnet.dart'; /// A base class representing authentication status abstract class AuthState { @@ -27,24 +26,5 @@ final class AuthErrorState extends AuthState { AuthErrorState(this.message); } -// Represents a partially authenticated state in the event that a user -// does not have internet access -class PartiallyAuthenticatedState extends AuthState { - final UserData user; - PartiallyAuthenticatedState({required this.user}); -} - -// Represents a fully authenticated state whereby the user is authenticated -// both by verisafe and magnet -final class FullyAuthenticatedState extends AuthState { - final UserData user; - final UserCredentialData creds; - - FullyAuthenticatedState({ - required this.user, - required this.creds, - }); -} - // Represents the unauthenticated state final class UnAuthenticatedState extends AuthState {} diff --git a/lib/features/auth/repository/user_remote_repository.dart b/lib/features/auth/repository/user_remote_repository.dart index 5850f1a..34269b1 100644 --- a/lib/features/auth/repository/user_remote_repository.dart +++ b/lib/features/auth/repository/user_remote_repository.dart @@ -1 +1,29 @@ -final class UserRemoteRepository {} +import 'package:academia/database/database.dart'; +import 'package:academia/utils/network/dio_client.dart'; +import 'package:dartz/dartz.dart'; + +final class UserRemoteRepository { + final DioClient _client = DioClient(); + + /// The function attempts to authenticate a users [credentials] with + /// verisafe. If okay it returns the [UserData] otherwise it just + /// returns a string with a message of what exactly went wrong + Future> verisafeAuthentication( + UserCredentialData credentials, + ) async { + try { + final response = await _client.dio.post( + "/auth/authenticate", + data: credentials.toJson(), + ); + + if (response.statusCode == 200) { + return right(UserData.fromJson(response.data)); + } + + return left(response.data["error"] ?? response.statusMessage); + } catch (e) { + return left("Please check your internet connection and try that again!"); + } + } +} diff --git a/lib/features/auth/repository/user_repository.dart b/lib/features/auth/repository/user_repository.dart index 7194648..ac48bf2 100644 --- a/lib/features/auth/repository/user_repository.dart +++ b/lib/features/auth/repository/user_repository.dart @@ -1,9 +1,13 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/auth/repository/user_local_repository.dart'; +import 'package:academia/features/auth/repository/user_remote_repository.dart'; import 'package:dartz/dartz.dart'; +import 'package:get_it/get_it.dart'; +import 'package:magnet/magnet.dart'; final class UserRepository { final UserLocalRepository _userLocalRepository = UserLocalRepository(); + final UserRemoteRepository _userRemoteRepository = UserRemoteRepository(); /// Fetches all users from the local cache /// incase of an error it will return a [String] to the left @@ -24,4 +28,32 @@ final class UserRepository { Future> deleteUserFromCache(UserData userData) async { return await _userLocalRepository.deleteUserFromCache(userData); } + + Future> authenticateRemotely( + UserCredentialData credentials) async { + // Register a magnet singleton instance + GetIt.instance.registerSingletonIfAbsent( + () => Magnet(credentials.admno, credentials.password), + instanceName: "magnet", + ); + + // authenticate with magnet + final magnetResult = + await (GetIt.instance.get(instanceName: "magnet").login()); + + return magnetResult.fold((error) { + return left(error.toString()); + }, (session) async { + final results = + await _userRemoteRepository.verisafeAuthentication(credentials); + return results.fold((error) { + return left(error); + }, (user) async { + await addUserToCache(user); + return right(user); + }); + }); + + // authenticate with verisafe + } } diff --git a/lib/features/auth/views/login_page.dart b/lib/features/auth/views/login_page.dart index 5d238f9..a296f3f 100644 --- a/lib/features/auth/views/login_page.dart +++ b/lib/features/auth/views/login_page.dart @@ -100,10 +100,7 @@ class _LoginPageState extends State { ), const Spacer(), IconButton( - onPressed: () { - GoRouter.of(context) - .pushNamed(AcademiaRouter.register); - }, + onPressed: () {}, icon: const Icon(Bootstrap.question_circle), ), ], @@ -181,20 +178,16 @@ class _LoginPageState extends State { ); return; } - // final result = await authCubit - // .authenticate(UserCredentialData( - // username: "", - // email: "", - // admno: _admissionController.text, - // password: _passwordController.text, - // lastLogin: DateTime.now(), - // )); - // - // result.fold((l) { - // _showMessageDialog("Authentication Error", l); - // }, (r) { - // context.pushReplacementNamed("/home"); - // }); + + // attempt to autheticate + authCubit.authenticate( + UserCredentialData( + password: _passwordController.text, + admno: _admissionController.text, + username: "", + email: "", + ), + ); }, child: state is AuthLoadingState ? const CircularProgressIndicator.adaptive() diff --git a/lib/features/auth/views/sign_up_page.dart b/lib/features/auth/views/sign_up_page.dart deleted file mode 100644 index ba677cc..0000000 --- a/lib/features/auth/views/sign_up_page.dart +++ /dev/null @@ -1,405 +0,0 @@ -import 'package:academia/database/database.dart'; -import 'package:academia/features/auth/cubit/auth_cubit.dart'; -import 'package:academia/utils/validator/validator.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:icons_plus/icons_plus.dart'; -import 'package:intl/intl.dart'; -import 'package:intl_phone_field/intl_phone_field.dart'; - -class SignUpPage extends StatefulWidget { - const SignUpPage({super.key}); - - @override - State createState() => _SignUpPageState(); -} - -class _SignUpPageState extends State { - final _formKey = GlobalKey(); - final TextEditingController _admissionNumberController = - TextEditingController(); - final TextEditingController _passwordController = TextEditingController(); - final TextEditingController _nationalIdController = TextEditingController(); - final TextEditingController _firstNameController = TextEditingController(); - final TextEditingController _lastNameController = TextEditingController(); - final TextEditingController _phoneController = TextEditingController(); - final TextEditingController _emailController = TextEditingController(); - final TextEditingController _usernameController = TextEditingController(); - final TextEditingController _dateOfBirthController = TextEditingController(); - bool _showPassword = false; - bool _dataFetched = false; - DateTime? _dateOfBirth; - - late AuthCubit authCubit; - - @override - void initState() { - super.initState(); - authCubit = BlocProvider.of(context); - } - - Future _selectDate(BuildContext context) async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: _dateOfBirth ?? DateTime.now(), - firstDate: DateTime(1900), - lastDate: DateTime.now(), - ); - - if (picked != null && picked != _dateOfBirth) { - setState(() { - _dateOfBirth = picked; - }); - } - } - - @override - void dispose() { - // Dispose the controllers when the widget is removed - _admissionNumberController.dispose(); - _passwordController.dispose(); - _nationalIdController.dispose(); - _firstNameController.dispose(); - _lastNameController.dispose(); - _phoneController.dispose(); - _emailController.dispose(); - _usernameController.dispose(); - super.dispose(); - } - - void _checkValidity() async { - if (_formKey.currentState?.validate() ?? false) { - // Perform your validity check (e.g., check admission number and password) - // This is a placeholder for your validity check logic - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Checking validity...", softWrap: true)), - ); - - // final result = await authCubit.authenticate(UserCredentialData( - // username: "", - // email: "", - // admno: _admissionNumberController.text, - // password: _passwordController.text, - // lastLogin: DateTime.now(), - // )); - // - // result.fold((l) { - // _showAlertDialog(l); - // }, (r) { - // // print(r); - // // print(r.toJson()); - // - // _nationalIdController.text = r.nationalId; - // _firstNameController.text = r.firstname; - // _lastNameController.text = r.othernames!; - // _phoneController.text = r.phone; - // _emailController.text = r.email!; - // _usernameController.text = r.username; - // _dateOfBirth = r.dateOfBirth; - // _dateOfBirthController.text = - // DateFormat.yMMMMEEEEd().format(r.dateOfBirth); - // - // setState(() {}); - // }); - // - // setState(() { - // _dataFetched = true; - // }); - // } else { - // _showAlertDialog("Please fill in all required fields."); - } - } - - void _getStarted() { - if (_formKey.currentState?.validate() ?? false) { - // Perform sign-up logic - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Signing up...")), - ); - } else { - _showAlertDialog("Please fill in all required fields."); - } - } - - void _showAlertDialog(String message) { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text("Validation Error"), - content: Text(message), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text("OK"), - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Add your info"), - ), - body: SafeArea( - minimum: const EdgeInsets.all(12), - child: SingleChildScrollView( - child: Form( - key: _formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 8, - children: [ - TextFormField( - controller: _admissionNumberController, - textAlign: TextAlign.center, - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^\d{0,2}-?\d{0,4}$'), - ), - AdmnoDashFormatter(), - ], - decoration: InputDecoration( - label: const Text("Admission Number"), - hintText: "00-0000", - helperText: "Please input your school admission number", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your admission number.'; - } - return null; - }, - ), - - // Password field - TextFormField( - controller: _passwordController, - textAlign: TextAlign.center, - obscureText: !_showPassword, - decoration: InputDecoration( - suffixIcon: IconButton( - onPressed: () { - setState(() { - _showPassword = !_showPassword; - }); - }, - icon: Icon( - _showPassword ? EvaIcons.eye_off_outline : EvaIcons.eye, - ), - ), - label: const Text("Password"), - hintText: "Provide your school portal password", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your password.'; - } - return null; - }, - ), - Visibility( - visible: _dataFetched, - child: TextFormField( - controller: _usernameController, - textAlign: TextAlign.center, - inputFormatters: [ - FilteringTextInputFormatter.singleLineFormatter, - ], - decoration: InputDecoration( - label: const Text("Username"), - hintText: "Your username", - helperText: "Provide a username or nickname", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please fill your username'; - } - return null; - }, - ), - ), - - Visibility( - visible: _dataFetched, - child: TextFormField( - controller: _emailController, - textAlign: TextAlign.center, - inputFormatters: [ - EmailInputFormatter(), - ], - decoration: InputDecoration( - label: const Text("National ID"), - hintText: "Your national identity", - helperText: - "In case you're a minor use your admission number", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your National ID.'; - } - return null; - }, - ), - ), - - Visibility( - visible: _dataFetched, - child: TextFormField( - controller: _firstNameController, - textAlign: TextAlign.center, - decoration: InputDecoration( - label: const Text("First name"), - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your first name.'; - } - return null; - }, - ), - ), - - Visibility( - visible: _dataFetched, - child: TextFormField( - controller: _lastNameController, - textAlign: TextAlign.center, - decoration: InputDecoration( - label: const Text("Last name"), - helperText: - "Please make sure your names appear correctly", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your last name.'; - } - return null; - }, - ), - ), - - Visibility( - visible: _dataFetched, - child: IntlPhoneField( - controller: _phoneController, - showCountryFlag: true, - keyboardType: TextInputType.phone, - initialCountryCode: "KE", - disableLengthCheck: true, - invalidNumberMessage: "Please enter a valid phone number", - decoration: InputDecoration( - label: const Text("Phone"), - hintText: "712345678", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - ), - ), - - Visibility( - visible: _dataFetched, - child: TextFormField( - controller: _emailController, - textAlign: TextAlign.center, - inputFormatters: [ - EmailInputFormatter(), - ], - decoration: InputDecoration( - label: const Text("Email"), - hintText: "someone@daystar.ac.ke", - helperText: "Use your school email", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your email.'; - } - return null; - }, - ), - ), - // Date of Birth Picker - Visibility( - visible: _dataFetched, - child: GestureDetector( - onTap: () => _selectDate(context), - child: AbsorbPointer( - child: TextFormField( - controller: _dateOfBirthController, - textAlign: TextAlign.center, - decoration: InputDecoration( - label: const Text("Date of Birth"), - hintText: _dateOfBirth != null - ? "${_dateOfBirth!.day}/${_dateOfBirth!.month}/${_dateOfBirth!.year}" - : "Select your date of birth", - border: OutlineInputBorder( - borderSide: const BorderSide(width: 2), - borderRadius: BorderRadius.circular(4), - ), - ), - validator: (value) { - if (_dateOfBirth == null) { - return 'Please select your date of birth.'; - } - return null; - }, - ), - ), - ), - ), - - _dataFetched - ? FilledButton.icon( - icon: const Icon(Bootstrap.person_fill_add), - onPressed: _getStarted, - label: const Text("Get started"), - ) - : FilledButton.icon( - icon: const Icon(EvaIcons.person_done_outline), - onPressed: _checkValidity, - label: const Text("Check validity"), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/features/onboarding/views/onboarding_page.dart b/lib/features/onboarding/views/onboarding_page.dart index 62f0115..1c2ab12 100644 --- a/lib/features/onboarding/views/onboarding_page.dart +++ b/lib/features/onboarding/views/onboarding_page.dart @@ -63,7 +63,7 @@ class OnboardingPage extends StatelessWidget { width: double.infinity, child: FilledButton( onPressed: () { - context.push( + GoRouter.of(context).pushNamed( AcademiaRouter.auth, ); }, diff --git a/lib/utils/network/auth_interceptor.dart b/lib/utils/network/auth_interceptor.dart index 28f1154..c143e47 100644 --- a/lib/utils/network/auth_interceptor.dart +++ b/lib/utils/network/auth_interceptor.dart @@ -1,25 +1,41 @@ -import 'package:academia/database/database.dart'; import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; class AuthInterceptor extends Interceptor { final Dio dio; - final UserCredentialData creds; - final AppDatabase database; - AuthInterceptor( - {required this.dio, required this.creds, required this.database}); + AuthInterceptor({required this.dio}) { + GetIt.instance.registerSingletonIfAbsent(() => "", + instanceName: "accessToken"); + } @override void onRequest( RequestOptions options, RequestInterceptorHandler handler) async { // Add your API key & other stuff to the headers. - options.headers.addAll({"Authorization": "Bearer ${creds.accessToken!}"}); + final token = GetIt.instance.get(instanceName: "accessToken"); + options.headers.addAll( + {"Authorization": "Bearer $token"}, + ); handler.next(options); } + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + if (response.headers.value("Authorization") != null) { + GetIt.instance.unregister(instanceName: "accessToken"); + GetIt.instance.registerSingleton( + response.headers.value("Authorization")!.split(" ").last, + instanceName: "accessToken", + ); + } + + super.onResponse(response, handler); + } + @override void onError(DioException err, ErrorInterceptorHandler handler) async { - // TODO Add automatic token refreshing + // TODO: erick Add automatic token refreshing if (err.response?.statusCode == 401) { return handler .resolve(await dio.fetch(err.requestOptions)); // Repeat the request. diff --git a/lib/utils/network/dio_client.dart b/lib/utils/network/dio_client.dart index 1a372f2..3a780be 100644 --- a/lib/utils/network/dio_client.dart +++ b/lib/utils/network/dio_client.dart @@ -1,39 +1,39 @@ -import 'package:academia/database/database.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import './auth_interceptor.dart'; class DioClient { - final UserCredentialData creds; - final AppDatabase database; static const String _baseUrl = "http://192.168.2.115:8000/v2"; // static const String _baseUrl = "http://192.168.26.183:8000/v2"; - DioClient({ - required this.creds, - required this.database, - }) { - dio.interceptors.add(LogInterceptor( - error: true, - responseBody: true, - request: true, - requestBody: true, - requestHeader: true, - responseHeader: true, - logPrint: (o) => debugPrint(o.toString()), - )); + DioClient() { + dio.interceptors.add( + LogInterceptor( + error: true, + responseBody: true, + request: true, + requestBody: true, + requestHeader: true, + responseHeader: true, + logPrint: (o) => debugPrint(o.toString()), + ), + ); + + _addAuthInterceptor(); } final Dio dio = Dio( - BaseOptions(baseUrl: _baseUrl), + BaseOptions( + baseUrl: _baseUrl, + preserveHeaderCase: true, + receiveDataWhenStatusError: true, + ), ); - void addAuthInterceptor() { + void _addAuthInterceptor() { dio.interceptors.add( AuthInterceptor( dio: dio, - creds: creds, - database: database, ), ); } diff --git a/lib/utils/router/default_route.dart b/lib/utils/router/default_route.dart index 50e2993..820b021 100644 --- a/lib/utils/router/default_route.dart +++ b/lib/utils/router/default_route.dart @@ -10,25 +10,20 @@ class DefaultRoute extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (context) => AuthCubit()), - ], - child: BlocBuilder( - builder: (context, state) { - switch (state.runtimeType) { - case AuthErrorState: - return Center( - child: Text( - (state as AuthErrorState).message, - ), - ); - case AuthCachedUsersRetrieved: - return const UserSelectionPage(); - } - return const OnboardingPage(); - }, - ), + return BlocBuilder( + builder: (context, state) { + switch (state.runtimeType) { + case AuthErrorState: + return Center( + child: Text( + (state as AuthErrorState).message, + ), + ); + case AuthCachedUsersRetrieved: + return const UserSelectionPage(); + } + return const OnboardingPage(); + }, ); } } diff --git a/lib/utils/router/router.dart b/lib/utils/router/router.dart index 520f4c7..19a5b7d 100644 --- a/lib/utils/router/router.dart +++ b/lib/utils/router/router.dart @@ -27,11 +27,6 @@ class AcademiaRouter { name: auth, builder: (context, state) => const LoginPage(), ), - GoRoute( - path: "/$register", - name: register, - builder: (context, state) => const SignUpPage(), - ), GoRoute( path: "/$home", name: home, diff --git a/pubspec.yaml b/pubspec.yaml index 93fa6de..27ad866 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: flutter_svg: ^2.0.14 dio: ^5.7.0 skeletonizer: ^1.4.2 + get_it: ^8.0.2 dev_dependencies: flutter_test: From 4216febec2430cfe072601b086ccc6ab5aba6159 Mon Sep 17 00:00:00 2001 From: Erick Date: Tue, 10 Dec 2024 00:31:37 +0300 Subject: [PATCH 3/7] feat: completed basic authentication flow Possibly remaining --- - Multiple accounts login - Testing on various hardware --- lib/features/auth/cubit/auth_cubit.dart | 2 +- lib/features/auth/cubit/auth_states.dart | 9 ++++ .../repository/user_local_repository.dart | 22 ++++++++ .../auth/repository/user_repository.dart | 16 ++++++ lib/features/auth/views/login_page.dart | 15 +++++- .../views/widgets/user_selection_page.dart | 53 ++++++++++++++++++- 6 files changed, 113 insertions(+), 4 deletions(-) diff --git a/lib/features/auth/cubit/auth_cubit.dart b/lib/features/auth/cubit/auth_cubit.dart index d0e2655..61e09c0 100644 --- a/lib/features/auth/cubit/auth_cubit.dart +++ b/lib/features/auth/cubit/auth_cubit.dart @@ -36,7 +36,7 @@ class AuthCubit extends Cubit { return left((result as Left).value); } - emit(AuthInitialState()); + emit(AuthenticatedState(user: (result as Right).value)); return right(true); } } diff --git a/lib/features/auth/cubit/auth_states.dart b/lib/features/auth/cubit/auth_states.dart index f0c1eed..9f595e9 100644 --- a/lib/features/auth/cubit/auth_states.dart +++ b/lib/features/auth/cubit/auth_states.dart @@ -28,3 +28,12 @@ final class AuthErrorState extends AuthState { // Represents the unauthenticated state final class UnAuthenticatedState extends AuthState {} + +final class AuthenticatedState extends AuthState { + final UserData user; + final bool localAuth; + AuthenticatedState({ + required this.user, + this.localAuth = false, + }); +} diff --git a/lib/features/auth/repository/user_local_repository.dart b/lib/features/auth/repository/user_local_repository.dart index 5bc1ffc..4c998c4 100644 --- a/lib/features/auth/repository/user_local_repository.dart +++ b/lib/features/auth/repository/user_local_repository.dart @@ -58,4 +58,26 @@ final class UserLocalRepository { ); } } + + /// Adds or updates a user's information into local credentials cache depending + /// on whether the user data exists + Future> addUserCredsToCache( + UserCredentialData credentials) async { + try { + final ok = + await _localDb.into(_localDb.userCredential).insertOnConflictUpdate( + credentials.toCompanion(true), + ); + if (ok != 0) { + return right(true); + } + return left( + "The specified user credentials data was not inserted since it exists and confliced", + ); + } catch (e) { + return left( + "Failed to append user credentials to cache with error description ${e.toString()}", + ); + } + } } diff --git a/lib/features/auth/repository/user_repository.dart b/lib/features/auth/repository/user_repository.dart index ac48bf2..75d0965 100644 --- a/lib/features/auth/repository/user_repository.dart +++ b/lib/features/auth/repository/user_repository.dart @@ -2,6 +2,7 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/auth/repository/user_local_repository.dart'; import 'package:academia/features/auth/repository/user_remote_repository.dart'; import 'package:dartz/dartz.dart'; +import 'package:drift/drift.dart'; import 'package:get_it/get_it.dart'; import 'package:magnet/magnet.dart'; @@ -29,6 +30,13 @@ final class UserRepository { return await _userLocalRepository.deleteUserFromCache(userData); } + /// Adds or updates a user's information into local credentials cache depending + /// on whether the user data exists + Future> addUserCredsToCache( + UserCredentialData credentials) async { + return await _userLocalRepository.addUserCredsToCache(credentials); + } + Future> authenticateRemotely( UserCredentialData credentials) async { // Register a magnet singleton instance @@ -50,6 +58,14 @@ final class UserRepository { return left(error); }, (user) async { await addUserToCache(user); + await addUserCredsToCache(UserCredentialData( + email: user.email!, + username: user.username, + admno: credentials.admno, + password: credentials.password, + userId: user.id, + lastLogin: DateTime.now(), + )); return right(user); }); }); diff --git a/lib/features/auth/views/login_page.dart b/lib/features/auth/views/login_page.dart index a296f3f..e30ddac 100644 --- a/lib/features/auth/views/login_page.dart +++ b/lib/features/auth/views/login_page.dart @@ -4,6 +4,7 @@ import 'package:academia/features/auth/cubit/auth_states.dart'; import 'package:academia/utils/router/router.dart'; import 'package:academia/utils/validator/validator.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; @@ -180,7 +181,7 @@ class _LoginPageState extends State { } // attempt to autheticate - authCubit.authenticate( + final result = await authCubit.authenticate( UserCredentialData( password: _passwordController.text, admno: _admissionController.text, @@ -188,6 +189,18 @@ class _LoginPageState extends State { email: "", ), ); + + result.fold((l) { + _showMessageDialog( + "Verification error", + "l", + ); + }, (r) { + HapticFeedback.heavyImpact(); + GoRouter.of(context).pushNamed( + AcademiaRouter.home, + ); + }); }, child: state is AuthLoadingState ? const CircularProgressIndicator.adaptive() diff --git a/lib/features/auth/views/widgets/user_selection_page.dart b/lib/features/auth/views/widgets/user_selection_page.dart index 46bb71f..4788632 100644 --- a/lib/features/auth/views/widgets/user_selection_page.dart +++ b/lib/features/auth/views/widgets/user_selection_page.dart @@ -3,6 +3,7 @@ import 'package:academia/features/auth/cubit/auth_cubit.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; import 'package:academia/utils/router/router.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; @@ -16,6 +17,27 @@ class UserSelectionPage extends StatefulWidget { } class _UserSelectionPageState extends State { + late AuthCubit authCubit = BlocProvider.of(context); + + /// Shows a dialog with [title] and [content] + void _showMessageDialog(String title, String content) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(content), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text("Ok"), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -64,8 +86,35 @@ class _UserSelectionPageState extends State { itemCount: users.length, itemBuilder: (context, index) { return ListTile( - onTap: () { - context.pushReplacementNamed('/home'); + onTap: () async { + // attempt to autheticate + // final result = await authCubit.authenticate( + // UserCredentialData( + // password: _passwordController.text, + // admno: _admissionController.text, + // username: "", + // email: "", + // ), + // ); + + // result.fold((l) { + // _showMessageDialog( + // "Verification error", + // "l", + // ); + // }, (r) { + // HapticFeedback.heavyImpact(); + // GoRouter.of(context).pushNamed( + // AcademiaRouter.home, + // ); + // }); + // + // if (!mounted) { + // return; + // } + // GoRouter.of(context).pushReplacementNamed( + // AcademiaRouter.home, + // ); }, leading: const CircleAvatar(), title: Text( From 87f72e5a27adce86ff86b1ca7d2af90902b4d381 Mon Sep 17 00:00:00 2001 From: Erick Date: Mon, 9 Dec 2024 21:46:00 +0300 Subject: [PATCH 4/7] chore: added flutter pretty logger --- lib/utils/network/dio_client.dart | 9 ++++++--- pubspec.yaml | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/utils/network/dio_client.dart b/lib/utils/network/dio_client.dart index 3a780be..d1fbff2 100644 --- a/lib/utils/network/dio_client.dart +++ b/lib/utils/network/dio_client.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import './auth_interceptor.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; class DioClient { static const String _baseUrl = "http://192.168.2.115:8000/v2"; @@ -8,14 +9,16 @@ class DioClient { DioClient() { dio.interceptors.add( - LogInterceptor( + PrettyDioLogger( error: true, responseBody: true, request: true, requestBody: true, requestHeader: true, responseHeader: true, - logPrint: (o) => debugPrint(o.toString()), + maxWidth: 90, + compact: true, + enabled: kDebugMode, ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 27ad866..0581c04 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,7 @@ dependencies: dio: ^5.7.0 skeletonizer: ^1.4.2 get_it: ^8.0.2 + pretty_dio_logger: ^1.4.0 dev_dependencies: flutter_test: From 1536b803ecad03f9698a31cf4ca2780ecc2c296a Mon Sep 17 00:00:00 2001 From: Erick Date: Wed, 11 Dec 2024 20:22:00 +0300 Subject: [PATCH 5/7] feat: online user login --- build.yaml | 9 ++ lib/database/database.dart | 4 +- lib/database/database.g.dart | 136 +++++++++--------- lib/database/date_time_converter.dart | 12 ++ lib/features/auth/auth.dart | 2 +- lib/features/auth/cubit/auth_cubit.dart | 19 ++- lib/features/auth/models/user.dart | 2 - lib/features/auth/models/user_profile.dart | 2 + .../repository/user_local_repository.dart | 36 +++++ .../repository/user_remote_repository.dart | 3 + .../auth/repository/user_repository.dart | 15 +- lib/features/auth/views/login_page.dart | 2 +- .../{widgets => }/user_selection_page.dart | 41 +----- .../views/widgets/user_selection_tile.dart | 112 +++++++++++++++ lib/utils/router/default_route.dart | 26 ++-- lib/utils/router/router.dart | 19 ++- 16 files changed, 312 insertions(+), 128 deletions(-) create mode 100644 build.yaml create mode 100644 lib/database/date_time_converter.dart rename lib/features/auth/views/{widgets => }/user_selection_page.dart (68%) create mode 100644 lib/features/auth/views/widgets/user_selection_tile.dart diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000..74d085d --- /dev/null +++ b/build.yaml @@ -0,0 +1,9 @@ +# build.yaml for drift +# build.yaml. This file is quite powerful, see https://pub.dev/packages/build_config + +targets: + $default: + builders: + drift_dev: + options: + store_date_time_values_as_text: true diff --git a/lib/database/database.dart b/lib/database/database.dart index 8624189..a3a42fc 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -51,11 +51,13 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 1; + int get schemaVersion => 2; static QueryExecutor _openConnection() { // driftDatabase from package:drift_flutter stores the database in // getApplicationDocumentsDirectory(). + driftRuntimeOptions.defaultSerializer = + const ValueSerializer.defaults(serializeDateTimeValuesAsString: true); return driftDatabase(name: 'academia'); } } diff --git a/lib/database/database.g.dart b/lib/database/database.g.dart index 80162db..4083e81 100644 --- a/lib/database/database.g.dart +++ b/lib/database/database.g.dart @@ -85,12 +85,6 @@ class $UserTable extends User with TableInfo<$UserTable, UserData> { late final GeneratedColumn modifiedAt = GeneratedColumn( 'modified_at', aliasedName, false, type: DriftSqlType.dateTime, requiredDuringInsert: true); - static const VerificationMeta _dateOfBirthMeta = - const VerificationMeta('dateOfBirth'); - @override - late final GeneratedColumn dateOfBirth = GeneratedColumn( - 'date_of_birth', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); static const VerificationMeta _nationalIdMeta = const VerificationMeta('nationalId'); @override @@ -112,7 +106,6 @@ class $UserTable extends User with TableInfo<$UserTable, UserData> { active, createdAt, modifiedAt, - dateOfBirth, nationalId ]; @override @@ -182,14 +175,6 @@ class $UserTable extends User with TableInfo<$UserTable, UserData> { } else if (isInserting) { context.missing(_modifiedAtMeta); } - if (data.containsKey('date_of_birth')) { - context.handle( - _dateOfBirthMeta, - dateOfBirth.isAcceptableOrUnknown( - data['date_of_birth']!, _dateOfBirthMeta)); - } else if (isInserting) { - context.missing(_dateOfBirthMeta); - } if (data.containsKey('national_id')) { context.handle( _nationalIdMeta, @@ -227,8 +212,6 @@ class $UserTable extends User with TableInfo<$UserTable, UserData> { .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, modifiedAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}modified_at'])!, - dateOfBirth: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}date_of_birth'])!, nationalId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}national_id'])!, ); @@ -251,7 +234,6 @@ class UserData extends DataClass implements Insertable { final bool active; final DateTime createdAt; final DateTime modifiedAt; - final DateTime dateOfBirth; final String nationalId; const UserData( {required this.id, @@ -264,7 +246,6 @@ class UserData extends DataClass implements Insertable { required this.active, required this.createdAt, required this.modifiedAt, - required this.dateOfBirth, required this.nationalId}); @override Map toColumns(bool nullToAbsent) { @@ -283,7 +264,6 @@ class UserData extends DataClass implements Insertable { map['active'] = Variable(active); map['created_at'] = Variable(createdAt); map['modified_at'] = Variable(modifiedAt); - map['date_of_birth'] = Variable(dateOfBirth); map['national_id'] = Variable(nationalId); return map; } @@ -303,7 +283,6 @@ class UserData extends DataClass implements Insertable { active: Value(active), createdAt: Value(createdAt), modifiedAt: Value(modifiedAt), - dateOfBirth: Value(dateOfBirth), nationalId: Value(nationalId), ); } @@ -322,7 +301,6 @@ class UserData extends DataClass implements Insertable { active: serializer.fromJson(json['active']), createdAt: serializer.fromJson(json['created_at']), modifiedAt: serializer.fromJson(json['modified_at']), - dateOfBirth: serializer.fromJson(json['date_of_birth']), nationalId: serializer.fromJson(json['national_id']), ); } @@ -340,7 +318,6 @@ class UserData extends DataClass implements Insertable { 'active': serializer.toJson(active), 'created_at': serializer.toJson(createdAt), 'modified_at': serializer.toJson(modifiedAt), - 'date_of_birth': serializer.toJson(dateOfBirth), 'national_id': serializer.toJson(nationalId), }; } @@ -356,7 +333,6 @@ class UserData extends DataClass implements Insertable { bool? active, DateTime? createdAt, DateTime? modifiedAt, - DateTime? dateOfBirth, String? nationalId}) => UserData( id: id ?? this.id, @@ -369,7 +345,6 @@ class UserData extends DataClass implements Insertable { active: active ?? this.active, createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt ?? this.modifiedAt, - dateOfBirth: dateOfBirth ?? this.dateOfBirth, nationalId: nationalId ?? this.nationalId, ); UserData copyWithCompanion(UserCompanion data) { @@ -386,8 +361,6 @@ class UserData extends DataClass implements Insertable { createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, modifiedAt: data.modifiedAt.present ? data.modifiedAt.value : this.modifiedAt, - dateOfBirth: - data.dateOfBirth.present ? data.dateOfBirth.value : this.dateOfBirth, nationalId: data.nationalId.present ? data.nationalId.value : this.nationalId, ); @@ -406,7 +379,6 @@ class UserData extends DataClass implements Insertable { ..write('active: $active, ') ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') - ..write('dateOfBirth: $dateOfBirth, ') ..write('nationalId: $nationalId') ..write(')')) .toString(); @@ -414,7 +386,7 @@ class UserData extends DataClass implements Insertable { @override int get hashCode => Object.hash(id, username, firstname, othernames, phone, - email, gender, active, createdAt, modifiedAt, dateOfBirth, nationalId); + email, gender, active, createdAt, modifiedAt, nationalId); @override bool operator ==(Object other) => identical(this, other) || @@ -429,7 +401,6 @@ class UserData extends DataClass implements Insertable { other.active == this.active && other.createdAt == this.createdAt && other.modifiedAt == this.modifiedAt && - other.dateOfBirth == this.dateOfBirth && other.nationalId == this.nationalId); } @@ -444,7 +415,6 @@ class UserCompanion extends UpdateCompanion { final Value active; final Value createdAt; final Value modifiedAt; - final Value dateOfBirth; final Value nationalId; final Value rowid; const UserCompanion({ @@ -458,7 +428,6 @@ class UserCompanion extends UpdateCompanion { this.active = const Value.absent(), this.createdAt = const Value.absent(), this.modifiedAt = const Value.absent(), - this.dateOfBirth = const Value.absent(), this.nationalId = const Value.absent(), this.rowid = const Value.absent(), }); @@ -473,7 +442,6 @@ class UserCompanion extends UpdateCompanion { this.active = const Value.absent(), required DateTime createdAt, required DateTime modifiedAt, - required DateTime dateOfBirth, required String nationalId, this.rowid = const Value.absent(), }) : id = Value(id), @@ -483,7 +451,6 @@ class UserCompanion extends UpdateCompanion { gender = Value(gender), createdAt = Value(createdAt), modifiedAt = Value(modifiedAt), - dateOfBirth = Value(dateOfBirth), nationalId = Value(nationalId); static Insertable custom({ Expression? id, @@ -496,7 +463,6 @@ class UserCompanion extends UpdateCompanion { Expression? active, Expression? createdAt, Expression? modifiedAt, - Expression? dateOfBirth, Expression? nationalId, Expression? rowid, }) { @@ -511,7 +477,6 @@ class UserCompanion extends UpdateCompanion { if (active != null) 'active': active, if (createdAt != null) 'created_at': createdAt, if (modifiedAt != null) 'modified_at': modifiedAt, - if (dateOfBirth != null) 'date_of_birth': dateOfBirth, if (nationalId != null) 'national_id': nationalId, if (rowid != null) 'rowid': rowid, }); @@ -528,7 +493,6 @@ class UserCompanion extends UpdateCompanion { Value? active, Value? createdAt, Value? modifiedAt, - Value? dateOfBirth, Value? nationalId, Value? rowid}) { return UserCompanion( @@ -542,7 +506,6 @@ class UserCompanion extends UpdateCompanion { active: active ?? this.active, createdAt: createdAt ?? this.createdAt, modifiedAt: modifiedAt ?? this.modifiedAt, - dateOfBirth: dateOfBirth ?? this.dateOfBirth, nationalId: nationalId ?? this.nationalId, rowid: rowid ?? this.rowid, ); @@ -581,9 +544,6 @@ class UserCompanion extends UpdateCompanion { if (modifiedAt.present) { map['modified_at'] = Variable(modifiedAt.value); } - if (dateOfBirth.present) { - map['date_of_birth'] = Variable(dateOfBirth.value); - } if (nationalId.present) { map['national_id'] = Variable(nationalId.value); } @@ -606,7 +566,6 @@ class UserCompanion extends UpdateCompanion { ..write('active: $active, ') ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') - ..write('dateOfBirth: $dateOfBirth, ') ..write('nationalId: $nationalId, ') ..write('rowid: $rowid') ..write(')')) @@ -693,6 +652,12 @@ class $UserProfileTable extends UserProfile type: DriftSqlType.string, requiredDuringInsert: false, defaultValue: const Constant("athi")); + static const VerificationMeta _dateOfBirthMeta = + const VerificationMeta('dateOfBirth'); + @override + late final GeneratedColumn dateOfBirth = GeneratedColumn( + 'date_of_birth', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); @override List get $columns => [ id, @@ -704,7 +669,8 @@ class $UserProfileTable extends UserProfile createdAt, modifiedAt, admissionNumber, - campus + campus, + dateOfBirth ]; @override String get aliasedName => _alias ?? actualTableName; @@ -765,6 +731,14 @@ class $UserProfileTable extends UserProfile context.handle(_campusMeta, campus.isAcceptableOrUnknown(data['campus']!, _campusMeta)); } + if (data.containsKey('date_of_birth')) { + context.handle( + _dateOfBirthMeta, + dateOfBirth.isAcceptableOrUnknown( + data['date_of_birth']!, _dateOfBirthMeta)); + } else if (isInserting) { + context.missing(_dateOfBirthMeta); + } return context; } @@ -794,6 +768,8 @@ class $UserProfileTable extends UserProfile DriftSqlType.string, data['${effectivePrefix}admission_number']), campus: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}campus'])!, + dateOfBirth: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}date_of_birth'])!, ); } @@ -814,6 +790,7 @@ class UserProfileData extends DataClass implements Insertable { final DateTime modifiedAt; final String? admissionNumber; final String campus; + final DateTime dateOfBirth; const UserProfileData( {required this.id, required this.userId, @@ -824,7 +801,8 @@ class UserProfileData extends DataClass implements Insertable { required this.createdAt, required this.modifiedAt, this.admissionNumber, - required this.campus}); + required this.campus, + required this.dateOfBirth}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -844,6 +822,7 @@ class UserProfileData extends DataClass implements Insertable { map['admission_number'] = Variable(admissionNumber); } map['campus'] = Variable(campus); + map['date_of_birth'] = Variable(dateOfBirth); return map; } @@ -863,6 +842,7 @@ class UserProfileData extends DataClass implements Insertable { ? const Value.absent() : Value(admissionNumber), campus: Value(campus), + dateOfBirth: Value(dateOfBirth), ); } @@ -881,6 +861,7 @@ class UserProfileData extends DataClass implements Insertable { modifiedAt: serializer.fromJson(json['modified_at']), admissionNumber: serializer.fromJson(json['admission_number']), campus: serializer.fromJson(json['campus']), + dateOfBirth: serializer.fromJson(json['date_of_birth']), ); } @override @@ -897,6 +878,7 @@ class UserProfileData extends DataClass implements Insertable { 'modified_at': serializer.toJson(modifiedAt), 'admission_number': serializer.toJson(admissionNumber), 'campus': serializer.toJson(campus), + 'date_of_birth': serializer.toJson(dateOfBirth), }; } @@ -910,7 +892,8 @@ class UserProfileData extends DataClass implements Insertable { DateTime? createdAt, DateTime? modifiedAt, Value admissionNumber = const Value.absent(), - String? campus}) => + String? campus, + DateTime? dateOfBirth}) => UserProfileData( id: id ?? this.id, userId: userId ?? this.userId, @@ -926,6 +909,7 @@ class UserProfileData extends DataClass implements Insertable { ? admissionNumber.value : this.admissionNumber, campus: campus ?? this.campus, + dateOfBirth: dateOfBirth ?? this.dateOfBirth, ); UserProfileData copyWithCompanion(UserProfileCompanion data) { return UserProfileData( @@ -945,6 +929,8 @@ class UserProfileData extends DataClass implements Insertable { ? data.admissionNumber.value : this.admissionNumber, campus: data.campus.present ? data.campus.value : this.campus, + dateOfBirth: + data.dateOfBirth.present ? data.dateOfBirth.value : this.dateOfBirth, ); } @@ -960,7 +946,8 @@ class UserProfileData extends DataClass implements Insertable { ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') ..write('admissionNumber: $admissionNumber, ') - ..write('campus: $campus') + ..write('campus: $campus, ') + ..write('dateOfBirth: $dateOfBirth') ..write(')')) .toString(); } @@ -976,7 +963,8 @@ class UserProfileData extends DataClass implements Insertable { createdAt, modifiedAt, admissionNumber, - campus); + campus, + dateOfBirth); @override bool operator ==(Object other) => identical(this, other) || @@ -990,7 +978,8 @@ class UserProfileData extends DataClass implements Insertable { other.createdAt == this.createdAt && other.modifiedAt == this.modifiedAt && other.admissionNumber == this.admissionNumber && - other.campus == this.campus); + other.campus == this.campus && + other.dateOfBirth == this.dateOfBirth); } class UserProfileCompanion extends UpdateCompanion { @@ -1004,6 +993,7 @@ class UserProfileCompanion extends UpdateCompanion { final Value modifiedAt; final Value admissionNumber; final Value campus; + final Value dateOfBirth; const UserProfileCompanion({ this.id = const Value.absent(), this.userId = const Value.absent(), @@ -1015,6 +1005,7 @@ class UserProfileCompanion extends UpdateCompanion { this.modifiedAt = const Value.absent(), this.admissionNumber = const Value.absent(), this.campus = const Value.absent(), + this.dateOfBirth = const Value.absent(), }); UserProfileCompanion.insert({ this.id = const Value.absent(), @@ -1027,7 +1018,9 @@ class UserProfileCompanion extends UpdateCompanion { this.modifiedAt = const Value.absent(), this.admissionNumber = const Value.absent(), this.campus = const Value.absent(), - }) : userId = Value(userId); + required DateTime dateOfBirth, + }) : userId = Value(userId), + dateOfBirth = Value(dateOfBirth); static Insertable custom({ Expression? id, Expression? userId, @@ -1039,6 +1032,7 @@ class UserProfileCompanion extends UpdateCompanion { Expression? modifiedAt, Expression? admissionNumber, Expression? campus, + Expression? dateOfBirth, }) { return RawValuesInsertable({ if (id != null) 'id': id, @@ -1051,6 +1045,7 @@ class UserProfileCompanion extends UpdateCompanion { if (modifiedAt != null) 'modified_at': modifiedAt, if (admissionNumber != null) 'admission_number': admissionNumber, if (campus != null) 'campus': campus, + if (dateOfBirth != null) 'date_of_birth': dateOfBirth, }); } @@ -1064,7 +1059,8 @@ class UserProfileCompanion extends UpdateCompanion { Value? createdAt, Value? modifiedAt, Value? admissionNumber, - Value? campus}) { + Value? campus, + Value? dateOfBirth}) { return UserProfileCompanion( id: id ?? this.id, userId: userId ?? this.userId, @@ -1076,6 +1072,7 @@ class UserProfileCompanion extends UpdateCompanion { modifiedAt: modifiedAt ?? this.modifiedAt, admissionNumber: admissionNumber ?? this.admissionNumber, campus: campus ?? this.campus, + dateOfBirth: dateOfBirth ?? this.dateOfBirth, ); } @@ -1112,6 +1109,9 @@ class UserProfileCompanion extends UpdateCompanion { if (campus.present) { map['campus'] = Variable(campus.value); } + if (dateOfBirth.present) { + map['date_of_birth'] = Variable(dateOfBirth.value); + } return map; } @@ -1127,7 +1127,8 @@ class UserProfileCompanion extends UpdateCompanion { ..write('createdAt: $createdAt, ') ..write('modifiedAt: $modifiedAt, ') ..write('admissionNumber: $admissionNumber, ') - ..write('campus: $campus') + ..write('campus: $campus, ') + ..write('dateOfBirth: $dateOfBirth') ..write(')')) .toString(); } @@ -1543,6 +1544,9 @@ abstract class _$AppDatabase extends GeneratedDatabase { @override List get allSchemaEntities => [user, userProfile, userCredential]; + @override + DriftDatabaseOptions get options => + const DriftDatabaseOptions(storeDateTimeAsText: true); } typedef $$UserTableCreateCompanionBuilder = UserCompanion Function({ @@ -1556,7 +1560,6 @@ typedef $$UserTableCreateCompanionBuilder = UserCompanion Function({ Value active, required DateTime createdAt, required DateTime modifiedAt, - required DateTime dateOfBirth, required String nationalId, Value rowid, }); @@ -1571,7 +1574,6 @@ typedef $$UserTableUpdateCompanionBuilder = UserCompanion Function({ Value active, Value createdAt, Value modifiedAt, - Value dateOfBirth, Value nationalId, Value rowid, }); @@ -1633,9 +1635,6 @@ class $$UserTableFilterComposer extends Composer<_$AppDatabase, $UserTable> { ColumnFilters get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get dateOfBirth => $composableBuilder( - column: $table.dateOfBirth, builder: (column) => ColumnFilters(column)); - ColumnFilters get nationalId => $composableBuilder( column: $table.nationalId, builder: (column) => ColumnFilters(column)); @@ -1699,9 +1698,6 @@ class $$UserTableOrderingComposer extends Composer<_$AppDatabase, $UserTable> { ColumnOrderings get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get dateOfBirth => $composableBuilder( - column: $table.dateOfBirth, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get nationalId => $composableBuilder( column: $table.nationalId, builder: (column) => ColumnOrderings(column)); } @@ -1745,9 +1741,6 @@ class $$UserTableAnnotationComposer GeneratedColumn get modifiedAt => $composableBuilder( column: $table.modifiedAt, builder: (column) => column); - GeneratedColumn get dateOfBirth => $composableBuilder( - column: $table.dateOfBirth, builder: (column) => column); - GeneratedColumn get nationalId => $composableBuilder( column: $table.nationalId, builder: (column) => column); @@ -1806,7 +1799,6 @@ class $$UserTableTableManager extends RootTableManager< Value active = const Value.absent(), Value createdAt = const Value.absent(), Value modifiedAt = const Value.absent(), - Value dateOfBirth = const Value.absent(), Value nationalId = const Value.absent(), Value rowid = const Value.absent(), }) => @@ -1821,7 +1813,6 @@ class $$UserTableTableManager extends RootTableManager< active: active, createdAt: createdAt, modifiedAt: modifiedAt, - dateOfBirth: dateOfBirth, nationalId: nationalId, rowid: rowid, ), @@ -1836,7 +1827,6 @@ class $$UserTableTableManager extends RootTableManager< Value active = const Value.absent(), required DateTime createdAt, required DateTime modifiedAt, - required DateTime dateOfBirth, required String nationalId, Value rowid = const Value.absent(), }) => @@ -1851,7 +1841,6 @@ class $$UserTableTableManager extends RootTableManager< active: active, createdAt: createdAt, modifiedAt: modifiedAt, - dateOfBirth: dateOfBirth, nationalId: nationalId, rowid: rowid, ), @@ -1909,6 +1898,7 @@ typedef $$UserProfileTableCreateCompanionBuilder = UserProfileCompanion Value modifiedAt, Value admissionNumber, Value campus, + required DateTime dateOfBirth, }); typedef $$UserProfileTableUpdateCompanionBuilder = UserProfileCompanion Function({ @@ -1922,6 +1912,7 @@ typedef $$UserProfileTableUpdateCompanionBuilder = UserProfileCompanion Value modifiedAt, Value admissionNumber, Value campus, + Value dateOfBirth, }); final class $$UserProfileTableReferences @@ -1980,6 +1971,9 @@ class $$UserProfileTableFilterComposer ColumnFilters get campus => $composableBuilder( column: $table.campus, builder: (column) => ColumnFilters(column)); + ColumnFilters get dateOfBirth => $composableBuilder( + column: $table.dateOfBirth, builder: (column) => ColumnFilters(column)); + $$UserTableFilterComposer get userId { final $$UserTableFilterComposer composer = $composerBuilder( composer: this, @@ -2039,6 +2033,9 @@ class $$UserProfileTableOrderingComposer ColumnOrderings get campus => $composableBuilder( column: $table.campus, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get dateOfBirth => $composableBuilder( + column: $table.dateOfBirth, builder: (column) => ColumnOrderings(column)); + $$UserTableOrderingComposer get userId { final $$UserTableOrderingComposer composer = $composerBuilder( composer: this, @@ -2096,6 +2093,9 @@ class $$UserProfileTableAnnotationComposer GeneratedColumn get campus => $composableBuilder(column: $table.campus, builder: (column) => column); + GeneratedColumn get dateOfBirth => $composableBuilder( + column: $table.dateOfBirth, builder: (column) => column); + $$UserTableAnnotationComposer get userId { final $$UserTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -2150,6 +2150,7 @@ class $$UserProfileTableTableManager extends RootTableManager< Value modifiedAt = const Value.absent(), Value admissionNumber = const Value.absent(), Value campus = const Value.absent(), + Value dateOfBirth = const Value.absent(), }) => UserProfileCompanion( id: id, @@ -2162,6 +2163,7 @@ class $$UserProfileTableTableManager extends RootTableManager< modifiedAt: modifiedAt, admissionNumber: admissionNumber, campus: campus, + dateOfBirth: dateOfBirth, ), createCompanionCallback: ({ Value id = const Value.absent(), @@ -2174,6 +2176,7 @@ class $$UserProfileTableTableManager extends RootTableManager< Value modifiedAt = const Value.absent(), Value admissionNumber = const Value.absent(), Value campus = const Value.absent(), + required DateTime dateOfBirth, }) => UserProfileCompanion.insert( id: id, @@ -2186,6 +2189,7 @@ class $$UserProfileTableTableManager extends RootTableManager< modifiedAt: modifiedAt, admissionNumber: admissionNumber, campus: campus, + dateOfBirth: dateOfBirth, ), withReferenceMapper: (p0) => p0 .map((e) => ( diff --git a/lib/database/date_time_converter.dart b/lib/database/date_time_converter.dart new file mode 100644 index 0000000..e1d6791 --- /dev/null +++ b/lib/database/date_time_converter.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; + +// Custom TypeConverter to convert UTC/local time +class UtcConverter extends TypeConverter { + const UtcConverter(); + + @override + DateTime fromSql(DateTime fromDb) => fromDb.toLocal(); + + @override + DateTime toSql(DateTime value) => value.toUtc(); +} diff --git a/lib/features/auth/auth.dart b/lib/features/auth/auth.dart index fe68583..fd4af2f 100644 --- a/lib/features/auth/auth.dart +++ b/lib/features/auth/auth.dart @@ -1,2 +1,2 @@ export './views/login_page.dart'; -export './views/widgets/user_selection_page.dart'; +export './views/user_selection_page.dart'; diff --git a/lib/features/auth/cubit/auth_cubit.dart b/lib/features/auth/cubit/auth_cubit.dart index 61e09c0..afc79ea 100644 --- a/lib/features/auth/cubit/auth_cubit.dart +++ b/lib/features/auth/cubit/auth_cubit.dart @@ -1,6 +1,7 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/auth/repository/user_repository.dart'; import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; @@ -27,7 +28,8 @@ class AuthCubit extends Cubit { /// Authenticate performs authentication mechanisms with both verisafe /// and magnet to authenticate a user Future> authenticate( - UserCredentialData credentials) async { + UserCredentialData credentials, + ) async { emit(AuthLoadingState()); final result = await _userRepository.authenticateRemotely(credentials); @@ -39,4 +41,19 @@ class AuthCubit extends Cubit { emit(AuthenticatedState(user: (result as Right).value)); return right(true); } + + Future fetchUserProfile(UserData user) async { + final result = await _userRepository.fetchUserProfile(user); + result.fold((l) { + debugPrint(l); + return null; + }, (r) { + return r; + }); + } + + Future> fetchUserCredsFromCache( + UserData user) async { + return await _userRepository.fetchUserCredsFromCache(user); + } } diff --git a/lib/features/auth/models/user.dart b/lib/features/auth/models/user.dart index 0f43ec0..982a05b 100644 --- a/lib/features/auth/models/user.dart +++ b/lib/features/auth/models/user.dart @@ -13,8 +13,6 @@ class User extends Table { DateTimeColumn get createdAt => dateTime()(); @JsonKey("modified_at") DateTimeColumn get modifiedAt => dateTime()(); - @JsonKey("date_of_birth") - DateTimeColumn get dateOfBirth => dateTime()(); @JsonKey("national_id") TextColumn get nationalId => text().withLength(min: 1, max: 20)(); diff --git a/lib/features/auth/models/user_profile.dart b/lib/features/auth/models/user_profile.dart index 06c2512..147ef0b 100644 --- a/lib/features/auth/models/user_profile.dart +++ b/lib/features/auth/models/user_profile.dart @@ -25,4 +25,6 @@ class UserProfile extends Table { TextColumn get admissionNumber => text().nullable()(); @JsonKey("campus") TextColumn get campus => text().withDefault(const Constant("athi"))(); + @JsonKey("date_of_birth") + DateTimeColumn get dateOfBirth => dateTime()(); } diff --git a/lib/features/auth/repository/user_local_repository.dart b/lib/features/auth/repository/user_local_repository.dart index 4c998c4..2a13621 100644 --- a/lib/features/auth/repository/user_local_repository.dart +++ b/lib/features/auth/repository/user_local_repository.dart @@ -80,4 +80,40 @@ final class UserLocalRepository { ); } } + + /// Retrieves user credentials from cache depending + /// on whether the user data exists + Future> fetchUserCredsFromCache( + UserData user) async { + try { + final creds = await _localDb.managers.userCredential + .filter((f) => f.userId.id.equals(user.id)) + .getSingleOrNull(distinct: true); + + if (creds == null) { + throw ("User credentials does not exist in cache!"); + } + return right(creds); + } catch (e) { + return left( + "Failed to retrieve user credentials from cache with error description ${e.toString()}", + ); + } + } + + /// Retrieves a user's profile from the cache + Future> fetchUserProfile( + UserData user) async { + try { + // final profile = + final profile = await _localDb.managers.userProfile + .filter((f) => f.userId.id.equals(user.id)) + .getSingleOrNull(distinct: true); + return right(profile); + } catch (e) { + return left( + "Failed to fetch cached user profile to cache with error description ${e.toString()}", + ); + } + } } diff --git a/lib/features/auth/repository/user_remote_repository.dart b/lib/features/auth/repository/user_remote_repository.dart index 34269b1..8c470d9 100644 --- a/lib/features/auth/repository/user_remote_repository.dart +++ b/lib/features/auth/repository/user_remote_repository.dart @@ -1,6 +1,7 @@ import 'package:academia/database/database.dart'; import 'package:academia/utils/network/dio_client.dart'; import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; final class UserRemoteRepository { final DioClient _client = DioClient(); @@ -22,6 +23,8 @@ final class UserRemoteRepository { } return left(response.data["error"] ?? response.statusMessage); + } on DioException catch (de) { + return left(de.response?.data["error"] ?? de.response!.statusMessage); } catch (e) { return left("Please check your internet connection and try that again!"); } diff --git a/lib/features/auth/repository/user_repository.dart b/lib/features/auth/repository/user_repository.dart index 75d0965..0dca726 100644 --- a/lib/features/auth/repository/user_repository.dart +++ b/lib/features/auth/repository/user_repository.dart @@ -2,7 +2,6 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/auth/repository/user_local_repository.dart'; import 'package:academia/features/auth/repository/user_remote_repository.dart'; import 'package:dartz/dartz.dart'; -import 'package:drift/drift.dart'; import 'package:get_it/get_it.dart'; import 'package:magnet/magnet.dart'; @@ -37,6 +36,13 @@ final class UserRepository { return await _userLocalRepository.addUserCredsToCache(credentials); } + /// Retrieves user credentials from cache depending + /// on whether the user data exists + Future> fetchUserCredsFromCache( + UserData user) async { + return await _userLocalRepository.fetchUserCredsFromCache(user); + } + Future> authenticateRemotely( UserCredentialData credentials) async { // Register a magnet singleton instance @@ -72,4 +78,11 @@ final class UserRepository { // authenticate with verisafe } + + /// Retrieves a user's profile from the cache + Future> fetchUserProfile( + UserData user, + ) async { + return await _userLocalRepository.fetchUserProfile(user); + } } diff --git a/lib/features/auth/views/login_page.dart b/lib/features/auth/views/login_page.dart index e30ddac..cf91def 100644 --- a/lib/features/auth/views/login_page.dart +++ b/lib/features/auth/views/login_page.dart @@ -193,7 +193,7 @@ class _LoginPageState extends State { result.fold((l) { _showMessageDialog( "Verification error", - "l", + l, ); }, (r) { HapticFeedback.heavyImpact(); diff --git a/lib/features/auth/views/widgets/user_selection_page.dart b/lib/features/auth/views/user_selection_page.dart similarity index 68% rename from lib/features/auth/views/widgets/user_selection_page.dart rename to lib/features/auth/views/user_selection_page.dart index 4788632..7f1f49b 100644 --- a/lib/features/auth/views/widgets/user_selection_page.dart +++ b/lib/features/auth/views/user_selection_page.dart @@ -1,9 +1,9 @@ import 'package:academia/database/database.dart'; import 'package:academia/features/auth/cubit/auth_cubit.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:academia/features/auth/views/widgets/user_selection_tile.dart'; import 'package:academia/utils/router/router.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; @@ -85,43 +85,8 @@ class _UserSelectionPageState extends State { return SliverList.builder( itemCount: users.length, itemBuilder: (context, index) { - return ListTile( - onTap: () async { - // attempt to autheticate - // final result = await authCubit.authenticate( - // UserCredentialData( - // password: _passwordController.text, - // admno: _admissionController.text, - // username: "", - // email: "", - // ), - // ); - - // result.fold((l) { - // _showMessageDialog( - // "Verification error", - // "l", - // ); - // }, (r) { - // HapticFeedback.heavyImpact(); - // GoRouter.of(context).pushNamed( - // AcademiaRouter.home, - // ); - // }); - // - // if (!mounted) { - // return; - // } - // GoRouter.of(context).pushReplacementNamed( - // AcademiaRouter.home, - // ); - }, - leading: const CircleAvatar(), - title: Text( - users[index].username, - ), - subtitle: const Text("The user bio will appear here"), - trailing: const Icon(Bootstrap.person_check), + return UserSelectionTile( + user: users[index], ); }, ); diff --git a/lib/features/auth/views/widgets/user_selection_tile.dart b/lib/features/auth/views/widgets/user_selection_tile.dart new file mode 100644 index 0000000..b4a043c --- /dev/null +++ b/lib/features/auth/views/widgets/user_selection_tile.dart @@ -0,0 +1,112 @@ +import 'package:academia/database/database.dart'; +import 'package:academia/features/auth/cubit/auth_cubit.dart'; +import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:academia/utils/router/router.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:icons_plus/icons_plus.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class UserSelectionTile extends StatefulWidget { + const UserSelectionTile({ + super.key, + required this.user, + }); + + final UserData user; + + @override + State createState() => _UserSelectionTileState(); +} + +class _UserSelectionTileState extends State { + bool _isLoading = true; + late AuthCubit _authCubit = BlocProvider.of(context); + + UserProfileData? profile; + UserCredentialData? creds; + + void _showMessageDialog(String title, String content) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(content), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text("ok"), + ) + ], + ), + ); + } + + Future _fetchUserProfile(UserData user) async { + return await _authCubit.fetchUserProfile(user); + } + + Future _fetchUserCredentials(UserData user) async { + final result = await _authCubit.fetchUserCredsFromCache(user); + return result.fold((l) { + _showMessageDialog("Error", l); + return null; + }, (r) { + return r; + }); + } + + Future _loadCachedData() async { + creds = await _fetchUserCredentials(widget.user); + profile = await _fetchUserProfile(widget.user); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _loadCachedData(), + builder: (context, snapshot) { + return Skeletonizer( + enabled: snapshot.connectionState != ConnectionState.done, + child: ListTile( + onTap: () async { + final result = await _authCubit.authenticate(creds!); + result.fold((l) { + _showMessageDialog("Authentication Error", l); + }, (r) { + if (!context.mounted) return; + GoRouter.of(context) + .pushReplacementNamed(AcademiaRouter.home); + }); + }, + title: Text("@${widget.user.username}"), + leading: (profile?.profilePictureUrl) != null + ? CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + profile!.profilePictureUrl!), + ) + : const CircleAvatar( + child: Icon(Bootstrap.person), + ), + subtitle: + BlocBuilder(builder: (context, state) { + if (state is AuthLoadingState) { + return const LinearProgressIndicator(); + } else if (state is AuthErrorState) { + return Text(state.message); + } + return Text( + profile?.bio ?? + "${widget.user.firstname} ${widget.user.othernames}", + ); + }), + trailing: const Icon(Bootstrap.arrow_right), + ), + ); + }); + } +} diff --git a/lib/utils/router/default_route.dart b/lib/utils/router/default_route.dart index 820b021..7b27079 100644 --- a/lib/utils/router/default_route.dart +++ b/lib/utils/router/default_route.dart @@ -1,7 +1,6 @@ import 'package:academia/features/auth/cubit/auth_cubit.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; -import 'package:academia/features/auth/views/widgets/user_selection_page.dart'; -import 'package:academia/features/onboarding/views/onboarding_page.dart'; +import 'package:academia/features/features.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -10,20 +9,17 @@ class DefaultRoute extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - switch (state.runtimeType) { - case AuthErrorState: - return Center( - child: Text( - (state as AuthErrorState).message, - ), - ); - case AuthCachedUsersRetrieved: + return Scaffold( + body: BlocBuilder( + builder: (context, state) { + if (state is AuthenticatedState) { + return const Layout(); + } else if (state is AuthCachedUsersRetrieved) { return const UserSelectionPage(); - } - return const OnboardingPage(); - }, + } + return const OnboardingPage(); + }, + ), ); } } diff --git a/lib/utils/router/router.dart b/lib/utils/router/router.dart index 19a5b7d..8126d78 100644 --- a/lib/utils/router/router.dart +++ b/lib/utils/router/router.dart @@ -1,5 +1,8 @@ +import 'package:academia/features/auth/cubit/auth_cubit.dart'; +import 'package:academia/features/auth/cubit/auth_states.dart'; import 'package:academia/utils/router/default_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:academia/features/features.dart'; @@ -12,6 +15,8 @@ class AcademiaRouter { static const String auth = "auth"; static const String profile = "profile"; static const String home = "home"; + static const String userSelection = "user-selection"; + static const String onboarding = "onboarding"; static final GoRouter _router = GoRouter( initialLocation: "/", @@ -22,6 +27,11 @@ class AcademiaRouter { name: "/", builder: (context, state) => const DefaultRoute(), ), + GoRoute( + path: "/$onboarding", + name: onboarding, + builder: (context, state) => const OnboardingPage(), + ), GoRoute( path: "/$auth", name: auth, @@ -33,10 +43,15 @@ class AcademiaRouter { builder: (context, state) => const Layout(), ), GoRoute( - path: profile, - name: "/$profile", + name: profile, + path: "/$profile", builder: (context, state) => const ProfilePage(), ), + GoRoute( + name: userSelection, + path: "/$userSelection", + builder: (context, state) => const UserSelectionPage(), + ), ], ); } From 9cfc5e7b61101ffa570344927cbfff16194b4c95 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 12 Dec 2024 12:22:00 +0300 Subject: [PATCH 6/7] fix: better error messages on login --- lib/features/auth/cubit/auth_cubit.dart | 18 ++++++++++++++- .../repository/user_remote_repository.dart | 6 ++--- .../auth/views/user_selection_page.dart | 9 +++++++- .../views/widgets/user_selection_tile.dart | 10 ++++---- lib/utils/network/dio_error_handler.dart | 23 +++++++++++++++++++ lib/utils/network/network.dart | 1 + lib/utils/router/default_route.dart | 17 ++++++++------ 7 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 lib/utils/network/dio_error_handler.dart diff --git a/lib/features/auth/cubit/auth_cubit.dart b/lib/features/auth/cubit/auth_cubit.dart index afc79ea..6d4657c 100644 --- a/lib/features/auth/cubit/auth_cubit.dart +++ b/lib/features/auth/cubit/auth_cubit.dart @@ -17,7 +17,23 @@ class AuthCubit extends Cubit { }, (users) { if (users.isEmpty) { emit(AuthFirstAppLaunch()); - return; + } else if (users.length == 1) { + return fetchUserCredsFromCache(users.first).then((result) { + result.fold((error) { + emit(AuthErrorState(error)); + return; + }, (creds) { + authenticate(creds).then((auth) { + auth.fold((error) { + emit(AuthErrorState(error)); + return; + }, (r) { + AuthenticatedState(user: users.first); + return; + }); + }); + }); + }); } emit(AuthCachedUsersRetrieved(cachedUsers: users)); }); diff --git a/lib/features/auth/repository/user_remote_repository.dart b/lib/features/auth/repository/user_remote_repository.dart index 8c470d9..6b34c6d 100644 --- a/lib/features/auth/repository/user_remote_repository.dart +++ b/lib/features/auth/repository/user_remote_repository.dart @@ -1,9 +1,9 @@ import 'package:academia/database/database.dart'; -import 'package:academia/utils/network/dio_client.dart'; +import 'package:academia/utils/network/network.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; -final class UserRemoteRepository { +final class UserRemoteRepository with DioErrorHandler { final DioClient _client = DioClient(); /// The function attempts to authenticate a users [credentials] with @@ -24,7 +24,7 @@ final class UserRemoteRepository { return left(response.data["error"] ?? response.statusMessage); } on DioException catch (de) { - return left(de.response?.data["error"] ?? de.response!.statusMessage); + return handleDioError(de); } catch (e) { return left("Please check your internet connection and try that again!"); } diff --git a/lib/features/auth/views/user_selection_page.dart b/lib/features/auth/views/user_selection_page.dart index 7f1f49b..d8a0713 100644 --- a/lib/features/auth/views/user_selection_page.dart +++ b/lib/features/auth/views/user_selection_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:icons_plus/icons_plus.dart'; +import 'package:skeletonizer/skeletonizer.dart'; import 'package:sliver_tools/sliver_tools.dart'; class UserSelectionPage extends StatefulWidget { @@ -77,11 +78,17 @@ class _UserSelectionPageState extends State { ), // Sliverlist + BlocBuilder( + buildWhen: (previous, current) { + if (current is AuthCachedUsersRetrieved) { + return true; + } + return false; + }, builder: (context, state) { final List users = (state as AuthCachedUsersRetrieved).cachedUsers; - return SliverList.builder( itemCount: users.length, itemBuilder: (context, index) { diff --git a/lib/features/auth/views/widgets/user_selection_tile.dart b/lib/features/auth/views/widgets/user_selection_tile.dart index b4a043c..a46abda 100644 --- a/lib/features/auth/views/widgets/user_selection_tile.dart +++ b/lib/features/auth/views/widgets/user_selection_tile.dart @@ -22,7 +22,6 @@ class UserSelectionTile extends StatefulWidget { } class _UserSelectionTileState extends State { - bool _isLoading = true; late AuthCubit _authCubit = BlocProvider.of(context); UserProfileData? profile; @@ -76,7 +75,9 @@ class _UserSelectionTileState extends State { onTap: () async { final result = await _authCubit.authenticate(creds!); result.fold((l) { - _showMessageDialog("Authentication Error", l); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(l), + )); }, (r) { if (!context.mounted) return; GoRouter.of(context) @@ -96,12 +97,9 @@ class _UserSelectionTileState extends State { BlocBuilder(builder: (context, state) { if (state is AuthLoadingState) { return const LinearProgressIndicator(); - } else if (state is AuthErrorState) { - return Text(state.message); } return Text( - profile?.bio ?? - "${widget.user.firstname} ${widget.user.othernames}", + profile?.bio ?? "No bio yet", ); }), trailing: const Icon(Bootstrap.arrow_right), diff --git a/lib/utils/network/dio_error_handler.dart b/lib/utils/network/dio_error_handler.dart new file mode 100644 index 0000000..867c740 --- /dev/null +++ b/lib/utils/network/dio_error_handler.dart @@ -0,0 +1,23 @@ +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; + +mixin DioErrorHandler { + Either handleDioError(DioException de) { + switch (de.type) { + case DioExceptionType.connectionError: + return left("Connection refused by server, please try again later"); + case DioExceptionType.connectionTimeout: + return left( + "Server took too long to respond, please retry later or check your connection"); + case DioExceptionType.receiveTimeout: + return left( + "Server did not send a response in time, please try again later."); + case DioExceptionType.sendTimeout: + return left("Sending request took too long, please try again later."); + default: + return left(de.response?.data["error"] ?? + de.response?.statusMessage ?? + "An unexpected error occurred. Please try again later."); + } + } +} diff --git a/lib/utils/network/network.dart b/lib/utils/network/network.dart index 90ecd02..45faf38 100644 --- a/lib/utils/network/network.dart +++ b/lib/utils/network/network.dart @@ -1 +1,2 @@ export 'dio_client.dart'; +export 'dio_error_handler.dart'; diff --git a/lib/utils/router/default_route.dart b/lib/utils/router/default_route.dart index 7b27079..96b42cf 100644 --- a/lib/utils/router/default_route.dart +++ b/lib/utils/router/default_route.dart @@ -1,8 +1,10 @@ import 'package:academia/features/auth/cubit/auth_cubit.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; import 'package:academia/features/features.dart'; +import 'package:academia/utils/router/router.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class DefaultRoute extends StatelessWidget { const DefaultRoute({super.key}); @@ -10,15 +12,16 @@ class DefaultRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - body: BlocBuilder( - builder: (context, state) { - if (state is AuthenticatedState) { - return const Layout(); - } else if (state is AuthCachedUsersRetrieved) { - return const UserSelectionPage(); + body: BlocListener( + listener: (context, state) { + if (state is AuthCachedUsersRetrieved) { + GoRouter.of(context) + .pushReplacementNamed(AcademiaRouter.userSelection); + } else if (state is AuthenticatedState) { + GoRouter.of(context).pushReplacementNamed(AcademiaRouter.home); } - return const OnboardingPage(); }, + child: const OnboardingPage(), ), ); } From 164dafd245473761ba1dfc4a6c085db649eabcfb Mon Sep 17 00:00:00 2001 From: Erick Date: Fri, 13 Dec 2024 15:20:00 +0300 Subject: [PATCH 7/7] chore: added connectivity plus for offline auths --- lib/features/auth/cubit/auth_cubit.dart | 9 +++++ lib/features/auth/views/login_page.dart | 51 +++++++++++++------------ pubspec.yaml | 1 + 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/lib/features/auth/cubit/auth_cubit.dart b/lib/features/auth/cubit/auth_cubit.dart index 6d4657c..feaf058 100644 --- a/lib/features/auth/cubit/auth_cubit.dart +++ b/lib/features/auth/cubit/auth_cubit.dart @@ -1,9 +1,12 @@ +import 'dart:async'; + import 'package:academia/database/database.dart'; import 'package:academia/features/auth/repository/user_repository.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:academia/features/auth/cubit/auth_states.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; class AuthCubit extends Cubit { final UserRepository _userRepository = UserRepository(); @@ -41,6 +44,12 @@ class AuthCubit extends Cubit { ); } + StreamSubscription> subscription = Connectivity() + .onConnectivityChanged + .listen((List result) { + // Received changes in available connectivity types! + }); + /// Authenticate performs authentication mechanisms with both verisafe /// and magnet to authenticate a user Future> authenticate( diff --git a/lib/features/auth/views/login_page.dart b/lib/features/auth/views/login_page.dart index cf91def..517fc50 100644 --- a/lib/features/auth/views/login_page.dart +++ b/lib/features/auth/views/login_page.dart @@ -35,25 +35,25 @@ class _LoginPageState extends State { bool validateForm() { return _formState.currentState!.validate(); } - - /// Shows a dialog with [title] and [content] - void _showMessageDialog(String title, String content) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(title), - content: Text(content), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: const Text("Ok"), - ), - ], - ), - ); - } + // + // /// Shows a dialog with [title] and [content] + // void _showMessageDialog(String title, String content) { + // showDialog( + // context: context, + // builder: (context) => AlertDialog( + // title: Text(title), + // content: Text(content), + // actions: [ + // TextButton( + // onPressed: () { + // context.pop(); + // }, + // child: const Text("Ok"), + // ), + // ], + // ), + // ); + // } @override Widget build(BuildContext context) { @@ -173,9 +173,11 @@ class _LoginPageState extends State { ? null : () async { if (!validateForm()) { - _showMessageDialog( - "Validation error", - "Please ensure that the form was well filled", + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + "Please ensure that the form was well filled"), + ), ); return; } @@ -191,9 +193,8 @@ class _LoginPageState extends State { ); result.fold((l) { - _showMessageDialog( - "Verification error", - l, + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l)), ); }, (r) { HapticFeedback.heavyImpact(); diff --git a/pubspec.yaml b/pubspec.yaml index 0581c04..606a5ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: skeletonizer: ^1.4.2 get_it: ^8.0.2 pretty_dio_logger: ^1.4.0 + connectivity_plus: ^6.1.1 dev_dependencies: flutter_test: