From 7d25e771383b1f7253484f0e3a818f4c349f1dbf Mon Sep 17 00:00:00 2001 From: MAGESHWARAN Date: Sun, 19 Apr 2026 21:44:01 +0530 Subject: [PATCH] mpin add and 2026-04-19 updates --- lib/auth/employee_login_screen.dart | 707 ++++++++++++++---------- lib/auth/employee_otp_screen.dart | 762 ++++++++++++-------------- lib/auth/forgot_screen.dart | 168 ++++++ lib/auth/login_screen.dart | 804 ++++++++++++++++------------ lib/auth/mpin_set_screen.dart | 252 +++++++++ lib/auth/otp_screen.dart | 756 ++++++++++++-------------- lib/auth/register_otp_screen.dart | 81 ++- lib/consts/validation_popup.dart | 233 ++++---- lib/controller/api_consts.dart | 8 +- lib/controller/api_contoller.dart | 97 +++- lib/controller/api_repository.dart | 224 +++++++- lib/model/login_model.dart | 61 ++- lib/router/consts_routers.dart | 2 + lib/router/router.dart | 7 + pubspec.lock | 36 +- 15 files changed, 2555 insertions(+), 1643 deletions(-) create mode 100644 lib/auth/forgot_screen.dart create mode 100644 lib/auth/mpin_set_screen.dart diff --git a/lib/auth/employee_login_screen.dart b/lib/auth/employee_login_screen.dart index 1aae07b..ccb76de 100644 --- a/lib/auth/employee_login_screen.dart +++ b/lib/auth/employee_login_screen.dart @@ -1,283 +1,424 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:get/get.dart'; -import 'package:taxglide/consts/app_asstes.dart'; -import 'package:taxglide/consts/app_colors.dart'; -import 'package:taxglide/consts/app_style.dart'; -import 'package:taxglide/consts/comman_button.dart'; -import 'package:taxglide/consts/comman_container_auth.dart'; -import 'package:taxglide/consts/comman_textformfileds.dart'; -import 'package:taxglide/consts/responsive_helper.dart'; -import 'package:taxglide/consts/validation_popup.dart'; -import 'package:taxglide/controller/api_contoller.dart'; -import 'package:taxglide/router/consts_routers.dart'; - -class EmployeeLoginScreen extends ConsumerStatefulWidget { - const EmployeeLoginScreen({super.key}); - - @override - ConsumerState createState() => - _EmployeeLoginScreenState(); -} - -class _EmployeeLoginScreenState extends ConsumerState { - final ValidationPopup _validationPopup = ValidationPopup(); - final TextEditingController _mobileController = TextEditingController(); - - @override - void initState() { - super.initState(); - final args = Get.arguments; - if (args != null && args is Map) { - final mobile = args['mobile'] ?? ''; - if (mobile.isNotEmpty) { - _mobileController.text = mobile; - } - } - } - - @override - void dispose() { - _mobileController.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - final mobile = _mobileController.text.trim(); - - if (!_validationPopup.validateMobileNumber(context, mobile)) { - return; - } - - await ref.read(employeeloginProvider.notifier).login(mobile); - final state = ref.read(employeeloginProvider); - - state.when( - data: (data) { - if (data['success'] == true) { - Get.toNamed(ConstRouters.employeeotp, arguments: {'mobile': mobile}); - _validationPopup.showSuccessMessage( - context, - "OTP sent successfully!", - ); - } else if (data['error'] != null) { - _validationPopup.showErrorMessage(context, data['error'].toString()); - } - }, - loading: () {}, - error: (err, _) { - _validationPopup.showErrorMessage(context, "Error: $err"); - }, - ); - } - - @override - Widget build(BuildContext context) { - final loginState = ref.watch(employeeloginProvider); - - // Initialize responsive utils - final r = ResponsiveUtils(context); - - // Responsive values - final logoWidth = r.getValue( - mobile: 120, - tablet: 141, - desktop: 160, - ); - final logoHeight = r.getValue( - mobile: 85, - tablet: 100, - desktop: 115, - ); - final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); - final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12); - final linkFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); - - final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); - final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24); - final spacingMD = r.spacing(mobile: 20, tablet: 20, desktop: 24); - final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28); - - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) { - if (didPop) return; - Get.offAllNamed(ConstRouters.login); - }, - child: Scaffold( - resizeToAvoidBottomInset: true, - body: Container( - width: double.infinity, - height: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFFFFF8F0), - Color(0xFFEBC894), - Color(0xFFE8DAF2), - Color(0xFFB49EF4), - ], - stops: [0.0, 0.3, 0.6, 1.0], - ), - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: r.screenHeight), - child: IntrinsicHeight( - child: Center( - child: CommonContainerAuth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Logo - Image.asset( - AppAssets.taxgildelogoauth, - width: logoWidth, - height: logoHeight, - fit: BoxFit.contain, - ), - SizedBox(height: spacingMD), - - // Login Title - Text( - "Login", - textAlign: TextAlign.center, - style: AppTextStyles.bold.copyWith( - fontSize: titleFontSize, - height: 1.3, - letterSpacing: 0.01 * titleFontSize, - color: AppColors.authheading, - ), - ), - SizedBox(height: spacingMD), - - // Subtitle - Text( - "Enter your registered Mobile Number", - textAlign: TextAlign.center, - style: AppTextStyles.bold.copyWith( - fontSize: subtitleFontSize, - height: 1.4, - letterSpacing: 0.03 * subtitleFontSize, - color: AppColors.authleading, - ), - ), - SizedBox(height: spacingLG), - - // Mobile Number Input - CommanTextFormField( - controller: _mobileController, - hintText: 'Enter your Mobile Number', - keyboardType: TextInputType.phone, - prefixIcon: Icons.phone_android, - ), - SizedBox(height: spacingLG), - - // Terms and Conditions - Text.rich( - TextSpan( - text: "By signing up, you agree to the ", - style: AppTextStyles.medium.copyWith( - fontSize: termsFontSize, - height: 1.8, - letterSpacing: 0.01, - color: AppColors.authleading, - ), - children: [ - TextSpan( - text: "Terms of Service ", - style: AppTextStyles.bold.copyWith( - fontSize: termsFontSize, - height: 1.8, - letterSpacing: 0.01, - color: AppColors.authtermsandcondition, - ), - ), - TextSpan( - text: "and ", - style: AppTextStyles.medium.copyWith( - fontSize: termsFontSize, - height: 1.8, - letterSpacing: 0.01, - color: AppColors.authleading, - ), - ), - TextSpan( - text: "Data Processing Agreement", - style: AppTextStyles.bold.copyWith( - fontSize: termsFontSize, - height: 1.8, - letterSpacing: 0.01, - color: AppColors.authtermsandcondition, - ), - ), - ], - ), - textAlign: TextAlign.center, - ), - - SizedBox(height: spacingLG), - - // Login Button or Loading - loginState.isLoading - ? const CircularProgressIndicator() - : CommanButton( - text: "Login", - onPressed: _handleLogin, - ), - - SizedBox(height: spacingSM), - - Text( - "Other Login", - textAlign: TextAlign.center, - style: AppTextStyles.semiBold.copyWith( - fontSize: otherLoginFontSize, - height: 1.8, - letterSpacing: 0.13, - color: AppColors.authleading, - ), - ), - - SizedBox(height: spacingXS), - - Text.rich( - TextSpan( - text: "User Login? ", - style: AppTextStyles.bold.copyWith( - fontSize: linkFontSize, - color: AppColors.black, - ), - children: [ - TextSpan( - text: "Click Here", - style: AppTextStyles.bold.copyWith( - fontSize: linkFontSize, - color: AppColors.authsignup, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Get.offNamed(ConstRouters.login); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ), - ); - } -} +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; +import 'package:taxglide/consts/app_style.dart'; +import 'package:taxglide/consts/comman_button.dart'; +import 'package:taxglide/consts/comman_container_auth.dart'; +import 'package:taxglide/consts/comman_textformfileds.dart'; +import 'package:taxglide/consts/responsive_helper.dart'; +import 'package:taxglide/consts/validation_popup.dart'; +import 'package:taxglide/controller/api_contoller.dart'; +import 'package:taxglide/router/consts_routers.dart'; +import 'package:taxglide/view/Main_controller/main_controller.dart'; + +class EmployeeLoginScreen extends ConsumerStatefulWidget { + const EmployeeLoginScreen({super.key}); + + @override + ConsumerState createState() => + _EmployeeLoginScreenState(); +} + +class _EmployeeLoginScreenState extends ConsumerState { + final ValidationPopup _validationPopup = ValidationPopup(); + final TextEditingController _emailController = TextEditingController(); + + // Multi-box PIN logic + final int pinLength = 4; + final List _pinControllers = []; + final List _pinFocusNodes = []; + String pinValue = ''; + + // Error states + bool _emailHasError = false; + bool _pinHasError = false; + + @override + void initState() { + super.initState(); + // Initialize PIN controllers and focus nodes + for (int i = 0; i < pinLength; i++) { + _pinControllers.add(TextEditingController()); + _pinFocusNodes.add(FocusNode()); + } + + final args = Get.arguments; + if (args != null && args is Map) { + final email = args['email'] ?? ''; + if (email.isNotEmpty) { + _emailController.text = email; + } + } + } + + @override + void dispose() { + _emailController.dispose(); + for (var controller in _pinControllers) { + controller.dispose(); + } + for (var node in _pinFocusNodes) { + node.dispose(); + } + super.dispose(); + } + + void _onPinChange(int index, String value) { + if (_pinHasError) { + setState(() => _pinHasError = false); + } + if (value.isNotEmpty && index < pinLength - 1) { + _pinFocusNodes[index + 1].requestFocus(); + } + if (value.isEmpty && index > 0) { + _pinFocusNodes[index - 1].requestFocus(); + } + setState(() { + pinValue = _pinControllers.map((c) => c.text).join(); + }); + } + + Future _handleLogin() async { + final email = _emailController.text.trim(); + final pin = pinValue.trim(); + + setState(() { + _emailHasError = false; + _pinHasError = false; + }); + + if (!_validationPopup.validateEmail(context, email)) { + setState(() => _emailHasError = true); + return; + } + if (!_validationPopup.validatePin(context, pin)) { + setState(() => _pinHasError = true); + return; + } + + await ref.read(employeeloginProvider.notifier).loginWithPin(email, pin); + final state = ref.read(employeeloginProvider); + + state.when( + data: (data) { + if (data['success'] == true) { + _validationPopup.showSuccessMessage( + context, + "Login Successful!", + ); + Future.delayed(const Duration(seconds: 1), () { + Get.offAll(() => const MainController()); + }); + } else if (data['error'] != null) { + _validationPopup.showErrorMessage(context, data['error'].toString()); + setState(() { + _emailHasError = true; + _pinHasError = true; + }); + } + }, + loading: () {}, + error: (err, _) { + _validationPopup.showErrorMessage(context, "Error: $err"); + }, + ); + } + + @override + Widget build(BuildContext context) { + final loginState = ref.watch(employeeloginProvider); + + // Initialize responsive utils + final r = ResponsiveUtils(context); + + // Responsive values + final logoWidth = r.getValue(mobile: 120, tablet: 141, desktop: 160); + final logoHeight = r.getValue(mobile: 85, tablet: 100, desktop: 115); + final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); + final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12); + final linkFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); + + final pinBoxWidth = r.getValue(mobile: 55, tablet: 60, desktop: 68); + final pinBoxHeight = r.getValue(mobile: 55, tablet: 60, desktop: 68); + final pinFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); + final pinBoxMargin = r.getValue(mobile: 8, tablet: 6, desktop: 8); + final pinBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); + + final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); + final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24); + final spacingMD = r.spacing(mobile: 20, tablet: 20, desktop: 24); + final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28); + + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + Get.offAllNamed(ConstRouters.login); + }, + child: Scaffold( + resizeToAvoidBottomInset: true, + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFF8F0), + Color(0xFFEBC894), + Color(0xFFE8DAF2), + Color(0xFFB49EF4), + ], + stops: [0.0, 0.3, 0.6, 1.0], + ), + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: r.screenHeight), + child: IntrinsicHeight( + child: Center( + child: CommonContainerAuth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Logo + Image.asset( + AppAssets.taxgildelogoauth, + width: logoWidth, + height: logoHeight, + fit: BoxFit.contain, + ), + SizedBox(height: spacingMD), + + // Login Title + Text( + "Employee Login", + textAlign: TextAlign.center, + style: AppTextStyles.bold.copyWith( + fontSize: titleFontSize, + color: AppColors.authheading, + ), + ), + SizedBox(height: spacingMD), + + // Subtitle + Text( + "Enter your Email and PIN", + textAlign: TextAlign.center, + style: AppTextStyles.bold.copyWith( + fontSize: subtitleFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingLG), + + // Email Input + CommanTextFormField( + controller: _emailController, + hintText: 'Enter your Email', + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + hasError: _emailHasError, + onChanged: (value) { + if (_emailHasError) { + setState(() => _emailHasError = false); + } + }, + ), + SizedBox(height: spacingMD), + + // PIN Boxes + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(pinLength, (index) { + bool isFilled = _pinControllers[index].text.isNotEmpty; + return Container( + margin: EdgeInsets.symmetric(horizontal: pinBoxMargin), + width: pinBoxWidth, + height: pinBoxHeight, + decoration: BoxDecoration( + color: isFilled ? AppColors.commanbutton : Colors.white, + borderRadius: BorderRadius.circular(pinBoxRadius), + border: Border.all( + color: _pinHasError + ? Colors.red + : const Color(0xFFDFDFDF), + width: _pinHasError ? 1.5 : 1.0, + ), + boxShadow: [ + BoxShadow( + color: _pinHasError + ? Colors.red.withOpacity(0.1) + : const Color(0xFFBDBDBD).withOpacity(0.25), + blurRadius: 7, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == + LogicalKeyboardKey.backspace) { + if (_pinControllers[index] + .text + .isEmpty && + index > 0) { + _pinFocusNodes[index - 1] + .requestFocus(); + } + } + }, + child: TextField( + controller: _pinControllers[index], + focusNode: _pinFocusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + obscureText: true, + enabled: !loginState.isLoading, + style: TextStyle( + fontFamily: "Gilroy", + fontWeight: FontWeight.w400, + fontSize: pinFontSize, + color: isFilled + ? Colors.white + : Colors.black, + ), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + onChanged: (value) => + _onPinChange(index, value), + ), + ), + ), + ); + }), + ), + + SizedBox(height: spacingXS), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () { + Get.toNamed( + ConstRouters.forgotPassword, + arguments: {'isEmployee': true}, + ); + }, + child: Text( + "Forgot Password?", + style: AppTextStyles.bold.copyWith( + fontSize: linkFontSize, + color: AppColors.authchange, + ), + ), + ), + ), + + SizedBox(height: spacingLG), + + // Terms and Conditions + Text.rich( + TextSpan( + text: "By signing up, you agree to the ", + style: AppTextStyles.medium.copyWith( + fontSize: termsFontSize, + color: AppColors.authleading, + ), + children: [ + TextSpan( + text: "Terms of Service ", + style: AppTextStyles.bold.copyWith( + fontSize: termsFontSize, + color: AppColors.authtermsandcondition, + ), + ), + TextSpan( + text: "and ", + style: AppTextStyles.medium.copyWith( + fontSize: termsFontSize, + color: AppColors.authleading, + ), + ), + TextSpan( + text: "Data Processing Agreement", + style: AppTextStyles.bold.copyWith( + fontSize: termsFontSize, + color: AppColors.authtermsandcondition, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + + SizedBox(height: spacingLG), + + // Login Button or Loading + loginState.isLoading + ? const CircularProgressIndicator() + : CommanButton( + text: "Login", + onPressed: _handleLogin, + ), + + SizedBox(height: spacingSM), + + Text( + "Other Login", + textAlign: TextAlign.center, + style: AppTextStyles.semiBold.copyWith( + fontSize: otherLoginFontSize, + color: AppColors.authleading, + ), + ), + + SizedBox(height: spacingXS), + + Text.rich( + TextSpan( + text: "User Login? ", + style: AppTextStyles.bold.copyWith( + fontSize: linkFontSize, + color: AppColors.black, + ), + children: [ + TextSpan( + text: "Click Here", + style: AppTextStyles.bold.copyWith( + fontSize: linkFontSize, + color: AppColors.authsignup, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.offNamed(ConstRouters.login); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/auth/employee_otp_screen.dart b/lib/auth/employee_otp_screen.dart index f7cd6b5..0c0fdc6 100644 --- a/lib/auth/employee_otp_screen.dart +++ b/lib/auth/employee_otp_screen.dart @@ -1,420 +1,342 @@ -import 'dart:async'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:get/get.dart'; -import 'package:taxglide/consts/app_asstes.dart'; -import 'package:taxglide/consts/app_colors.dart'; -import 'package:taxglide/consts/comman_button.dart'; -import 'package:taxglide/consts/comman_container_auth.dart'; -import 'package:taxglide/consts/responsive_helper.dart'; -import 'package:taxglide/consts/validation_popup.dart'; -import 'package:taxglide/controller/api_contoller.dart'; - -import 'package:taxglide/view/Main_controller/main_controller.dart'; -import '../router/consts_routers.dart'; - -class EmployeeOtpScreen extends ConsumerStatefulWidget { - const EmployeeOtpScreen({super.key}); - - @override - ConsumerState createState() => _EmployeeOtpScreenState(); -} - -class _EmployeeOtpScreenState extends ConsumerState { - final ValidationPopup _validationPopup = ValidationPopup(); - final int otpLength = 4; - final List _controllers = []; - final List _focusNodes = []; - String otpValue = ''; - - Timer? _timer; - int _remainingSeconds = 600; - bool _canResend = false; - - late String mobile; - - @override - void initState() { - super.initState(); - final args = Get.arguments as Map?; - mobile = args?['mobile'] ?? ''; - - for (int i = 0; i < otpLength; i++) { - _controllers.add(TextEditingController()); - _focusNodes.add(FocusNode()); - } - _startTimer(); - } - - @override - void dispose() { - _timer?.cancel(); - for (var c in _controllers) c.dispose(); - for (var f in _focusNodes) f.dispose(); - super.dispose(); - } - - void _startTimer() { - _canResend = false; - _remainingSeconds = 30; - _timer?.cancel(); - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_remainingSeconds > 0) { - setState(() => _remainingSeconds--); - } else { - setState(() => _canResend = true); - timer.cancel(); - } - }); - } - - String _formatTime(int seconds) { - int minutes = seconds ~/ 60; - int remainingSeconds = seconds % 60; - return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; - } - - Future _resendOtp() async { - if (!_canResend) return; - - for (var controller in _controllers) controller.clear(); - setState(() => otpValue = ''); - _focusNodes[0].requestFocus(); - - try { - await ref.read(employeeloginProvider.notifier).login(mobile); - final loginState = ref.read(employeeloginProvider); - loginState.when( - data: (result) { - if (result['success'] == true) { - _validationPopup.showSuccessMessage( - context, - "OTP has been resent successfully!", - ); - _startTimer(); - } else { - _validationPopup.showErrorMessage( - context, - result['error'] ?? "Failed to resend OTP", - ); - } - }, - loading: () {}, - error: (error, _) { - _validationPopup.showErrorMessage( - context, - "Failed to resend OTP. Please try again.", - ); - }, - ); - } catch (e) { - _validationPopup.showErrorMessage( - context, - "An error occurred. Please try again.", - ); - } - } - - Future _verifyOtp() async { - if (otpValue.length != otpLength) { - _validationPopup.showErrorMessage(context, "Please enter all OTP digits"); - return; - } - - await ref.read(employeeloginProvider.notifier).verifyOtp(mobile, otpValue); - final loginState = ref.read(employeeloginProvider); - - loginState.when( - data: (result) { - if (result['success'] == true) { - _validationPopup.showSuccessMessage( - context, - "OTP Verified Successfully!", - ); - Future.delayed(const Duration(seconds: 1), () { - Get.offAll(() => const MainController()); - }); - } else { - _validationPopup.showErrorMessage( - context, - result['error'] ?? "Invalid OTP. Please try again.", - ); - } - }, - loading: () {}, - error: (error, _) { - _validationPopup.showErrorMessage( - context, - "Failed to verify OTP. Please try again.", - ); - }, - ); - } - - void _onOtpChange(int index, String value) { - if (value.isNotEmpty && index < otpLength - 1) { - _focusNodes[index + 1].requestFocus(); - } else if (value.isEmpty && index > 0) { - _focusNodes[index - 1].requestFocus(); - } - setState(() { - otpValue = _controllers.map((c) => c.text).join(); - }); - } - - @override - Widget build(BuildContext context) { - final loginState = ref.watch(employeeloginProvider); - final isLoading = loginState.isLoading; - - // Initialize responsive utils - final r = ResponsiveUtils(context); - - // Responsive values - final logoWidth = r.getValue( - mobile: 120, - tablet: 141, - desktop: 160, - ); - final logoHeight = r.getValue( - mobile: 85, - tablet: 100, - desktop: 115, - ); - final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); - final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final mobileFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); - final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); - - final otpBoxWidth = r.getValue(mobile: 52, tablet: 60, desktop: 68); - final otpBoxHeight = r.getValue( - mobile: 48, - tablet: 56, - desktop: 64, - ); - final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); - final otpBoxMargin = r.getValue(mobile: 5, tablet: 6, desktop: 8); - final otpBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); - - final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); - final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); - final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); - final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); - - return Scaffold( - body: Stack( - children: [ - // Gradient background - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFFFFF8F0), - Color(0xFFEBC894), - Color(0xFFE8DAF2), - Color(0xFFB49EF4), - ], - stops: [0.0, 0.3, 0.6, 1.0], - ), - ), - ), - - // Scrollable content - SingleChildScrollView( - child: SizedBox( - height: r.screenHeight, - child: Center( - child: CommonContainerAuth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Logo - Image.asset( - AppAssets.taxgildelogoauth, - width: logoWidth, - height: logoHeight, - fit: BoxFit.contain, - ), - SizedBox(height: spacingSM), - - // Title - Text( - "Enter OTP", - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Gilroy", - fontSize: titleFontSize, - fontWeight: FontWeight.w600, - height: 1.3, - letterSpacing: 0.01 * titleFontSize, - color: AppColors.authheading, - ), - ), - SizedBox(height: spacingSM), - - // Subtitle - Text( - "OTP has been sent to your registered mobile number", - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Gilroy", - fontSize: subtitleFontSize, - fontWeight: FontWeight.w600, - height: 1.4, - letterSpacing: 0.03 * subtitleFontSize, - color: AppColors.authleading, - ), - ), - SizedBox(height: spacingMD), - - // Mobile + Change Number - Text.rich( - TextSpan( - text: mobile, - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w500, - fontSize: mobileFontSize, - height: 1.8, - letterSpacing: 0.01, - color: AppColors.authleading, - ), - children: [ - TextSpan( - text: " ( Change Number )", - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w800, - fontSize: mobileFontSize, - height: 1.8, - letterSpacing: 0.04, - color: AppColors.authchange, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Get.offNamed( - ConstRouters.employeelogin, - arguments: {'mobile': mobile}, - ); - }, - ), - ], - ), - textAlign: TextAlign.center, - ), - SizedBox(height: spacingLG), - - // OTP Input Fields - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(otpLength, (index) { - bool isFilled = _controllers[index].text.isNotEmpty; - return Container( - margin: EdgeInsets.symmetric( - horizontal: otpBoxMargin, - ), - width: otpBoxWidth, - height: otpBoxHeight, - decoration: BoxDecoration( - color: isFilled - ? AppColors.commanbutton - : Colors.white, - borderRadius: BorderRadius.circular(otpBoxRadius), - border: Border.all( - color: const Color(0xFFDFDFDF), - ), - boxShadow: [ - BoxShadow( - color: const Color( - 0xFFBDBDBD, - ).withOpacity(0.25), - blurRadius: 7, - offset: const Offset(0, 1), - ), - ], - ), - child: Center( - child: TextField( - controller: _controllers[index], - focusNode: _focusNodes[index], - textAlign: TextAlign.center, - keyboardType: TextInputType.number, - maxLength: 1, - enabled: !isLoading, - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w400, - fontSize: otpFontSize, - letterSpacing: 0.03 * otpFontSize, - color: isFilled ? Colors.white : Colors.black, - ), - decoration: const InputDecoration( - counterText: '', - border: InputBorder.none, - ), - onChanged: (value) => - _onOtpChange(index, value), - ), - ), - ); - }), - ), - SizedBox(height: spacingXS), - - // Resend OTP - Align( - alignment: Alignment.centerRight, - child: GestureDetector( - onTap: _canResend && !isLoading ? _resendOtp : null, - child: Text( - "Resend", - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w500, - fontSize: resendFontSize, - height: 1.4, - letterSpacing: 0.02 * resendFontSize, - color: _canResend && !isLoading - ? AppColors.authchange - : Colors.grey, - ), - ), - ), - ), - SizedBox(height: spacingSM), - - // Timer - Text( - _formatTime(_remainingSeconds), - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w600, - fontSize: timerFontSize, - color: _remainingSeconds > 0 - ? AppColors.authheading - : Colors.red, - ), - ), - SizedBox(height: spacingLG), - - // Verify Button - isLoading - ? const CircularProgressIndicator() - : CommanButton(text: "Verify", onPressed: _verifyOtp), - ], - ), - ), - ), - ), - ), - ], - ), - ); - } -} +import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; +import 'package:taxglide/consts/comman_button.dart'; +import 'package:taxglide/consts/comman_container_auth.dart'; +import 'package:taxglide/consts/responsive_helper.dart'; +import 'package:taxglide/consts/validation_popup.dart'; +import 'package:taxglide/controller/api_contoller.dart'; +import 'package:taxglide/router/consts_routers.dart'; +import 'package:flutter/services.dart'; +import 'package:taxglide/consts/app_style.dart'; + +class EmployeeOtpScreen extends ConsumerStatefulWidget { + const EmployeeOtpScreen({super.key}); + + @override + ConsumerState createState() => _EmployeeOtpScreenState(); +} + +class _EmployeeOtpScreenState extends ConsumerState { + final ValidationPopup _validationPopup = ValidationPopup(); + final int otpLength = 4; + final List _controllers = []; + final List _focusNodes = []; + String otpValue = ''; + + Timer? _timer; + int _remainingSeconds = 600; + bool _canResend = false; + + late String _identifier; + bool _fromForgot = false; + bool _isEmployee = true; + + @override + void initState() { + super.initState(); + final args = Get.arguments as Map? ?? {}; + _fromForgot = args['fromForgot'] ?? false; + _identifier = args['email'] ?? args['mobile'] ?? ''; + + for (int i = 0; i < otpLength; i++) { + _controllers.add(TextEditingController()); + _focusNodes.add(FocusNode()); + } + _startTimer(); + } + + @override + void dispose() { + _timer?.cancel(); + for (var c in _controllers) c.dispose(); + for (var f in _focusNodes) f.dispose(); + super.dispose(); + } + + void _startTimer() { + _canResend = false; + _remainingSeconds = 30; + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_remainingSeconds > 0) { + setState(() => _remainingSeconds--); + } else { + setState(() => _canResend = true); + timer.cancel(); + } + }); + } + + String _formatTime(int seconds) { + int minutes = seconds ~/ 60; + int remainingSeconds = seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; + } + + void _resendOtp() async { + if (!_canResend) return; + for (var controller in _controllers) controller.clear(); + setState(() => otpValue = ''); + _focusNodes[0].requestFocus(); + + if (_fromForgot) { + await ref.read(forgotPasswordProvider.notifier).requestOtp(_identifier, _isEmployee); + final state = ref.read(forgotPasswordProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!"); + _startTimer(); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP"); + } + }, + loading: () {}, + error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"), + ); + } else { + await ref.read(employeeloginProvider.notifier).login(_identifier); + final state = ref.read(employeeloginProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!"); + _startTimer(); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP"); + } + }, + loading: () {}, + error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"), + ); + } + } + + void _verifyOtp() async { + if (otpValue.length != otpLength) { + _validationPopup.showErrorMessage(context, "Please enter all OTP digits"); + return; + } + + if (_fromForgot) { + await ref.read(forgotPasswordProvider.notifier).verifyOtp(_identifier, otpValue); + final state = ref.read(forgotPasswordProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP Verified successfully!"); + Get.toNamed(ConstRouters.mpinSet); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP"); + } + }, + loading: () {}, + error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"), + ); + } else { + await ref.read(employeeloginProvider.notifier).verifyOtp(_identifier, otpValue); + final state = ref.read(employeeloginProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP Verified successfully!"); + Get.offAllNamed(ConstRouters.mpinSet); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP"); + } + }, + loading: () {}, + error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"), + ); + } + } + + void _onOtpChange(int index, String value) { + if (value.isNotEmpty && index < otpLength - 1) { + _focusNodes[index + 1].requestFocus(); + } + if (value.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + } + setState(() { + otpValue = _controllers.map((c) => c.text).join(); + }); + } + + @override + Widget build(BuildContext context) { + final isLoading = _fromForgot + ? ref.watch(forgotPasswordProvider).isLoading + : ref.watch(employeeloginProvider).isLoading; + + final r = ResponsiveUtils(context); + + final logoWidth = r.getValue(mobile: 120, tablet: 141, desktop: 160); + final logoHeight = r.getValue(mobile: 85, tablet: 100, desktop: 115); + final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); + final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); + + final otpBoxWidth = r.getValue(mobile: 52, tablet: 60, desktop: 68); + final otpBoxHeight = r.getValue(mobile: 48, tablet: 56, desktop: 64); + final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); + final otpBoxMargin = r.getValue(mobile: 5, tablet: 6, desktop: 8); + final otpBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); + + final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); + final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); + final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); + + return Scaffold( + body: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFF8F0), + Color(0xFFEBC894), + Color(0xFFE8DAF2), + Color(0xFFB49EF4), + ], + stops: [0.0, 0.3, 0.6, 1.0], + ), + ), + ), + SingleChildScrollView( + child: SizedBox( + height: r.screenHeight, + child: Center( + child: CommonContainerAuth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + AppAssets.taxgildelogoauth, + width: logoWidth, + height: logoHeight, + fit: BoxFit.contain, + ), + SizedBox(height: spacingSM), + Text( + "Enter Staff OTP", + style: AppTextStyles.bold.copyWith( + fontSize: titleFontSize, + color: AppColors.authheading, + ), + ), + SizedBox(height: spacingSM), + Text( + "OTP has been sent to ${_identifier}", + textAlign: TextAlign.center, + style: AppTextStyles.medium.copyWith( + fontSize: subtitleFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingMD), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(otpLength, (index) { + bool isFilled = _controllers[index].text.isNotEmpty; + return Container( + margin: EdgeInsets.symmetric(horizontal: otpBoxMargin), + width: otpBoxWidth, + height: otpBoxHeight, + decoration: BoxDecoration( + color: isFilled ? AppColors.commanbutton : Colors.white, + borderRadius: BorderRadius.circular(otpBoxRadius), + border: Border.all(color: const Color(0xFFDFDFDF)), + boxShadow: [ + BoxShadow( + color: const Color(0xFFBDBDBD).withOpacity(0.25), + blurRadius: 7, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == + LogicalKeyboardKey.backspace) { + if (_controllers[index].text.isEmpty && + index > 0) { + _focusNodes[index - 1].requestFocus(); + } + } + }, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + enabled: !isLoading, + style: TextStyle( + fontFamily: "Gilroy", + fontWeight: FontWeight.w400, + fontSize: otpFontSize, + color: isFilled ? Colors.white : Colors.black, + ), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + onChanged: (value) => + _onOtpChange(index, value), + ), + ), + ), + ); + }), + ), + SizedBox(height: spacingSM), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: _canResend && !isLoading ? _resendOtp : null, + child: Text( + "Resend", + style: AppTextStyles.medium.copyWith( + fontSize: resendFontSize, + color: _canResend && !isLoading + ? AppColors.authchange + : Colors.grey, + ), + ), + ), + ), + SizedBox(height: spacingSM), + Text( + _formatTime(_remainingSeconds), + style: AppTextStyles.semiBold.copyWith( + fontSize: timerFontSize, + color: _remainingSeconds > 0 ? AppColors.authheading : Colors.red, + ), + ), + SizedBox(height: spacingLG), + isLoading + ? const CircularProgressIndicator() + : CommanButton(text: "Verify", onPressed: _verifyOtp), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/auth/forgot_screen.dart b/lib/auth/forgot_screen.dart new file mode 100644 index 0000000..9ad790d --- /dev/null +++ b/lib/auth/forgot_screen.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; +import 'package:taxglide/consts/app_style.dart'; +import 'package:taxglide/consts/comman_button.dart'; +import 'package:taxglide/consts/comman_container_auth.dart'; +import 'package:taxglide/consts/comman_textformfileds.dart'; +import 'package:taxglide/consts/responsive_helper.dart'; +import 'package:taxglide/consts/validation_popup.dart'; +import 'package:taxglide/controller/api_contoller.dart'; +import 'package:taxglide/router/consts_routers.dart'; + +class ForgotScreen extends ConsumerStatefulWidget { + const ForgotScreen({super.key}); + + @override + ConsumerState createState() => _ForgotScreenState(); +} + +class _ForgotScreenState extends ConsumerState { + final TextEditingController _emailController = TextEditingController(); + final ValidationPopup _validationPopup = ValidationPopup(); + bool _isEmployee = false; + + @override + void initState() { + super.initState(); + final args = Get.arguments; + if (args != null && args is Map) { + _isEmployee = args['isEmployee'] ?? false; + } + } + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + Future _handleForgotRequest() async { + final email = _emailController.text.trim(); + + if (!_validationPopup.validateEmail(context, email)) { + return; + } + + await ref.read(forgotPasswordProvider.notifier).requestOtp(email, _isEmployee); + final state = ref.read(forgotPasswordProvider); + + state.when( + data: (data) { + if (data['success'] == true) { + _validationPopup.showSuccessMessage( + context, + "OTP sent to your email!", + ); + Get.toNamed( + _isEmployee ? ConstRouters.employeeotp : ConstRouters.otp, + arguments: { + 'email': email, + 'isEmployee': _isEmployee, + 'fromForgot': true, + }, + ); + } else if (data['error'] != null) { + _validationPopup.showErrorMessage(context, data['error'].toString()); + } + }, + loading: () {}, + error: (err, _) { + _validationPopup.showErrorMessage(context, "Error: $err"); + }, + ); + } + + @override + Widget build(BuildContext context) { + final forgotState = ref.watch(forgotPasswordProvider); + final r = ResponsiveUtils(context); + + final logoWidth = r.getValue(mobile: 120, tablet: 141, desktop: 160); + final logoHeight = r.getValue(mobile: 85, tablet: 100, desktop: 115); + final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); + final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24); + final spacingMD = r.spacing(mobile: 20, tablet: 22, desktop: 26); + final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28); + + return Scaffold( + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFF8F0), + Color(0xFFEBC894), + Color(0xFFE8DAF2), + Color(0xFFB49EF4), + ], + stops: [0.0, 0.3, 0.6, 1.0], + ), + ), + child: Center( + child: SingleChildScrollView( + child: CommonContainerAuth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + AppAssets.taxgildelogoauth, + width: logoWidth, + height: logoHeight, + ), + SizedBox(height: spacingSM), + Text( + "Forgot Password", + style: AppTextStyles.bold.copyWith( + fontSize: titleFontSize, + color: AppColors.authheading, + ), + ), + SizedBox(height: spacingSM), + Text( + "Enter your email to receive an OTP", + textAlign: TextAlign.center, + style: AppTextStyles.medium.copyWith( + fontSize: subtitleFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingLG), + CommanTextFormField( + controller: _emailController, + hintText: 'Enter your Email', + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + ), + SizedBox(height: spacingLG), + forgotState.isLoading + ? const CircularProgressIndicator() + : CommanButton( + text: "Send OTP", + onPressed: _handleForgotRequest, + ), + SizedBox(height: spacingMD), + TextButton( + onPressed: () => Get.back(), + child: Text( + "Back to Login", + style: AppTextStyles.semiBold.copyWith( + color: AppColors.authsignup, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/auth/login_screen.dart b/lib/auth/login_screen.dart index 873f28f..ea28318 100644 --- a/lib/auth/login_screen.dart +++ b/lib/auth/login_screen.dart @@ -1,351 +1,453 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:get/get.dart'; -import 'package:taxglide/consts/app_asstes.dart'; -import 'package:taxglide/consts/app_colors.dart'; -import 'package:taxglide/consts/comman_button.dart'; -import 'package:taxglide/consts/comman_container_auth.dart'; -import 'package:taxglide/consts/comman_textformfileds.dart'; -import 'package:taxglide/consts/comman_popup.dart'; -import 'package:taxglide/consts/responsive_helper.dart'; -import 'package:taxglide/consts/validation_popup.dart'; -import 'package:taxglide/controller/api_contoller.dart'; -import 'package:taxglide/router/consts_routers.dart'; -import 'package:taxglide/consts/app_style.dart'; - -class LoginScreen extends ConsumerStatefulWidget { - const LoginScreen({super.key}); - - @override - ConsumerState createState() => _LoginScreenState(); -} - -class _LoginScreenState extends ConsumerState { - final TextEditingController _mobileController = TextEditingController(); - final ValidationPopup _validationPopup = ValidationPopup(); - - @override - void initState() { - super.initState(); - final args = Get.arguments; - if (args != null && args is Map) { - final mobile = args['mobile'] ?? ''; - if (mobile.isNotEmpty) { - _mobileController.text = mobile; - } - } - } - - @override - void dispose() { - _mobileController.dispose(); - super.dispose(); - } - - Future _handleLogin() async { - final mobile = _mobileController.text.trim(); - - if (!_validationPopup.validateMobileNumber(context, mobile)) { - return; - } - - await ref.read(loginProvider.notifier).login(mobile); - final state = ref.read(loginProvider); - - state.when( - data: (data) { - if (data['success'] == true) { - Get.toNamed(ConstRouters.otp, arguments: {'mobile': mobile}); - _validationPopup.showSuccessMessage( - context, - "OTP sent successfully!", - ); - } else if (data['error'] != null) { - _validationPopup.showErrorMessage(context, data['error'].toString()); - } - }, - loading: () {}, - error: (err, _) { - _validationPopup.showErrorMessage(context, "Error: $err"); - }, - ); - } - - @override - Widget build(BuildContext context) { - final loginState = ref.watch(loginProvider); - final policyAsync = ref.watch(policyProvider); - final termsAsync = ref.watch(termsProvider); - - // Initialize responsive utils - final r = ResponsiveUtils(context); - - // Responsive values - final logoWidth = r.getValue( - mobile: 120, - tablet: 141, - desktop: 160, - ); - final logoHeight = r.getValue( - mobile: 85, - tablet: 100, - desktop: 115, - ); - final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); - final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12); - final signupFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); - - final spacingXS = r.spacing(mobile: 10, tablet: 15, desktop: 18); - final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24); - final spacingMD = r.spacing(mobile: 20, tablet: 22, desktop: 26); - final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28); - - return PopScope( - canPop: true, - onPopInvokedWithResult: (didPop, result) { - // Handle custom behavior here if needed in the future - }, - child: Scaffold( - resizeToAvoidBottomInset: true, - body: Container( - width: double.infinity, - height: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFFFFF8F0), - Color(0xFFEBC894), - Color(0xFFE8DAF2), - Color(0xFFB49EF4), - ], - stops: [0.0, 0.3, 0.6, 1.0], - ), - ), - child: SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: r.screenHeight), - child: IntrinsicHeight( - child: Center( - child: CommonContainerAuth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Logo - Image.asset( - AppAssets.taxgildelogoauth, - width: logoWidth, - height: logoHeight, - ), - SizedBox(height: spacingSM), - - // Title - Text( - "Login", - style: AppTextStyles.semiBold.copyWith( - fontSize: titleFontSize, - color: AppColors.authheading, - ), - ), - SizedBox(height: spacingSM), - - // Subtitle - Text( - "Enter your Mobile Number", - textAlign: TextAlign.center, - style: AppTextStyles.semiBold.copyWith( - fontSize: subtitleFontSize, - color: AppColors.authleading, - ), - ), - SizedBox(height: spacingLG), - - // Mobile TextField - CommanTextFormField( - controller: _mobileController, - hintText: 'Enter your Mobile Number', - keyboardType: TextInputType.phone, - prefixIcon: Icons.mobile_screen_share, - ), - SizedBox(height: spacingLG), - - // Terms and Policy - Text.rich( - TextSpan( - text: "By signing up, you agree to the ", - style: AppTextStyles.medium.copyWith( - fontSize: termsFontSize, - color: AppColors.authleading, - ), - children: [ - TextSpan( - text: "Terms of Service ", - style: AppTextStyles.bold.copyWith( - fontSize: termsFontSize, - color: AppColors.authtermsandcondition, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - termsAsync.when( - data: (terms) { - CommonInfoPopup.show( - context: context, - title: - terms.data?.title ?? - "Terms of Service", - content: - terms.data?.content ?? - "No terms found.", - ); - }, - loading: () { - _validationPopup.showErrorMessage( - context, - "Loading Terms...", - ); - }, - error: (err, _) { - _validationPopup.showErrorMessage( - context, - "Failed to load Terms", - ); - }, - ); - }, - ), - const TextSpan(text: "and "), - TextSpan( - text: "Data Processing Agreement", - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: termsFontSize, - color: AppColors.authtermsandcondition, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - policyAsync.when( - data: (policy) { - CommonInfoPopup.show( - context: context, - title: - policy.data?.title ?? - "Data Processing Agreement", - content: - policy.data?.content ?? - "No policy found.", - ); - }, - loading: () { - _validationPopup.showErrorMessage( - context, - "Loading Policy...", - ); - }, - error: (err, _) { - _validationPopup.showErrorMessage( - context, - "Failed to load Policy", - ); - }, - ); - }, - ), - ], - ), - textAlign: TextAlign.center, - ), - - SizedBox(height: spacingSM), - Text("Or",style: AppTextStyles.medium.copyWith( - fontSize: spacingSM, - color: AppColors.authleading, - ),), - SizedBox(height: spacingSM), - - // Sign up - Text.rich( - TextSpan( - text: "Didn't Have account? ", - style: AppTextStyles.bold.copyWith( - fontSize: signupFontSize, - color: AppColors.black, - ), - children: [ - TextSpan( - text: "Sign Up", - style: AppTextStyles.bold.copyWith( - fontSize: signupFontSize, - color: AppColors.authsignup, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Get.offNamed(ConstRouters.signup); - }, - ), - ], - ), - ), - SizedBox(height: spacingLG), - - // Login Button - loginState.isLoading - ? const CircularProgressIndicator() - : CommanButton( - text: "Login", - onPressed: _handleLogin, - ), - - SizedBox(height: spacingMD), - - Text( - "Other Login", - textAlign: TextAlign.center, - style: AppTextStyles.semiBold.copyWith( - fontSize: otherLoginFontSize, - height: 1.8, - letterSpacing: 0.13, - color: AppColors.authleading, - ), - ), - - SizedBox(height: spacingXS), - - Text.rich( - TextSpan( - text: "Staff Login? ", - style: AppTextStyles.bold.copyWith( - fontSize: signupFontSize, - color: AppColors.black, - ), - children: [ - TextSpan( - text: "Click Here", - style: AppTextStyles.bold.copyWith( - fontSize: signupFontSize, - color: AppColors.authsignup, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Get.offNamed(ConstRouters.employeelogin); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - ), - ), - ), - ); - } -} +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; +import 'package:taxglide/consts/comman_button.dart'; +import 'package:taxglide/consts/comman_container_auth.dart'; +import 'package:taxglide/consts/comman_textformfileds.dart'; +import 'package:taxglide/consts/comman_popup.dart'; +import 'package:taxglide/consts/responsive_helper.dart'; +import 'package:taxglide/consts/validation_popup.dart'; +import 'package:taxglide/controller/api_contoller.dart'; +import 'package:taxglide/router/consts_routers.dart'; +import 'package:taxglide/consts/app_style.dart'; +import 'package:taxglide/view/Main_controller/main_controller.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final TextEditingController _emailController = TextEditingController(); + final ValidationPopup _validationPopup = ValidationPopup(); + + // Multi-box PIN logic + final int pinLength = 4; + final List _pinControllers = []; + final List _pinFocusNodes = []; + String pinValue = ''; + + // Error states + bool _emailHasError = false; + bool _pinHasError = false; + + @override + void initState() { + super.initState(); + // Initialize PIN controllers and focus nodes + for (int i = 0; i < pinLength; i++) { + _pinControllers.add(TextEditingController()); + _pinFocusNodes.add(FocusNode()); + } + + final args = Get.arguments; + if (args != null && args is Map) { + final email = args['email'] ?? ''; + if (email.isNotEmpty) { + _emailController.text = email; + } + } + } + + @override + void dispose() { + _emailController.dispose(); + for (var controller in _pinControllers) { + controller.dispose(); + } + for (var node in _pinFocusNodes) { + node.dispose(); + } + super.dispose(); + } + + void _onPinChange(int index, String value) { + if (_pinHasError) { + setState(() => _pinHasError = false); + } + if (value.isNotEmpty && index < pinLength - 1) { + _pinFocusNodes[index + 1].requestFocus(); + } + if (value.isEmpty && index > 0) { + _pinFocusNodes[index - 1].requestFocus(); + } + setState(() { + pinValue = _pinControllers.map((c) => c.text).join(); + }); + } + + Future _handleLogin() async { + final email = _emailController.text.trim(); + final pin = pinValue.trim(); + + setState(() { + _emailHasError = false; + _pinHasError = false; + }); + + if (!_validationPopup.validateEmail(context, email)) { + setState(() => _emailHasError = true); + return; + } + if (!_validationPopup.validatePin(context, pin)) { + setState(() => _pinHasError = true); + return; + } + + await ref.read(loginProvider.notifier).loginWithPin(email, pin); + final state = ref.read(loginProvider); + + state.when( + data: (data) { + if (data['success'] == true) { + _validationPopup.showSuccessMessage( + context, + "Login Successful!", + ); + Future.delayed(const Duration(seconds: 1), () { + Get.offAll(() => const MainController()); + }); + } else if (data['error'] != null) { + _validationPopup.showErrorMessage(context, data['error'].toString()); + setState(() { + _emailHasError = true; + _pinHasError = true; + }); + } + }, + loading: () {}, + error: (err, _) { + _validationPopup.showErrorMessage(context, "Error: $err"); + }, + ); + } + + @override + Widget build(BuildContext context) { + final loginState = ref.watch(loginProvider); + final policyAsync = ref.watch(policyProvider); + final termsAsync = ref.watch(termsProvider); + + final r = ResponsiveUtils(context); + + final logoWidth = r.getValue(mobile: 120, tablet: 141, desktop: 160); + final logoHeight = r.getValue(mobile: 85, tablet: 100, desktop: 115); + final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); + final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final termsFontSize = r.fontSize(mobile: 10.5, tablet: 11.34, desktop: 12); + final signupFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final otherLoginFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); + + final pinBoxWidth = r.getValue(mobile: 55, tablet: 60, desktop: 68); + final pinBoxHeight = r.getValue(mobile: 55, tablet: 60, desktop: 68); + final pinFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); + final pinBoxMargin = r.getValue(mobile: 8, tablet: 6, desktop: 8); + final pinBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); + + final spacingXS = r.spacing(mobile: 10, tablet: 15, desktop: 18); + final spacingSM = r.spacing(mobile: 15, tablet: 20, desktop: 24); + final spacingMD = r.spacing(mobile: 20, tablet: 22, desktop: 26); + final spacingLG = r.spacing(mobile: 20, tablet: 22, desktop: 28); + + return PopScope( + canPop: true, + child: Scaffold( + resizeToAvoidBottomInset: true, + body: Container( + width: double.infinity, + height: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFF8F0), + Color(0xFFEBC894), + Color(0xFFE8DAF2), + Color(0xFFB49EF4), + ], + stops: [0.0, 0.3, 0.6, 1.0], + ), + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: r.screenHeight), + child: IntrinsicHeight( + child: Center( + child: CommonContainerAuth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + AppAssets.taxgildelogoauth, + width: logoWidth, + height: logoHeight, + ), + SizedBox(height: spacingSM), + Text( + "Login", + style: AppTextStyles.semiBold.copyWith( + fontSize: titleFontSize, + color: AppColors.authheading, + ), + ), + SizedBox(height: spacingSM), + Text( + "Enter your Email and PIN", + textAlign: TextAlign.center, + style: AppTextStyles.semiBold.copyWith( + fontSize: subtitleFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingLG), + CommanTextFormField( + controller: _emailController, + hintText: 'Enter your Email', + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + hasError: _emailHasError, + onChanged: (value) { + if (_emailHasError) { + setState(() => _emailHasError = false); + } + }, + ), + SizedBox(height: spacingMD), + + // PIN Boxes + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(pinLength, (index) { + bool isFilled = _pinControllers[index].text.isNotEmpty; + return Container( + margin: EdgeInsets.symmetric(horizontal: pinBoxMargin), + width: pinBoxWidth, + height: pinBoxHeight, + decoration: BoxDecoration( + color: isFilled ? AppColors.commanbutton : Colors.white, + borderRadius: BorderRadius.circular(pinBoxRadius), + border: Border.all( + color: _pinHasError + ? Colors.red + : const Color(0xFFDFDFDF), + width: _pinHasError ? 1.5 : 1.0, + ), + boxShadow: [ + BoxShadow( + color: _pinHasError + ? Colors.red.withOpacity(0.1) + : const Color(0xFFBDBDBD).withOpacity(0.25), + blurRadius: 7, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == + LogicalKeyboardKey.backspace) { + if (_pinControllers[index] + .text + .isEmpty && + index > 0) { + _pinFocusNodes[index - 1] + .requestFocus(); + } + } + }, + child: TextField( + controller: _pinControllers[index], + focusNode: _pinFocusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + obscureText: true, + enabled: !loginState.isLoading, + style: TextStyle( + fontFamily: "Gilroy", + fontWeight: FontWeight.w400, + fontSize: pinFontSize, + color: isFilled + ? Colors.white + : Colors.black, + ), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + onChanged: (value) => + _onPinChange(index, value), + ), + ), + ), + ); + }), + ), + + SizedBox(height: spacingXS), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () { + Get.toNamed( + ConstRouters.forgotPassword, + arguments: {'isEmployee': false}, + ); + }, + child: Text( + "Forgot Password?", + style: AppTextStyles.bold.copyWith( + fontSize: signupFontSize, + color: AppColors.authchange, + ), + ), + ), + ), + + SizedBox(height: spacingLG), + Text.rich( + TextSpan( + text: "By signing up, you agree to the ", + style: AppTextStyles.medium.copyWith( + fontSize: termsFontSize, + color: AppColors.authleading, + ), + children: [ + TextSpan( + text: "Terms of Service ", + style: AppTextStyles.bold.copyWith( + fontSize: termsFontSize, + color: AppColors.authtermsandcondition, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + termsAsync.when( + data: (terms) { + CommonInfoPopup.show( + context: context, + title: terms.data?.title ?? "Terms of Service", + content: terms.data?.content ?? "No terms found.", + ); + }, + loading: () => _validationPopup.showErrorMessage(context, "Loading Terms..."), + error: (err, _) => _validationPopup.showErrorMessage(context, "Failed to load Terms"), + ); + }, + ), + const TextSpan(text: "and "), + TextSpan( + text: "Data Processing Agreement", + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: termsFontSize, + color: AppColors.authtermsandcondition, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + policyAsync.when( + data: (policy) { + CommonInfoPopup.show( + context: context, + title: policy.data?.title ?? "Data Processing Agreement", + content: policy.data?.content ?? "No policy found.", + ); + }, + loading: () => _validationPopup.showErrorMessage(context, "Loading Policy..."), + error: (err, _) => _validationPopup.showErrorMessage(context, "Failed to load Policy"), + ); + }, + ), + ], + ), + textAlign: TextAlign.center, + ), + SizedBox(height: spacingSM), + Text("Or", style: AppTextStyles.medium.copyWith( + fontSize: 14, + color: AppColors.authleading, + )), + SizedBox(height: spacingSM), + Text.rich( + TextSpan( + text: "Didn't Have account? ", + style: AppTextStyles.bold.copyWith( + fontSize: signupFontSize, + color: AppColors.black, + ), + children: [ + TextSpan( + text: "Sign Up", + style: AppTextStyles.bold.copyWith( + fontSize: signupFontSize, + color: AppColors.authsignup, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.offNamed(ConstRouters.signup); + }, + ), + ], + ), + ), + SizedBox(height: spacingLG), + loginState.isLoading + ? const CircularProgressIndicator() + : CommanButton( + text: "Login", + onPressed: _handleLogin, + ), + SizedBox(height: spacingMD), + Text( + "Other Login", + textAlign: TextAlign.center, + style: AppTextStyles.semiBold.copyWith( + fontSize: otherLoginFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingXS), + Text.rich( + TextSpan( + text: "Staff Login? ", + style: AppTextStyles.bold.copyWith( + fontSize: signupFontSize, + color: AppColors.black, + ), + children: [ + TextSpan( + text: "Click Here", + style: AppTextStyles.bold.copyWith( + fontSize: signupFontSize, + color: AppColors.authsignup, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + Get.offNamed(ConstRouters.employeelogin); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/auth/mpin_set_screen.dart b/lib/auth/mpin_set_screen.dart new file mode 100644 index 0000000..bdf25ac --- /dev/null +++ b/lib/auth/mpin_set_screen.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; +import 'package:taxglide/consts/comman_button.dart'; +import 'package:taxglide/consts/comman_container_auth.dart'; +import 'package:taxglide/consts/responsive_helper.dart'; +import 'package:taxglide/consts/validation_popup.dart'; +import 'package:taxglide/controller/api_contoller.dart'; +import 'package:taxglide/view/Main_controller/main_controller.dart'; +import 'package:taxglide/consts/app_style.dart'; + +class MpinSetScreen extends ConsumerStatefulWidget { + const MpinSetScreen({super.key}); + + @override + ConsumerState createState() => _MpinSetScreenState(); +} + +class _MpinSetScreenState extends ConsumerState { + final ValidationPopup _validationPopup = ValidationPopup(); + final int pinLength = 4; + final List _controllers = []; + final List _focusNodes = []; + String pinValue = ''; + + @override + void initState() { + super.initState(); + _initControllers(); + } + + void _initControllers() { + for (var c in _controllers) c.dispose(); + for (var f in _focusNodes) f.dispose(); + _controllers.clear(); + _focusNodes.clear(); + pinValue = ''; + for (int i = 0; i < pinLength; i++) { + _controllers.add(TextEditingController()); + _focusNodes.add(FocusNode()); + } + } + + @override + void dispose() { + for (var c in _controllers) c.dispose(); + for (var f in _focusNodes) f.dispose(); + super.dispose(); + } + + void _handleAction() async { + if (pinValue.length != pinLength) { + _validationPopup.showErrorMessage(context, "Please enter all 4 digits"); + return; + } + + // Call API to set/change PIN + await ref.read(changePinProvider.notifier).changePin(pinValue); + final state = ref.read(changePinProvider); + + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage( + context, + "MPIN Set Successfully!", + ); + Future.delayed(const Duration(seconds: 1), () { + Get.offAll(() => const MainController()); + }); + } else { + _validationPopup.showErrorMessage( + context, + result['error'] ?? "Failed to set MPIN. Please try again.", + ); + } + }, + loading: () {}, + error: (error, stack) { + _validationPopup.showErrorMessage( + context, + "Failed to set MPIN. Please try again.", + ); + }, + ); + } + + void _onPinChange(int index, String value) { + if (value.isNotEmpty && index < pinLength - 1) { + _focusNodes[index + 1].requestFocus(); + } + if (value.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + } + setState(() { + pinValue = _controllers.map((c) => c.text).join(); + }); + } + + @override + Widget build(BuildContext context) { + final pinState = ref.watch(changePinProvider); + final isLoading = pinState is AsyncLoading; + + final r = ResponsiveUtils(context); + + final logoWidth = r.getValue(mobile: 120, tablet: 141, desktop: 160); + final logoHeight = r.getValue(mobile: 85, tablet: 100, desktop: 115); + final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); + final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + + final boxWidth = r.getValue(mobile: 57, tablet: 60, desktop: 68); + final boxHeight = r.getValue(mobile: 57, tablet: 56, desktop: 64); + final pinFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); + final boxMargin = r.getValue(mobile: 5, tablet: 6, desktop: 8); + final boxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); + + final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); + final spacingLG = r.spacing(mobile: 50, tablet: 50, desktop: 50); + + return Scaffold( + body: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFF8F0), + Color(0xFFEBC894), + Color(0xFFE8DAF2), + Color(0xFFB49EF4), + ], + stops: [0.0, 0.3, 0.6, 1.0], + ), + ), + ), + SingleChildScrollView( + child: SizedBox( + height: r.screenHeight, + child: Center( + child: CommonContainerAuth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + AppAssets.taxgildelogoauth, + width: logoWidth, + height: logoHeight, + fit: BoxFit.contain, + ), + SizedBox(height: spacingSM), + Text( + "Set New MPIN", + textAlign: TextAlign.center, + style: AppTextStyles.semiBold.copyWith( + fontSize: titleFontSize, + color: AppColors.authheading, + ), + ), + SizedBox(height: spacingSM), + Text( + "Set a 4-digit security PIN for your account", + textAlign: TextAlign.center, + style: AppTextStyles.semiBold.copyWith( + fontSize: subtitleFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingLG), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(pinLength, (index) { + bool isFilled = _controllers[index].text.isNotEmpty; + return Container( + margin: EdgeInsets.symmetric(horizontal: boxMargin), + width: boxWidth, + height: boxHeight, + decoration: BoxDecoration( + color: isFilled ? AppColors.commanbutton : Colors.white, + borderRadius: BorderRadius.circular(boxRadius), + border: Border.all(color: const Color(0xFFDFDFDF)), + boxShadow: [ + BoxShadow( + color: const Color(0xFFBDBDBD).withOpacity(0.25), + blurRadius: 7, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == + LogicalKeyboardKey.backspace) { + if (_controllers[index].text.isEmpty && + index > 0) { + _focusNodes[index - 1].requestFocus(); + } + } + }, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + obscureText: true, + enabled: !isLoading, + style: TextStyle( + fontFamily: "Gilroy", + fontWeight: FontWeight.w400, + fontSize: pinFontSize, + color: isFilled ? Colors.white : Colors.black, + ), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + onChanged: (value) => + _onPinChange(index, value), + ), + ), + ), + ); + }), + ), + SizedBox(height: spacingLG), + isLoading + ? const CircularProgressIndicator() + : CommanButton( + text: "Set PIN", + onPressed: _handleAction, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/auth/otp_screen.dart b/lib/auth/otp_screen.dart index dab58ff..895d210 100644 --- a/lib/auth/otp_screen.dart +++ b/lib/auth/otp_screen.dart @@ -1,411 +1,345 @@ -import 'dart:async'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:get/get.dart'; -import 'package:taxglide/consts/app_asstes.dart'; -import 'package:taxglide/consts/app_colors.dart'; -import 'package:taxglide/consts/comman_button.dart'; -import 'package:taxglide/consts/comman_container_auth.dart'; -import 'package:taxglide/consts/responsive_helper.dart'; -import 'package:taxglide/consts/validation_popup.dart'; -import 'package:taxglide/controller/api_contoller.dart'; -import 'package:taxglide/router/consts_routers.dart'; - -import 'package:taxglide/view/Main_controller/main_controller.dart'; -import 'package:taxglide/consts/app_style.dart'; - -class OtpScreen extends ConsumerStatefulWidget { - const OtpScreen({super.key}); - - @override - ConsumerState createState() => _OtpScreenState(); -} - -class _OtpScreenState extends ConsumerState { - final ValidationPopup _validationPopup = ValidationPopup(); - final int otpLength = 4; - final List _controllers = []; - final List _focusNodes = []; - String otpValue = ''; - - Timer? _timer; - int _remainingSeconds = 600; - bool _canResend = false; - - late String mobile; - - @override - void initState() { - super.initState(); - final args = Get.arguments as Map; - mobile = args['mobile'] ?? ''; - - for (int i = 0; i < otpLength; i++) { - _controllers.add(TextEditingController()); - _focusNodes.add(FocusNode()); - } - _startTimer(); - } - - @override - void dispose() { - _timer?.cancel(); - for (var c in _controllers) c.dispose(); - for (var f in _focusNodes) f.dispose(); - super.dispose(); - } - - void _startTimer() { - _canResend = false; - _remainingSeconds = 30; - _timer?.cancel(); - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_remainingSeconds > 0) { - setState(() => _remainingSeconds--); - } else { - setState(() => _canResend = true); - timer.cancel(); - } - }); - } - - String _formatTime(int seconds) { - int minutes = seconds ~/ 60; - int remainingSeconds = seconds % 60; - return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; - } - - void _resendOtp() async { - if (!_canResend) return; - for (var controller in _controllers) controller.clear(); - setState(() => otpValue = ''); - _focusNodes[0].requestFocus(); - - try { - await ref.read(loginProvider.notifier).login(mobile); - final loginState = ref.read(loginProvider); - loginState.when( - data: (result) { - if (result['success'] == true) { - _validationPopup.showSuccessMessage( - context, - "OTP has been resent successfully!", - ); - _startTimer(); - for (var controller in _controllers) controller.clear(); - setState(() => otpValue = ''); - } else { - _validationPopup.showErrorMessage( - context, - result['error'] ?? "Failed to resend OTP", - ); - } - }, - loading: () {}, - error: (error, stack) { - _validationPopup.showErrorMessage( - context, - "Failed to resend OTP. Please try again.", - ); - }, - ); - } catch (e) { - _validationPopup.showErrorMessage( - context, - "An error occurred. Please try again.", - ); - } - } - - void _verifyOtp() async { - if (otpValue.length != otpLength) { - _validationPopup.showErrorMessage(context, "Please enter all OTP digits"); - return; - } - - await ref.read(loginProvider.notifier).verifyOtp(mobile, otpValue); - final loginState = ref.read(loginProvider); - - loginState.when( - data: (result) { - if (result['success'] == true) { - _validationPopup.showSuccessMessage( - context, - "OTP Verified Successfully!", - ); - Future.delayed(const Duration(seconds: 1), () { - Get.offAll(() => MainController()); - }); - } else { - _validationPopup.showErrorMessage( - context, - result['error'] ?? "Invalid OTP. Please try again.", - ); - } - }, - loading: () {}, - error: (error, stack) { - _validationPopup.showErrorMessage( - context, - "Failed to verify OTP. Please try again.", - ); - }, - ); - } - - void _onOtpChange(int index, String value) { - if (value.isNotEmpty && index < otpLength - 1) { - _focusNodes[index + 1].requestFocus(); - } - if (value.isEmpty && index > 0) { - _focusNodes[index - 1].requestFocus(); - } - setState(() { - otpValue = _controllers.map((c) => c.text).join(); - }); - } - - @override - Widget build(BuildContext context) { - final loginState = ref.watch(loginProvider); - final isLoading = loginState is AsyncLoading; - - // Initialize responsive utils - final r = ResponsiveUtils(context); - - // Responsive values - final logoWidth = r.getValue( - mobile: 120, - tablet: 141, - desktop: 160, - ); - final logoHeight = r.getValue( - mobile: 85, - tablet: 100, - desktop: 115, - ); - final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); - final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final mobileFontSize = r.fontSize(mobile: 12, tablet: 13, desktop: 14); - final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); - final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); - - final otpBoxWidth = r.getValue(mobile: 52, tablet: 60, desktop: 68); - final otpBoxHeight = r.getValue( - mobile: 48, - tablet: 56, - desktop: 64, - ); - final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); - final otpBoxMargin = r.getValue(mobile: 5, tablet: 6, desktop: 8); - final otpBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); - - final spacingXS = r.spacing(mobile: 10, tablet: 10, desktop: 12); - final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); - final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); - final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); - - return Scaffold( - body: Stack( - children: [ - // Background gradient - Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Color(0xFFFFF8F0), - Color(0xFFEBC894), - Color(0xFFE8DAF2), - Color(0xFFB49EF4), - ], - stops: [0.0, 0.3, 0.6, 1.0], - ), - ), - ), - - // Scrollable content - SingleChildScrollView( - child: SizedBox( - height: r.screenHeight, - child: Center( - child: CommonContainerAuth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Logo - Image.asset( - AppAssets.taxgildelogoauth, - width: logoWidth, - height: logoHeight, - fit: BoxFit.contain, - ), - SizedBox(height: spacingSM), - - // Title - Text( - "Enter OTP", - textAlign: TextAlign.center, - style: AppTextStyles.semiBold.copyWith( - fontSize: titleFontSize, - height: 1.3, - letterSpacing: 0.01 * titleFontSize, - color: AppColors.authheading, - ), - ), - SizedBox(height: spacingSM), - - // Subtitle - Text( - "OTP has been sent to your registered mobile number", - textAlign: TextAlign.center, - style: AppTextStyles.semiBold.copyWith( - fontSize: subtitleFontSize, - height: 1.4, - letterSpacing: 0.03 * subtitleFontSize, - color: AppColors.authleading, - ), - ), - SizedBox(height: spacingMD), - - // Mobile number + Change - Text.rich( - TextSpan( - text: mobile, - style: AppTextStyles.medium.copyWith( - fontSize: mobileFontSize, - height: 1.8, - letterSpacing: 0.01, - color: AppColors.authleading, - ), - children: [ - TextSpan( - text: " ( Change Number )", - style: AppTextStyles.extraBold.copyWith( - fontSize: mobileFontSize, - height: 1.8, - letterSpacing: 0.04, - color: AppColors.authchange, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - Get.offNamed( - ConstRouters.login, - arguments: {'mobile': mobile}, - ); - }, - ), - ], - ), - textAlign: TextAlign.center, - ), - SizedBox(height: spacingLG), - - // OTP Boxes - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(otpLength, (index) { - bool isFilled = _controllers[index].text.isNotEmpty; - return Container( - margin: EdgeInsets.symmetric( - horizontal: otpBoxMargin, - ), - width: otpBoxWidth, - height: otpBoxHeight, - decoration: BoxDecoration( - color: isFilled - ? AppColors.commanbutton - : Colors.white, - borderRadius: BorderRadius.circular(otpBoxRadius), - border: Border.all( - color: const Color(0xFFDFDFDF), - ), - boxShadow: [ - BoxShadow( - color: const Color( - 0xFFBDBDBD, - ).withOpacity(0.25), - blurRadius: 7, - offset: const Offset(0, 1), - ), - ], - ), - child: Center( - child: TextField( - controller: _controllers[index], - focusNode: _focusNodes[index], - textAlign: TextAlign.center, - keyboardType: TextInputType.number, - maxLength: 1, - enabled: !isLoading, - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w400, - fontSize: otpFontSize, - letterSpacing: 0.03 * otpFontSize, - color: isFilled ? Colors.white : Colors.black, - ), - decoration: const InputDecoration( - counterText: '', - border: InputBorder.none, - ), - onChanged: (value) => - _onOtpChange(index, value), - ), - ), - ); - }), - ), - SizedBox(height: spacingXS), - - // Resend - Align( - alignment: Alignment.centerRight, - child: GestureDetector( - onTap: _canResend && !isLoading ? _resendOtp : null, - child: Text( - "Resend", - textAlign: TextAlign.center, - style: AppTextStyles.medium.copyWith( - fontSize: resendFontSize, - height: 1.4, - letterSpacing: 0.02 * resendFontSize, - color: _canResend && !isLoading - ? AppColors.authchange - : Colors.grey, - ), - ), - ), - ), - SizedBox(height: spacingSM), - - // Timer - Text( - _formatTime(_remainingSeconds), - style: AppTextStyles.semiBold.copyWith( - fontSize: timerFontSize, - color: _remainingSeconds > 0 - ? AppColors.authheading - : Colors.red, - ), - ), - SizedBox(height: spacingLG), - - // Verify Button - isLoading - ? const CircularProgressIndicator() - : CommanButton(text: "Verify", onPressed: _verifyOtp), - ], - ), - ), - ), - ), - ), - ], - ), - ); - } -} +import 'dart:async'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:get/get.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; +import 'package:taxglide/consts/comman_button.dart'; +import 'package:taxglide/consts/comman_container_auth.dart'; +import 'package:taxglide/consts/responsive_helper.dart'; +import 'package:taxglide/consts/validation_popup.dart'; +import 'package:taxglide/controller/api_contoller.dart'; +import 'package:taxglide/router/consts_routers.dart'; +import 'package:flutter/services.dart'; +import 'package:taxglide/consts/app_style.dart'; + +class OtpScreen extends ConsumerStatefulWidget { + const OtpScreen({super.key}); + + @override + ConsumerState createState() => _OtpScreenState(); +} + +class _OtpScreenState extends ConsumerState { + final ValidationPopup _validationPopup = ValidationPopup(); + final int otpLength = 4; + final List _controllers = []; + final List _focusNodes = []; + String otpValue = ''; + + Timer? _timer; + int _remainingSeconds = 600; + bool _canResend = false; + + late String _identifier; // Email or Mobile + bool _fromForgot = false; + bool _isEmployee = false; + + @override + void initState() { + super.initState(); + final args = Get.arguments as Map? ?? {}; + _fromForgot = args['fromForgot'] ?? false; + _isEmployee = args['isEmployee'] ?? false; + _identifier = args['email'] ?? args['mobile'] ?? ''; + + for (int i = 0; i < otpLength; i++) { + _controllers.add(TextEditingController()); + _focusNodes.add(FocusNode()); + } + _startTimer(); + } + + @override + void dispose() { + _timer?.cancel(); + for (var c in _controllers) c.dispose(); + for (var f in _focusNodes) f.dispose(); + super.dispose(); + } + + void _startTimer() { + _canResend = false; + _remainingSeconds = 30; + _timer?.cancel(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_remainingSeconds > 0) { + setState(() => _remainingSeconds--); + } else { + setState(() => _canResend = true); + timer.cancel(); + } + }); + } + + String _formatTime(int seconds) { + int minutes = seconds ~/ 60; + int remainingSeconds = seconds % 60; + return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; + } + + void _resendOtp() async { + if (!_canResend) return; + for (var controller in _controllers) controller.clear(); + setState(() => otpValue = ''); + _focusNodes[0].requestFocus(); + + if (_fromForgot) { + await ref.read(forgotPasswordProvider.notifier).requestOtp(_identifier, _isEmployee); + final state = ref.read(forgotPasswordProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!"); + _startTimer(); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP"); + } + }, + loading: () {}, + error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"), + ); + } else { + // Original mobile login resend (if still needed) + await ref.read(loginProvider.notifier).login(_identifier); + final loginState = ref.read(loginProvider); + loginState.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP has been resent successfully!"); + _startTimer(); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Failed to resend OTP"); + } + }, + loading: () {}, + error: (error, _) => _validationPopup.showErrorMessage(context, "Failed to resend OTP"), + ); + } + } + + void _verifyOtp() async { + if (otpValue.length != otpLength) { + _validationPopup.showErrorMessage(context, "Please enter all OTP digits"); + return; + } + + if (_fromForgot) { + await ref.read(forgotPasswordProvider.notifier).verifyOtp(_identifier, otpValue); + final state = ref.read(forgotPasswordProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP Verified successfully!"); + Get.toNamed(ConstRouters.mpinSet); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP"); + } + }, + loading: () {}, + error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"), + ); + } else { + // Original mobile login verify + await ref.read(loginProvider.notifier).verifyOtp(_identifier, otpValue); + final state = ref.read(loginProvider); + state.when( + data: (result) { + if (result['success'] == true) { + _validationPopup.showSuccessMessage(context, "OTP Verified successfully!"); + Get.offAllNamed(ConstRouters.mpinSet); + } else { + _validationPopup.showErrorMessage(context, result['error'] ?? "Invalid OTP"); + } + }, + loading: () {}, + error: (err, _) => _validationPopup.showErrorMessage(context, "Verification failed"), + ); + } + } + + void _onOtpChange(int index, String value) { + if (value.isNotEmpty && index < otpLength - 1) { + _focusNodes[index + 1].requestFocus(); + } + if (value.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + } + setState(() { + otpValue = _controllers.map((c) => c.text).join(); + }); + } + + @override + Widget build(BuildContext context) { + final isLoading = _fromForgot + ? ref.watch(forgotPasswordProvider).isLoading + : ref.watch(loginProvider).isLoading; + + final r = ResponsiveUtils(context); + + final logoWidth = r.getValue(mobile: 120, tablet: 141, desktop: 160); + final logoHeight = r.getValue(mobile: 85, tablet: 100, desktop: 115); + final titleFontSize = r.fontSize(mobile: 26, tablet: 32, desktop: 36); + final subtitleFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final resendFontSize = r.fontSize(mobile: 13, tablet: 14, desktop: 15); + final timerFontSize = r.fontSize(mobile: 14, tablet: 16, desktop: 18); + + final otpBoxWidth = r.getValue(mobile: 52, tablet: 60, desktop: 68); + final otpBoxHeight = r.getValue(mobile: 48, tablet: 56, desktop: 64); + final otpFontSize = r.fontSize(mobile: 22, tablet: 28, desktop: 32); + final otpBoxMargin = r.getValue(mobile: 5, tablet: 6, desktop: 8); + final otpBoxRadius = r.getValue(mobile: 5, tablet: 6, desktop: 8); + + final spacingSM = r.spacing(mobile: 20, tablet: 20, desktop: 24); + final spacingMD = r.spacing(mobile: 22, tablet: 22, desktop: 26); + final spacingLG = r.spacing(mobile: 30, tablet: 30, desktop: 36); + + return Scaffold( + body: Stack( + children: [ + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFFFF8F0), + Color(0xFFEBC894), + Color(0xFFE8DAF2), + Color(0xFFB49EF4), + ], + stops: [0.0, 0.3, 0.6, 1.0], + ), + ), + ), + SingleChildScrollView( + child: SizedBox( + height: r.screenHeight, + child: Center( + child: CommonContainerAuth( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + AppAssets.taxgildelogoauth, + width: logoWidth, + height: logoHeight, + fit: BoxFit.contain, + ), + SizedBox(height: spacingSM), + Text( + "Enter OTP", + style: AppTextStyles.bold.copyWith( + fontSize: titleFontSize, + color: AppColors.authheading, + ), + ), + SizedBox(height: spacingSM), + Text( + "OTP has been sent to ${_identifier}", + textAlign: TextAlign.center, + style: AppTextStyles.medium.copyWith( + fontSize: subtitleFontSize, + color: AppColors.authleading, + ), + ), + SizedBox(height: spacingMD), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(otpLength, (index) { + bool isFilled = _controllers[index].text.isNotEmpty; + return Container( + margin: EdgeInsets.symmetric(horizontal: otpBoxMargin), + width: otpBoxWidth, + height: otpBoxHeight, + decoration: BoxDecoration( + color: isFilled ? AppColors.commanbutton : Colors.white, + borderRadius: BorderRadius.circular(otpBoxRadius), + border: Border.all(color: const Color(0xFFDFDFDF)), + boxShadow: [ + BoxShadow( + color: const Color(0xFFBDBDBD).withOpacity(0.25), + blurRadius: 7, + offset: const Offset(0, 1), + ), + ], + ), + child: Center( + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == + LogicalKeyboardKey.backspace) { + if (_controllers[index].text.isEmpty && + index > 0) { + _focusNodes[index - 1].requestFocus(); + } + } + }, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + enabled: !isLoading, + style: TextStyle( + fontFamily: "Gilroy", + fontWeight: FontWeight.w400, + fontSize: otpFontSize, + color: isFilled ? Colors.white : Colors.black, + ), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + onChanged: (value) => + _onOtpChange(index, value), + ), + ), + ), + ); + }), + ), + SizedBox(height: spacingSM), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: _canResend && !isLoading ? _resendOtp : null, + child: Text( + "Resend", + style: AppTextStyles.medium.copyWith( + fontSize: resendFontSize, + color: _canResend && !isLoading + ? AppColors.authchange + : Colors.grey, + ), + ), + ), + ), + SizedBox(height: spacingSM), + Text( + _formatTime(_remainingSeconds), + style: AppTextStyles.semiBold.copyWith( + fontSize: timerFontSize, + color: _remainingSeconds > 0 ? AppColors.authheading : Colors.red, + ), + ), + SizedBox(height: spacingLG), + isLoading + ? const CircularProgressIndicator() + : CommanButton(text: "Verify", onPressed: _verifyOtp), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/auth/register_otp_screen.dart b/lib/auth/register_otp_screen.dart index 82ca785..336cd3d 100644 --- a/lib/auth/register_otp_screen.dart +++ b/lib/auth/register_otp_screen.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:taxglide/auth/mpin_set_screen.dart'; import 'package:taxglide/consts/app_asstes.dart'; import 'package:taxglide/consts/app_colors.dart'; import 'package:taxglide/consts/comman_button.dart'; @@ -155,7 +157,7 @@ class _RegisterOtpScreenState extends ConsumerState { } // Call verify OTP API for signup - await ref.read(signupProvider.notifier).verifySignupOtp(mobile, otpValue); + await ref.read(signupProvider.notifier).verifySignupOtp(email, otpValue); final signupState = ref.read(signupProvider); @@ -169,7 +171,7 @@ class _RegisterOtpScreenState extends ConsumerState { // Navigate to main screen after successful verification Future.delayed(const Duration(seconds: 1), () { - Get.offAll(() => MainController()); + Get.offAll(() => MpinSetScreen()); }); } else { _validationPopup.showErrorMessage( @@ -208,7 +210,20 @@ class _RegisterOtpScreenState extends ConsumerState { final signupState = ref.watch(signupProvider); final isLoading = signupState is AsyncLoading; - return Scaffold( + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + Get.offNamed( + ConstRouters.signup, + arguments: { + 'name': name, + 'email': email, + 'mobile': mobile, + }, + ); + }, + child: Scaffold( body: Stack( children: [ // Background gradient @@ -257,10 +272,10 @@ class _RegisterOtpScreenState extends ConsumerState { ), const SizedBox(height: 20), Text( - "OTP has been sent to your registered mobile number", + "OTP has been sent to your registered mail id", textAlign: TextAlign.center, style: AppTextStyles.semiBold.copyWith( - fontSize: 14, + fontSize: 12, height: 1.4, letterSpacing: 0.03 * 14, color: AppColors.authleading, @@ -269,7 +284,7 @@ class _RegisterOtpScreenState extends ConsumerState { const SizedBox(height: 22), Text.rich( TextSpan( - text: mobile, + text: email, style: AppTextStyles.medium.copyWith( fontSize: 13, height: 1.8, @@ -278,7 +293,7 @@ class _RegisterOtpScreenState extends ConsumerState { ), children: [ TextSpan( - text: " ( Change Number )", + text: " ( Change mail id )", style: AppTextStyles.extraBold.copyWith( fontSize: 13, height: 1.8, @@ -331,26 +346,39 @@ class _RegisterOtpScreenState extends ConsumerState { ], ), child: Center( - child: TextField( - controller: _controllers[index], - focusNode: _focusNodes[index], - textAlign: TextAlign.center, - keyboardType: TextInputType.number, - maxLength: 1, - enabled: !isLoading, - style: TextStyle( - fontFamily: "Gilroy", - fontWeight: FontWeight.w400, - fontSize: 28, - letterSpacing: 0.03 * 28, - color: isFilled ? Colors.white : Colors.black, + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + if (event is KeyDownEvent && + event.logicalKey == + LogicalKeyboardKey.backspace) { + if (_controllers[index].text.isEmpty && + index > 0) { + _focusNodes[index - 1].requestFocus(); + } + } + }, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + enabled: !isLoading, + style: TextStyle( + fontFamily: "Gilroy", + fontWeight: FontWeight.w400, + fontSize: 28, + letterSpacing: 0.03 * 28, + color: isFilled ? Colors.white : Colors.black, + ), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + ), + onChanged: (value) => + _onOtpChange(index, value), ), - decoration: const InputDecoration( - counterText: '', - border: InputBorder.none, - ), - onChanged: (value) => - _onOtpChange(index, value), ), ), ); @@ -403,6 +431,7 @@ class _RegisterOtpScreenState extends ConsumerState { ), ], ), + ) ); } } diff --git a/lib/consts/validation_popup.dart b/lib/consts/validation_popup.dart index 7193f61..6383076 100644 --- a/lib/consts/validation_popup.dart +++ b/lib/consts/validation_popup.dart @@ -1,106 +1,127 @@ -import 'package:flutter/material.dart'; -import 'package:taxglide/consts/app_asstes.dart'; -import 'package:taxglide/consts/app_colors.dart'; - -class ValidationPopup { - // Show SnackBar - void _showSnackBar(BuildContext context, String msg, {bool isError = false}) { - print("Mageshwaran $msg"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 1), - backgroundColor: isError - ? Colors.red - : AppColors.commanbutton, // 🔴 Error red - behavior: SnackBarBehavior.floating, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - elevation: 6, - margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), - content: SizedBox( - width: 200, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18), - const SizedBox(width: 8), - Expanded( - child: Text( - msg, - style: const TextStyle(color: Colors.white, fontSize: 12), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ), - ], - ), - ), - ), - ); - } - - // Validate Mobile Number - bool validateMobileNumber(BuildContext context, String mobileNumber) { - // Check if empty - if (mobileNumber.isEmpty) { - _showSnackBar(context, "Please enter mobile number", isError: true); - return false; - } - - // Remove spaces and special characters - String cleanNumber = mobileNumber.replaceAll(RegExp(r'[^\d]'), ''); - - // Check if contains only digits - if (!RegExp(r'^[0-9]+$').hasMatch(cleanNumber)) { - _showSnackBar( - context, - "Mobile number must contain only digits", - isError: true, - ); - return false; - } - - // Check length (Indian mobile numbers are 10 digits) - if (cleanNumber.length != 10) { - _showSnackBar(context, "Mobile number must be 10 digits", isError: true); - return false; - } - - // Check if starts with valid digit (6-9 for Indian numbers) - if (!RegExp(r'^[6-9]').hasMatch(cleanNumber)) { - _showSnackBar( - context, - "Mobile number must start with 6, 7, 8, or 9", - isError: true, - ); - return false; - } - - // All validations passed - return true; - } - - // Add this method to your ValidationPopup class - bool validateEmail(BuildContext context, String email) { - // Email regex pattern - final emailRegex = RegExp( - r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', - ); - - if (!emailRegex.hasMatch(email)) { - showErrorMessage(context, "Please enter a valid email address"); - return false; - } - return true; - } - - // Show success message - void showSuccessMessage(BuildContext context, String msg) { - _showSnackBar(context, msg, isError: false); - } - - // Show error message - void showErrorMessage(BuildContext context, String msg) { - _showSnackBar(context, msg, isError: true); - } -} +import 'package:flutter/material.dart'; +import 'package:taxglide/consts/app_asstes.dart'; +import 'package:taxglide/consts/app_colors.dart'; + +class ValidationPopup { + // Show SnackBar + void _showSnackBar(BuildContext context, String msg, {bool isError = false}) { + print("Mageshwaran $msg"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 1), + backgroundColor: isError + ? Colors.red + : AppColors.commanbutton, // 🔴 Error red + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 6, + margin: const EdgeInsets.symmetric(horizontal: 70, vertical: 25), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + content: SizedBox( + width: 200, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset(AppAssets.taxgildelogoauth, height: 18, width: 18), + const SizedBox(width: 8), + Expanded( + child: Text( + msg, + style: const TextStyle(color: Colors.white, fontSize: 12), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ), + ], + ), + ), + ), + ); + } + + // Validate Mobile Number + bool validateMobileNumber(BuildContext context, String mobileNumber) { + // Check if empty + if (mobileNumber.isEmpty) { + _showSnackBar(context, "Please enter mobile number", isError: true); + return false; + } + + // Remove spaces and special characters + String cleanNumber = mobileNumber.replaceAll(RegExp(r'[^\d]'), ''); + + // Check if contains only digits + if (!RegExp(r'^[0-9]+$').hasMatch(cleanNumber)) { + _showSnackBar( + context, + "Mobile number must contain only digits", + isError: true, + ); + return false; + } + + // Check length (Indian mobile numbers are 10 digits) + if (cleanNumber.length != 10) { + _showSnackBar(context, "Mobile number must be 10 digits", isError: true); + return false; + } + + // Check if starts with valid digit (6-9 for Indian numbers) + if (!RegExp(r'^[6-9]').hasMatch(cleanNumber)) { + _showSnackBar( + context, + "Mobile number must start with 6, 7, 8, or 9", + isError: true, + ); + return false; + } + + // All validations passed + return true; + } + + // Validate Email + bool validateEmail(BuildContext context, String email) { + if (email.isEmpty) { + showErrorMessage(context, "Please enter your email address"); + return false; + } + // Email regex pattern + final emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + + if (!emailRegex.hasMatch(email)) { + showErrorMessage(context, "Please enter a valid email address"); + return false; + } + return true; + } + + // Validate PIN + bool validatePin(BuildContext context, String pin) { + if (pin.isEmpty) { + showErrorMessage(context, "Please enter your 4-digit PIN"); + return false; + } + if (pin.length != 4) { + showErrorMessage(context, "PIN must be 4 digits"); + return false; + } + if (!RegExp(r'^[0-9]+$').hasMatch(pin)) { + showErrorMessage(context, "PIN must contain only digits"); + return false; + } + return true; + } + + // Show success message + void showSuccessMessage(BuildContext context, String msg) { + _showSnackBar(context, msg, isError: false); + } + + // Show error message + void showErrorMessage(BuildContext context, String msg) { + _showSnackBar(context, msg, isError: true); + } +} diff --git a/lib/controller/api_consts.dart b/lib/controller/api_consts.dart index b797dce..686ff66 100644 --- a/lib/controller/api_consts.dart +++ b/lib/controller/api_consts.dart @@ -2,7 +2,7 @@ class ConstsApi { static const String baseUrl = "https://www.taxglide.amrithaa.net"; static const String login = "$baseUrl/api/otp/sms/request"; - static const String verifyOtp = "$baseUrl/api/otp/sms/verify"; + static const String verifyOtp = "$baseUrl/api/otp/email/verify"; static const String signup = "$baseUrl/api/register"; static const String terms = "$baseUrl/api/get_terms_conditions"; static const String policy = "$baseUrl/api/get_privacy_policy"; @@ -28,4 +28,10 @@ class ConstsApi { static const String dashboard = "$baseUrl/api/get_dashboard_data"; static const String proformaaccept = "$baseUrl/api/proforma/accept"; static const String payNow = "$baseUrl/api/pay_now"; + static const String changePin = "$baseUrl/api/change_pin"; + + static const String loginWithPin = "$baseUrl/api/login"; + static const String employeeLoginWithPin = "$baseUrl/api/employee/login"; + static const String forgotPassword = "$baseUrl/api/otp/email/request"; + static const String employeeForgotPassword = "$baseUrl/api/otp/employee/email/request"; } diff --git a/lib/controller/api_contoller.dart b/lib/controller/api_contoller.dart index 5526cd3..f97db24 100644 --- a/lib/controller/api_contoller.dart +++ b/lib/controller/api_contoller.dart @@ -33,6 +33,17 @@ class LoginNotifier extends StateNotifier>> { LoginNotifier(this._apiRepository) : super(const AsyncValue.data({})); + Future loginWithPin(String email, String pin) async { + if (!mounted) return; + state = const AsyncValue.loading(); + try { + final result = await _apiRepository.loginWithPin(email, pin); + if (mounted) state = AsyncValue.data(result); + } catch (e, st) { + if (mounted) state = AsyncValue.error(e, st); + } + } + Future login(String mobile) async { if (!mounted) return; state = const AsyncValue.loading(); @@ -86,11 +97,11 @@ class SignupNotifier extends StateNotifier>> { } // Verify OTP for signup - Future verifySignupOtp(String mobile, String otp) async { + Future verifySignupOtp(String email, String otp) async { if (!mounted) return; state = const AsyncValue.loading(); try { - final result = await _apiRepository.verifySignupOtp(mobile, otp); + final result = await _apiRepository.verifySignupOtp(email, otp); if (mounted) state = AsyncValue.data(result); } catch (e, st) { if (mounted) state = AsyncValue.error(e, st); @@ -115,6 +126,17 @@ class EmployeeLoginNotifier EmployeeLoginNotifier(this._apiRepository) : super(const AsyncValue.data({})); + Future loginWithPin(String email, String pin) async { + if (!mounted) return; + state = const AsyncValue.loading(); + try { + final result = await _apiRepository.employeeLoginWithPin(email, pin); + if (mounted) state = AsyncValue.data(result); + } catch (e, st) { + if (mounted) state = AsyncValue.error(e, st); + } + } + Future login(String mobile) async { if (!mounted) return; state = const AsyncValue.loading(); @@ -137,6 +159,77 @@ class EmployeeLoginNotifier if (mounted) state = AsyncValue.error(e, st); } } + + void reset() { + state = const AsyncValue.data({}); + } +} + +final forgotPasswordProvider = + StateNotifierProvider< + ForgotPasswordNotifier, + AsyncValue> + >((ref) => ForgotPasswordNotifier(ref.read(apiRepositoryProvider))); + +class ForgotPasswordNotifier + extends StateNotifier>> { + final ApiRepository _apiRepository; + + ForgotPasswordNotifier(this._apiRepository) + : super(const AsyncValue.data({})); + + Future requestOtp(String email, bool isEmployee) async { + if (!mounted) return; + state = const AsyncValue.loading(); + try { + final result = await _apiRepository.requestForgotPasswordOtp( + email, + isEmployee, + ); + if (mounted) state = AsyncValue.data(result); + } catch (e, st) { + if (mounted) state = AsyncValue.error(e, st); + } + } + + Future verifyOtp(String email, String otp) async { + if (!mounted) return; + state = const AsyncValue.loading(); + try { + final result = await _apiRepository.verifyEmailOtp(email, otp); + if (mounted) state = AsyncValue.data(result); + } catch (e, st) { + if (mounted) state = AsyncValue.error(e, st); + } + } + + void reset() { + state = const AsyncValue.data({}); + } +} + + +// PIN controller +final changePinProvider = + StateNotifierProvider>>( + (ref) => PinNotifier(ref.read(apiRepositoryProvider)), +); + +class PinNotifier extends StateNotifier>> { + final ApiRepository _apiRepository; + + PinNotifier(this._apiRepository) : super(const AsyncValue.data({})); + + Future changePin(String pinOrOtp) async { + if (!mounted) return; + state = const AsyncValue.loading(); + try { + final result = await _apiRepository.changePin(pinOrOtp); + if (mounted) state = AsyncValue.data(result); + } catch (e, st) { + if (mounted) state = AsyncValue.error(e, st); + } + } } final termsProvider = FutureProvider((ref) async { diff --git a/lib/controller/api_repository.dart b/lib/controller/api_repository.dart index 4acbfe9..2eba9bf 100644 --- a/lib/controller/api_repository.dart +++ b/lib/controller/api_repository.dart @@ -159,6 +159,54 @@ class ApiRepository { } } + // 🔹 DIRECT LOGIN (Email + PIN) - ✅ Regenerates FCM token + Future> loginWithPin(String email, String pin) async { + try { + final params = {'email': email, 'pin': pin}; + + // ✅ Regenerate FCM and APNS tokens on every login + final tokens = await _regenerateFcmToken(); + final fcmToken = tokens['fcm_token']; + final apnsToken = tokens['apns_token']; + + if (fcmToken != null && fcmToken.isNotEmpty) { + params['fcm_token'] = fcmToken; + } + if (apnsToken != null && apnsToken.isNotEmpty) { + params['apns_token'] = apnsToken; + } + + if (params['fcm_token'] == null) { + debugPrint('⚠️ Failed to generate FCM token, trying fallback...'); + final fallbackToken = await _getFreshFcmToken(); + if (fallbackToken != null && fallbackToken.isNotEmpty) { + params['fcm_token'] = fallbackToken; + } + } + + final uri = Uri.parse(ConstsApi.loginWithPin).replace( + queryParameters: params.map( + (key, value) => MapEntry(key, value.toString()), + ), + ); + + final response = await http.post(uri, headers: _baseHeaders); + final data = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + await _localStore.saveLoginData(data); + return {'success': true, 'data': data}; + } else { + return {'success': false, 'error': _extractErrorMessage(data)}; + } + } catch (e) { + return { + 'success': false, + 'error': 'Connection error. Please check your internet.', + }; + } + } + // 🔹 VERIFY OTP (LOGIN) - ✅ Regenerates FCM token Future> verifyOtp(LoginModel model) async { try { @@ -230,6 +278,143 @@ class ApiRepository { } } + // 🔹 EMPLOYEE LOGIN (Email + PIN) - ✅ Regenerates FCM token + Future> employeeLoginWithPin( + String email, + String pin, + ) async { + try { + final params = {'email': email, 'pin': pin}; + + // ✅ Regenerate FCM and APNS tokens on every login + final tokens = await _regenerateFcmToken(); + final fcmToken = tokens['fcm_token']; + final apnsToken = tokens['apns_token']; + + if (fcmToken != null && fcmToken.isNotEmpty) { + params['fcm_token'] = fcmToken; + } + if (apnsToken != null && apnsToken.isNotEmpty) { + params['apns_token'] = apnsToken; + } + + if (params['fcm_token'] == null) { + debugPrint('⚠️ Failed to generate FCM token, trying fallback...'); + final fallbackToken = await _getFreshFcmToken(); + if (fallbackToken != null && fallbackToken.isNotEmpty) { + params['fcm_token'] = fallbackToken; + } + } + + final uri = Uri.parse(ConstsApi.employeeLoginWithPin).replace( + queryParameters: params.map( + (key, value) => MapEntry(key, value.toString()), + ), + ); + + final response = await http.post(uri, headers: _baseHeaders); + final data = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + await _localStore.saveLoginData(data); + return {'success': true, 'data': data}; + } else { + return {'success': false, 'error': _extractErrorMessage(data)}; + } + } catch (e) { + return { + 'success': false, + 'error': 'Connection error. Please check your internet.', + }; + } + } + + // 🔹 FORGOT PASSWORD REQUEST (Email OTP) + Future> requestForgotPasswordOtp( + String email, + bool isEmployee, + ) async { + try { + final endpoint = isEmployee + ? ConstsApi.employeeForgotPassword + : ConstsApi.forgotPassword; + final uri = Uri.parse(endpoint).replace(queryParameters: {'email': email}); + + final response = await http.post(uri, headers: _baseHeaders); + final data = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + return {'success': true, 'data': data}; + } else { + return {'success': false, 'error': _extractErrorMessage(data)}; + } + } catch (e) { + return { + 'success': false, + 'error': 'Connection error. Please check your internet.', + }; + } + } + + // 🔹 VERIFY EMAIL OTP (Generic) + Future> verifyEmailOtp(String email, String otp) async { + try { + final fcmToken = await _getFreshFcmToken(); + final uri = Uri.parse(ConstsApi.verifyOtp).replace( + queryParameters: { + 'email': email, + 'otp': otp, + if (fcmToken != null) 'fcm_token': fcmToken, + }, + ); + + final response = await http.post(uri, headers: _baseHeaders); + final data = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + // ✅ CRITICAL: Save login data to get the token for subsequent requests (like set_pin) + await _localStore.saveLoginData(data); + return {'success': true, 'data': data}; + } else { + return {'success': false, 'error': _extractErrorMessage(data)}; + } + } catch (e) { + return { + 'success': false, + 'error': 'Connection error. Please check your internet.', + }; + } + } + + // 🔹 CHANGE/SET PIN + Future> changePin(String pin) async { + try { + final token = await _localStore.getToken(); + final uri = Uri.parse(ConstsApi.changePin).replace( + queryParameters: {'otp': pin}, + ); + + final response = await http.post( + uri, + headers: { + 'Authorization': 'Bearer $token', + 'Accept': 'application/json', + }, + ); + final data = jsonDecode(response.body); + + if (response.statusCode == 200 || response.statusCode == 201) { + return {'success': true, 'data': data}; + } else { + return {'success': false, 'error': _extractErrorMessage(data)}; + } + } catch (e) { + return { + 'success': false, + 'error': 'Connection error. Please check your internet.', + }; + } + } // 🔹 SIGNUP API Future> signupUser(SignupModel model) async { try { @@ -258,7 +443,7 @@ class ApiRepository { // 🔹 VERIFY OTP (SIGNUP) - ✅ Regenerates FCM token Future> verifySignupOtp( - String mobile, + String email, String otp, ) async { try { @@ -267,7 +452,7 @@ class ApiRepository { final fcmToken = tokens['fcm_token']; final apnsToken = tokens['apns_token']; - final params = {'mobile': mobile, 'otp': otp}; + final params = {'email': email, 'otp': otp}; if (fcmToken != null && fcmToken.isNotEmpty) { params['fcm_token'] = fcmToken; @@ -945,6 +1130,41 @@ debugPrint('📦 KYC Response Body: ${response.body}'); throw {'error': 'Failed to accept proforma: ${e.toString()}'}; } } + + // // 🔹 CHANGE PIN (Verify OTP) + // Future> changePin(String otp) async { + // try { + // final token = await _localStore.getToken(); + // final uri = Uri.parse(ConstsApi.changePin).replace( + // queryParameters: {'otp': otp}, + // ); + + // final response = await http.post( + // uri, + // headers: { + // 'Authorization': 'Bearer $token', + // 'Accept': 'application/json', + // }, + // ); + + // final data = jsonDecode(response.body); + // debugPrint("🚀 Change Pin (Verify) API Response: $data"); + + // if (response.statusCode == 200 || response.statusCode == 201) { + // return {'success': true, 'data': data}; + // } else { + // return {'success': false, 'error': _extractErrorMessage(data)}; + // } + // } catch (e) { + // debugPrint('❌ Change Pin API Error: $e'); + // return { + // 'success': false, + // 'error': 'Connection error. Please try again.', + // }; + // } + // } + + // 🔹 LOGOUT - ✅ Deletes FCM token Future logout() async { diff --git a/lib/model/login_model.dart b/lib/model/login_model.dart index 0f61e7e..02a6aaf 100644 --- a/lib/model/login_model.dart +++ b/lib/model/login_model.dart @@ -1,23 +1,38 @@ -// login_model.dart - -class LoginModel { - String? mobile; - String? otp; - - LoginModel({this.mobile, this.otp}); - - // For sending mobile number to request OTP - Map toJsonMobile() { - return {'mobile': mobile}; - } - - // For sending mobile + OTP for verification - Map toJsonOtp() { - return {'mobile': mobile, 'otp': otp}; - } - - // Response from API - factory LoginModel.fromJson(Map json) { - return LoginModel(mobile: json['mobile'], otp: json['otp']); - } -} +// login_model.dart + +class LoginModel { + String? email; + String? pin; + String? mobile; + String? otp; + + LoginModel({this.email, this.pin, this.mobile, this.otp}); + + // For sending mobile number to request OTP (Legacy) + Map toJsonMobile() { + return {'mobile': mobile}; + } + + // For sending mobile + OTP for verification (Legacy) + Map toJsonOtp() { + return {'mobile': mobile, 'otp': otp}; + } + + // For Email + PIN Login + Map toJsonEmailPin() { + return { + 'email': email, + 'pin': pin, + }; + } + + // Response from API + factory LoginModel.fromJson(Map json) { + return LoginModel( + email: json['email'], + pin: json['pin'], + mobile: json['mobile'], + otp: json['otp'], + ); + } +} diff --git a/lib/router/consts_routers.dart b/lib/router/consts_routers.dart index 2ae30dc..ef7eaaa 100644 --- a/lib/router/consts_routers.dart +++ b/lib/router/consts_routers.dart @@ -13,4 +13,6 @@ class ConstRouters { static const String servicerequest = '/servicerequest'; static const String staff = '/staff'; static const String employeekycdetailslist = '/employeekycdetailslist'; + static const String mpinSet = '/mpinSet'; + static const String forgotPassword = '/forgotPassword'; } diff --git a/lib/router/router.dart b/lib/router/router.dart index c021869..0860e29 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -1,7 +1,9 @@ import 'package:get/get.dart'; import 'package:taxglide/auth/employee_login_screen.dart'; import 'package:taxglide/auth/employee_otp_screen.dart'; +import 'package:taxglide/auth/forgot_screen.dart'; import 'package:taxglide/auth/login_screen.dart'; +import 'package:taxglide/auth/mpin_set_screen.dart'; import 'package:taxglide/auth/otp_screen.dart'; import 'package:taxglide/auth/register_otp_screen.dart'; import 'package:taxglide/auth/signup_screen.dart'; @@ -50,5 +52,10 @@ class AppRoutes { ), GetPage(name: ConstRouters.policy, page: () => const PolicyScreen()), GetPage(name: ConstRouters.staff, page: () => const StaffListScreen()), + GetPage(name: ConstRouters.mpinSet, page: () => const MpinSetScreen()), + GetPage( + name: ConstRouters.forgotPassword, + page: () => const ForgotScreen(), + ), ]; } diff --git a/pubspec.lock b/pubspec.lock index 182912a..7b5020c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "93.0.0" + version: "85.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "7.7.1" animated_notch_bottom_bar: dependency: "direct main" description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -724,26 +724,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1137,26 +1137,26 @@ packages: dependency: transitive description: name: test - sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.29.0" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.15" + version: "0.6.11" timezone: dependency: transitive description: