Merge pull request #13 from SyncrowIOT/token_validation

Refactor HTTPInterceptor and add CustomSnackBar helper
This commit is contained in:
Mohammad Salameh
2024-04-17 12:06:00 +03:00
committed by GitHub
28 changed files with 654 additions and 429 deletions
+27 -28
View File
@@ -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<HomeState> {
}
}
@override
Future<void> close() {
_instance = null;
selectedSpace = null;
selectedRoom = null;
return super.close();
}
static HomeCubit get(context) => BlocProvider.of(context);
List<SpaceModel>? spaces;
@@ -118,14 +125,17 @@ class HomeCubit extends Cubit<HomeState> {
emitSafe(GetSpacesLoading());
try {
spaces = await SpacesAPI.getSpaces();
selectedSpace = spaces!.isNotEmpty
?
// selectSpace(spaces!.first)
selectedSpace = spaces!.first
: null;
emitSafe(GetSpacesLoaded(spaces!));
} on DioException catch (e) {
emitSafe(GetSpacesError(ServerFailure.fromDioError(e).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"));
}
}
@@ -133,22 +143,19 @@ class HomeCubit extends Cubit<HomeState> {
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 DioException catch (e) {
emitSafe(GetSpacesError(ServerFailure.fromDioError(e).errMessage));
} catch (failure) {
emitSafe(GetSpaceRoomsError(failure.toString()));
return;
}
if (space.rooms != null) {
emitSafe(GetSpaceRoomsSuccess(space.rooms!));
} else {
emitSafe(GetSpaceRoomsError("No rooms found"));
}
}
/////////////////////////////////////// Nav ///////////////////////////////////////
static clear() {
pageIndex = 0;
}
static int pageIndex = 0;
static Map<String, List<Widget>> appBarActions = {
@@ -166,14 +173,6 @@ class HomeCubit extends Cubit<HomeState> {
),
],
'Devices': [
// IconButton(
// icon: Image.asset(
// Assets.iconsFilter,
// height: 20,
// width: 20,
// ),
// onPressed: () {},
// ),
IconButton(
icon: const Icon(
Icons.add,
+25 -13
View File
@@ -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<SpaceModel> 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<RoomModel> 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;
+71 -47
View File
@@ -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<HomeCubit, HomeState>(
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<AuthCubit, AuthState>(
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<HomeCubit, HomeState>(
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),
// ),
),
),
);
},
),
);
},
);
}
}
@@ -12,7 +12,16 @@ class AppBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<HomeCubit, HomeState>(
return BlocConsumer<HomeCubit, HomeState>(
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<HomeCubit, HomeState>(
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()),
);
},
);
@@ -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';
+83 -28
View File
@@ -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';
@@ -20,8 +20,9 @@ class AuthCubit extends Cubit<AuthState> {
static AuthCubit get(context) => BlocProvider.of(context);
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
final TextEditingController emailController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final loginFormKey = GlobalKey<FormState>();
bool isPasswordVisible = false;
static GlobalKey<FormState> formKey = GlobalKey<FormState>();
@@ -42,8 +43,58 @@ class AuthCubit extends Cubit<AuthState> {
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(AuthLoading());
emit(AuthLoginLoading());
try {
token = await AuthenticationAPI.loginWithEmail(
model: LoginWithEmailModel(
@@ -51,33 +102,37 @@ class AuthCubit extends Cubit<AuthState> {
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);
emit(AuthSuccess());
} else {
emit(AuthError('Something went wrong'));
}
} on DioException catch (e) {
emit(AuthError(ServerFailure.fromDioError(e).toString()));
} 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'));
}
}
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());
} catch (failure) {
emit(AuthLogoutError(message: failure.toString()));
return;
}
}
@@ -87,7 +142,7 @@ class AuthCubit extends Cubit<AuthState> {
await const FlutterSecureStorage().read(key: Token.loginAccessTokenKey);
if (value == null) {
emit(AuthTokenError("Token not found"));
emit(AuthTokenError(message: "Token not found"));
return;
}
@@ -100,10 +155,10 @@ class AuthCubit extends Cubit<AuthState> {
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"));
}
}
}
+25 -11
View File
@@ -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});
}
@@ -13,9 +13,10 @@ class LoginForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
var formKey = GlobalKey<FormState>();
var pressed = false;
return BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
final formKey = AuthCubit.get(context).loginFormKey;
return Form(
key: formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
@@ -28,26 +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.isEmpty) {
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';
}
}
if (state is AuthTokenError && !pressed) {
return null;
}
return null;
return AuthCubit.get(context).emailAddressValidator(value);
},
onTapOutside: (event) {
formKey.currentState!.validate();
FocusScope.of(context).unfocus();
},
onChanged: (value) {},
decoration: defaultInputDecoration(context,
hint: "[email protected]"),
),
@@ -57,23 +55,21 @@ 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) {
if (value.length < 6) {
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) {
formKey.currentState!.validate();
FocusScope.of(context).unfocus();
},
obscureText: !AuthCubit.get(context).isPasswordVisible,
decoration: defaultInputDecoration(context,
@@ -88,7 +84,7 @@ class LoginForm extends StatelessWidget {
children: [
Expanded(
child: DefaultButton(
isDone: state is AuthSuccess,
isDone: state is AuthLoginSuccess,
isLoading: state is AuthLoading,
customButtonStyle: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
@@ -102,9 +98,12 @@ class LoginForm extends StatelessWidget {
'Login',
),
onPressed: () {
pressed = true;
if (formKey.currentState!.validate()) {
AuthCubit.get(context).login();
FocusScope.of(context).unfocus();
if ((state is! AuthLoading)) {
AuthCubit.get(context).login();
FocusScope.of(context).unfocus();
}
}
},
),
@@ -18,18 +18,16 @@ class LoginView extends StatelessWidget {
@override
Widget build(BuildContext context) {
//TODO move to strings manager
return BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
),
);
} else if (state is AuthSuccess) {
Navigator.popAndPushNamed(context, Routes.homeRoute);
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text(state.message),
// ),
// );
} else if (state is AuthLoginSuccess) {
Navigator.pushNamed(context, Routes.homeRoute);
}
},
builder: (context, state) {
+60 -73
View File
@@ -31,12 +31,9 @@ class DevicesCubit extends Cubit<DevicesState> {
}
}
}
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<DevicesState> {
@override
Future<void> close() {
_isClosed = true;
_instance = null;
return super.close();
}
@@ -255,101 +251,92 @@ class DevicesCubit extends Cubit<DevicesState> {
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 DioException catch (e) {
emitSafe(DeviceControlError(ServerFailure.fromDioError(e).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 {
if (_isClosed) return;
emitSafe(DevicesCategoriesLoading());
try {
emitSafe(DevicesCategoriesLoading());
allCategories = await DevicesAPI.fetchGroups(spaceId);
} catch (e) {
emitSafe(DevicesCategoriesError(e.toString()));
return;
}
if (allCategories!.isNotEmpty && allCategories != null) {
emitSafe(DevicesCategoriesSuccess());
} on DioException catch (error) {
emitSafe(
DevicesCategoriesError(ServerFailure.fromDioError(error).errMessage),
);
} else {
emitSafe(DevicesCategoriesError('No Groups found'));
}
}
fetchDevicesByRoomId(int? roomId) async {
if (_isClosed) return;
if (roomId == null) return;
emitSafe(GetDevicesLoading());
int roomIndex = HomeCubit.getInstance()
.selectedSpace!
.rooms!
.indexWhere((element) => element.id == roomId);
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);
}
await DevicesAPI.getDevicesByRoomId(roomId);
} catch (e) {
emitSafe(GetDevicesError(e.toString()));
return;
}
emitSafe(GetDevicesSuccess());
emitSafe(GetDevicesSuccess());
} on DioException catch (error) {
emitSafe(
GetDevicesError(ServerFailure.fromDioError(error).errMessage),
);
//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);
}
}
getDevicesStatues(String deviceId, int roomIndex, {String? code}) async {
if (_isClosed) return;
emitSafe(GetDeviceStatusLoading(code: code));
int deviceIndex = HomeCubit.getInstance()
.selectedSpace!
.rooms![roomIndex]
.devices!
.indexWhere((element) => element.id == deviceId);
List<StatusModel> statuses = [];
try {
emitSafe(GetDeviceStatusLoading(code: code));
int deviceIndex = HomeCubit.getInstance()
.selectedSpace!
.rooms![roomIndex]
.devices!
.indexWhere((element) => element.id == deviceId);
List<StatusModel> 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 DioException catch (error) {
emitSafe(
GetDeviceStatusError(ServerFailure.fromDioError(error).errMessage),
);
} catch (e) {
emitSafe(GetDeviceStatusError(e.toString()));
return;
}
HomeCubit.getInstance()
.selectedSpace!
.rooms![roomIndex]
.devices![deviceIndex]
.status = statuses;
emitSafe(GetDeviceStatusSuccess(code: code));
}
///Lights
@@ -27,11 +27,11 @@ class AcInterface extends StatelessWidget {
child: BlocConsumer<DevicesCubit, DevicesState>(
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) {
@@ -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 {
@@ -19,25 +19,25 @@ class DevicesViewBody extends StatelessWidget {
Widget build(BuildContext context) {
return BlocConsumer<DevicesCubit, DevicesState>(
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 ||
@@ -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,
// ),
// ),
// ],
// )),
// ),
// ),
// ),
// );
// }
// }
+2 -4
View File
@@ -12,10 +12,8 @@ class SceneCubit extends Cubit<SceneState> {
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<SceneModel> scenes = [];
+8 -16
View File
@@ -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,
),
),
+3
View File
@@ -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',
+7
View File
@@ -0,0 +1,7 @@
import 'package:flutter/material.dart';
class NavigationService {
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static GlobalKey<ScaffoldMessengerState>? snackbarKey =
GlobalKey<ScaffoldMessengerState>();
}
+6 -11
View File
@@ -16,16 +16,11 @@ class AuthenticationAPI {
static Future<Token> 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']));
// debugPrint("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;
}
}
+31 -11
View File
@@ -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,17 +9,19 @@ class DevicesAPI {
static Future<Map<String, dynamic>> 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<List<DevicesCategoryModel>> fetchGroups(int spaceId) async {
@@ -27,6 +30,7 @@ class DevicesAPI {
"pageSize": 100,
"pageNo": 1
};
final response = await _httpService.get(
path: ApiEndpoints.groups,
queryParameters: params,
@@ -47,4 +51,20 @@ class DevicesAPI {
);
return response;
}
static Future<List<DeviceModel>> getDevicesByRoomId(int roomId) async {
final response = await _httpService.get(
path: ApiEndpoints.devicesByRoom,
queryParameters: {"roomId": roomId, "pageSize": 10},
showServerMessage: false,
expectedResponseModel: (json) {
List<DeviceModel> devices = [];
for (var device in json['devices']) {
devices.add(DeviceModel.fromJson(device));
}
return devices;
},
);
return response;
}
}
+71 -33
View File
@@ -1,46 +1,84 @@
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
// void onResponse(Response response, ResponseInterceptorHandler handler) async {
// // Pass the response to the next interceptor or response handler.
// return handler.next(response);
// }
//
List<String> headerExclusionList = [];
List<String> headerExclusionListOfAddedParameters = [
ApiEndpoints.login,
];
@override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
if (await validateResponse(response)) {
super.onResponse(response, handler);
} else {
handler.reject(DioException(
requestOptions: response.requestOptions, response: 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';
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 {
// // 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<bool> 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 {
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<bool> 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;
}
}
checkHeaderExclusionListOfAddedParameters(String path) {
bool shouldAddHeader = true;
for (var urlConstant in headerExclusionListOfAddedParameters) {
if (path.contains(urlConstant)) {
shouldAddHeader = false;
}
}
return shouldAddHeader;
}
}
+3 -20
View File
@@ -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,6 @@ class HTTPService {
queryParameters: queryParameters,
options: options,
);
// debugPrint("status code is ${response.statusCode}");
// debugPrint("response data is ${response.data}");
return expectedResponseModel(response.data);
} catch (error) {
rethrow;
@@ -79,11 +74,8 @@ class HTTPService {
data: body,
queryParameters: queryParameters,
);
debugPrint("status code is ${response.statusCode}");
return expectedResponseModel(response.data);
} catch (error) {
debugPrint("******* Error");
debugPrint(error.toString());
rethrow;
}
}
@@ -94,26 +86,19 @@ class HTTPService {
Map<String, dynamic>? queryParameters,
required T Function(dynamic) expectedResponseModel}) async {
try {
debugPrint("download begins");
final response = await client.download(
path,
savePath,
onReceiveProgress: (current, total) {
debugPrint("current = $current, while total = $total");
},
onReceiveProgress: (current, total) {},
);
debugPrint("download ends");
return expectedResponseModel(response.data);
// return expectedResponseModel(response.data);
} catch (error) {
debugPrint("******* Error");
debugPrint("download error");
debugPrint(error.toString());
rethrow;
}
}
//delete
Future<T> delete<T>({
required String path,
Map<String, dynamic>? queryParameters,
@@ -127,8 +112,6 @@ class HTTPService {
);
return expectedResponseModel(response.data);
} catch (error) {
debugPrint("******* Error");
debugPrint(error.toString());
rethrow;
}
}
+2 -1
View File
@@ -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;
}
}
+22 -28
View File
@@ -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'] ?? "Error");
}
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<String> 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 responseMessage) {
switch (statusCode) {
case 401:
case 403:
return ServerFailure(responseMessage);
case 400:
List<String> errors = [];
if (responseMessage is List) {
for (var error in responseMessage) {
errors.add(error);
}
} else {
return ServerFailure(responseMessage);
}
return ServerFailure(errors.join('\n'));
case 404:
return ServerFailure("Your request not found, Please try later!");
case 500:
return ServerFailure(responseMessage);
default:
return ServerFailure("Opps there was an Error, Please try again!");
}
}
}
class ResponseFailure extends Failure {
ResponseFailure(super.errMessage);
@override
String toString() {
return errMessage;
}
}
-16
View File
@@ -39,20 +39,4 @@ class SpacesAPI {
);
return response;
}
static Future<List<DeviceModel>> getDevicesByRoomId(int roomId) async {
final response = await _httpService.get(
path: ApiEndpoints.devicesByRoom,
queryParameters: {"roomId": roomId, "pageSize": 10},
showServerMessage: false,
expectedResponseModel: (json) {
List<DeviceModel> devices = [];
for (var device in json['devices']) {
devices.add(DeviceModel.fromJson(device));
}
return devices;
},
);
return response;
}
}
+1 -3
View File
@@ -1,13 +1,11 @@
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);
final fileStringArray = filePath.split("/");
fileName = fileStringArray.last;
if (fileName.length > 20) {
+40
View File
@@ -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);
}
}
}
@@ -207,7 +207,6 @@ abstract class ThemeManager {
),
///card theme
//TODO edit card theme
cardTheme: const CardTheme(
elevation: 0,
shape: RoundedRectangleBorder(