From 72bbbed26829a5b734e31d022c2d2a44c0e6de42 Mon Sep 17 00:00:00 2001 From: Mozart299 Date: Tue, 4 Mar 2025 19:12:52 +0300 Subject: [PATCH 1/4] Add navigation to login and profile pages in DashboardAppBar; update EditProfile text --- .../dashboard/widgets/dashboard_app_bar.dart | 58 +++++++++++++------ .../src/app/profile/pages/edit_profile.dart | 11 +--- .../src/app/profile/pages/profile_page.dart | 51 ++++++---------- 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart index 4a822b7558..1c0dfb9261 100644 --- a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart +++ b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart @@ -1,3 +1,5 @@ +import 'package:airqo/src/app/auth/pages/login_page.dart'; +import 'package:airqo/src/app/profile/pages/profile_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; @@ -9,10 +11,10 @@ import '../../shared/widgets/loading_widget.dart'; class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { const DashboardAppBar({super.key}); - + @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); - + @override Widget build(BuildContext context) { return AppBar( @@ -32,10 +34,9 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildThemeToggle(BuildContext context) { final themeBloc = context.read(); - return GestureDetector( onTap: () => themeBloc.add(ToggleTheme(true)), child: CircleAvatar( @@ -47,7 +48,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildUserAvatar(BuildContext context) { return BlocBuilder( builder: (context, authState) { @@ -59,31 +60,50 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { }, ); } - + Widget _buildGuestAvatar(BuildContext context) { - return CircleAvatar( - backgroundColor: Theme.of(context).highlightColor, - radius: 24, - child: Center( - child: SvgPicture.asset( - "assets/icons/user_icon.svg", - height: 22, - width: 22, + return GestureDetector( + onTap: () { + // Navigate to login/signup screen for guest users + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => LoginPage(), + ), + ); + }, + child: CircleAvatar( + backgroundColor: Theme.of(context).highlightColor, + radius: 24, + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg", + height: 22, + width: 22, + ), ), ), ); } - + Widget _buildUserProfileAvatar(BuildContext context) { return BlocBuilder( builder: (context, userState) { if (userState is UserLoaded) { String firstName = userState.model.users[0].firstName[0].toUpperCase(); String lastName = userState.model.users[0].lastName[0].toUpperCase(); - return CircleAvatar( - radius: 24, - backgroundColor: Theme.of(context).highlightColor, - child: Center(child: Text("$firstName$lastName")), + return GestureDetector( + onTap: () { + // Navigate to profile screen with user data + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ProfilePage(), + )); + }, + child: CircleAvatar( + radius: 24, + backgroundColor: Theme.of(context).highlightColor, + child: Center(child: Text("$firstName$lastName")), + ), ); } else if (userState is UserLoadingError) { return Container(); // Handle error state (optional) diff --git a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart index 588030bb64..9376834d49 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class EditProfile extends StatefulWidget { + const EditProfile({super.key}); + @override _EditProfileState createState() => _EditProfileState(); } @@ -98,20 +100,13 @@ class _EditProfileState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Edit your name, email and add', + 'Edit your prfile details here', style: TextStyle( fontWeight: FontWeight.bold, fontSize: screenWidth * 0.035, ), ), SizedBox(height: screenHeight * 0.005), - Text( - 'an optional profile picture', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: screenWidth * 0.035, - ), - ), ], ), ), diff --git a/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart index d82cecc6d2..905a3f5689 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -1,4 +1,5 @@ import 'package:airqo/src/app/profile/bloc/user_bloc.dart'; +import 'package:airqo/src/app/profile/pages/edit_profile.dart'; import 'package:airqo/src/app/profile/pages/widgets/settings_widget.dart'; import 'package:airqo/src/meta/utils/colors.dart'; import 'package:flutter/material.dart'; @@ -52,11 +53,11 @@ class _ProfilePageState extends State { child: CircleAvatar( backgroundColor: Theme.of(context).highlightColor, + radius: 50, child: Center( child: SvgPicture.asset( "assets/icons/user_icon.svg"), ), - radius: 50, ), ), Expanded( @@ -78,26 +79,26 @@ class _ProfilePageState extends State { padding: const EdgeInsets.symmetric( horizontal: 32), height: 50, - child: Center( - child: Text("Edit your profile")), - // child: InkWell( - // onTap: () => Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => - // EditProfile())), - // child: Text( - // "Edit your profile", - // style: TextStyle( - // fontWeight: FontWeight.w500, - // color: Colors.white, - // ), - // ), - //)), decoration: BoxDecoration( color: Theme.of(context) .highlightColor, borderRadius: BorderRadius.circular(200)), + child: Center( + + child: InkWell( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + EditProfile())), + child: Text( + "Edit your profile", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + )), ), SizedBox(width: 8), CircleAvatar( @@ -130,21 +131,6 @@ class _ProfilePageState extends State { ? Colors.white : AppColors.primaryColor, tabs: [ - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/exposure.svg", - // label: "Exposure")), - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/places.svg", - // label: "Places")), - // Tab( - // height: 60, - // icon: TabIcon( - // image: "assets/profile/devices.svg", - // label: "Devices")), Tab( height: 60, icon: TabIcon( @@ -153,9 +139,6 @@ class _ProfilePageState extends State { ]), Expanded( child: TabBarView(children: [ - // ExposureWidget(), - // Container(child: Text("devices")), - // DevicesWidget(), SettingsWidget() ]), ) From ee4690177b9898280d85d777226c9e63c3a24652 Mon Sep 17 00:00:00 2001 From: Mozart299 Date: Tue, 4 Mar 2025 19:58:44 +0300 Subject: [PATCH 2/4] Refactor user profile management: add update functionality and improve error handling --- .../dashboard/widgets/dashboard_app_bar.dart | 5 +- .../lib/src/app/profile/bloc/user_bloc.dart | 46 ++++++++++++----- .../lib/src/app/profile/bloc/user_event.dart | 16 +++++- .../lib/src/app/profile/bloc/user_state.dart | 26 ++++++++++ .../profile/repository/user_repository.dart | 49 ++++++++++++++++--- .../shared/repository/base_repository.dart | 35 +++++++++++++ .../repository/base_repository_extension.dart | 44 +++++++++++++++++ 7 files changed, 199 insertions(+), 22 deletions(-) create mode 100644 src/mobile-v3/lib/src/app/shared/repository/base_repository_extension.dart diff --git a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart index 1c0dfb9261..07c1d50a60 100644 --- a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart +++ b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart @@ -93,11 +93,12 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { String lastName = userState.model.users[0].lastName[0].toUpperCase(); return GestureDetector( onTap: () { - // Navigate to profile screen with user data + // Navigate to profile page Navigator.of(context).push( MaterialPageRoute( builder: (context) => ProfilePage(), - )); + ), + ); }, child: CircleAvatar( radius: 24, diff --git a/src/mobile-v3/lib/src/app/profile/bloc/user_bloc.dart b/src/mobile-v3/lib/src/app/profile/bloc/user_bloc.dart index 220382495b..e18b14e649 100644 --- a/src/mobile-v3/lib/src/app/profile/bloc/user_bloc.dart +++ b/src/mobile-v3/lib/src/app/profile/bloc/user_bloc.dart @@ -8,20 +8,40 @@ part 'user_state.dart'; class UserBloc extends Bloc { final UserRepository repository; + UserBloc(this.repository) : super(UserInitial()) { - on((event, emit) async { - if (event is LoadUser) { - emit(UserLoading()); + on(_onLoadUser); + on(_onUpdateUser); + } + + Future _onLoadUser(LoadUser event, Emitter emit) async { + emit(UserLoading()); + + try { + ProfileResponseModel model = await repository.loadUserProfile(); + emit(UserLoaded(model)); + } catch (e) { + print(e.toString()); + emit(UserLoadingError(e.toString())); + } + } - try { - ProfileResponseModel model = await this.repository.loadUserProfile(); + Future _onUpdateUser(UpdateUser event, Emitter emit) async { + emit(UserUpdating()); - emit(UserLoaded(model)); - } catch (e) { - print(e.toString()); - emit(UserLoadingError(e.toString())); - } - } - }); + try { + ProfileResponseModel model = await repository.updateUserProfile( + firstName: event.firstName, + lastName: event.lastName, + email: event.email, + ); + + emit(UserUpdateSuccess(model)); + // Also emit UserLoaded state to update the UI + emit(UserLoaded(model)); + } catch (e) { + print(e.toString()); + emit(UserUpdateError(e.toString())); + } } -} +} \ No newline at end of file diff --git a/src/mobile-v3/lib/src/app/profile/bloc/user_event.dart b/src/mobile-v3/lib/src/app/profile/bloc/user_event.dart index 9c9a2a1baf..ddbb412b70 100644 --- a/src/mobile-v3/lib/src/app/profile/bloc/user_event.dart +++ b/src/mobile-v3/lib/src/app/profile/bloc/user_event.dart @@ -7,5 +7,19 @@ sealed class UserEvent extends Equatable { List get props => []; } +final class LoadUser extends UserEvent {} -final class LoadUser extends UserEvent{} \ No newline at end of file +final class UpdateUser extends UserEvent { + final String firstName; + final String lastName; + final String email; + + const UpdateUser({ + required this.firstName, + required this.lastName, + required this.email, + }); + + @override + List get props => [firstName, lastName, email]; +} \ No newline at end of file diff --git a/src/mobile-v3/lib/src/app/profile/bloc/user_state.dart b/src/mobile-v3/lib/src/app/profile/bloc/user_state.dart index 09fdbfa158..ed5d2c1895 100644 --- a/src/mobile-v3/lib/src/app/profile/bloc/user_state.dart +++ b/src/mobile-v3/lib/src/app/profile/bloc/user_state.dart @@ -15,10 +15,36 @@ final class UserLoaded extends UserState { final ProfileResponseModel model; const UserLoaded(this.model); + + @override + List get props => [model]; } final class UserLoadingError extends UserState { final String message; const UserLoadingError(this.message); + + @override + List get props => [message]; +} + +final class UserUpdating extends UserState {} + +final class UserUpdateSuccess extends UserState { + final ProfileResponseModel model; + + const UserUpdateSuccess(this.model); + + @override + List get props => [model]; } + +final class UserUpdateError extends UserState { + final String message; + + const UserUpdateError(this.message); + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart index 8dea977c55..7f690387a8 100644 --- a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart +++ b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart @@ -1,26 +1,63 @@ +import 'dart:convert'; import 'package:airqo/src/app/profile/models/profile_response_model.dart'; import 'package:airqo/src/app/shared/repository/base_repository.dart'; +import 'package:airqo/src/app/shared/repository/base_repository_extension.dart'; import 'package:airqo/src/app/shared/repository/hive_repository.dart'; -import 'package:http/http.dart'; +import 'package:http/http.dart' as http; abstract class UserRepository extends BaseRepository { Future loadUserProfile(); + Future updateUserProfile({ + required String firstName, + required String lastName, + required String email, + }); } class UserImpl extends UserRepository { @override Future loadUserProfile() async { final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); - if (userId == null) { throw Exception("User ID not found"); } - - Response profileResponse = + + http.Response profileResponse = await createAuthenticatedGetRequest("/api/v2/users/${userId}", {}); - + ProfileResponseModel model = profileResponseModelFromJson(profileResponse.body); return model; } -} + + @override + Future updateUserProfile({ + required String firstName, + required String lastName, + required String email, + }) async { + final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); + if (userId == null) { + throw Exception("User ID not found"); + } + + // Prepare the request body with the updated fields + final Map requestBody = { + "firstName": firstName, + "lastName": lastName, + "email": email, + }; + + // The extension should handle error responses + http.Response updateResponse = await createAuthenticatedPutRequest( + data: requestBody, + path: "/api/v2/users/$userId", + ); + + // Parse the updated user data + ProfileResponseModel model = + profileResponseModelFromJson(updateResponse.body); + + return model; + } +} \ No newline at end of file diff --git a/src/mobile-v3/lib/src/app/shared/repository/base_repository.dart b/src/mobile-v3/lib/src/app/shared/repository/base_repository.dart index 917f63701f..f73676810d 100644 --- a/src/mobile-v3/lib/src/app/shared/repository/base_repository.dart +++ b/src/mobile-v3/lib/src/app/shared/repository/base_repository.dart @@ -14,6 +14,41 @@ class BaseRepository { return _cachedToken; } + Future createAuthenticatedPutRequest({ + required String path, + required dynamic data +}) async { + String? token = await _getToken(); + if (token == null) { + throw Exception('Authentication token not found'); + } + + String url = ApiUtils.baseUrl + path; + print(url); + + Response response = await http.put( + Uri.parse(url), + body: json.encode(data), + headers: { + "Authorization": "Bearer ${token}", + "Accept": "*/*", + "Content-Type": "application/json" + } + ); + + print(response.statusCode); + + if (response.statusCode != 200) { + final responseBody = json.decode(response.body); + final errorMessage = responseBody is Map && responseBody.containsKey('message') + ? responseBody['message'] + : 'An error occurred'; + throw new Exception(errorMessage); + } + + return response; +} + Future createPostRequest( {required String path, dynamic data}) async { final token = await _getToken(); diff --git a/src/mobile-v3/lib/src/app/shared/repository/base_repository_extension.dart b/src/mobile-v3/lib/src/app/shared/repository/base_repository_extension.dart new file mode 100644 index 0000000000..96010c2e15 --- /dev/null +++ b/src/mobile-v3/lib/src/app/shared/repository/base_repository_extension.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; +import 'package:airqo/src/app/shared/repository/base_repository.dart'; +import 'package:airqo/src/app/shared/repository/hive_repository.dart'; +import 'package:airqo/src/meta/utils/api_utils.dart'; +import 'package:http/http.dart' as http; + +// Extension to add PUT request functionality to BaseRepository +extension BaseRepositoryExtension on BaseRepository { + // Method to create authenticated PUT requests + Future createAuthenticatedPutRequest({ + required String path, + required dynamic data + }) async { + final token = await HiveRepository.getData("token", HiveBoxNames.authBox); + if (token == null) { + throw Exception('Authentication token not found'); + } + + String url = ApiUtils.baseUrl + path; + print(url); + + http.Response response = await http.put( + Uri.parse(url), + body: json.encode(data), + headers: { + "Authorization": "Bearer ${token}", + "Accept": "*/*", + "Content-Type": "application/json" + } + ); + + print(response.statusCode); + + if (response.statusCode != 200) { + final responseBody = json.decode(response.body); + final errorMessage = responseBody is Map && responseBody.containsKey('message') + ? responseBody['message'] + : 'An error occurred'; + throw new Exception(errorMessage); + } + + return response; + } +} \ No newline at end of file From e956e67ee6fc48207f73486231461b47a05676fb Mon Sep 17 00:00:00 2001 From: Mozart299 Date: Tue, 4 Mar 2025 20:19:30 +0300 Subject: [PATCH 3/4] Refactor updateUserProfile method: enhance error handling and add debugging output --- .../src/app/profile/pages/edit_profile.dart | 424 +++++++++++++----- .../profile/repository/user_repository.dart | 75 ++-- 2 files changed, 365 insertions(+), 134 deletions(-) diff --git a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart index 9376834d49..2ed1712d42 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart @@ -1,17 +1,141 @@ +import 'dart:async'; +import 'package:airqo/src/app/profile/bloc/user_bloc.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; class EditProfile extends StatefulWidget { const EditProfile({super.key}); @override - _EditProfileState createState() => _EditProfileState(); + State createState() => _EditProfileState(); } class _EditProfileState extends State { - final _firstNameController = TextEditingController(text: 'Nagawa'); - final _lastNameController = TextEditingController(text: 'Alice'); - final _emailController = TextEditingController(text: 'nagawaalice@gmail.com'); + final _firstNameController = TextEditingController(); + final _lastNameController = TextEditingController(); + final _emailController = TextEditingController(); + bool _isLoading = false; + bool _formChanged = false; + Timer? _loadingTimeout; + + @override + void initState() { + super.initState(); + _populateFields(); + } + + void _populateFields() { + final userState = context.read().state; + if (userState is UserLoaded) { + final user = userState.model.users[0]; + setState(() { + _firstNameController.text = user.firstName; + _lastNameController.text = user.lastName; + _emailController.text = user.email; + }); + } + } + + bool _validateEmail(String email) { + final emailRegExp = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); + return emailRegExp.hasMatch(email); + } + + bool _validateForm() { + if (_firstNameController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('First name cannot be empty')), + ); + return false; + } + + if (_lastNameController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Last name cannot be empty')), + ); + return false; + } + + if (_emailController.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Email cannot be empty')), + ); + return false; + } + + if (!_validateEmail(_emailController.text.trim())) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Please enter a valid email address')), + ); + return false; + } + + return true; + } + + void _resetLoadingState() { + if (mounted) { + setState(() { + _isLoading = false; + }); + + _loadingTimeout?.cancel(); + _loadingTimeout = null; + } + } + + void _updateProfile() { + // Only update if form has changed and not already loading + if (!_formChanged || _isLoading) return; + + // Validate form fields + if (!_validateForm()) return; + + setState(() { + _isLoading = true; + }); + + // Set a timeout to reset loading state if the API takes too long + _loadingTimeout = Timer(Duration(seconds: 15), () { + if (_isLoading && mounted) { + _resetLoadingState(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Update request timed out. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + }); + + try { + context.read().add( + UpdateUser( + firstName: _firstNameController.text.trim(), + lastName: _lastNameController.text.trim(), + email: _emailController.text.trim(), + ), + ); + } catch (e) { + print('Error dispatching UpdateUser event: $e'); + _resetLoadingState(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating profile: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + + void _onFieldChanged() { + if (!_formChanged) { + setState(() { + _formChanged = true; + }); + } + } @override Widget build(BuildContext context) { @@ -20,127 +144,213 @@ class _EditProfileState extends State { final avatarRadius = screenWidth * 0.15; final padding = screenWidth * 0.05; - final iconSize = screenWidth * 0.07; - - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: Icon(Icons.arrow_back), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Center( - child: Text( - 'Edit Profile', - style: TextStyle(fontSize: 20), - ), - ), - actions: [ - TextButton( - onPressed: () { - - }, - child: Text( - 'Done', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16.0, + final iconSize = screenWidth * 0.07; + + return BlocConsumer( + listener: (context, state) { + print('Current state: $state'); + + if (state is UserUpdateSuccess) { + _resetLoadingState(); + setState(() { + _formChanged = false; + }); + + // Load the user profile to ensure UI is up-to-date + context.read().add(LoadUser()); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Profile updated successfully'), + backgroundColor: Colors.green, + ), + ); + + // Navigate back after successful update + Navigator.of(context).pop(); + } else if (state is UserUpdateError) { + _resetLoadingState(); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating profile: ${state.message}'), + backgroundColor: Colors.red, + ), + ); + } + }, + // Also build the UI based on state changes + builder: (context, state) { + // If the state has changed to UserUpdating and we're not already loading, + // update our local loading state + if (state is UserUpdating && !_isLoading) { + _isLoading = true; + } + + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: () { + if (_formChanged) { + // Show confirmation dialog if changes were made + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Discard Changes?'), + content: Text('You have unsaved changes. Are you sure you want to go back?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: Text('Discard'), + ), + ], + ), + ); + } else { + Navigator.of(context).pop(); + } + }, + ), + title: Center( + child: Text( + 'Edit Profile', + style: TextStyle(fontSize: 20), ), ), + actions: [ + TextButton( + onPressed: _isLoading ? null : _updateProfile, + child: _isLoading + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Text( + 'Done', + style: TextStyle( + color: _formChanged ? Colors.white : Colors.white.withOpacity(0.5), + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), + ), + ), + ], ), - ], - ), - body: SingleChildScrollView( - child: Padding( - padding: EdgeInsets.all(padding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(padding), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Stack( + Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CircleAvatar( - backgroundColor: Theme.of(context).highlightColor, - radius: avatarRadius, - child: SvgPicture.asset( - 'assets/icons/user_icon.svg', - width: avatarRadius * 1.5, - height: avatarRadius * 1.5, - ), + Stack( + children: [ + CircleAvatar( + backgroundColor: Theme.of(context).highlightColor, + radius: avatarRadius, + child: SvgPicture.asset( + 'assets/icons/user_icon.svg', + width: avatarRadius * 1.5, + height: avatarRadius * 1.5, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + decoration: BoxDecoration( + color: Colors.blue, + shape: BoxShape.circle, + ), + padding: EdgeInsets.all(iconSize * 0.4), + child: Icon( + Icons.edit, + color: Colors.white, + size: iconSize, + ), + ), + ), + ], ), - Positioned( - bottom: 0, - right: 0, - child: Container( - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, + SizedBox(width: padding), + Expanded( + child: Padding( + padding: EdgeInsets.symmetric( + vertical: screenHeight * 0.05, ), - padding: EdgeInsets.all(iconSize * 0.4), - child: Icon( - Icons.edit, - color: Colors.white, - size: iconSize, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Edit your profile details here', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: screenWidth * 0.035, + ), + ), + SizedBox(height: screenHeight * 0.005), + ], ), ), ), ], ), - SizedBox(width: padding), - Padding( - padding: EdgeInsets.symmetric( - vertical: screenHeight * 0.05, + SizedBox(height: screenHeight * 0.05), + TextField( + controller: _firstNameController, + decoration: InputDecoration( + labelText: 'First Name', + border: OutlineInputBorder(), ), - child: Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Edit your prfile details here', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: screenWidth * 0.035, - ), - ), - SizedBox(height: screenHeight * 0.005), - ], - ), + onChanged: (_) => _onFieldChanged(), + ), + SizedBox(height: screenHeight * 0.03), + TextField( + controller: _lastNameController, + decoration: InputDecoration( + labelText: 'Last Name', + border: OutlineInputBorder(), + ), + onChanged: (_) => _onFieldChanged(), + ), + SizedBox(height: screenHeight * 0.03), + TextField( + controller: _emailController, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), ), + onChanged: (_) => _onFieldChanged(), ), ], ), - SizedBox(height: screenHeight * 0.05), - TextField( - controller: _firstNameController, - decoration: InputDecoration( - labelText: 'First Name', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: screenHeight * 0.03), - TextField( - controller: _lastNameController, - decoration: InputDecoration( - labelText: 'Last Name', - border: OutlineInputBorder(), - ), - ), - SizedBox(height: screenHeight * 0.03), - TextField( - controller: _emailController, - decoration: InputDecoration( - labelText: 'Email', - border: OutlineInputBorder(), - ), - ), - ], + ), ), - ), - ), + ); + }, ); } -} + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _emailController.dispose(); + _loadingTimeout?.cancel(); + super.dispose(); + } +} \ No newline at end of file diff --git a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart index 7f690387a8..8b5cdd3e6d 100644 --- a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart +++ b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart @@ -1,7 +1,7 @@ import 'dart:convert'; + import 'package:airqo/src/app/profile/models/profile_response_model.dart'; import 'package:airqo/src/app/shared/repository/base_repository.dart'; -import 'package:airqo/src/app/shared/repository/base_repository_extension.dart'; import 'package:airqo/src/app/shared/repository/hive_repository.dart'; import 'package:http/http.dart' as http; @@ -23,41 +23,62 @@ class UserImpl extends UserRepository { } http.Response profileResponse = - await createAuthenticatedGetRequest("/api/v2/users/${userId}", {}); + await createAuthenticatedGetRequest("/api/v2/users/$userId", {}); ProfileResponseModel model = profileResponseModelFromJson(profileResponse.body); return model; } - @override - Future updateUserProfile({ - required String firstName, - required String lastName, - required String email, - }) async { - final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); - if (userId == null) { - throw Exception("User ID not found"); - } +@override +Future updateUserProfile({ + required String firstName, + required String lastName, + required String email, +}) async { + final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); + if (userId == null) { + throw Exception("User ID not found"); + } + + // Prepare the request body with the updated fields + final Map requestBody = { + "firstName": firstName, + "lastName": lastName, + "email": email, + }; + + http.Response updateResponse = await createAuthenticatedPutRequest( + path: "/api/v2/users/$userId", + data: requestBody, + ); + + print("https://api.airqo.net/api/v2/users/$userId"); + print(updateResponse.statusCode); + + // Print the full response body for debugging + print("Full response body: ${updateResponse.body}"); + + try { + final responseBody = json.decode(updateResponse.body); + print("Response structure: ${responseBody.keys}"); - // Prepare the request body with the updated fields - final Map requestBody = { - "firstName": firstName, - "lastName": lastName, - "email": email, - }; + // Print details of each key to identify null values + responseBody.forEach((key, value) { + print("Key: $key, Value: $value, Type: ${value?.runtimeType}"); + }); - // The extension should handle error responses - http.Response updateResponse = await createAuthenticatedPutRequest( - data: requestBody, - path: "/api/v2/users/$userId", - ); + // Print user object structure if it exists + if (responseBody.containsKey('user') && responseBody['user'] != null) { + print("User object keys: ${(responseBody['user'] as Map).keys}"); + } - // Parse the updated user data - ProfileResponseModel model = - profileResponseModelFromJson(updateResponse.body); + // Try a simpler approach - just reload the profile + return await loadUserProfile(); - return model; + } catch (e) { + print("Error parsing update response: $e"); + throw Exception("Failed to update profile: $e"); } +} } \ No newline at end of file From c04d1d9b592045d7f966bd74ff7b06577263865d Mon Sep 17 00:00:00 2001 From: Mozart299 Date: Tue, 4 Mar 2025 20:21:30 +0300 Subject: [PATCH 4/4] Refactor DashboardHeader and DashboardAppBar for improved readability; streamline ProfilePage and UserRepository code --- .../location_selection_screen.dart | 244 +++++++++--------- .../dashboard/widgets/dashboard_app_bar.dart | 17 +- .../dashboard/widgets/dashboard_header.dart | 29 +-- .../src/app/profile/pages/edit_profile.dart | 36 +-- .../src/app/profile/pages/profile_page.dart | 31 +-- .../profile/repository/user_repository.dart | 101 ++++---- 6 files changed, 228 insertions(+), 230 deletions(-) diff --git a/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart b/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart index 760cb183eb..ec266dada4 100644 --- a/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart +++ b/src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart @@ -47,51 +47,52 @@ class _LocationSelectionScreenState extends State UserPreferencesModel? userPreferences; @override -void initState() { - super.initState(); - loggy.info('initState called'); + void initState() { + super.initState(); + loggy.info('initState called'); - _initializeUserData(); + _initializeUserData(); - googlePlacesBloc = context.read() - ..add(ResetGooglePlaces()); + googlePlacesBloc = context.read() + ..add(ResetGooglePlaces()); - loggy.info('Checking dashboard state'); - final dashboardBloc = context.read(); - final currentState = dashboardBloc.state; - loggy.info('Current dashboard state: ${currentState.runtimeType}'); + loggy.info('Checking dashboard state'); + final dashboardBloc = context.read(); + final currentState = dashboardBloc.state; + loggy.info('Current dashboard state: ${currentState.runtimeType}'); + + if (currentState is DashboardLoaded) { + loggy.info('Dashboard already loaded, populating measurements'); + if (currentState.response.measurements != null) { + loggy.info( + 'Found ${currentState.response.measurements!.length} measurements in loaded state'); + _populateMeasurements(currentState.response.measurements!); + + // IMPORTANT ADDITION: Pre-select existing locations from the current DashboardState + if (currentState.userPreferences != null && + currentState.userPreferences!.selectedSites.isNotEmpty) { + final existingIds = currentState.userPreferences!.selectedSites + .map((site) => site.id) + .toSet(); - if (currentState is DashboardLoaded) { - loggy.info('Dashboard already loaded, populating measurements'); - if (currentState.response.measurements != null) { - loggy.info( - 'Found ${currentState.response.measurements!.length} measurements in loaded state'); - _populateMeasurements(currentState.response.measurements!); - - // IMPORTANT ADDITION: Pre-select existing locations from the current DashboardState - if (currentState.userPreferences != null && - currentState.userPreferences!.selectedSites.isNotEmpty) { - final existingIds = currentState.userPreferences!.selectedSites - .map((site) => site.id) - .toSet(); - - loggy.info('Pre-selecting ${existingIds.length} existing locations from dashboard state'); + loggy.info( + 'Pre-selecting ${existingIds.length} existing locations from dashboard state'); + setState(() { + selectedLocations = existingIds; + }); + } + } else { + loggy.warning('No measurements in loaded state'); setState(() { - selectedLocations = existingIds; + isLoading = false; + errorMessage = "No measurements available in loaded state"; }); } } else { - loggy.warning('No measurements in loaded state'); - setState(() { - isLoading = false; - errorMessage = "No measurements available in loaded state"; - }); + loggy.info('Dispatching LoadDashboard event'); + dashboardBloc.add(LoadDashboard()); } - } else { - loggy.info('Dispatching LoadDashboard event'); - dashboardBloc.add(LoadDashboard()); } -} Future _initializeUserData() async { try { @@ -174,100 +175,99 @@ void initState() { // File: src/mobile-v3/lib/src/app/dashboard/pages/location_selection/location_selection_screen.dart // Update the _saveSelectedLocations method in the LocationSelectionScreen -Future _saveSelectedLocations() async { - loggy.info( - 'Save button pressed with ${selectedLocations.length} selected locations'); + Future _saveSelectedLocations() async { + loggy.info( + 'Save button pressed with ${selectedLocations.length} selected locations'); + + // Debug token + await AuthHelper.debugToken(); + + // Check auth state from the bloc + final authState = context.read().state; + final isLoggedIn = authState is AuthLoaded; + + loggy.info('Current auth state: ${authState.runtimeType}'); + loggy.info('Is user logged in? $isLoggedIn'); + + if (!isLoggedIn) { + loggy.warning('❌ User not logged in, cannot save'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please log in to save your locations')), + ); + return; + } - // Debug token - await AuthHelper.debugToken(); + // Use enhanced token checker + final isExpired = await TokenDebugger.checkTokenExpiration(); + + if (isExpired) { + loggy.warning('❌ Token is expired, cannot save'); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Your session has expired. Please log in again.'), + duration: const Duration(seconds: 8), + action: SnackBarAction( + label: 'Log In', + onPressed: () { + // Navigate directly to login screen + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (context) => const LoginPage(), + ), + (route) => false, + ); + }, + ), + ), + ); + return; + } - // Check auth state from the bloc - final authState = context.read().state; - final isLoggedIn = authState is AuthLoaded; + setState(() { + isSaving = true; + }); - loggy.info('Current auth state: ${authState.runtimeType}'); - loggy.info('Is user logged in? $isLoggedIn'); + try { + // IMPORTANT CHANGE: Instead of creating a new preference, we dispatch + // the UpdateSelectedLocations event to the DashboardBloc, which will + // merge these with existing locations - if (!isLoggedIn) { - loggy.warning('❌ User not logged in, cannot save'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please log in to save your locations')), - ); - return; - } + final dashboardBloc = context.read(); - // Use enhanced token checker - final isExpired = await TokenDebugger.checkTokenExpiration(); - - if (isExpired) { - loggy.warning('❌ Token is expired, cannot save'); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text('Your session has expired. Please log in again.'), - duration: const Duration(seconds: 8), - action: SnackBarAction( - label: 'Log In', - onPressed: () { - // Navigate directly to login screen - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (context) => const LoginPage(), - ), - (route) => false, - ); - }, - ), - ), - ); - return; - } - - setState(() { - isSaving = true; - }); - - try { - // IMPORTANT CHANGE: Instead of creating a new preference, we dispatch - // the UpdateSelectedLocations event to the DashboardBloc, which will - // merge these with existing locations - - final dashboardBloc = context.read(); - - // Convert the Set to a List - final locationIdsList = selectedLocations.toList(); - - loggy.info('Dispatching UpdateSelectedLocations with ${locationIdsList.length} locations'); - - // Dispatch the event - dashboardBloc.add(UpdateSelectedLocations(locationIdsList)); - - // Show success message - loggy.info('✅ Successfully dispatched update event'); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Locations saved successfully')), - ); - - // Return to previous screen with the selected locations - Navigator.pop(context, locationIdsList); - } catch (e) { - loggy.error('❌ Error saving locations: $e'); - loggy.error('Stack trace: ${StackTrace.current}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'An error occurred while saving locations: ${e.toString()}')), - ); - } finally { - if (mounted) { - setState(() { - isSaving = false; - }); - } - } -} + // Convert the Set to a List + final locationIdsList = selectedLocations.toList(); + + loggy.info( + 'Dispatching UpdateSelectedLocations with ${locationIdsList.length} locations'); + // Dispatch the event + dashboardBloc.add(UpdateSelectedLocations(locationIdsList)); + // Show success message + loggy.info('✅ Successfully dispatched update event'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Locations saved successfully')), + ); + + // Return to previous screen with the selected locations + Navigator.pop(context, locationIdsList); + } catch (e) { + loggy.error('❌ Error saving locations: $e'); + loggy.error('Stack trace: ${StackTrace.current}'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'An error occurred while saving locations: ${e.toString()}')), + ); + } finally { + if (mounted) { + setState(() { + isSaving = false; + }); + } + } + } Future _loadUserPreferences(String userId) async { try { diff --git a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart index 07c1d50a60..c2b13b9573 100644 --- a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart +++ b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_app_bar.dart @@ -11,10 +11,10 @@ import '../../shared/widgets/loading_widget.dart'; class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { const DashboardAppBar({super.key}); - + @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); - + @override Widget build(BuildContext context) { return AppBar( @@ -34,7 +34,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildThemeToggle(BuildContext context) { final themeBloc = context.read(); return GestureDetector( @@ -48,7 +48,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildUserAvatar(BuildContext context) { return BlocBuilder( builder: (context, authState) { @@ -60,7 +60,7 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { }, ); } - + Widget _buildGuestAvatar(BuildContext context) { return GestureDetector( onTap: () { @@ -84,12 +84,13 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { ), ); } - + Widget _buildUserProfileAvatar(BuildContext context) { return BlocBuilder( builder: (context, userState) { if (userState is UserLoaded) { - String firstName = userState.model.users[0].firstName[0].toUpperCase(); + String firstName = + userState.model.users[0].firstName[0].toUpperCase(); String lastName = userState.model.users[0].lastName[0].toUpperCase(); return GestureDetector( onTap: () { @@ -118,4 +119,4 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { }, ); } -} \ No newline at end of file +} diff --git a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart index 94d682a560..02b4d3d2bf 100644 --- a/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart +++ b/src/mobile-v3/lib/src/app/dashboard/widgets/dashboard_header.dart @@ -13,22 +13,19 @@ class DashboardHeader extends StatelessWidget { Widget build(BuildContext context) { return PagePadding( padding: 16, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 16), - _buildGreeting(context), - Text( - "Today's Air Quality • ${DateFormat.MMMMd().format(DateTime.now())}", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.headlineMedium?.color, - ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox(height: 16), + _buildGreeting(context), + Text( + "Today's Air Quality • ${DateFormat.MMMMd().format(DateTime.now())}", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.headlineMedium?.color, ), - SizedBox(height: 16) - ] - ), + ), + SizedBox(height: 16) + ]), ); } @@ -97,4 +94,4 @@ class DashboardHeader extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart index 2ed1712d42..c98c6ae05a 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/edit_profile.dart @@ -38,7 +38,8 @@ class _EditProfileState extends State { } bool _validateEmail(String email) { - final emailRegExp = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); + final emailRegExp = RegExp( + r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+"); return emailRegExp.hasMatch(email); } @@ -49,28 +50,28 @@ class _EditProfileState extends State { ); return false; } - + if (_lastNameController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Last name cannot be empty')), ); return false; } - + if (_emailController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Email cannot be empty')), ); return false; } - + if (!_validateEmail(_emailController.text.trim())) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Please enter a valid email address')), ); return false; } - + return true; } @@ -79,7 +80,7 @@ class _EditProfileState extends State { setState(() { _isLoading = false; }); - + _loadingTimeout?.cancel(); _loadingTimeout = null; } @@ -88,7 +89,7 @@ class _EditProfileState extends State { void _updateProfile() { // Only update if form has changed and not already loading if (!_formChanged || _isLoading) return; - + // Validate form fields if (!_validateForm()) return; @@ -149,28 +150,28 @@ class _EditProfileState extends State { return BlocConsumer( listener: (context, state) { print('Current state: $state'); - + if (state is UserUpdateSuccess) { _resetLoadingState(); setState(() { _formChanged = false; }); - + // Load the user profile to ensure UI is up-to-date context.read().add(LoadUser()); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Profile updated successfully'), backgroundColor: Colors.green, ), ); - + // Navigate back after successful update Navigator.of(context).pop(); } else if (state is UserUpdateError) { _resetLoadingState(); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error updating profile: ${state.message}'), @@ -186,7 +187,7 @@ class _EditProfileState extends State { if (state is UserUpdating && !_isLoading) { _isLoading = true; } - + return Scaffold( appBar: AppBar( leading: IconButton( @@ -198,7 +199,8 @@ class _EditProfileState extends State { context: context, builder: (context) => AlertDialog( title: Text('Discard Changes?'), - content: Text('You have unsaved changes. Are you sure you want to go back?'), + content: Text( + 'You have unsaved changes. Are you sure you want to go back?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -240,7 +242,9 @@ class _EditProfileState extends State { : Text( 'Done', style: TextStyle( - color: _formChanged ? Colors.white : Colors.white.withOpacity(0.5), + color: _formChanged + ? Colors.white + : Colors.white.withOpacity(0.5), fontWeight: FontWeight.bold, fontSize: 16.0, ), @@ -353,4 +357,4 @@ class _EditProfileState extends State { _loadingTimeout?.cancel(); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart index 905a3f5689..f3502b408a 100644 --- a/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/src/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -85,25 +85,24 @@ class _ProfilePageState extends State { borderRadius: BorderRadius.circular(200)), child: Center( - - child: InkWell( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - EditProfile())), - child: Text( - "Edit your profile", - style: TextStyle( - fontWeight: FontWeight.w500, - color: Colors.white, + child: InkWell( + onTap: () => Navigator.of(context) + .push(MaterialPageRoute( + builder: (context) => + EditProfile())), + child: Text( + "Edit your profile", + style: TextStyle( + fontWeight: FontWeight.w500, + color: Colors.white, + ), ), - ), )), ), SizedBox(width: 8), CircleAvatar( - backgroundColor: - Theme.of(context).highlightColor, + backgroundColor: Theme.of(context) + .highlightColor, radius: 26, child: SvgPicture.asset( "assets/icons/notification.svg")) @@ -138,9 +137,7 @@ class _ProfilePageState extends State { label: "Settings")), ]), Expanded( - child: TabBarView(children: [ - SettingsWidget() - ]), + child: TabBarView(children: [SettingsWidget()]), ) ], )), diff --git a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart index 8b5cdd3e6d..92edb716b1 100644 --- a/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart +++ b/src/mobile-v3/lib/src/app/profile/repository/user_repository.dart @@ -21,64 +21,63 @@ class UserImpl extends UserRepository { if (userId == null) { throw Exception("User ID not found"); } - + http.Response profileResponse = await createAuthenticatedGetRequest("/api/v2/users/$userId", {}); - + ProfileResponseModel model = profileResponseModelFromJson(profileResponse.body); return model; } -@override -Future updateUserProfile({ - required String firstName, - required String lastName, - required String email, -}) async { - final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); - if (userId == null) { - throw Exception("User ID not found"); - } - - // Prepare the request body with the updated fields - final Map requestBody = { - "firstName": firstName, - "lastName": lastName, - "email": email, - }; - - http.Response updateResponse = await createAuthenticatedPutRequest( - path: "/api/v2/users/$userId", - data: requestBody, - ); - - print("https://api.airqo.net/api/v2/users/$userId"); - print(updateResponse.statusCode); - - // Print the full response body for debugging - print("Full response body: ${updateResponse.body}"); - - try { - final responseBody = json.decode(updateResponse.body); - print("Response structure: ${responseBody.keys}"); - - // Print details of each key to identify null values - responseBody.forEach((key, value) { - print("Key: $key, Value: $value, Type: ${value?.runtimeType}"); - }); - - // Print user object structure if it exists - if (responseBody.containsKey('user') && responseBody['user'] != null) { - print("User object keys: ${(responseBody['user'] as Map).keys}"); + @override + Future updateUserProfile({ + required String firstName, + required String lastName, + required String email, + }) async { + final userId = await HiveRepository.getData("userId", HiveBoxNames.authBox); + if (userId == null) { + throw Exception("User ID not found"); + } + + // Prepare the request body with the updated fields + final Map requestBody = { + "firstName": firstName, + "lastName": lastName, + "email": email, + }; + + http.Response updateResponse = await createAuthenticatedPutRequest( + path: "/api/v2/users/$userId", + data: requestBody, + ); + + print("https://api.airqo.net/api/v2/users/$userId"); + print(updateResponse.statusCode); + + // Print the full response body for debugging + print("Full response body: ${updateResponse.body}"); + + try { + final responseBody = json.decode(updateResponse.body); + print("Response structure: ${responseBody.keys}"); + + // Print details of each key to identify null values + responseBody.forEach((key, value) { + print("Key: $key, Value: $value, Type: ${value?.runtimeType}"); + }); + + // Print user object structure if it exists + if (responseBody.containsKey('user') && responseBody['user'] != null) { + print("User object keys: ${(responseBody['user'] as Map).keys}"); + } + + // Try a simpler approach - just reload the profile + return await loadUserProfile(); + } catch (e) { + print("Error parsing update response: $e"); + throw Exception("Failed to update profile: $e"); } - - // Try a simpler approach - just reload the profile - return await loadUserProfile(); - - } catch (e) { - print("Error parsing update response: $e"); - throw Exception("Failed to update profile: $e"); } } -} \ No newline at end of file