diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 9287665..ad16c69 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,12 +2,17 @@
+
+
+
+
+
+ android:allowBackup="false">
+ NSPhotoLibraryUsageDescription
+ We need access to your photo library to allow you to select and upload photos.
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
diff --git a/lib/features/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart
index eee72a1..a964a53 100644
--- a/lib/features/app_layout/bloc/home_cubit.dart
+++ b/lib/features/app_layout/bloc/home_cubit.dart
@@ -22,6 +22,7 @@ import 'package:syncrow_app/features/scene/view/scene_view.dart';
import 'package:syncrow_app/generated/assets.dart';
import 'package:syncrow_app/navigation/navigation_service.dart';
import 'package:syncrow_app/services/api/devices_api.dart';
+import 'package:syncrow_app/services/api/profile_api.dart';
import 'package:syncrow_app/services/api/spaces_api.dart';
import 'package:syncrow_app/utils/helpers/custom_page_route.dart';
import 'package:syncrow_app/utils/helpers/snack_bar.dart';
@@ -33,6 +34,7 @@ part 'home_state.dart';
class HomeCubit extends Cubit {
HomeCubit._() : super(HomeInitial()) {
checkIfNotificationPermissionGranted();
+ fetchUserInfo();
if (selectedSpace == null) {
fetchUnitsByUserId();
// .then((value) {
@@ -42,7 +44,7 @@ class HomeCubit extends Cubit {
// });
}
}
-
+ static UserModel? user;
static HomeCubit? _instance;
static HomeCubit getInstance() {
// If an instance already exists, return it
@@ -50,6 +52,18 @@ class HomeCubit extends Cubit {
return _instance!;
}
+
+ Future fetchUserInfo() async {
+ try {
+ var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
+ user = await ProfileApi().fetchUserInfo(uuid);
+ emit(HomeUserInfoLoaded(user!)); // Emit state after fetching user info
+
+ } catch (e) {
+ return;
+ }
+ }
+
void emitSafe(HomeState newState) {
final cubit = this;
if (!cubit.isClosed) {
diff --git a/lib/features/app_layout/bloc/home_state.dart b/lib/features/app_layout/bloc/home_state.dart
index 9c20d4e..1125e01 100644
--- a/lib/features/app_layout/bloc/home_state.dart
+++ b/lib/features/app_layout/bloc/home_state.dart
@@ -58,3 +58,9 @@ class RoomSelected extends HomeState {
class RoomUnSelected extends HomeState {}
class NavChangePage extends HomeState {}
+// Define new state classes
+class HomeUserInfoLoaded extends HomeState {
+ final UserModel user;
+
+ HomeUserInfoLoaded(this.user);
+}
diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart
index efe9794..5f05563 100644
--- a/lib/features/auth/bloc/auth_cubit.dart
+++ b/lib/features/auth/bloc/auth_cubit.dart
@@ -1,3 +1,4 @@
+
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -8,6 +9,7 @@ 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/profile_api.dart';
import 'package:syncrow_app/utils/helpers/shared_preferences_helper.dart';
import 'package:syncrow_app/utils/helpers/snack_bar.dart';
import 'package:syncrow_app/utils/resource_manager/strings_manager.dart';
@@ -179,12 +181,14 @@ class AuthCubit extends Cubit {
if (token.accessTokenIsNotEmpty) {
debugPrint('token: ${token.accessToken}');
FlutterSecureStorage storage = const FlutterSecureStorage();
- await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken);
-
+ await storage.write(
+ key: Token.loginAccessTokenKey,
+ value: token.accessToken
+ );
const FlutterSecureStorage().write(
key: UserModel.userUuidKey,
- value: Token.decodeToken(token.accessToken)['uuid'].toString());
-
+ value: Token.decodeToken(token.accessToken)['uuid'].toString()
+ );
user = UserModel.fromToken(token);
emailController.clear();
passwordController.clear();
@@ -278,8 +282,7 @@ class AuthCubit extends Cubit {
try {
emit(AuthTokenLoading());
const storage = FlutterSecureStorage();
- final firstLaunch =
- await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true;
+ final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true;
if (firstLaunch) {
storage.deleteAll();
@@ -312,6 +315,7 @@ class AuthCubit extends Cubit {
}
}
+
sendToForgetPassword({required String password}) async {
try {
emit(AuthForgetPassLoading());
@@ -321,4 +325,8 @@ class AuthCubit extends Cubit {
emit(AuthForgetPassError(message: 'Something went wrong'));
}
}
+
+
+
+
}
diff --git a/lib/features/auth/model/user_model.dart b/lib/features/auth/model/user_model.dart
index 5491d73..fd68fb4 100644
--- a/lib/features/auth/model/user_model.dart
+++ b/lib/features/auth/model/user_model.dart
@@ -1,63 +1,81 @@
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
import 'package:syncrow_app/features/auth/model/token.dart';
class UserModel {
static String userUuidKey = 'userUuid';
final String? uuid;
final String? email;
- final String? name;
- final String? photoUrl;
-
+ final String? firstName;
+ final String? lastName;
+ final Uint8List? profilePicture;
final String? phoneNumber;
-
final bool? isEmailVerified;
-
+ final String? regionName;
+ final String? timeZone;
final bool? isAgreementAccepted;
UserModel({
required this.uuid,
required this.email,
- required this.name,
- required this.photoUrl,
+ required this.firstName,
+ required this.lastName,
+ required this.profilePicture,
required this.phoneNumber,
required this.isEmailVerified,
required this.isAgreementAccepted,
+ required this.regionName, // Add this line
+ required this.timeZone, // Add this line
+
});
factory UserModel.fromJson(Map json) {
return UserModel(
- uuid: json['id'],
+ uuid: json['uuid'],
email: json['email'],
- name: json['name'],
- photoUrl: json['photoUrl'],
+ firstName: json['firstName'],
+ lastName: json['lastName'],
+ profilePicture: UserModel.decodeBase64Image(json['profilePicture']),
phoneNumber: json['phoneNumber'],
isEmailVerified: json['isEmailVerified'],
isAgreementAccepted: json['isAgreementAccepted'],
+ regionName: json['region']?['regionName'], // Extract regionName
+ timeZone: json['timeZone']?['timeZoneOffset'], // Extract regionName
);
}
-
//uuid to json
-
//from token
- factory UserModel.fromToken(Token token) {
+ factory UserModel.fromToken(Token token) {
Map tempJson = Token.decodeToken(token.accessToken);
-
return UserModel(
uuid: tempJson['uuid'].toString(),
email: tempJson['email'],
- name: null,
- photoUrl: null,
+ lastName: tempJson['lastName'],
+ firstName:tempJson['firstName'] ,
+ profilePicture: UserModel.decodeBase64Image(tempJson['profilePicture']),
phoneNumber: null,
isEmailVerified: null,
isAgreementAccepted: null,
+ regionName: tempJson['region']?['regionName'],
+ timeZone: tempJson['timezone']?['timeZoneOffset'],
);
}
+ static Uint8List? decodeBase64Image(String? base64String) {
+ if (base64String != null) {
+ return base64.decode(base64String);
+ }
+ return null;
+ }
+
Map toJson() {
return {
'id': uuid,
'email': email,
- 'name': name,
- 'photoUrl': photoUrl,
+ 'lastName': lastName,
+ 'firstName': firstName,
+ 'photoUrl': profilePicture,
'phoneNumber': phoneNumber,
'isEmailVerified': isEmailVerified,
'isAgreementAccepted': isAgreementAccepted,
diff --git a/lib/features/menu/bloc/menu_cubit.dart b/lib/features/menu/bloc/menu_cubit.dart
index a51a41e..068d947 100644
--- a/lib/features/menu/bloc/menu_cubit.dart
+++ b/lib/features/menu/bloc/menu_cubit.dart
@@ -7,85 +7,6 @@ class MenuCubit extends Cubit {
static MenuCubit of(context) => BlocProvider.of(context);
- // List menuLists = [
- // MenuListModel(
- // label: 'Home Management',
- // listItems: [
- // ListItemModel(
- // label: 'Create a Home',
- // ),
- // ListItemModel(
- // label: 'Join a Home',
- // ),
- // ListItemModel(
- // label: 'Manage Your Home',
- // ),
- // ],
- // ),
- // MenuListModel(
- // label: 'General Settings',
- // listItems: [
- // ListItemModel(
- // label: 'Voice Assistant',
- // ),
- // ListItemModel(
- // label: 'Temperature unit',
- // ),
- // ListItemModel(
- // label: 'Touch tone on panel',
- // ),
- // ListItemModel(
- // label: 'Language',
- // ),
- // ListItemModel(
- // label: 'Network diagnosis',
- // ),
- // ListItemModel(
- // label: 'Clear cache',
- // ),
- // ],
- // ),
- // MenuListModel(
- // label: 'Messages Center',
- // listItems: [
- // ListItemModel(
- // label: 'Alerts',
- // ),
- // ListItemModel(
- // label: 'Messages',
- // ),
- // ListItemModel(
- // label: 'FAQs',
- // ),
- // ListItemModel(
- // label: 'Help & Feedback',
- // ),
- // ],
- // ),
- // MenuListModel(
- // label: 'Security and Privacy',
- // listItems: [
- // ListItemModel(
- // label: 'Security',
- // ),
- // ListItemModel(
- // label: 'privacy',
- // ),
- // ],
- // ),
- // MenuListModel(
- // label: 'Legal Information',
- // listItems: [
- // ListItemModel(
- // label: 'About',
- // ),
- // ListItemModel(
- // label: 'Privacy Policy',
- // ),
- // ListItemModel(
- // label: 'User Agreement',
- // ),
- // ],
- // ),
- // ];
+ String name = '';
+
}
diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart
new file mode 100644
index 0000000..603fe20
--- /dev/null
+++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart
@@ -0,0 +1,309 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart';
+import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart';
+import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart';
+import 'package:syncrow_app/features/menu/model/region_model.dart';
+import 'package:syncrow_app/features/menu/model/time_zone_model.dart';
+import 'package:syncrow_app/services/api/profile_api.dart';
+import 'package:syncrow_app/utils/helpers/snack_bar.dart';
+import 'package:image_picker/image_picker.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+class ProfileBloc extends Bloc {
+ bool isSaving = false;
+ bool editName = false;
+ final FocusNode focusNode = FocusNode();
+ File? image;
+ final ImagePicker _picker = ImagePicker();
+ String timeZoneSelected = '';
+ String regionSelected = '';
+ final TextEditingController searchController = TextEditingController();
+ final TextEditingController nameController = TextEditingController(
+ text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}');
+ List? timeZoneList;
+ List? regionList;
+
+
+ ProfileBloc() : super(InitialState()) {
+ on(_fetchUserInfo);
+ on(_fetchTimeZone);
+ on(_fetchRegion);
+ on(saveName);
+ on(_selectImage);
+ on(_changeName);
+ on(selectTimeZone);
+ on(searchRegion);
+ on(searchTimeZone);
+ on(selectRegion);
+ }
+
+ Future saveName(SaveNameEvent event, Emitter emit) async {
+ if (_validateInputs()) return;
+ try {
+ add(const ChangeNameEvent(value: false));
+ isSaving = true;
+ emit(LoadingInitialState());
+ final fullName = nameController.text;
+ final nameParts = fullName.split(' ');
+ final firstName = nameParts[0];
+ final lastName = nameParts.length > 1 ? nameParts[1] : '';
+ var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName);
+ add(InitialProfileEvent());
+ await HomeCubit.getInstance().fetchUserInfo();
+ CustomSnackBar.displaySnackBar('Save Successfully');
+ emit(SaveState());
+ } catch (e) {
+ emit(FailedState(errorMessage: e.toString()));
+ } finally {
+ isSaving = false;
+ }
+ }
+
+ void _changeName(ChangeNameEvent event, Emitter emit) {
+ emit(LoadingInitialState());
+ editName = event.value!;
+ if (editName) {
+ Future.delayed(const Duration(milliseconds: 500), () {
+ focusNode.requestFocus();
+ });
+ }else {
+ focusNode.unfocus();
+ }
+ emit(NameEditingState(editName: editName));
+ }
+
+ void _fetchUserInfo(InitialProfileEvent event, Emitter emit) async {
+ try {
+ emit(LoadingInitialState());
+ HomeCubit.user = await ProfileApi().fetchUserInfo(HomeCubit.user!.uuid);
+ emit(SaveState());
+ } catch (e) {
+ emit(FailedState(errorMessage: e.toString()));
+ return;
+ }
+ }
+
+ Future _fetchTimeZone(TimeZoneInitialEvent event, Emitter emit) async {
+ emit(LoadingInitialState());
+ try {
+ timeZoneList = await ProfileApi.fetchTimeZone();
+ emit(UpdateState(timeZoneList: timeZoneList!));
+ return timeZoneList;
+ } catch (e) {
+ emit(FailedState(errorMessage: e.toString()));
+ return;
+ }
+ }
+
+ Future selectTimeZone(SelectTimeZoneEvent event, Emitter emit) async {
+ try {
+ emit(LoadingInitialState());
+ timeZoneSelected = event.val;
+ await ProfileApi.saveTimeZone(regionUuid: event.val);
+ CustomSnackBar.displaySnackBar('Save Successfully');
+ emit(SaveState());
+ } catch (e) {
+ emit(FailedState(errorMessage: e.toString()));
+ }
+ }
+
+ Future selectRegion(SelectRegionEvent event, Emitter emit) async {
+ try {
+ emit(LoadingInitialState());
+ await ProfileApi.saveRegion(regionUuid:event.val );
+ CustomSnackBar.displaySnackBar('Save Successfully');
+ emit(SaveState());
+ } catch (e) {
+ emit(FailedState(errorMessage: e.toString()));
+ return;
+ }
+ }
+
+ Future searchRegion(SearchRegionEvent event, Emitter emit) async {
+ emit(LoadingInitialState());
+ final query = event.query.toLowerCase();
+ if(event.query.isNotEmpty){
+ final filteredRegions = regionList?.where((region) {
+ return region.name.toLowerCase().contains(query);
+ }).toList() ?? [];
+ regionList = filteredRegions;
+ emit(RegionsLoadedState(regions: filteredRegions));
+ }else{
+ regionList = await ProfileApi.fetchRegion();
+ emit(RegionsLoadedState(regions: regionList!));
+ }
+ }
+
+ Future searchTimeZone(SearchTimeZoneEvent event, Emitter emit) async {
+ emit(LoadingInitialState());
+ final query = event.query.toLowerCase();
+ if(event.query.isNotEmpty){
+ final filtered = timeZoneList?.where((region) {
+ return region.name.toLowerCase().contains(query);
+ }).toList() ?? [];
+ timeZoneList = filtered;
+ emit(TimeZoneLoadedState(regions: filtered));
+ }else{
+ timeZoneList = await ProfileApi.fetchTimeZone();
+ emit(UpdateState(timeZoneList: timeZoneList!));
+ }
+ }
+
+
+ void _fetchRegion(RegionInitialEvent event, Emitter emit) async {
+ try {
+ emit(LoadingInitialState());
+ regionList = await ProfileApi.fetchRegion();
+ emit(RegionsLoadedState(regions: regionList!));
+ } catch (e) {
+ emit(FailedState(errorMessage: e.toString()));
+ }
+ }
+
+ Future _selectImage(SelectImageEvent event, Emitter emit) async {
+ if (await _requestPermission()) {
+ emit(ChangeImageState());
+ final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
+ if (pickedFile != null) {
+ image = File(pickedFile.path);
+ final bytes = image!.readAsBytesSync().lengthInBytes;
+ final kb = bytes / 1024;
+ final mb = kb / 1024;
+ if(mb>1){
+ image=null;
+ CustomSnackBar.displaySnackBar('Image size must be 1 MB or less');
+ }else{
+ await _saveImage();
+ }
+ } else {
+ print('No image selected.');
+ }
+ emit(ImageSelectedState());
+ } else {
+ _showPermissionDeniedDialog(event.context);
+ }
+ }
+
+ Future _saveImage() async {
+ emit(LoadingInitialState());
+ List imageBytes = image!.readAsBytesSync();
+ String base64Image = base64Encode(imageBytes);
+ print(base64Image);
+ var response = await ProfileApi.saveImage(base64Image);
+ emit(ImageSelectedState());
+ }
+
+ void _showPermissionDeniedDialog(BuildContext context) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Permission Denied'),
+ content: const Text(
+ 'Photo access is required to select an image. Please allow photo access in the app settings.'),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: const Text('Cancel'),
+ ),
+ TextButton(
+ onPressed: () {
+ openAppSettings();
+ },
+ child: const Text('Settings'),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+
+
+
+ bool _validateInputs() {
+ final nameError = fullNameValidator(nameController.text);
+ if (nameError != null) {
+ CustomSnackBar.displaySnackBar(nameError);
+ return true;
+ }
+ return false;
+ }
+
+
+ String? fullNameValidator(String? value) {
+ if (value == null) return 'Full name is required';
+
+ final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim();
+
+ if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) {
+ return 'Full name must be between 2 and 30 characters long';
+ }
+
+ // Test if it contains anything but alphanumeric spaces and single quote
+
+ if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) {
+ return 'Only alphanumeric characters, space, dash and single quote are allowed';
+ }
+
+ final parts = withoutExtraSpaces.split(' ');
+
+ if (parts.length < 2) return 'Full name must contain first and last names';
+
+ if (parts.length > 3) return 'Full name can at most contain 3 parts';
+
+ if (parts.any((part) => part.length < 2 || part.length > 30)) {
+ return 'Full name parts must be between 2 and 30 characters long';
+ }
+ return null;
+ }
+
+
+ Future _requestPermission() async {
+ DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
+ if (Platform.isAndroid ) {
+ AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
+ PermissionStatus status = await Permission.photos.status;
+ if(androidInfo.version.sdkInt<33){
+ if (status.isDenied) {
+ PermissionStatus status = await Permission.storage.request();
+ if (status.isGranted) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }else{
+ if (status.isGranted) {
+ return true;
+ } else if (status.isDenied) {
+ PermissionStatus status = await Permission.photos.request();
+ if (status.isGranted) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+ return false;
+ } else {
+ SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
+ bool firstClick = sharedPreferences.getBool('firstPermission') ?? true;
+ await sharedPreferences.setBool('firstPermission', false);
+ if (firstClick == false) {
+ var status = await Permission.photos.status;
+ return status.isGranted;
+ } else {
+ return true;
+ }
+ }
+ }
+
+}
diff --git a/lib/features/menu/bloc/profile_bloc/profile_event.dart b/lib/features/menu/bloc/profile_bloc/profile_event.dart
new file mode 100644
index 0000000..15d9336
--- /dev/null
+++ b/lib/features/menu/bloc/profile_bloc/profile_event.dart
@@ -0,0 +1,76 @@
+import 'package:equatable/equatable.dart';
+import 'package:flutter/cupertino.dart';
+
+abstract class ProfileEvent extends Equatable {
+ const ProfileEvent();
+
+ @override
+ List