From cfc395e210ccb0dcf6746441b8236d97a38a1f6c Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Mon, 15 Apr 2024 12:02:34 +0300 Subject: [PATCH 1/8] Refactor HTTPInterceptor and add CustomSnackBar helper Refactor HTTPInterceptor to handle error responses and add a CustomSnackBar helper to display snack bars. This will improve error handling and user feedback in the application. --- lib/features/app_layout/bloc/home_cubit.dart | 21 ++-- lib/features/app_layout/view/app_layout.dart | 114 +++++++++++------- lib/features/auth/bloc/auth_cubit.dart | 40 +++--- .../auth/view/widgets/login/login_form.dart | 54 ++++++--- lib/features/devices/bloc/devices_cubit.dart | 63 +++------- lib/my_app.dart | 3 + lib/navigation/navigation_service.dart | 7 ++ lib/services/api/http_interceptor.dart | 83 ++++++++----- lib/services/api/network_exception.dart | 50 ++++---- lib/utils/helpers/snack_bar.dart | 40 ++++++ 10 files changed, 287 insertions(+), 188 deletions(-) create mode 100644 lib/navigation/navigation_service.dart create mode 100644 lib/utils/helpers/snack_bar.dart diff --git a/lib/features/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart index 8fee3a4..73bbe88 100644 --- a/lib/features/app_layout/bloc/home_cubit.dart +++ b/lib/features/app_layout/bloc/home_cubit.dart @@ -1,4 +1,3 @@ -import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -42,6 +41,14 @@ class HomeCubit extends Cubit { } } + @override + Future close() { + _instance = null; + selectedSpace = null; + selectedRoom = null; + return super.close(); + } + static HomeCubit get(context) => BlocProvider.of(context); List? spaces; @@ -124,8 +131,8 @@ class HomeCubit extends Cubit { selectedSpace = spaces!.first : null; emitSafe(GetSpacesLoaded(spaces!)); - } on DioException catch (e) { - emitSafe(GetSpacesError(ServerFailure.fromDioError(e).errMessage)); + } on ServerFailure catch (failure) { + emitSafe(GetSpacesError(failure.errMessage)); } } @@ -138,17 +145,13 @@ class HomeCubit extends Cubit { } else { emitSafe(GetSpaceRoomsError("No rooms found")); } - } on DioException catch (e) { - emitSafe(GetSpacesError(ServerFailure.fromDioError(e).errMessage)); + } on ServerFailure catch (failure) { + emitSafe(GetSpacesError(failure.errMessage)); } } /////////////////////////////////////// Nav /////////////////////////////////////// - static clear() { - pageIndex = 0; - } - static int pageIndex = 0; static Map> appBarActions = { diff --git a/lib/features/app_layout/view/app_layout.dart b/lib/features/app_layout/view/app_layout.dart index 6fbdb42..81296cc 100644 --- a/lib/features/app_layout/view/app_layout.dart +++ b/lib/features/app_layout/view/app_layout.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/app_layout/view/widgets/app_body.dart'; import 'package:syncrow_app/features/app_layout/view/widgets/default_app_bar.dart'; import 'package:syncrow_app/features/app_layout/view/widgets/default_nav_bar.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/auth/model/token.dart'; import 'package:syncrow_app/navigation/routing_constants.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; @@ -14,53 +17,74 @@ class AppLayout extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeCubit.getInstance(), - child: BlocConsumer( - listener: (context, state) { - if (state is GetSpacesError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errMessage), - ), - ); - Navigator.of(context) - .popUntil((route) => route.settings.name == Routes.authLogin); - } - }, - builder: (context, state) { - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: ColorsManager.primaryColor.withOpacity(0.5), - statusBarIconBrightness: Brightness.light, - ), - child: SafeArea( - child: Scaffold( - backgroundColor: ColorsManager.backgroundColor, - extendBodyBehindAppBar: true, - extendBody: true, - appBar: HomeCubit.getInstance().spaces != null - ? const DefaultAppBar() - : null, - body: const AppBody(), - bottomNavigationBar: const DefaultNavBar(), - // floatingActionButton: FloatingActionButton( - // onPressed: () { - // Navigator.push( - // context, - // CustomPageRoute( - // builder: (context) => - // const ThreeGangSwitchesView(), - // ), - // ); - // }, - // child: const Icon(Icons.arrow_forward_ios_sharp), - // ), - ), + return BlocConsumer( + listener: (context, state) { + if (state is AuthError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), ), ); - }, - ), + Navigator.of(context) + .popUntil((route) => route.settings.name == Routes.authLogin); + } + }, + builder: (context, state) { + return BlocProvider( + create: (context) => HomeCubit.getInstance(), + child: BlocConsumer( + listener: (context, state) async { + if (state is GetSpacesError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errMessage), + ), + ); + Navigator.of(context).popUntil( + (route) => route.settings.name == Routes.authLogin); + } + String? token = await const FlutterSecureStorage() + .read(key: Token.loginAccessTokenKey); + if (token == null) { + // ignore: use_build_context_synchronously + Navigator.of(context).popAndPushNamed(Routes.authLogin); + } + }, + builder: (context, state) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: ColorsManager.primaryColor.withOpacity(0.5), + statusBarIconBrightness: Brightness.light, + ), + child: SafeArea( + child: Scaffold( + backgroundColor: ColorsManager.backgroundColor, + extendBodyBehindAppBar: true, + extendBody: true, + appBar: HomeCubit.getInstance().spaces != null + ? const DefaultAppBar() + : null, + body: const AppBody(), + bottomNavigationBar: const DefaultNavBar(), + // floatingActionButton: FloatingActionButton( + // onPressed: () { + // Navigator.push( + // context, + // CustomPageRoute( + // builder: (context) => + // const ThreeGangSwitchesView(), + // ), + // ); + // }, + // child: const Icon(Icons.arrow_forward_ios_sharp), + // ), + ), + ), + ); + }, + ), + ); + }, ); } } diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index d966e90..2ef1173 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -1,13 +1,13 @@ -import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/auth/model/login_with_email_model.dart'; import 'package:syncrow_app/features/auth/model/token.dart'; import 'package:syncrow_app/features/auth/model/user_model.dart'; +import 'package:syncrow_app/navigation/navigation_service.dart'; +import 'package:syncrow_app/navigation/routing_constants.dart'; import 'package:syncrow_app/services/api/authentication_api.dart'; import 'package:syncrow_app/services/api/network_exception.dart'; @@ -43,7 +43,7 @@ class AuthCubit extends Cubit { static Token token = Token.emptyConstructor(); login() async { - emit(AuthLoading()); + emit(AuthLoginLoading()); try { token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( @@ -60,24 +60,27 @@ class AuthCubit extends Cubit { key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString()); user = UserModel.fromToken(token); - emit(AuthSuccess()); + emailController.clear(); + passwordController.clear(); + emit(AuthLoginSuccess()); } else { - emit(AuthError('Something went wrong')); + emit(AuthLoginError(message: 'Something went wrong')); } - } on DioException catch (e) { - emit(AuthError(ServerFailure.fromDioError(e).toString())); + } on ServerFailure catch (failure) { + emit(AuthError(message: failure.errMessage)); } } logout() async { - emit(AuthLoading()); + emit(AuthLogoutLoading()); try { FlutterSecureStorage storage = const FlutterSecureStorage(); await storage.delete(key: Token.loginAccessTokenKey); - HomeCubit.clear(); - emit(AuthLoggedOut()); - } on DioException catch (e) { - emit(AuthError(ServerFailure.fromDioError(e).errMessage)); + NavigationService.navigatorKey.currentState! + .popAndPushNamed(Routes.authLogin); + emit(AuthLogoutSuccess()); + } on ServerFailure catch (failure) { + emit(AuthError(message: failure.errMessage)); } } @@ -87,7 +90,7 @@ class AuthCubit extends Cubit { await const FlutterSecureStorage().read(key: Token.loginAccessTokenKey); if (value == null) { - emit(AuthTokenError("Token not found")); + emit(AuthTokenError(message: "Token not found")); return; } @@ -100,10 +103,17 @@ class AuthCubit extends Cubit { if (currentTime < exp) { emit(AuthTokenSuccess()); } else { - emit(AuthTokenError("Token expired")); + emit(AuthTokenError(message: "Token expired")); } } else { - emit(AuthTokenError("Something went wrong")); + emit(AuthTokenError(message: "Something went wrong")); } } + + static void logUserOut() async { + user = null; + token = Token.emptyConstructor(); + FlutterSecureStorage storage = const FlutterSecureStorage(); + await storage.delete(key: Token.loginAccessTokenKey); + } } diff --git a/lib/features/auth/view/widgets/login/login_form.dart b/lib/features/auth/view/widgets/login/login_form.dart index ed76b7b..a1f1792 100644 --- a/lib/features/auth/view/widgets/login/login_form.dart +++ b/lib/features/auth/view/widgets/login/login_form.dart @@ -32,21 +32,25 @@ class LoginForm extends StatelessWidget { validator: (value) { if (state is! AuthTokenError) { if (value != null) { - if (value.isEmpty) { + if (value.isNotEmpty) { + if (RegExp( + r'^[a-zA-Z0-9._-]+@{1}[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$') + .hasMatch(value)) { + return null; + } else { + return 'Please enter a valid email'; + } + } else { return 'Please enter your email'; } - //Regex for email validation - if (!RegExp( - r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$') - .hasMatch(value)) { - return 'Please enter a valid email'; - } + } else { + return 'Please enter your email'; } } return null; }, onTapOutside: (event) { - formKey.currentState!.validate(); + FocusScope.of(context).unfocus(); }, decoration: defaultInputDecoration(context, hint: "Example@email.com"), @@ -62,9 +66,19 @@ class LoginForm extends StatelessWidget { if (state is! AuthTokenError) { if (value != null) { if (value.isNotEmpty) { - if (value.length < 6) { - return 'Password must be at least 8 characters'; - } + return null; + //TODO: uncomment this when the backend is ready + // if (value.length > 8) { + // if (RegExp( + // r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$') + // .hasMatch(value)) { + // return null; + // } else { + // return 'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character'; + // } + // } else { + // return 'Password must be at least 8 characters'; + // } } else { return 'Please enter your password'; } @@ -73,7 +87,7 @@ class LoginForm extends StatelessWidget { return null; }, onTapOutside: (event) { - formKey.currentState!.validate(); + FocusScope.of(context).unfocus(); }, obscureText: !AuthCubit.get(context).isPasswordVisible, decoration: defaultInputDecoration(context, @@ -88,8 +102,16 @@ class LoginForm extends StatelessWidget { children: [ Expanded( child: DefaultButton( - isDone: state is AuthSuccess, + isDone: state is AuthLoginSuccess, isLoading: state is AuthLoading, + // enabled: AuthCubit.get(context) + // .emailController + // .text + // .isNotEmpty && + // AuthCubit.get(context) + // .passwordController + // .text + // .isNotEmpty, customButtonStyle: ButtonStyle( backgroundColor: MaterialStateProperty.all( Colors.black.withOpacity(.25), @@ -103,8 +125,10 @@ class LoginForm extends StatelessWidget { ), onPressed: () { if (formKey.currentState!.validate()) { - AuthCubit.get(context).login(); - FocusScope.of(context).unfocus(); + if (state is! AuthLoginLoading) { + AuthCubit.get(context).login(); + FocusScope.of(context).unfocus(); + } } }, ), diff --git a/lib/features/devices/bloc/devices_cubit.dart b/lib/features/devices/bloc/devices_cubit.dart index 7b17513..d976f59 100644 --- a/lib/features/devices/bloc/devices_cubit.dart +++ b/lib/features/devices/bloc/devices_cubit.dart @@ -31,12 +31,9 @@ class DevicesCubit extends Cubit { } } } - bool _isClosed = false; static DevicesCubit? _instance; static DevicesCubit getInstance() { - print('device cubit instance found : ${_instance != null}'); - print('selected space : ${HomeCubit.getInstance().selectedSpace != null}'); return _instance ??= DevicesCubit._(); } @@ -44,7 +41,6 @@ class DevicesCubit extends Cubit { @override Future close() { - _isClosed = true; _instance = null; return super.close(); } @@ -272,56 +268,37 @@ class DevicesCubit extends Cubit { emitSafe(DeviceControlError('Failed to control the device')); } }); - } on DioException catch (e) { - emitSafe(DeviceControlError(ServerFailure.fromDioError(e).errMessage)); + } on ServerFailure catch (failure) { + emitSafe(DeviceControlError(failure.errMessage)); } } fetchGroups(int spaceId) async { - if (_isClosed) return; - - try { - emitSafe(DevicesCategoriesLoading()); - allCategories = await DevicesAPI.fetchGroups(spaceId); - emitSafe(DevicesCategoriesSuccess()); - } on DioException catch (error) { - emitSafe( - DevicesCategoriesError(ServerFailure.fromDioError(error).errMessage), - ); - } + emitSafe(DevicesCategoriesLoading()); + allCategories = await DevicesAPI.fetchGroups(spaceId); + emitSafe(DevicesCategoriesSuccess()); } fetchDevicesByRoomId(int? roomId) async { - if (_isClosed) return; if (roomId == null) return; - try { - emitSafe(GetDevicesLoading()); - int roomIndex = HomeCubit.getInstance() - .selectedSpace! - .rooms! - .indexWhere((element) => element.id == roomId); - HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices = - await SpacesAPI.getDevicesByRoomId(roomId); - //get status for each device - for (var device in HomeCubit.getInstance() - .selectedSpace! - .rooms![roomIndex] - .devices!) { - getDevicesStatues(device.id!, roomIndex); - } - - emitSafe(GetDevicesSuccess()); - } on DioException catch (error) { - emitSafe( - GetDevicesError(ServerFailure.fromDioError(error).errMessage), - ); + emitSafe(GetDevicesLoading()); + int roomIndex = HomeCubit.getInstance() + .selectedSpace! + .rooms! + .indexWhere((element) => element.id == roomId); + HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices = + await SpacesAPI.getDevicesByRoomId(roomId); + //get status for each device + for (var device + in HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices!) { + getDevicesStatues(device.id!, roomIndex); } + + emitSafe(GetDevicesSuccess()); } getDevicesStatues(String deviceId, int roomIndex, {String? code}) async { - if (_isClosed) return; - try { emitSafe(GetDeviceStatusLoading(code: code)); int deviceIndex = HomeCubit.getInstance() @@ -345,9 +322,9 @@ class DevicesCubit extends Cubit { .devices![deviceIndex] .status = statuses; emitSafe(GetDeviceStatusSuccess(code: code)); - } on DioException catch (error) { + } on ServerFailure catch (failure) { emitSafe( - GetDeviceStatusError(ServerFailure.fromDioError(error).errMessage), + GetDeviceStatusError(failure.errMessage), ); } } diff --git a/lib/my_app.dart b/lib/my_app.dart index 1ec3b08..46ae999 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/navigation/navigation_service.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; import 'package:syncrow_app/utils/resource_manager/constants.dart'; import 'package:syncrow_app/utils/resource_manager/theme_manager.dart'; @@ -21,6 +22,8 @@ class MyApp extends StatelessWidget { return BlocProvider( create: (context) => AuthCubit(), child: MaterialApp( + navigatorKey: NavigationService.navigatorKey, + scaffoldMessengerKey: NavigationService.snackbarKey, debugShowCheckedModeBanner: false, color: ColorsManager.primaryColor, title: 'Syncrow App', diff --git a/lib/navigation/navigation_service.dart b/lib/navigation/navigation_service.dart new file mode 100644 index 0000000..c0b1216 --- /dev/null +++ b/lib/navigation/navigation_service.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class NavigationService { + static GlobalKey navigatorKey = GlobalKey(); + static GlobalKey? snackbarKey = + GlobalKey(); +} diff --git a/lib/services/api/http_interceptor.dart b/lib/services/api/http_interceptor.dart index 3460bc4..38b43cb 100644 --- a/lib/services/api/http_interceptor.dart +++ b/lib/services/api/http_interceptor.dart @@ -1,46 +1,63 @@ import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/auth/model/token.dart'; +import 'package:syncrow_app/navigation/navigation_service.dart'; +import 'dart:async'; +import 'dart:developer' as developer; +import 'package:syncrow_app/services/api/network_exception.dart'; +import 'package:syncrow_app/utils/helpers/snack_bar.dart'; class HTTPInterceptor extends InterceptorsWrapper { - // @override - // void onResponse(Response response, ResponseInterceptorHandler handler) async { - // // Pass the response to the next interceptor or response handler. - // return handler.next(response); - // } - // + @override + @override + void onResponse(Response response, ResponseInterceptorHandler handler) async { + return handler.next(response); + } + @override void onRequest( RequestOptions options, RequestInterceptorHandler handler) async { var storage = const FlutterSecureStorage(); var token = await storage.read(key: Token.loginAccessTokenKey); - - options.headers['Authorization'] = 'Bearer $token'; + // options.headers['Authorization'] = 'Bearer $token'; + options.headers['Authorization'] = 'Bearer ${'${token!}123'}'; super.onRequest(options, handler); } -// -// @override -// void onError(DioException err, ErrorInterceptorHandler handler) async { -// // TODO: Implement error handling logic. -// // This method is called when an error occurs during a request. -// super.onError(err, handler); -// } -// -// /// Validates the response and returns true if it is successful (status code 2xx). -// Future validateResponse(Response response) async { -// if (response.statusCode != null) { -// if (response.statusCode! >= 200 && response.statusCode! < 300) { -// // If the response status code is within the successful range (2xx), -// // return true indicating a successful response. -// return true; -// } else { -// // If the response status code is not within the successful range (2xx), -// // return false indicating an unsuccessful response. -// return false; -// } -// } else { -// // If the response status code is null, return false indicating an unsuccessful response. -// return false; -// } -// } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) async { + developer.log('Error Message: ${err.message}'); + developer.log('Error res Code: ${err.response?.statusCode}'); + developer.log('Error res Data: ${err.response?.data}'); + developer.log('Error res status message: ${err.response?.statusMessage}'); + + ServerFailure failure = ServerFailure.fromDioError(err); + CustomSnackBar.displaySnackBar(failure.toString()); + var storage = const FlutterSecureStorage(); + var token = await storage.read(key: Token.loginAccessTokenKey); + if (err.response?.statusCode == 401 && token != null) { + await AuthCubit.get(NavigationService.navigatorKey.currentContext!) + .logout(); + super.onError(err, handler); + } + } + + /// Validates the response and returns true if it is successful (status code 2xx). + Future validateResponse(Response response) async { + if (response.statusCode != null) { + if (response.statusCode! >= 200 && response.statusCode! < 300) { + // If the response status code is within the successful range (2xx), + // return true indicating a successful response. + return true; + } else { + // If the response status code is not within the successful range (2xx), + // return false indicating an unsuccessful response. + return false; + } + } else { + // If the response status code is null, return false indicating an unsuccessful response. + return false; + } + } } diff --git a/lib/services/api/network_exception.dart b/lib/services/api/network_exception.dart index f8940de..2b61e28 100644 --- a/lib/services/api/network_exception.dart +++ b/lib/services/api/network_exception.dart @@ -32,9 +32,7 @@ class ServerFailure extends Failure { // var document = parser.parse(dioError.response!.data.toString()); // var message = document.body!.text; return ServerFailure.fromResponse(dioError.response!.statusCode!, - dioError.response!.data['message'].toString() - // message - ); + dioError.response!.data['message']); } case DioExceptionType.cancel: return ServerFailure("The request to ApiServer was canceled"); @@ -50,31 +48,27 @@ class ServerFailure extends Failure { } } - factory ServerFailure.fromResponse(int statusCode, dynamic response) { - if (statusCode == 401 || statusCode == 403) { - return ServerFailure(response); - } else if (statusCode == 400) { - //response is list of errors - List errors = []; - response.forEach((element) { - errors.add(element); - }); - return ServerFailure(errors.join('\n')); - } else if (statusCode == 404) { - return ServerFailure("Your request not found, Please try later!"); - } else if (statusCode == 500) { - return ServerFailure(response); - } else { - return ServerFailure("Opps there was an Error, Please try again!"); + factory ServerFailure.fromResponse(int? statusCode, dynamic response) { + switch (statusCode) { + case 401: + case 403: + return ServerFailure(response); + case 400: + List errors = []; + if (response['message'] is List) { + for (var error in response['message']) { + errors.add(error); + } + } else { + errors.add(response['message']); + } + return ServerFailure(errors.join('\n')); + case 404: + return ServerFailure("Your request not found, Please try later!"); + case 500: + return ServerFailure(response); + default: + return ServerFailure("Opps there was an Error, Please try again!"); } } } - -class ResponseFailure extends Failure { - ResponseFailure(super.errMessage); - - @override - String toString() { - return errMessage; - } -} diff --git a/lib/utils/helpers/snack_bar.dart b/lib/utils/helpers/snack_bar.dart new file mode 100644 index 0000000..61c99ee --- /dev/null +++ b/lib/utils/helpers/snack_bar.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_app/navigation/navigation_service.dart'; + +class CustomSnackBar { + static displaySnackBar(String message) { + final key = NavigationService.snackbarKey; + if (key != null) { + final snackBar = SnackBar(content: Text(message)); + key.currentState?.clearSnackBars(); + key.currentState?.showSnackBar(snackBar); + } + } + + static greenSnackBar(String message) { + final key = NavigationService.snackbarKey; + BuildContext? currentContext = key?.currentContext; + if (key != null && currentContext != null) { + final snackBar = SnackBar( + padding: const EdgeInsets.all(16), + backgroundColor: Colors.green, + content: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Icon( + Icons.check_circle, + color: Colors.white, + size: 32, + ), + const SizedBox( + width: 8, + ), + Text( + message, + style: Theme.of(currentContext).textTheme.bodySmall!.copyWith( + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.green), + ) + ]), + ); + key.currentState?.showSnackBar(snackBar); + } + } +} From 80d424f114b665c301f561c5aa4b69ff8f32027f Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Mon, 15 Apr 2024 12:03:25 +0300 Subject: [PATCH 2/8] Refactor code for consistency and readability - Removed unused imports and commented-out code - Updated class inheritance for AuthState subclasses - Reorganized code structure for better readability - Cleaned up debug print statements and replaced with dart:developer logs --- .../app_layout/view/widgets/app_body.dart | 34 +++---- .../view/widgets/default_nav_bar.dart | 2 - lib/features/auth/bloc/auth_state.dart | 36 +++++--- .../auth/view/widgets/login/login_view.dart | 6 +- .../widgets/ACs/ac_interface_temp_unit.dart | 1 - .../widgets/smart_door/door_interface.dart | 92 +++++++++++++++++++ .../shared_widgets/custom_switch.dart | 24 ++--- lib/services/api/authentication_api.dart | 2 +- lib/services/api/devices_api.dart | 1 + lib/services/api/http_service.dart | 32 +++---- lib/services/api/my_http_overrides.dart | 3 +- lib/utils/helpers/misc_string_helpers.dart | 5 +- lib/utils/resource_manager/theme_manager.dart | 1 - 13 files changed, 162 insertions(+), 77 deletions(-) create mode 100644 lib/features/devices/view/widgets/smart_door/door_interface.dart diff --git a/lib/features/app_layout/view/widgets/app_body.dart b/lib/features/app_layout/view/widgets/app_body.dart index 8e3fa72..cc1b9dc 100644 --- a/lib/features/app_layout/view/widgets/app_body.dart +++ b/lib/features/app_layout/view/widgets/app_body.dart @@ -12,7 +12,16 @@ class AppBody extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listener: (context, state) { + if (state is GetSpacesError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errMessage), + ), + ); + } + }, builder: (context, state) { return Container( width: MediaQuery.sizeOf(context).width, @@ -26,24 +35,11 @@ class AppBody extends StatelessWidget { opacity: 0.4, ), ), - child: BlocConsumer( - listener: (context, state) { - if (state is GetSpacesError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errMessage), - ), - ); - } - }, - builder: (context, state) { - return state is! GetSpacesLoading - ? state is! GetSpaceRoomsLoading - ? HomeCubit.getInstance().pages[HomeCubit.pageIndex] - : const Center(child: CircularProgressIndicator()) - : const Center(child: CircularProgressIndicator()); - }, - ), + child: state is! GetSpacesLoading + ? state is! GetSpaceRoomsLoading + ? HomeCubit.getInstance().pages[HomeCubit.pageIndex] + : const Center(child: CircularProgressIndicator()) + : const Center(child: CircularProgressIndicator()), ); }, ); diff --git a/lib/features/app_layout/view/widgets/default_nav_bar.dart b/lib/features/app_layout/view/widgets/default_nav_bar.dart index c5140b1..178faa5 100644 --- a/lib/features/app_layout/view/widgets/default_nav_bar.dart +++ b/lib/features/app_layout/view/widgets/default_nav_bar.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; -import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; -import 'package:syncrow_app/generated/assets.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; import 'package:syncrow_app/utils/resource_manager/constants.dart'; diff --git a/lib/features/auth/bloc/auth_state.dart b/lib/features/auth/bloc/auth_state.dart index e84791f..38c7180 100644 --- a/lib/features/auth/bloc/auth_state.dart +++ b/lib/features/auth/bloc/auth_state.dart @@ -4,30 +4,44 @@ abstract class AuthState {} class AuthInitial extends AuthState {} +//base states class AuthLoading extends AuthState {} class AuthError extends AuthState { final String message; - - AuthError(this.message) { - debugPrint(message); - } + String? code; + AuthError({required this.message, this.code}); } class AuthSuccess extends AuthState {} -class AuthLoggedOut extends AuthState {} +//user log states +class AuthLoginLoading extends AuthLoading {} +class AuthLoginSuccess extends AuthSuccess {} + +class AuthLoginError extends AuthError { + AuthLoginError({required super.message, super.code}); +} + +class AuthLogoutLoading extends AuthLoading {} + +class AuthLogoutSuccess extends AuthSuccess {} + +class AuthLogoutError extends AuthError { + AuthLogoutError({required super.message, super.code}); +} + +// UI states class AuthPasswordVisibilityChanged extends AuthState {} class AuthAgreeToTermsChanged extends AuthState {} -class AuthTokenLoading extends AuthState {} +//token states +class AuthTokenLoading extends AuthLoading {} -class AuthTokenSuccess extends AuthState {} +class AuthTokenSuccess extends AuthSuccess {} -class AuthTokenError extends AuthState { - final String message; - - AuthTokenError(this.message); +class AuthTokenError extends AuthError { + AuthTokenError({required super.message, super.code}); } diff --git a/lib/features/auth/view/widgets/login/login_view.dart b/lib/features/auth/view/widgets/login/login_view.dart index 06713c2..ca0e61d 100644 --- a/lib/features/auth/view/widgets/login/login_view.dart +++ b/lib/features/auth/view/widgets/login/login_view.dart @@ -18,8 +18,6 @@ class LoginView extends StatelessWidget { @override Widget build(BuildContext context) { - //TODO move to strings manager - return BlocConsumer( listener: (context, state) { if (state is AuthError) { @@ -28,8 +26,8 @@ class LoginView extends StatelessWidget { content: Text(state.message), ), ); - } else if (state is AuthSuccess) { - Navigator.popAndPushNamed(context, Routes.homeRoute); + } else if (state is AuthLoginSuccess) { + Navigator.pushNamed(context, Routes.homeRoute); } }, builder: (context, state) { diff --git a/lib/features/devices/view/widgets/ACs/ac_interface_temp_unit.dart b/lib/features/devices/view/widgets/ACs/ac_interface_temp_unit.dart index dab1d71..f417936 100644 --- a/lib/features/devices/view/widgets/ACs/ac_interface_temp_unit.dart +++ b/lib/features/devices/view/widgets/ACs/ac_interface_temp_unit.dart @@ -11,7 +11,6 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dar import 'package:syncrow_app/generated/assets.dart'; import 'package:syncrow_app/utils/context_extension.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; -import 'package:syncrow_app/utils/resource_manager/constants.dart'; import 'package:syncrow_app/utils/resource_manager/font_manager.dart'; class AcInterfaceTempUnit extends StatelessWidget { diff --git a/lib/features/devices/view/widgets/smart_door/door_interface.dart b/lib/features/devices/view/widgets/smart_door/door_interface.dart new file mode 100644 index 0000000..cf2a2ff --- /dev/null +++ b/lib/features/devices/view/widgets/smart_door/door_interface.dart @@ -0,0 +1,92 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter/services.dart'; +// import 'package:flutter_svg/flutter_svg.dart'; +// import 'package:syncrow_app/features/devices/model/device_model.dart'; +// import 'package:syncrow_app/features/shared_widgets/default_container.dart'; +// import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart'; +// import 'package:syncrow_app/generated/assets.dart'; +// import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; +// import 'package:syncrow_app/utils/resource_manager/constants.dart'; +// import 'package:syncrow_app/utils/resource_manager/font_manager.dart'; + +// class DoorInterface extends StatelessWidget { +// const DoorInterface({super.key, required this.doorlock}); + +// final DeviceModel doorlock; + +// @override +// Widget build(BuildContext context) { +// return AnnotatedRegion( +// value: SystemUiOverlayStyle( +// statusBarColor: ColorsManager.primaryColor.withOpacity(0.5), +// statusBarIconBrightness: Brightness.light, +// ), +// child: SafeArea( +// child: Scaffold( +// backgroundColor: ColorsManager.backgroundColor, +// extendBodyBehindAppBar: true, +// extendBody: true, +// appBar: AppBar( +// backgroundColor: Colors.transparent, +// centerTitle: true, +// title: BodyLarge( +// text: doorlock.name ?? "", +// fontColor: ColorsManager.primaryColor, +// fontWeight: FontsManager.bold, +// ), +// ), +// body: Container( +// width: MediaQuery.sizeOf(context).width, +// height: MediaQuery.sizeOf(context).height, +// decoration: const BoxDecoration( +// image: DecorationImage( +// image: AssetImage( +// Assets.imagesBackground, +// ), +// fit: BoxFit.cover, +// opacity: 0.4, +// ), +// ), +// child: Padding( +// padding: EdgeInsets.only( +// top: Constants.appBarHeight, +// left: Constants.defaultPadding, +// right: Constants.defaultPadding, +// bottom: Constants.bottomNavBarHeight, +// ), +// child: Column( +// children: [ +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceBetween, +// children: [ +// SvgPicture.asset(Assets.iconsWifi), +// SvgPicture.asset(Assets.iconsBatteryIndicator), +// ], +// ), +// SizedBox.square( +// dimension: 255, +// ), +// SizedBox( +// height: 80, +// ), +// GridView.builder( +// shrinkWrap: true, +// gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( +// crossAxisCount: 2, +// mainAxisSpacing: 10, +// crossAxisSpacing: 10, +// ), +// itemBuilder: (context, index) => DefaultContainer( +// child: SizedBox.shrink(), +// width: 175, +// height: 100, +// ), +// ), +// ], +// )), +// ), +// ), +// ), +// ); +// } +// } diff --git a/lib/features/shared_widgets/custom_switch.dart b/lib/features/shared_widgets/custom_switch.dart index cdb9eab..e83a69e 100644 --- a/lib/features/shared_widgets/custom_switch.dart +++ b/lib/features/shared_widgets/custom_switch.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/devices/bloc/devices_cubit.dart'; -import 'package:syncrow_app/features/devices/model/device_category_model.dart'; import 'package:syncrow_app/features/devices/model/device_control_model.dart'; import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; @@ -42,13 +41,10 @@ class CustomSwitch extends StatelessWidget { width: 45.0, height: 28.0, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24.0), - color: status != null - ? status - ? ColorsManager.primaryColor - : const Color(0xFFD9D9D9) - : const Color(0xFFD9D9D9), - ), + borderRadius: BorderRadius.circular(24.0), + color: status + ? ColorsManager.primaryColor + : const Color(0xFFD9D9D9)), child: Center( child: Container( width: 40.0, @@ -60,20 +56,16 @@ class CustomSwitch extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(2.0), child: Container( - alignment: status != null - ? status - ? Alignment.centerRight - : Alignment.centerLeft + alignment: status + ? Alignment.centerRight : Alignment.centerLeft, child: Container( width: 20.0, height: 20.0, decoration: BoxDecoration( shape: BoxShape.circle, - color: status != null - ? status - ? ColorsManager.primaryColor - : Colors.grey + color: status + ? ColorsManager.primaryColor : Colors.grey, ), ), diff --git a/lib/services/api/authentication_api.dart b/lib/services/api/authentication_api.dart index 70f502c..f83b99c 100644 --- a/lib/services/api/authentication_api.dart +++ b/lib/services/api/authentication_api.dart @@ -22,7 +22,7 @@ class AuthenticationAPI { body: model.toJson(), showServerMessage: false, expectedResponseModel: (json) => Token.fromJson(json['data'])); - // debugPrint("response: $response"); + // developer.log("response: $response"); return response; } catch (e) { rethrow; diff --git a/lib/services/api/devices_api.dart b/lib/services/api/devices_api.dart index c8c04c4..8968bad 100644 --- a/lib/services/api/devices_api.dart +++ b/lib/services/api/devices_api.dart @@ -10,6 +10,7 @@ class DevicesAPI { DeviceControlModel controlModel) async { // print( // 'contoling [${controlModel.deviceId}] with code [${controlModel.code}] and value [${controlModel.value}'); + final response = await _httpService.post( path: ApiEndpoints.control, body: controlModel.toJson(), diff --git a/lib/services/api/http_service.dart b/lib/services/api/http_service.dart index 9551d4a..690c11c 100644 --- a/lib/services/api/http_service.dart +++ b/lib/services/api/http_service.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; +import 'dart:developer' as developer; import 'package:syncrow_app/services/api/api_links_endpoints.dart'; import 'package:syncrow_app/services/api/http_interceptor.dart'; import 'package:syncrow_app/services/locator.dart'; @@ -39,9 +39,6 @@ class HTTPService { ); return expectedResponseModel(response.data); } catch (error) { - debugPrint("******* Error"); - debugPrint(error.toString()); - debugPrint("******* Error End"); rethrow; } } @@ -60,8 +57,8 @@ class HTTPService { queryParameters: queryParameters, options: options, ); - // debugPrint("status code is ${response.statusCode}"); - // debugPrint("response data is ${response.data}"); + // developer.log("status code is ${response.statusCode}"); + // developer.log("response data is ${response.data}"); return expectedResponseModel(response.data); } catch (error) { rethrow; @@ -79,11 +76,11 @@ class HTTPService { data: body, queryParameters: queryParameters, ); - debugPrint("status code is ${response.statusCode}"); + developer.log("status code is ${response.statusCode}"); return expectedResponseModel(response.data); } catch (error) { - debugPrint("******* Error"); - debugPrint(error.toString()); + developer.log("******* Error"); + developer.log(error.toString()); rethrow; } } @@ -94,26 +91,25 @@ class HTTPService { Map? queryParameters, required T Function(dynamic) expectedResponseModel}) async { try { - debugPrint("download begins"); + developer.log("download begins"); final response = await client.download( path, savePath, onReceiveProgress: (current, total) { - debugPrint("current = $current, while total = $total"); + developer.log("current = $current, while total = $total"); }, ); - debugPrint("download ends"); + developer.log("download ends"); return expectedResponseModel(response.data); // return expectedResponseModel(response.data); } catch (error) { - debugPrint("******* Error"); - debugPrint("download error"); - debugPrint(error.toString()); + developer.log("******* Error"); + developer.log("download error"); + developer.log(error.toString()); rethrow; } } - //delete Future delete({ required String path, Map? queryParameters, @@ -127,8 +123,8 @@ class HTTPService { ); return expectedResponseModel(response.data); } catch (error) { - debugPrint("******* Error"); - debugPrint(error.toString()); + developer.log("******* Error"); + developer.log(error.toString()); rethrow; } } diff --git a/lib/services/api/my_http_overrides.dart b/lib/services/api/my_http_overrides.dart index 512523c..3707be3 100644 --- a/lib/services/api/my_http_overrides.dart +++ b/lib/services/api/my_http_overrides.dart @@ -5,6 +5,7 @@ class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext? context) { return super.createHttpClient(context) - ..badCertificateCallback = (X509Certificate cert, String host, int port) => true; + ..badCertificateCallback = + (X509Certificate cert, String host, int port) => true; } } diff --git a/lib/utils/helpers/misc_string_helpers.dart b/lib/utils/helpers/misc_string_helpers.dart index 030515a..0b9a2c5 100644 --- a/lib/utils/helpers/misc_string_helpers.dart +++ b/lib/utils/helpers/misc_string_helpers.dart @@ -1,13 +1,12 @@ +import 'dart:developer' as developer; import 'dart:io'; -import 'package:flutter/material.dart'; - class StringHelpers { // TODO ( to Mohammad Salameh): convert this to extension method static String enhanceFileName(File file) { var fileName = " "; final filePath = file.path; - debugPrint(filePath); + developer.log(filePath); final fileStringArray = filePath.split("/"); fileName = fileStringArray.last; if (fileName.length > 20) { diff --git a/lib/utils/resource_manager/theme_manager.dart b/lib/utils/resource_manager/theme_manager.dart index 56cd784..fdde064 100644 --- a/lib/utils/resource_manager/theme_manager.dart +++ b/lib/utils/resource_manager/theme_manager.dart @@ -207,7 +207,6 @@ abstract class ThemeManager { ), ///card theme - //TODO edit card theme cardTheme: const CardTheme( elevation: 0, shape: RoundedRectangleBorder( From dd90a2133f2b5728969bb67639f9de022821e99f Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Mon, 15 Apr 2024 15:44:32 +0300 Subject: [PATCH 3/8] removed snackbar calling in any place in the app except in the intercepter to improve readability and reduce duplication of error message display logic. --- lib/features/app_layout/view/app_layout.dart | 20 +++++----- .../app_layout/view/widgets/app_body.dart | 10 ++--- .../auth/view/widgets/login/login_form.dart | 16 +++----- .../auth/view/widgets/login/login_view.dart | 10 ++--- .../view/widgets/ACs/ac_interface.dart | 10 ++--- .../view/widgets/devices_view_body.dart | 38 +++++++++---------- 6 files changed, 50 insertions(+), 54 deletions(-) diff --git a/lib/features/app_layout/view/app_layout.dart b/lib/features/app_layout/view/app_layout.dart index 81296cc..45a789c 100644 --- a/lib/features/app_layout/view/app_layout.dart +++ b/lib/features/app_layout/view/app_layout.dart @@ -20,11 +20,11 @@ class AppLayout extends StatelessWidget { return BlocConsumer( listener: (context, state) { if (state is AuthError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.message), + // ), + // ); Navigator.of(context) .popUntil((route) => route.settings.name == Routes.authLogin); } @@ -35,11 +35,11 @@ class AppLayout extends StatelessWidget { child: BlocConsumer( listener: (context, state) async { if (state is GetSpacesError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errMessage), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.errMessage), + // ), + // ); Navigator.of(context).popUntil( (route) => route.settings.name == Routes.authLogin); } diff --git a/lib/features/app_layout/view/widgets/app_body.dart b/lib/features/app_layout/view/widgets/app_body.dart index cc1b9dc..6e80454 100644 --- a/lib/features/app_layout/view/widgets/app_body.dart +++ b/lib/features/app_layout/view/widgets/app_body.dart @@ -15,11 +15,11 @@ class AppBody extends StatelessWidget { return BlocConsumer( listener: (context, state) { if (state is GetSpacesError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errMessage), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.errMessage), + // ), + // ); } }, builder: (context, state) { diff --git a/lib/features/auth/view/widgets/login/login_form.dart b/lib/features/auth/view/widgets/login/login_form.dart index a1f1792..4fe33ee 100644 --- a/lib/features/auth/view/widgets/login/login_form.dart +++ b/lib/features/auth/view/widgets/login/login_form.dart @@ -104,14 +104,6 @@ class LoginForm extends StatelessWidget { child: DefaultButton( isDone: state is AuthLoginSuccess, isLoading: state is AuthLoading, - // enabled: AuthCubit.get(context) - // .emailController - // .text - // .isNotEmpty && - // AuthCubit.get(context) - // .passwordController - // .text - // .isNotEmpty, customButtonStyle: ButtonStyle( backgroundColor: MaterialStateProperty.all( Colors.black.withOpacity(.25), @@ -124,10 +116,14 @@ class LoginForm extends StatelessWidget { 'Login', ), onPressed: () { - if (formKey.currentState!.validate()) { - if (state is! AuthLoginLoading) { + bool isValid = formKey.currentState!.validate(); + if (isValid) { + if ((state is! AuthLoading)) { + print('login'); AuthCubit.get(context).login(); FocusScope.of(context).unfocus(); + } else { + formKey.currentState!.save(); } } }, diff --git a/lib/features/auth/view/widgets/login/login_view.dart b/lib/features/auth/view/widgets/login/login_view.dart index ca0e61d..29e7920 100644 --- a/lib/features/auth/view/widgets/login/login_view.dart +++ b/lib/features/auth/view/widgets/login/login_view.dart @@ -21,11 +21,11 @@ class LoginView extends StatelessWidget { return BlocConsumer( listener: (context, state) { if (state is AuthError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.message), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.message), + // ), + // ); } else if (state is AuthLoginSuccess) { Navigator.pushNamed(context, Routes.homeRoute); } diff --git a/lib/features/devices/view/widgets/ACs/ac_interface.dart b/lib/features/devices/view/widgets/ACs/ac_interface.dart index 23c45f7..95a164a 100644 --- a/lib/features/devices/view/widgets/ACs/ac_interface.dart +++ b/lib/features/devices/view/widgets/ACs/ac_interface.dart @@ -27,11 +27,11 @@ class AcInterface extends StatelessWidget { child: BlocConsumer( listener: (context, state) { if (state is DeviceControlError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMsg), - ), - ); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.errorMsg), + // ), + // ); } }, builder: (context, state) { diff --git a/lib/features/devices/view/widgets/devices_view_body.dart b/lib/features/devices/view/widgets/devices_view_body.dart index 6edb972..7a9d8fe 100644 --- a/lib/features/devices/view/widgets/devices_view_body.dart +++ b/lib/features/devices/view/widgets/devices_view_body.dart @@ -19,25 +19,25 @@ class DevicesViewBody extends StatelessWidget { Widget build(BuildContext context) { return BlocConsumer( listener: (context, state) { - if (state is GetDevicesError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMsg), - ), - ); - } else if (state is DevicesCategoriesError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMsg), - ), - ); - } else if (state is DeviceControlError) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMsg), - ), - ); - } + // if (state is GetDevicesError) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.errorMsg), + // ), + // ); + // } else if (state is DevicesCategoriesError) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.errorMsg), + // ), + // ); + // } else if (state is DeviceControlError) { + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text(state.errorMsg), + // ), + // ); + // } }, builder: (context, state) { if (state is DevicesLoading || From df13c66b1a2828a55e5d395f19fe71f406bb4fc1 Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Mon, 15 Apr 2024 15:47:13 +0300 Subject: [PATCH 4/8] Refactor API error handling and add try-catch blocks Added try-catch blocks for error handling in API's files to rethrow the errors to the cubit so cubits can update the UI based on them. Refactored error handling in HTTPInterceptor and HTTPService classes. --- lib/features/app_layout/bloc/home_cubit.dart | 42 ++++---- lib/features/app_layout/bloc/home_state.dart | 38 ++++--- lib/features/auth/bloc/auth_cubit.dart | 48 ++++----- lib/features/devices/bloc/devices_cubit.dart | 104 ++++++++++--------- lib/features/scene/bloc/scene_cubit.dart | 6 +- lib/services/api/devices_api.dart | 86 ++++++++++----- lib/services/api/http_interceptor.dart | 44 ++++++-- lib/services/api/http_service.dart | 2 - lib/services/api/spaces_api.dart | 68 ++++++------ 9 files changed, 247 insertions(+), 191 deletions(-) diff --git a/lib/features/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart index 73bbe88..7ea5011 100644 --- a/lib/features/app_layout/bloc/home_cubit.dart +++ b/lib/features/app_layout/bloc/home_cubit.dart @@ -125,14 +125,17 @@ class HomeCubit extends Cubit { emitSafe(GetSpacesLoading()); try { spaces = await SpacesAPI.getSpaces(); - selectedSpace = spaces!.isNotEmpty - ? - // selectSpace(spaces!.first) - selectedSpace = spaces!.first - : null; - emitSafe(GetSpacesLoaded(spaces!)); - } on ServerFailure catch (failure) { - emitSafe(GetSpacesError(failure.errMessage)); + } catch (failure) { + emitSafe(GetSpacesError(failure.toString())); + return; + } + + if (spaces != null && spaces!.isNotEmpty) { + selectedSpace = spaces!.first; + emitSafe(GetSpacesSuccess(spaces!)); + fetchRooms(selectedSpace!); + } else { + emitSafe(GetSpacesError("No spaces found")); } } @@ -140,13 +143,14 @@ class HomeCubit extends Cubit { emitSafe(GetSpaceRoomsLoading()); try { space.rooms = await SpacesAPI.getRoomsBySpaceId(space.id!); - if (space.rooms != null) { - emitSafe(GetSpaceRoomsLoaded(space.rooms!)); - } else { - emitSafe(GetSpaceRoomsError("No rooms found")); - } - } on ServerFailure catch (failure) { - emitSafe(GetSpacesError(failure.errMessage)); + } catch (failure) { + emitSafe(GetSpaceRoomsError(failure.toString())); + return; + } + if (space.rooms != null) { + emitSafe(GetSpaceRoomsSuccess(space.rooms!)); + } else { + emitSafe(GetSpaceRoomsError("No rooms found")); } } @@ -169,14 +173,6 @@ class HomeCubit extends Cubit { ), ], 'Devices': [ - // IconButton( - // icon: Image.asset( - // Assets.iconsFilter, - // height: 20, - // width: 20, - // ), - // onPressed: () {}, - // ), IconButton( icon: const Icon( Icons.add, diff --git a/lib/features/app_layout/bloc/home_state.dart b/lib/features/app_layout/bloc/home_state.dart index d1f5b7d..9c20d4e 100644 --- a/lib/features/app_layout/bloc/home_state.dart +++ b/lib/features/app_layout/bloc/home_state.dart @@ -4,33 +4,45 @@ abstract class HomeState {} class HomeInitial extends HomeState {} -class GetSpacesLoading extends HomeState {} +//base states +class HomeLoading extends HomeState {} -class GetSpacesLoaded extends HomeState { +class HomeError extends HomeState { + final String errMessage; + + HomeError(this.errMessage); +} + +class HomeSuccess extends HomeState {} + +///specific states +//get spaces +class GetSpacesLoading extends HomeLoading {} + +class GetSpacesSuccess extends HomeSuccess { final List spaces; - GetSpacesLoaded(this.spaces); + GetSpacesSuccess(this.spaces); } -class GetSpacesError extends HomeState { - final String errMessage; - - GetSpacesError(this.errMessage); +class GetSpacesError extends HomeError { + GetSpacesError(super.errMessage); } -class GetSpaceRoomsLoading extends HomeState {} +//get rooms +class GetSpaceRoomsLoading extends HomeLoading {} -class GetSpaceRoomsLoaded extends HomeState { +class GetSpaceRoomsSuccess extends HomeSuccess { final List rooms; - GetSpaceRoomsLoaded(this.rooms); + GetSpaceRoomsSuccess(this.rooms); } -class GetSpaceRoomsError extends HomeState { - final String errMessage; - GetSpaceRoomsError(this.errMessage); +class GetSpaceRoomsError extends HomeError { + GetSpaceRoomsError(super.errMessage); } +//UI states class SpaceSelected extends HomeState { final SpaceModel space; diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 2ef1173..9e46276 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -41,7 +41,7 @@ class AuthCubit extends Cubit { static UserModel? user; static Token token = Token.emptyConstructor(); - +/////////////////////////////////////API CALLS///////////////////////////////////// login() async { emit(AuthLoginLoading()); try { @@ -51,23 +51,23 @@ class AuthCubit extends Cubit { password: passwordController.text, ), ); - - if (token.accessTokenIsNotEmpty) { - FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.write( - key: Token.loginAccessTokenKey, value: token.accessToken); - const FlutterSecureStorage().write( - key: UserModel.userUuidKey, - value: Token.decodeToken(token.accessToken)['uuid'].toString()); - user = UserModel.fromToken(token); - emailController.clear(); - passwordController.clear(); - emit(AuthLoginSuccess()); - } else { - emit(AuthLoginError(message: 'Something went wrong')); - } - } on ServerFailure catch (failure) { - emit(AuthError(message: failure.errMessage)); + } catch (failure) { + emit(AuthLoginError(message: failure.toString())); + return; + } + if (token.accessTokenIsNotEmpty) { + FlutterSecureStorage storage = const FlutterSecureStorage(); + await storage.write( + key: Token.loginAccessTokenKey, value: token.accessToken); + const FlutterSecureStorage().write( + key: UserModel.userUuidKey, + value: Token.decodeToken(token.accessToken)['uuid'].toString()); + user = UserModel.fromToken(token); + emailController.clear(); + passwordController.clear(); + emit(AuthLoginSuccess()); + } else { + emit(AuthLoginError(message: 'Something went wrong')); } } @@ -79,8 +79,9 @@ class AuthCubit extends Cubit { NavigationService.navigatorKey.currentState! .popAndPushNamed(Routes.authLogin); emit(AuthLogoutSuccess()); - } on ServerFailure catch (failure) { - emit(AuthError(message: failure.errMessage)); + } catch (failure) { + emit(AuthLogoutError(message: failure.toString())); + return; } } @@ -109,11 +110,4 @@ class AuthCubit extends Cubit { emit(AuthTokenError(message: "Something went wrong")); } } - - static void logUserOut() async { - user = null; - token = Token.emptyConstructor(); - FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.delete(key: Token.loginAccessTokenKey); - } } diff --git a/lib/features/devices/bloc/devices_cubit.dart b/lib/features/devices/bloc/devices_cubit.dart index d976f59..5784091 100644 --- a/lib/features/devices/bloc/devices_cubit.dart +++ b/lib/features/devices/bloc/devices_cubit.dart @@ -251,32 +251,42 @@ class DevicesCubit extends Cubit { code: control.code, )); try { - await DevicesAPI.controlDevice(control).then((response) { - emitSafe(DeviceControlSuccess( - code: control.code, - )); - if (response['success'] ?? false) { - Future.delayed(const Duration(milliseconds: 400), () { - getDevicesStatues( - deviceId, - HomeCubit.getInstance().selectedSpace!.rooms!.indexOf( - HomeCubit.getInstance().selectedRoom!, - ), - code: control.code); - }); - } else { - emitSafe(DeviceControlError('Failed to control the device')); - } - }); - } on ServerFailure catch (failure) { - emitSafe(DeviceControlError(failure.errMessage)); + var response = await DevicesAPI.controlDevice(control); + + if (response['success'] ?? false) { + emitSafe(DeviceControlSuccess(code: control.code)); + //this delay is to give tuya server time to update the status + Future.delayed(const Duration(milliseconds: 400), () { + getDevicesStatues( + deviceId, + HomeCubit.getInstance() + .selectedSpace! + .rooms! + .indexOf(HomeCubit.getInstance().selectedRoom!), + code: control.code); + }); + } else { + emitSafe(DeviceControlError('Failed to control the device')); + } + } catch (failure) { + emitSafe(DeviceControlError(failure.toString())); + return; } } fetchGroups(int spaceId) async { emitSafe(DevicesCategoriesLoading()); - allCategories = await DevicesAPI.fetchGroups(spaceId); - emitSafe(DevicesCategoriesSuccess()); + try { + allCategories = await DevicesAPI.fetchGroups(spaceId); + } catch (e) { + emitSafe(DevicesCategoriesError(e.toString())); + return; + } + if (allCategories!.isNotEmpty && allCategories != null) { + emitSafe(DevicesCategoriesSuccess()); + } else { + emitSafe(DevicesCategoriesError('No Groups found')); + } } fetchDevicesByRoomId(int? roomId) async { @@ -287,46 +297,46 @@ class DevicesCubit extends Cubit { .selectedSpace! .rooms! .indexWhere((element) => element.id == roomId); - HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices = - await SpacesAPI.getDevicesByRoomId(roomId); + try { + HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices = + await DevicesAPI.getDevicesByRoomId(roomId); + } catch (e) { + emitSafe(GetDevicesError(e.toString())); + return; + } + emitSafe(GetDevicesSuccess()); + //get status for each device + //TODO get devices status per page via page controller instead of getting all devices status at once for (var device in HomeCubit.getInstance().selectedSpace!.rooms![roomIndex].devices!) { getDevicesStatues(device.id!, roomIndex); } - - emitSafe(GetDevicesSuccess()); } getDevicesStatues(String deviceId, int roomIndex, {String? code}) async { + emitSafe(GetDeviceStatusLoading(code: code)); + int deviceIndex = HomeCubit.getInstance() + .selectedSpace! + .rooms![roomIndex] + .devices! + .indexWhere((element) => element.id == deviceId); + List statuses = []; try { - emitSafe(GetDeviceStatusLoading(code: code)); - int deviceIndex = HomeCubit.getInstance() - .selectedSpace! - .rooms![roomIndex] - .devices! - .indexWhere((element) => element.id == deviceId); - - List statuses = []; var response = await DevicesAPI.getDeviceStatus(deviceId); - // if (response['result']['status'].length > 4) - // print('response : ${response['result']['status'][4]}'); - for (var status in response['result']['status']) { statuses.add(StatusModel.fromJson(status)); } - - HomeCubit.getInstance() - .selectedSpace! - .rooms![roomIndex] - .devices![deviceIndex] - .status = statuses; - emitSafe(GetDeviceStatusSuccess(code: code)); - } on ServerFailure catch (failure) { - emitSafe( - GetDeviceStatusError(failure.errMessage), - ); + } catch (e) { + emitSafe(GetDeviceStatusError(e.toString())); + return; } + HomeCubit.getInstance() + .selectedSpace! + .rooms![roomIndex] + .devices![deviceIndex] + .status = statuses; + emitSafe(GetDeviceStatusSuccess(code: code)); } ///Lights diff --git a/lib/features/scene/bloc/scene_cubit.dart b/lib/features/scene/bloc/scene_cubit.dart index 350173a..f22d3e4 100644 --- a/lib/features/scene/bloc/scene_cubit.dart +++ b/lib/features/scene/bloc/scene_cubit.dart @@ -12,10 +12,8 @@ class SceneCubit extends Cubit { void getScenes() { emit(SceneLoading()); - //TODO: remove this it's causing the Bad State because its being after the cubit is closed - Future.delayed(const Duration(milliseconds: 50), () { - emit(SceneSuccess()); - }); + + emit(SceneSuccess()); } List scenes = []; diff --git a/lib/services/api/devices_api.dart b/lib/services/api/devices_api.dart index 8968bad..31bd309 100644 --- a/lib/services/api/devices_api.dart +++ b/lib/services/api/devices_api.dart @@ -1,5 +1,6 @@ import 'package:syncrow_app/features/devices/model/device_category_model.dart'; import 'package:syncrow_app/features/devices/model/device_control_model.dart'; +import 'package:syncrow_app/features/devices/model/device_model.dart'; import 'package:syncrow_app/services/api/api_links_endpoints.dart'; import 'package:syncrow_app/services/api/http_service.dart'; @@ -8,18 +9,19 @@ class DevicesAPI { static Future> controlDevice( DeviceControlModel controlModel) async { - // print( - // 'contoling [${controlModel.deviceId}] with code [${controlModel.code}] and value [${controlModel.value}'); - - final response = await _httpService.post( - path: ApiEndpoints.control, - body: controlModel.toJson(), - showServerMessage: false, - expectedResponseModel: (json) { - return json; - }, - ); - return response; + try { + final response = await _httpService.post( + path: ApiEndpoints.control, + body: controlModel.toJson(), + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } } static Future> fetchGroups(int spaceId) async { @@ -28,24 +30,52 @@ class DevicesAPI { "pageSize": 100, "pageNo": 1 }; - final response = await _httpService.get( - path: ApiEndpoints.groups, - queryParameters: params, - showServerMessage: false, - expectedResponseModel: (json) => - DevicesCategoryModel.fromJsonList(json['groups']), - ); - return response; + try { + final response = await _httpService.get( + path: ApiEndpoints.groups, + queryParameters: params, + showServerMessage: false, + expectedResponseModel: (json) => + DevicesCategoryModel.fromJsonList(json['groups']), + ); + return response; + } catch (e) { + rethrow; + } } static Future> getDeviceStatus(String deviceId) async { - final response = await _httpService.get( - path: '${ApiEndpoints.deviceStatus}/$deviceId/functions/status', - showServerMessage: false, - expectedResponseModel: (json) { - return json; - }, - ); - return response; + try { + final response = await _httpService.get( + path: '${ApiEndpoints.deviceStatus}/$deviceId/functions/status', + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future> getDevicesByRoomId(int roomId) async { + try { + final response = await _httpService.get( + path: ApiEndpoints.devicesByRoom, + queryParameters: {"roomId": roomId, "pageSize": 10}, + showServerMessage: false, + expectedResponseModel: (json) { + List devices = []; + for (var device in json['devices']) { + devices.add(DeviceModel.fromJson(device)); + } + return devices; + }, + ); + return response; + } catch (e) { + rethrow; + } } } diff --git a/lib/services/api/http_interceptor.dart b/lib/services/api/http_interceptor.dart index 38b43cb..baae847 100644 --- a/lib/services/api/http_interceptor.dart +++ b/lib/services/api/http_interceptor.dart @@ -1,18 +1,30 @@ +import 'dart:io'; + import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/auth/model/token.dart'; import 'package:syncrow_app/navigation/navigation_service.dart'; +import 'package:syncrow_app/services/api/api_links_endpoints.dart'; import 'dart:async'; import 'dart:developer' as developer; import 'package:syncrow_app/services/api/network_exception.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; class HTTPInterceptor extends InterceptorsWrapper { - @override + List headerExclusionList = []; + + List headerExclusionListOfAddedParameters = [ + ApiEndpoints.login, + ]; @override void onResponse(Response response, ResponseInterceptorHandler handler) async { - return handler.next(response); + if (await validateResponse(response)) { + super.onResponse(response, handler); + } else { + handler.reject(DioException( + requestOptions: response.requestOptions, response: response)); + } } @override @@ -20,17 +32,20 @@ class HTTPInterceptor extends InterceptorsWrapper { RequestOptions options, RequestInterceptorHandler handler) async { var storage = const FlutterSecureStorage(); var token = await storage.read(key: Token.loginAccessTokenKey); - // options.headers['Authorization'] = 'Bearer $token'; - options.headers['Authorization'] = 'Bearer ${'${token!}123'}'; + if (checkHeaderExclusionListOfAddedParameters(options.path)) { + options.headers + .putIfAbsent(HttpHeaders.authorizationHeader, () => "Bearer $token"); + } + // options.headers['Authorization'] = 'Bearer ${'${token!}123'}'; super.onRequest(options, handler); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { - developer.log('Error Message: ${err.message}'); - developer.log('Error res Code: ${err.response?.statusCode}'); - developer.log('Error res Data: ${err.response?.data}'); - developer.log('Error res status message: ${err.response?.statusMessage}'); + // developer.log('Error Message: ${err.message}'); + // developer.log('Error res Code: ${err.response?.statusCode}'); + // developer.log('Error res Data: ${err.response?.data}'); + // developer.log('Error res status message: ${err.response?.statusMessage}'); ServerFailure failure = ServerFailure.fromDioError(err); CustomSnackBar.displaySnackBar(failure.toString()); @@ -39,8 +54,8 @@ class HTTPInterceptor extends InterceptorsWrapper { if (err.response?.statusCode == 401 && token != null) { await AuthCubit.get(NavigationService.navigatorKey.currentContext!) .logout(); - super.onError(err, handler); } + super.onError(err, handler); } /// Validates the response and returns true if it is successful (status code 2xx). @@ -60,4 +75,15 @@ class HTTPInterceptor extends InterceptorsWrapper { return false; } } + + checkHeaderExclusionListOfAddedParameters(String path) { + bool shouldAddHeader = true; + + for (var urlConstant in headerExclusionListOfAddedParameters) { + if (path.contains(urlConstant)) { + shouldAddHeader = false; + } + } + return shouldAddHeader; + } } diff --git a/lib/services/api/http_service.dart b/lib/services/api/http_service.dart index 690c11c..414741d 100644 --- a/lib/services/api/http_service.dart +++ b/lib/services/api/http_service.dart @@ -57,8 +57,6 @@ class HTTPService { queryParameters: queryParameters, options: options, ); - // developer.log("status code is ${response.statusCode}"); - // developer.log("response data is ${response.data}"); return expectedResponseModel(response.data); } catch (error) { rethrow; diff --git a/lib/services/api/spaces_api.dart b/lib/services/api/spaces_api.dart index ebf2597..f05df9f 100644 --- a/lib/services/api/spaces_api.dart +++ b/lib/services/api/spaces_api.dart @@ -12,47 +12,39 @@ class SpacesAPI { static Future> getSpaces() async { var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); - final response = await _httpService.get( - path: ApiEndpoints.spaces, - queryParameters: { - "userUuid": uuid, - }, - showServerMessage: false, - expectedResponseModel: (json) => SpaceModel.fromJsonList(json), - ); - return response; + try { + final response = await _httpService.get( + path: ApiEndpoints.spaces, + queryParameters: { + "userUuid": uuid, + }, + showServerMessage: false, + expectedResponseModel: (json) => SpaceModel.fromJsonList(json), + ); + return response; + } catch (e) { + rethrow; + } } //get rooms by space id static Future> getRoomsBySpaceId(int spaceId) async { - final response = await _httpService.get( - path: ApiEndpoints.rooms, - queryParameters: {"homeId": spaceId}, - showServerMessage: false, - expectedResponseModel: (json) { - List rooms = []; - for (var room in json) { - rooms.add(RoomModel.fromJson(room)); - } - return rooms; - }, - ); - return response; - } - - static Future> getDevicesByRoomId(int roomId) async { - final response = await _httpService.get( - path: ApiEndpoints.devicesByRoom, - queryParameters: {"roomId": roomId, "pageSize": 10}, - showServerMessage: false, - expectedResponseModel: (json) { - List devices = []; - for (var device in json['devices']) { - devices.add(DeviceModel.fromJson(device)); - } - return devices; - }, - ); - return response; + try { + final response = await _httpService.get( + path: ApiEndpoints.rooms, + queryParameters: {"homeId": spaceId}, + showServerMessage: false, + expectedResponseModel: (json) { + List rooms = []; + for (var room in json) { + rooms.add(RoomModel.fromJson(room)); + } + return rooms; + }, + ); + return response; + } catch (e) { + rethrow; + } } } From 95f7ade9e6f6d4d85d86f74ec84cd642ce6ea52f Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Tue, 16 Apr 2024 15:45:04 +0300 Subject: [PATCH 5/8] removed try-catch blocks from auth_api.dart, devices_api.dart and spaces_api.dart files --- lib/services/api/authentication_api.dart | 17 ++---- lib/services/api/devices_api.dart | 71 ++++++++++-------------- lib/services/api/spaces_api.dart | 52 ++++++++--------- 3 files changed, 58 insertions(+), 82 deletions(-) diff --git a/lib/services/api/authentication_api.dart b/lib/services/api/authentication_api.dart index f83b99c..a5e81e4 100644 --- a/lib/services/api/authentication_api.dart +++ b/lib/services/api/authentication_api.dart @@ -16,16 +16,11 @@ class AuthenticationAPI { static Future loginWithEmail( {required LoginWithEmailModel model}) async { - try { - final response = await HTTPService().post( - path: ApiEndpoints.login, - body: model.toJson(), - showServerMessage: false, - expectedResponseModel: (json) => Token.fromJson(json['data'])); - // developer.log("response: $response"); - return response; - } catch (e) { - rethrow; - } + final response = await HTTPService().post( + path: ApiEndpoints.login, + body: model.toJson(), + showServerMessage: false, + expectedResponseModel: (json) => Token.fromJson(json['data'])); + return response; } } diff --git a/lib/services/api/devices_api.dart b/lib/services/api/devices_api.dart index 31bd309..97c5588 100644 --- a/lib/services/api/devices_api.dart +++ b/lib/services/api/devices_api.dart @@ -30,52 +30,41 @@ class DevicesAPI { "pageSize": 100, "pageNo": 1 }; - try { - final response = await _httpService.get( - path: ApiEndpoints.groups, - queryParameters: params, - showServerMessage: false, - expectedResponseModel: (json) => - DevicesCategoryModel.fromJsonList(json['groups']), - ); - return response; - } catch (e) { - rethrow; - } + + final response = await _httpService.get( + path: ApiEndpoints.groups, + queryParameters: params, + showServerMessage: false, + expectedResponseModel: (json) => + DevicesCategoryModel.fromJsonList(json['groups']), + ); + return response; } static Future> getDeviceStatus(String deviceId) async { - try { - final response = await _httpService.get( - path: '${ApiEndpoints.deviceStatus}/$deviceId/functions/status', - showServerMessage: false, - expectedResponseModel: (json) { - return json; - }, - ); - return response; - } catch (e) { - rethrow; - } + final response = await _httpService.get( + path: '${ApiEndpoints.deviceStatus}/$deviceId/functions/status', + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; } static Future> getDevicesByRoomId(int roomId) async { - try { - final response = await _httpService.get( - path: ApiEndpoints.devicesByRoom, - queryParameters: {"roomId": roomId, "pageSize": 10}, - showServerMessage: false, - expectedResponseModel: (json) { - List devices = []; - for (var device in json['devices']) { - devices.add(DeviceModel.fromJson(device)); - } - return devices; - }, - ); - return response; - } catch (e) { - rethrow; - } + final response = await _httpService.get( + path: ApiEndpoints.devicesByRoom, + queryParameters: {"roomId": roomId, "pageSize": 10}, + showServerMessage: false, + expectedResponseModel: (json) { + List devices = []; + for (var device in json['devices']) { + devices.add(DeviceModel.fromJson(device)); + } + return devices; + }, + ); + return response; } } diff --git a/lib/services/api/spaces_api.dart b/lib/services/api/spaces_api.dart index f05df9f..3aa95dd 100644 --- a/lib/services/api/spaces_api.dart +++ b/lib/services/api/spaces_api.dart @@ -12,39 +12,31 @@ class SpacesAPI { static Future> getSpaces() async { var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); - try { - final response = await _httpService.get( - path: ApiEndpoints.spaces, - queryParameters: { - "userUuid": uuid, - }, - showServerMessage: false, - expectedResponseModel: (json) => SpaceModel.fromJsonList(json), - ); - return response; - } catch (e) { - rethrow; - } + final response = await _httpService.get( + path: ApiEndpoints.spaces, + queryParameters: { + "userUuid": uuid, + }, + showServerMessage: false, + expectedResponseModel: (json) => SpaceModel.fromJsonList(json), + ); + return response; } //get rooms by space id static Future> getRoomsBySpaceId(int spaceId) async { - try { - final response = await _httpService.get( - path: ApiEndpoints.rooms, - queryParameters: {"homeId": spaceId}, - showServerMessage: false, - expectedResponseModel: (json) { - List rooms = []; - for (var room in json) { - rooms.add(RoomModel.fromJson(room)); - } - return rooms; - }, - ); - return response; - } catch (e) { - rethrow; - } + final response = await _httpService.get( + path: ApiEndpoints.rooms, + queryParameters: {"homeId": spaceId}, + showServerMessage: false, + expectedResponseModel: (json) { + List rooms = []; + for (var room in json) { + rooms.add(RoomModel.fromJson(room)); + } + return rooms; + }, + ); + return response; } } From 214c4bb749770bcf0a194b1eec13a83fc8471231 Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Tue, 16 Apr 2024 15:45:32 +0300 Subject: [PATCH 6/8] Remove unnecessary developer log statements --- lib/services/api/http_interceptor.dart | 5 ----- lib/services/api/http_service.dart | 15 ++------------- lib/utils/helpers/misc_string_helpers.dart | 1 - 3 files changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/services/api/http_interceptor.dart b/lib/services/api/http_interceptor.dart index baae847..7f3b8be 100644 --- a/lib/services/api/http_interceptor.dart +++ b/lib/services/api/http_interceptor.dart @@ -42,11 +42,6 @@ class HTTPInterceptor extends InterceptorsWrapper { @override void onError(DioException err, ErrorInterceptorHandler handler) async { - // developer.log('Error Message: ${err.message}'); - // developer.log('Error res Code: ${err.response?.statusCode}'); - // developer.log('Error res Data: ${err.response?.data}'); - // developer.log('Error res status message: ${err.response?.statusMessage}'); - ServerFailure failure = ServerFailure.fromDioError(err); CustomSnackBar.displaySnackBar(failure.toString()); var storage = const FlutterSecureStorage(); diff --git a/lib/services/api/http_service.dart b/lib/services/api/http_service.dart index 414741d..dc995ff 100644 --- a/lib/services/api/http_service.dart +++ b/lib/services/api/http_service.dart @@ -74,11 +74,8 @@ class HTTPService { data: body, queryParameters: queryParameters, ); - developer.log("status code is ${response.statusCode}"); return expectedResponseModel(response.data); } catch (error) { - developer.log("******* Error"); - developer.log(error.toString()); rethrow; } } @@ -89,21 +86,15 @@ class HTTPService { Map? queryParameters, required T Function(dynamic) expectedResponseModel}) async { try { - developer.log("download begins"); final response = await client.download( path, savePath, - onReceiveProgress: (current, total) { - developer.log("current = $current, while total = $total"); - }, + onReceiveProgress: (current, total) {}, ); - developer.log("download ends"); + return expectedResponseModel(response.data); // return expectedResponseModel(response.data); } catch (error) { - developer.log("******* Error"); - developer.log("download error"); - developer.log(error.toString()); rethrow; } } @@ -121,8 +112,6 @@ class HTTPService { ); return expectedResponseModel(response.data); } catch (error) { - developer.log("******* Error"); - developer.log(error.toString()); rethrow; } } diff --git a/lib/utils/helpers/misc_string_helpers.dart b/lib/utils/helpers/misc_string_helpers.dart index 0b9a2c5..616e8a4 100644 --- a/lib/utils/helpers/misc_string_helpers.dart +++ b/lib/utils/helpers/misc_string_helpers.dart @@ -6,7 +6,6 @@ class StringHelpers { static String enhanceFileName(File file) { var fileName = " "; final filePath = file.path; - developer.log(filePath); final fileStringArray = filePath.split("/"); fileName = fileStringArray.last; if (fileName.length > 20) { From fd03d29e93c5d3e757e837d23f43f03838bb61a1 Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Tue, 16 Apr 2024 15:47:03 +0300 Subject: [PATCH 7/8] Avoided null value in network_exception.dart when calling fromResponse --- lib/services/api/network_exception.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/services/api/network_exception.dart b/lib/services/api/network_exception.dart index 2b61e28..d8f8d23 100644 --- a/lib/services/api/network_exception.dart +++ b/lib/services/api/network_exception.dart @@ -32,7 +32,7 @@ class ServerFailure extends Failure { // var document = parser.parse(dioError.response!.data.toString()); // var message = document.body!.text; return ServerFailure.fromResponse(dioError.response!.statusCode!, - dioError.response!.data['message']); + dioError.response?.data['message'] ?? "Error"); } case DioExceptionType.cancel: return ServerFailure("The request to ApiServer was canceled"); @@ -48,25 +48,25 @@ class ServerFailure extends Failure { } } - factory ServerFailure.fromResponse(int? statusCode, dynamic response) { + factory ServerFailure.fromResponse(int? statusCode, dynamic responseMessage) { switch (statusCode) { case 401: case 403: - return ServerFailure(response); + return ServerFailure(responseMessage); case 400: List errors = []; - if (response['message'] is List) { - for (var error in response['message']) { + if (responseMessage is List) { + for (var error in responseMessage) { errors.add(error); } } else { - errors.add(response['message']); + return ServerFailure(responseMessage); } return ServerFailure(errors.join('\n')); case 404: return ServerFailure("Your request not found, Please try later!"); case 500: - return ServerFailure(response); + return ServerFailure(responseMessage); default: return ServerFailure("Opps there was an Error, Please try again!"); } From 62667802584df6bd794ad44078bb1e8efa7092b8 Mon Sep 17 00:00:00 2001 From: Mohammad Salameh Date: Tue, 16 Apr 2024 16:44:17 +0300 Subject: [PATCH 8/8] Update form key to be a final field and add validators for email and password - Make the form key a final field in the AuthCubit class - Add validators for email and password fields in the AuthCubit class - Update the LoginForm widget to use the form key from AuthCubit class - Add validators for email and password fields in the LoginForm widget --- lib/features/auth/bloc/auth_cubit.dart | 55 ++++++++++++++- .../auth/view/widgets/login/login_form.dart | 67 +++++++------------ 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 9e46276..0c2fcdf 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -20,8 +20,9 @@ class AuthCubit extends Cubit { static AuthCubit get(context) => BlocProvider.of(context); - TextEditingController emailController = TextEditingController(); - TextEditingController passwordController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final loginFormKey = GlobalKey(); bool isPasswordVisible = false; static GlobalKey formKey = GlobalKey(); @@ -41,6 +42,56 @@ class AuthCubit extends Cubit { static UserModel? user; static Token token = Token.emptyConstructor(); + +/////////////////////////////////////VALIDATORS///////////////////////////////////// + String? passwordValidator(String? value) { + if (value != null) { + if (value.isNotEmpty) { + if (value.length >= 6) { + return null; + //TODO uncomment this code when the password validation is needed + // if (RegExp( + // r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$') + // .hasMatch(value)) { + // return null; + // } else { + // return 'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character'; + // } + } else { + return 'Password must be at least 6 characters'; + } + } else { + return 'Please enter your password'; + } + } + return null; + } + + String? emailAddressValidator(String? value) { + if (value != null && value.isNotEmpty && value != "") { + if (checkValidityOfEmail(value)) { + if (loginFormKey.currentState != null) { + loginFormKey.currentState!.save(); + } + return null; + } else { + return 'Please enter a valid email'; + } + } else { + return 'Email address is required'; + } + } + + bool checkValidityOfEmail(String? email) { + if (email != null) { + return RegExp( + r"^[a-zA-Z0-9]+([.!#$%&'*+/=?^_`{|}~-]?[a-zA-Z0-9]+)*@[a-zA-Z0-9]+([.-]?[a-zA-Z0-9]+)*\.[a-zA-Z0-9]{2,}$") + .hasMatch(email); + } else { + return false; + } + } + /////////////////////////////////////API CALLS///////////////////////////////////// login() async { emit(AuthLoginLoading()); diff --git a/lib/features/auth/view/widgets/login/login_form.dart b/lib/features/auth/view/widgets/login/login_form.dart index 4fe33ee..5296e23 100644 --- a/lib/features/auth/view/widgets/login/login_form.dart +++ b/lib/features/auth/view/widgets/login/login_form.dart @@ -13,9 +13,10 @@ class LoginForm extends StatelessWidget { @override Widget build(BuildContext context) { - var formKey = GlobalKey(); + var pressed = false; return BlocBuilder( builder: (context, state) { + final formKey = AuthCubit.get(context).loginFormKey; return Form( key: formKey, autovalidateMode: AutovalidateMode.onUserInteraction, @@ -28,30 +29,23 @@ class LoginForm extends StatelessWidget { fontColor: Colors.white, ), TextFormField( + autovalidateMode: AutovalidateMode.disabled, + textInputAction: TextInputAction.done, + keyboardType: TextInputType.text, + scrollPadding: EdgeInsets.zero, + autocorrect: false, + autofillHints: const [AutofillHints.email], controller: AuthCubit.get(context).emailController, validator: (value) { - if (state is! AuthTokenError) { - if (value != null) { - if (value.isNotEmpty) { - if (RegExp( - r'^[a-zA-Z0-9._-]+@{1}[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$') - .hasMatch(value)) { - return null; - } else { - return 'Please enter a valid email'; - } - } else { - return 'Please enter your email'; - } - } else { - return 'Please enter your email'; - } + if (state is AuthTokenError && !pressed) { + return null; } - return null; + return AuthCubit.get(context).emailAddressValidator(value); }, onTapOutside: (event) { FocusScope.of(context).unfocus(); }, + onChanged: (value) {}, decoration: defaultInputDecoration(context, hint: "Example@email.com"), ), @@ -61,30 +55,18 @@ class LoginForm extends StatelessWidget { fontColor: Colors.white, ), TextFormField( + autovalidateMode: AutovalidateMode.disabled, + textInputAction: TextInputAction.done, + keyboardType: TextInputType.text, + scrollPadding: EdgeInsets.zero, + autocorrect: false, + autofillHints: const [AutofillHints.password], controller: AuthCubit.get(context).passwordController, validator: (value) { - if (state is! AuthTokenError) { - if (value != null) { - if (value.isNotEmpty) { - return null; - //TODO: uncomment this when the backend is ready - // if (value.length > 8) { - // if (RegExp( - // r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$') - // .hasMatch(value)) { - // return null; - // } else { - // return 'Password must contain at least one uppercase letter, one lowercase letter, one number and one special character'; - // } - // } else { - // return 'Password must be at least 8 characters'; - // } - } else { - return 'Please enter your password'; - } - } + if (state is AuthTokenError && !pressed) { + return null; } - return null; + return AuthCubit.get(context).passwordValidator(value); }, onTapOutside: (event) { FocusScope.of(context).unfocus(); @@ -116,14 +98,11 @@ class LoginForm extends StatelessWidget { 'Login', ), onPressed: () { - bool isValid = formKey.currentState!.validate(); - if (isValid) { + pressed = true; + if (formKey.currentState!.validate()) { if ((state is! AuthLoading)) { - print('login'); AuthCubit.get(context).login(); FocusScope.of(context).unfocus(); - } else { - formKey.currentState!.save(); } } },