From 8b011614489e2189f6658baef90ca3e7f6d21968 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Thu, 29 May 2025 13:11:27 +0530 Subject: [PATCH] Implement forgot password functionality and enhance UI in the authentication flow --- .../auth/forgot_password_controller.dart | 27 +- lib/helpers/services/auth_service.dart | 87 ++++- lib/view/auth/forgot_password_screen.dart | 124 ++++--- lib/view/auth/login_screen.dart | 10 +- lib/view/auth/request_demo_bottom_sheet.dart | 325 ++++++++++++++++++ lib/view/layouts/left_bar.dart | 3 +- 6 files changed, 524 insertions(+), 52 deletions(-) create mode 100644 lib/view/auth/request_demo_bottom_sheet.dart diff --git a/lib/controller/auth/forgot_password_controller.dart b/lib/controller/auth/forgot_password_controller.dart index fd43d39..db69cdb 100644 --- a/lib/controller/auth/forgot_password_controller.dart +++ b/lib/controller/auth/forgot_password_controller.dart @@ -4,7 +4,7 @@ import 'package:marco/controller/my_controller.dart'; import 'package:marco/helpers/services/auth_service.dart'; import 'package:marco/helpers/widgets/my_form_validator.dart'; import 'package:marco/helpers/widgets/my_validators.dart'; - +import 'package:marco/helpers/widgets/my_snackbar.dart'; class ForgotPasswordController extends MyController { MyFormValidator basicValidator = MyFormValidator(); bool showPassword = false; @@ -35,6 +35,31 @@ class ForgotPasswordController extends MyController { } } + /// New: Forgot password function + Future onForgotPassword() async { + if (basicValidator.validateForm()) { + update(); + final data = basicValidator.getData(); + final email = data['email']?.toString() ?? ''; + final result = await AuthService.forgotPassword(email); + + if (result != null) { + showAppSnackbar( + title: "Success", + message: "Your password reset link was sent.", + type: SnackbarType.success, + ); + } else { + showAppSnackbar( + title: "Success", + message: "Your password reset link was sent.", + type: SnackbarType.success, + ); + } + update(); + } + } + void gotoLogIn() { Get.toNamed('/auth/login'); } diff --git a/lib/helpers/services/auth_service.dart b/lib/helpers/services/auth_service.dart index 0327d35..60a8290 100644 --- a/lib/helpers/services/auth_service.dart +++ b/lib/helpers/services/auth_service.dart @@ -3,7 +3,7 @@ import 'package:http/http.dart' as http; import 'package:get/get.dart'; import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/controller/permission_controller.dart'; -import 'package:logger/logger.dart'; +import 'package:logger/logger.dart'; final Logger logger = Logger(); class AuthService { @@ -112,4 +112,89 @@ class AuthService { return false; } } + +// Forgot password API + static Future?> forgotPassword(String email) async { + final requestBody = {"email": email}; + + logger.i("Sending forgot password request with email: $email"); + + try { + final response = await http.post( + Uri.parse("$_baseUrl/auth/forgot-password"), + headers: _headers, + body: jsonEncode(requestBody), + ); + + logger.i( + "Forgot password API response (${response.statusCode}): ${response.body}"); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200 && responseData['success'] == true) { + logger.i("Forgot password request successful."); + return null; + } else { + return { + "error": + responseData['message'] ?? "Failed to send password reset link." + }; + } + } catch (e) { + logger.e("Exception during forgot password request: $e"); + return {"error": "Network error. Please check your connection."}; + } + } + +// Request demo API + static Future?> requestDemo( + Map demoData) async { + try { + final response = await http.post( + Uri.parse("$_baseUrl/market/inquiry"), + headers: _headers, + body: jsonEncode(demoData), + ); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200 && responseData['success'] == true) { + logger.i("Request Demo submitted successfully."); + return null; + } else { + return { + "error": responseData['message'] ?? "Failed to submit demo request." + }; + } + } catch (e) { + logger.e("Exception during request demo: $e"); + return {"error": "Network error. Please check your connection."}; + } + } + + static Future>?> getIndustries() async { + try { + final response = await http.get( + Uri.parse("$_baseUrl/market/industries"), + headers: _headers, + ); + + logger.i( + "Get Industries API response (${response.statusCode}): ${response.body}"); + + final responseData = jsonDecode(response.body); + + if (response.statusCode == 200 && responseData['success'] == true) { + // Return the list of industries as List> + final List industriesData = responseData['data']; + return industriesData.cast>(); + } else { + logger.w("Failed to fetch industries: ${responseData['message']}"); + return null; + } + } catch (e) { + logger.e("Exception during getIndustries: $e"); + return null; + } + } } diff --git a/lib/view/auth/forgot_password_screen.dart b/lib/view/auth/forgot_password_screen.dart index 1d47eda..4c9f08a 100644 --- a/lib/view/auth/forgot_password_screen.dart +++ b/lib/view/auth/forgot_password_screen.dart @@ -8,6 +8,9 @@ import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/view/layouts/auth_layout.dart'; +import 'package:marco/images.dart'; +import 'package:marco/helpers/theme/app_theme.dart'; + class ForgotPasswordScreen extends StatefulWidget { const ForgotPasswordScreen({super.key}); @@ -26,57 +29,88 @@ class _ForgotPasswordScreenState extends State with UIMixi init: controller, builder: (controller) { return Form( - key: controller.basicValidator.formKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.titleLarge("Forgot Password", fontWeight: 600), - MySpacing.height(12), - MyText.bodyMedium( - "Enter the email address associated with your account and we'll send an email instructions on how to recover your password.", + key: controller.basicValidator.formKey, + child: SingleChildScrollView( + padding: MySpacing.xy(2, 40), + child: Container( + width: double.infinity, + padding: MySpacing.all(24), + decoration: BoxDecoration( + color: theme.colorScheme.primary.withOpacity(0.02), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: contentTheme.primary.withOpacity(0.5), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Image.asset( + Images.logoDark, + height: 120, + fit: BoxFit.contain, + ), + ), + MySpacing.height(10), + MyText.titleLarge("Forgot Password", fontWeight: 600), + MySpacing.height(12), + MyText.bodyMedium( + "Enter your email and we'll send you instructions to reset your password.", fontWeight: 600, - xMuted: true), - MySpacing.height(12), - TextFormField( + xMuted: true, + ), + MySpacing.height(12), + TextFormField( validator: controller.basicValidator.getValidation('email'), controller: controller.basicValidator.getController('email'), - keyboardType: TextInputType.emailAddress, - style: MyTextStyle.labelMedium(), - decoration: InputDecoration( - labelText: "Email Address", - labelStyle: MyTextStyle.bodySmall(xMuted: true), - border: OutlineInputBorder(borderSide: BorderSide.none), - filled: true, - fillColor: contentTheme.secondary.withAlpha(36), - prefixIcon: Icon(LucideIcons.mail, size: 16), - contentPadding: MySpacing.all(15), - isDense: true, - isCollapsed: true, - floatingLabelBehavior: FloatingLabelBehavior.never, + keyboardType: TextInputType.emailAddress, + style: MyTextStyle.labelMedium(), + decoration: InputDecoration( + labelText: "Email Address", + labelStyle: MyTextStyle.bodySmall(xMuted: true), + border: OutlineInputBorder(borderSide: BorderSide.none), + filled: true, + fillColor: theme.cardColor, + prefixIcon: Icon(LucideIcons.mail, size: 16), + contentPadding: MySpacing.all(15), + isDense: true, + isCollapsed: true, + floatingLabelBehavior: FloatingLabelBehavior.never, + ), ), - ), - MySpacing.height(20), - Center( - child: MyButton.rounded( - onPressed: controller.onLogin, - elevation: 0, - padding: MySpacing.xy(20, 16), - backgroundColor: contentTheme.primary, - child: MyText.labelMedium('Forgot Password', color: contentTheme.onPrimary), + MySpacing.height(20), + Center( + child: MyButton.rounded( + onPressed: controller.onForgotPassword, + elevation: 0, + padding: MySpacing.xy(20, 16), + backgroundColor: Colors.blueAccent, + child: MyText.labelMedium( + 'Send Reset Link', + color: contentTheme.onPrimary, + ), + ), ), - ), - Center( - child: MyButton.text( - onPressed: controller.gotoLogIn, - elevation: 0, - padding: MySpacing.x(16), - splashColor: contentTheme.secondary.withValues(alpha:0.1), - child: MyText.labelMedium('Back to log in', color: contentTheme.secondary), + Center( + child: MyButton.text( + onPressed: controller.gotoLogIn, + elevation: 0, + padding: MySpacing.x(16), + splashColor: + contentTheme.secondary.withValues(alpha: 0.1), + child: MyText.labelMedium( + 'Back to log in', + color: contentTheme.secondary, + ), + ), ), - ), - ], - )); + ], + ), + ), + ), + ); }, ), ); diff --git a/lib/view/auth/login_screen.dart b/lib/view/auth/login_screen.dart index 363c71d..2ad8a6b 100644 --- a/lib/view/auth/login_screen.dart +++ b/lib/view/auth/login_screen.dart @@ -10,6 +10,7 @@ import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/view/layouts/auth_layout.dart'; import 'package:marco/images.dart'; +import 'package:marco/view/auth/request_demo_bottom_sheet.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -194,7 +195,7 @@ class _LoginScreenState extends State with UIMixin { elevation: 2, padding: MySpacing.xy(24, 16), borderRadiusAll: 16, - backgroundColor: contentTheme.primary, + backgroundColor: Colors.blueAccent, child: MyText.labelMedium( 'Login', fontWeight: 600, @@ -207,10 +208,13 @@ class _LoginScreenState extends State with UIMixin { /// Register Link Center( child: MyButton.text( - onPressed: controller.gotoRegister, + onPressed: () { + OrganizationFormBottomSheet.show(context); + }, elevation: 0, padding: MySpacing.xy(12, 8), - splashColor: contentTheme.secondary.withAlpha(30), + splashColor: + contentTheme.secondary.withAlpha(30), child: MyText.bodySmall( "Request a Demo", color: contentTheme.secondary, diff --git a/lib/view/auth/request_demo_bottom_sheet.dart b/lib/view/auth/request_demo_bottom_sheet.dart new file mode 100644 index 0000000..ffed24c --- /dev/null +++ b/lib/view/auth/request_demo_bottom_sheet.dart @@ -0,0 +1,325 @@ +import 'package:flutter/material.dart'; +import 'package:marco/helpers/widgets/my_form_validator.dart'; +import 'package:marco/helpers/services/auth_service.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class OrganizationFormBottomSheet { + static void show(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => DraggableScrollableSheet( + expand: false, + initialChildSize: 0.85, + minChildSize: 0.5, + maxChildSize: 0.95, + builder: (context, scrollController) { + return _OrganizationForm(scrollController: scrollController); + }, + ), + ); + } +} + +class _OrganizationForm extends StatefulWidget { + final ScrollController scrollController; + const _OrganizationForm({required this.scrollController}); + + @override + State<_OrganizationForm> createState() => _OrganizationFormState(); +} + +class _OrganizationFormState extends State<_OrganizationForm> { + final MyFormValidator validator = MyFormValidator(); + + List> _industries = []; + String? _selectedIndustryId; + + final List _sizes = [ + '1-10', + '11-50', + '51-200', + '201-1000', + '1000+', + ]; + + final Map _sizeApiMap = { + '1-10': 'less than 10', + '11-50': '11 to 50', + '51-200': '51 to 200', + '201-1000': 'more than 200', + '1000+': 'more than 1000', + }; + + String? _selectedSize; + bool _agreed = false; + + @override + void initState() { + super.initState(); + + _loadIndustries(); + + validator.addField('organizationName', + required: true, controller: TextEditingController()); + validator.addField('email', + required: true, controller: TextEditingController()); + validator.addField('contactPerson', + required: true, controller: TextEditingController()); + validator.addField('contactNumber', + required: true, controller: TextEditingController()); + validator.addField('about', controller: TextEditingController()); + validator.addField('address', + required: true, controller: TextEditingController()); + } + + Future _loadIndustries() async { + final industries = await AuthService.getIndustries(); + if (industries != null) { + setState(() { + _industries = industries; + }); + } + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + controller: widget.scrollController, + child: Form( + key: validator.formKey, + child: Column( + children: [ + Container( + width: 40, + height: 5, + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(10), + ), + ), + Text( + 'Adventure starts here 🚀', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 4), + const Text("Make your app management easy and fun!"), + const SizedBox(height: 20), + _buildTextField('organizationName', 'Organization Name'), + _buildTextField('email', 'Email', + keyboardType: TextInputType.emailAddress), + _buildTextField('contactPerson', 'Contact Person'), + _buildTextField('contactNumber', 'Contact Number', + keyboardType: TextInputType.phone), + _buildTextField('about', 'About Organization'), + _buildTextField('address', 'Current Address'), + const SizedBox(height: 10), + _buildPopupMenuField( + 'Organization Size', + _sizes, + _selectedSize, + (val) => setState(() => _selectedSize = val), + 'Please select organization size', + ), + _buildPopupMenuField( + 'Industry', + _industries.map((e) => e['name'] as String).toList(), + _selectedIndustryId != null + ? _industries.firstWhere( + (e) => e['id'] == _selectedIndustryId)['name'] + : null, + (val) { + setState(() { + final selectedIndustry = _industries.firstWhere( + (element) => element['name'] == val, + orElse: () => {}); + _selectedIndustryId = selectedIndustry['id']; + }); + }, + 'Please select industry', + ), + const SizedBox(height: 10), + Row( + children: [ + Checkbox( + value: _agreed, + onChanged: (val) => setState(() => _agreed = val ?? false), + fillColor: MaterialStateProperty.all(Colors.white), + checkColor: Colors.white, + side: MaterialStateBorderSide.resolveWith( + (states) => + BorderSide(color: Colors.blueAccent, width: 2), + ), + ), + const Expanded( + child: Text('I agree to privacy policy & terms')), + ], + ), + const SizedBox(height: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + ), + onPressed: _submitForm, + child: const Text("Submit"), + ), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Back to login"), + ), + ], + ), + ), + ), + ); + } + + Widget _buildPopupMenuField( + String label, + List items, + String? selectedValue, + ValueChanged onSelected, + String errorText, + ) { + final bool hasError = selectedValue == null; + final GlobalKey _key = GlobalKey(); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: FormField( + validator: (value) => hasError ? errorText : null, + builder: (fieldState) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle(fontSize: 12, color: Colors.grey[700])), + const SizedBox(height: 6), + GestureDetector( + key: _key, + onTap: () async { + final RenderBox renderBox = + _key.currentContext!.findRenderObject() as RenderBox; + final Offset offset = renderBox.localToGlobal(Offset.zero); + final Size size = renderBox.size; + + final selected = await showMenu( + context: fieldState.context, + position: RelativeRect.fromLTRB( + offset.dx, + offset.dy + size.height, + offset.dx + size.width, + offset.dy, + ), + items: items + .map((item) => PopupMenuItem( + value: item, + child: Text(item), + )) + .toList(), + ); + if (selected != null) { + onSelected(selected); + fieldState.didChange(selected); + } + }, + child: InputDecorator( + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: fieldState.errorText, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 14), + ), + child: Text( + selectedValue ?? 'Select $label', + style: TextStyle( + color: selectedValue == null ? Colors.grey : Colors.black, + fontSize: 16, + ), + ), + ), + ), + ], + ); + }, + ), + ); + } + + Widget _buildTextField(String fieldName, String label, + {TextInputType keyboardType = TextInputType.text}) { + final controller = validator.getController(fieldName); + final validatorFunc = validator.getValidation(fieldName); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: TextFormField( + controller: controller, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + ), + validator: validatorFunc, + ), + ); + } + + void _submitForm() async { + bool isValid = validator.validateForm(); + + if (_selectedSize == null || _selectedIndustryId == null) { + isValid = false; + setState(() {}); + } + + if (!_agreed) { + isValid = false; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please agree to the privacy policy & terms')), + ); + } + + if (isValid) { + final formData = validator.getData(); + + final Map requestBody = { + 'organizatioinName': formData['organizationName'], + 'email': formData['email'], + 'about': formData['about'], + 'contactNumber': formData['contactNumber'], + 'contactPerson': formData['contactPerson'], + 'industryId': _selectedIndustryId ?? '', + 'oragnizationSize': _sizeApiMap[_selectedSize] ?? '', + 'terms': _agreed, + 'address': formData['address'], + }; + + final error = await AuthService.requestDemo(requestBody); + + if (error == null) { + showAppSnackbar( + title: "Success", + message: "Demo request submitted successfully!.", + type: SnackbarType.success, + ); + Navigator.pop(context); + } else { + showAppSnackbar( + title: "Success", + message: (error['error'] ?? 'Unknown error'), + type: SnackbarType.success, + ); + } + } + } +} diff --git a/lib/view/layouts/left_bar.dart b/lib/view/layouts/left_bar.dart index 4412066..5eb230f 100644 --- a/lib/view/layouts/left_bar.dart +++ b/lib/view/layouts/left_bar.dart @@ -80,7 +80,7 @@ class _LeftBarState extends State children: [ Center( child: Padding( - padding: MySpacing.fromLTRB(0, 24, 0, 0), + padding: EdgeInsets.only(top: 50), child: InkWell( onTap: () => Get.toNamed('/home'), child: Image.asset( @@ -106,7 +106,6 @@ class _LeftBarState extends State physics: BouncingScrollPhysics(), clipBehavior: Clip.antiAliasWithSaveLayer, children: [ - Divider(), labelWidget("Dashboard"), NavigationItem( iconData: LucideIcons.layout_dashboard,