diff --git a/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java index 5f086b275f..2de4621042 100644 --- a/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +++ b/mobile-v3/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -30,6 +30,11 @@ public static void registerWith(@NonNull FlutterEngine flutterEngine) { } catch (Exception e) { Log.e(TAG, "Error registering plugin google_maps_flutter_android, io.flutter.plugins.googlemaps.GoogleMapsPlugin", e); } + try { + flutterEngine.getPlugins().add(new dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin package_info_plus, dev.fluttercommunity.plus.packageinfo.PackageInfoPlugin", e); + } try { flutterEngine.getPlugins().add(new io.flutter.plugins.pathprovider.PathProviderPlugin()); } catch (Exception e) { diff --git a/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m b/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m index 523e1cd8f2..e1a144462e 100644 --- a/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m +++ b/mobile-v3/ios/Runner/GeneratedPluginRegistrant.m @@ -18,6 +18,12 @@ @import google_maps_flutter_ios; #endif +#if __has_include() +#import +#else +@import package_info_plus; +#endif + #if __has_include() #import #else @@ -29,6 +35,7 @@ @implementation GeneratedPluginRegistrant + (void)registerWithRegistry:(NSObject*)registry { [ConnectivityPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"ConnectivityPlusPlugin"]]; [FLTGoogleMapsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTGoogleMapsPlugin"]]; + [FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]]; [PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]]; } diff --git a/mobile-v3/lib/src/app/profile/pages/profile_page.dart b/mobile-v3/lib/src/app/profile/pages/profile_page.dart index ec80c54825..d82cecc6d2 100644 --- a/mobile-v3/lib/src/app/profile/pages/profile_page.dart +++ b/mobile-v3/lib/src/app/profile/pages/profile_page.dart @@ -1,6 +1,4 @@ import 'package:airqo/src/app/profile/bloc/user_bloc.dart'; -import 'package:airqo/src/app/profile/pages/widgets/devices_widget.dart'; -import 'package:airqo/src/app/profile/pages/widgets/exposure_widget.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'; @@ -29,7 +27,7 @@ class _ProfilePageState extends State { String firstName = state.model.users[0].firstName; String lastName = state.model.users[0].lastName; return DefaultTabController( - length: 3, + length: 1, child: Scaffold( appBar: AppBar( automaticallyImplyLeading: false, @@ -44,69 +42,78 @@ class _ProfilePageState extends State { children: [ SizedBox( height: 100, - child: Row( - children: [ - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - child: CircleAvatar( - backgroundColor: Theme.of(context).highlightColor, - child: Center( - child: SvgPicture.asset( - "assets/icons/user_icon.svg"), - ), - radius: 50, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: LayoutBuilder( + builder: (context, constraints) { + return Row( children: [ - Text( - "${firstName} ${lastName}", - style: TextStyle( - color: AppColors.boldHeadlineColor, - fontSize: 24, - fontWeight: FontWeight.w700, + Container( + margin: + const EdgeInsets.symmetric(horizontal: 16), + child: CircleAvatar( + backgroundColor: + Theme.of(context).highlightColor, + child: Center( + child: SvgPicture.asset( + "assets/icons/user_icon.svg"), + ), + radius: 50, ), ), - Spacer(), - Row( - children: [ - Container( - 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)), - ), - SizedBox(width: 8), - CircleAvatar( - backgroundColor: - Theme.of(context).highlightColor, - radius: 26, - child: SvgPicture.asset( - "assets/icons/notification.svg")) - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${firstName} ${lastName}", + style: TextStyle( + color: AppColors.boldHeadlineColor, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + ), + Spacer(), + Row( + children: [ + Container( + 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)), + ), + SizedBox(width: 8), + CircleAvatar( + backgroundColor: + Theme.of(context).highlightColor, + radius: 26, + child: SvgPicture.asset( + "assets/icons/notification.svg")) + ], + ) + ], + ), ) ], - ) - ], + ); + }, ), ), SizedBox(height: 32), @@ -123,21 +130,21 @@ 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/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( + // image: "assets/profile/devices.svg", + // label: "Devices")), Tab( height: 60, icon: TabIcon( @@ -146,9 +153,9 @@ class _ProfilePageState extends State { ]), Expanded( child: TabBarView(children: [ - ExposureWidget(), + // ExposureWidget(), // Container(child: Text("devices")), - DevicesWidget(), + // DevicesWidget(), SettingsWidget() ]), ) diff --git a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart index 30ba5db175..1a02748f34 100644 --- a/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart +++ b/mobile-v3/lib/src/app/profile/pages/widgets/settings_widget.dart @@ -1,68 +1,248 @@ -import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; import 'package:flutter/material.dart'; +import 'package:airqo/src/app/profile/pages/widgets/settings_tile.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:package_info_plus/package_info_plus.dart'; -class SettingsWidget extends StatelessWidget { +class SettingsWidget extends StatefulWidget { const SettingsWidget({super.key}); @override - Widget build(BuildContext context) { - return Column( - children: [ - SizedBox(height: 8), - SettingsTile( - switchValue: true, - iconPath: "assets/images/shared/location_icon.svg", - title: "Location", - onChanged: (value) { - print(value); - }, - description: - "AirQo to use your precise location to locate the Air Quality of your nearest location"), - SettingsTile( - switchValue: true, - iconPath: "assets/icons/notification.svg", - title: "Notifications", - onChanged: (value) { - print(value); + State createState() => _SettingsWidgetState(); +} +class _SettingsWidgetState extends State { + String _appVersion = ''; + bool _locationEnabled = true; + bool _notificationsEnabled = true; + + @override + void initState() { + super.initState(); + _getAppVersion(); + } + + Future _getAppVersion() async { + final packageInfo = await PackageInfo.fromPlatform(); + setState(() { + _appVersion = '${packageInfo.version}(${packageInfo.buildNumber})'; + }); + } + + void _showLogoutConfirmation() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Confirm Logout'), + content: const Text('Are you sure you want to log out?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + // TODO: Implement actual logout logic + // e.g., clear user session, revoke tokens + Navigator.of(context).pushReplacementNamed('/login'); }, - description: - "AirQo to send you in-app & push notifications & spike alerts."), - SettingsTile( - iconPath: "assets/images/shared/feedback_icon.svg", - title: "Send Feedback", - onChanged: (value) { - print(value); - }, - ), - SettingsTile( - iconPath: "assets/images/shared/airqo_story_icon.svg", - title: "Our Story", - onChanged: (value) { - print(value); - }, - ), - SettingsTile( - iconPath: "assets/images/shared/rate_app_icon.svg", - title: "Rate the App", - onChanged: (value) { - print(value); - }, + child: const Text('Log Out'), + ), + ], + ), + ); + } + + void _showDeleteAccountDialog() { + final TextEditingController passwordController = TextEditingController(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Account'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'WARNING: This action cannot be undone. All your data will be permanently deleted.', + style: TextStyle(color: Colors.red), + ), + const SizedBox(height: 16), + TextField( + controller: passwordController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'Enter Password to Confirm', + border: OutlineInputBorder(), + ), + ), + ], ), - SettingsTile( - iconPath: "assets/images/shared/terms_and_privacy.svg", - title: "Terms and Privacy Policy", - onChanged: (value) { - print(value); - }, + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + // TODO: Implement actual account deletion logic + // Validate password, call backend deletion endpoint + Navigator.of(context).pushReplacementNamed('/login'); + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: const Text('Delete Account'), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final screenWidth = MediaQuery.of(context).size.width; + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: screenHeight * 0.02), + + // Location Setting + SettingsTile( + switchValue: _locationEnabled, + iconPath: "assets/images/shared/location_icon.svg", + title: "Location", + onChanged: (value) { + setState(() { + _locationEnabled = value; + }); + print("Location setting: $value"); + }, + description: + "AirQo to use your precise location to locate the Air Quality of your nearest location", + ), + + // Notifications Setting + SettingsTile( + switchValue: _notificationsEnabled, + iconPath: "assets/icons/notification.svg", + title: "Notifications", + onChanged: (value) { + setState(() { + _notificationsEnabled = value; + }); + print("Notifications setting: $value"); + }, + description: + "AirQo to send you in-app & push notifications & spike alerts.", + ), + + // Send Feedback + SettingsTile( + iconPath: "assets/images/shared/feedback_icon.svg", + title: "Send Feedback", + onChanged: (value) { + print("Send Feedback tapped"); + }, + ), + + // Our Story + SettingsTile( + iconPath: "assets/images/shared/airqo_story_icon.svg", + title: "Our Story", + onChanged: (value) { + print("Our Story tapped"); + }, + ), + + // Terms and Privacy Policy + SettingsTile( + iconPath: "assets/images/shared/terms_and_privacy.svg", + title: "Terms and Privacy Policy", + onChanged: (value) { + print("Terms and Privacy Policy tapped"); + }, + ), + + // Logout Button + Padding( + padding: EdgeInsets.symmetric(vertical: screenHeight * 0.05), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: Size.fromHeight(screenHeight * 0.07), + ), + onPressed: _showLogoutConfirmation, + child: const Text( + "Log out", + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + // Delete Account Section + Padding( + padding: EdgeInsets.symmetric(horizontal: screenWidth * 0.3), + child: InkWell( + onTap: _showDeleteAccountDialog, + child: Text( + "Delete Account", + style: TextStyle( + color: Colors.red.shade300, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ), + ), + + SizedBox(height: screenHeight * 0.03), + + // App Info + Center( + child: Column( + children: [ + SvgPicture.asset( + "assets/images/shared/logo.svg", + height: screenHeight * 0.05, + ), + SizedBox(height: screenHeight * 0.01), + Text( + _appVersion, + style: const TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + SizedBox(height: screenHeight * 0.01), + const Text( + "A PROJECT BY", + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + ), + Text( + "Makerere University".toUpperCase(), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.white, + ), + ), + ], + ), + ), + ], ), - SettingsTile( - iconPath: "assets/images/shared/terms_and_privacy.svg", - title: "Logout", - onChanged: (value) { - print(value); - }, - ) - ], + ), ); } } diff --git a/mobile-v3/pubspec.lock b/mobile-v3/pubspec.lock index 1f13bd2d3b..8dd601d822 100644 --- a/mobile-v3/pubspec.lock +++ b/mobile-v3/pubspec.lock @@ -645,6 +645,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" + url: "https://pub.dev" + source: hosted + version: "8.1.3" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b + url: "https://pub.dev" + source: hosted + version: "3.0.2" path: dependency: transitive description: @@ -978,6 +994,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + url: "https://pub.dev" + source: hosted + version: "5.10.0" xdg_directories: dependency: transitive description: diff --git a/mobile-v3/pubspec.yaml b/mobile-v3/pubspec.yaml index a8f89d228d..6fcb1b26a8 100644 --- a/mobile-v3/pubspec.yaml +++ b/mobile-v3/pubspec.yaml @@ -56,6 +56,7 @@ dependencies: connectivity_plus: ^6.1.0 flutter_loggy: ^2.0.3+1 jwt_decoder: ^2.0.1 + package_info_plus: ^8.1.3 dev_dependencies: flutter_test: